Merge 'v2.4.3'

* commit 'v2.4.3':
  Update JGit to patch security hole during clone

Change-Id: I5a236e06384b7a1ba4c923f3219c04b6922a395d
diff --git a/.gitignore b/.gitignore
index f318b65..465893d 100644
--- a/.gitignore
+++ b/.gitignore
@@ -4,3 +4,5 @@
 /.settings/org.eclipse.jdt.core.prefs
 /.settings/org.maven.ide.eclipse.prefs
 /test_site
+/.idea
+/gerrit-parent.iml
diff --git a/Documentation/Makefile b/Documentation/Makefile
index 5522239..4c64dfe 100644
--- a/Documentation/Makefile
+++ b/Documentation/Makefile
@@ -76,6 +76,7 @@
 	@$(ASCIIDOC) -a toc \
 		-a data-uri \
 		-a 'revision=$(REVISION)' \
+		-a 'newline=\n' \
 		-b xhtml11 \
 		-f asciidoc.conf \
 		$(ASCIIDOC_EXTRA) \
diff --git a/Documentation/access-control.txt b/Documentation/access-control.txt
index 8d89fa2..879d1ac 100644
--- a/Documentation/access-control.txt
+++ b/Documentation/access-control.txt
@@ -23,7 +23,7 @@
 
 Users in the 'Administrators' group can perform any action under
 the Admin menu, to any group or project, without further validation
-of any other access controls.  In most installations only those
+or any other access controls.  In most installations only those
 users who have direct filesystem and database access would be
 placed into this group.
 
@@ -444,6 +444,17 @@
 A restart is required after making database changes.
 See <<restart_changes,below>>.
 
+[[category_abandon]]
+Abandon
+~~~~~~~
+
+This category controls whether users are allowed to abandon changes
+to projects in Gerrit. It can give permission to abandon a specific
+change to a given ref.
+
+This also grants the permission to restore a change if the change
+can be uploaded.
+
 [[category_create]]
 Create reference
 ~~~~~~~~~~~~~~~~
@@ -451,7 +462,7 @@
 The create reference category controls whether it is possible to
 create new references, branches or tags.  This implies that the
 reference must not already exist, it's not a destructive permission
-in that you can't overwrite or remove any previosuly existing
+in that you can't overwrite or remove any previously existing
 references (and also discard any commits in the process).
 
 It's probably most common to either permit the creation of a single
@@ -462,7 +473,7 @@
 branch permissions, allowing the holder of both to create new branches
 as well as bypass review for new commits on that branch.
 
-To push lightweight (non annotated) tags, grant
+To push lightweight (non-annotated) tags, grant
 `Create Reference` for reference name `refs/tags/*`, as lightweight
 tags are implemented just like branches in Git.
 
@@ -622,13 +633,19 @@
 Push Merge Commits
 ~~~~~~~~~~~~~~~~~~~~
 
-The `Push Merge Commit` permits the user to upload merge commits.
-It's an addon to the <<category_push,Push>> access right, and so it
-won't be sufficient with only `Push Merge Commit` granted for a push
-to happen.  Some projects wish to restrict merges to being created by
-Gerrit. By granting `Push` without `Push Merge Commit`, the only
+The `Push Merge Commit` access right permits the user to upload merge
+commits.  It's an addon to the <<category_push,Push>> access right, and
+so it won't be sufficient with only `Push Merge Commit` granted for a
+push to happen.  Some projects wish to restrict merges to being created
+by Gerrit. By granting `Push` without `Push Merge Commit`, the only
 merges that enter the system will be those created by Gerrit.
 
+The reference name connected to a `Push Merge Commit` entry must always
+be prefixed with `refs/for/`, for example `refs/for/refs/heads/BRANCH`.
+This applies even for an entry that complements a `Push` entry for
+`refs/heads/BRANCH` that allows direct pushes of non-merge commits, and
+the intention of the `Push Merge Commit` entry is to allow direct pushes
+of merge commits.
 
 [[category_push_annotated]]
 Push Annotated Tag
@@ -809,7 +826,7 @@
 
 Below follows a set of typical roles on a server and which access
 rights these roles typically should be granted. You may see them as
-general guide lines for a typical way to set up your project on a
+general guidelines for a typical way to set up your project on a
 brand new Gerrit instance.
 
 [[examples_contributor]]
@@ -892,8 +909,8 @@
 project and how much the CI system can be trusted for accurate results, a
 blocking label might not be feasible.  A recommended alternative is to set the
 label `Code-review` to -1 instead, as it isn't a blocking label but still
-shows a red label in the Gerrit UI.  Optionally; to enable the possibility to
-deliver different results (build error vs unstable for instance) it's also
+shows a red label in the Gerrit UI.  Optionally, to enable the possibility to
+deliver different results (build error vs unstable for instance), it's also
 possible to set `Code-review` +1 as well.
 
 If pushing new changes is granted, it's possible to automate cherry-pick of
@@ -1064,6 +1081,15 @@
 either link:cmd-create-project.html[create new git projects via ssh]
 or via the web UI.
 
+[[capability_emailReviewers]]
+Email Reviewers
+~~~~~~~~~~~~~~~
+
+Allow or deny sending email to change reviewers and watchers.  This can be used
+to deny build bots from emailing reviewers and people who watch the change.
+Instead, only the authors of the change and those who starred it will be
+emailed.  The allow rules are evaluated before deny rules, however the default
+is to allow emailing, if no explicit rule is matched.
 
 [[capability_flushCaches]]
 Flush Caches
@@ -1136,7 +1162,8 @@
 Start Replication
 ~~~~~~~~~~~~~~~~~
 
-Allow access to execute link:cmd-replicate.html[the `gerrit replicate` command].
+Allow access to execute `replication start` command, if the
+replication plugin is installed on the server.
 
 
 [[capability_viewCaches]]
diff --git a/Documentation/cmd-ban-commit.txt b/Documentation/cmd-ban-commit.txt
new file mode 100644
index 0000000..fb4a2ac
--- /dev/null
+++ b/Documentation/cmd-ban-commit.txt
@@ -0,0 +1,60 @@
+gerrit ban-commit
+=================
+
+NAME
+----
+gerrit ban-commit - Bans a commit from a project's repository.
+
+SYNOPSIS
+--------
+[verse]
+'ssh' -p <port> <host> 'gerrit ban-commit'
+  [--reason <REASON>]
+  <PROJECT>
+  <COMMIT> ...
+
+DESCRIPTION
+-----------
+Marks a commit as banned for the specified repository.  If a commit is
+banned Gerrit rejects every push that includes this commit with
+link:error-contains-banned-commit.html[contains banned commit ...].
+
+[NOTE]
+This command just marks the commit as banned, but it does not remove
+the commit from the history of any central branch.  This needs to be
+done manually.
+
+ACCESS
+------
+Caller must be owner of the project or be a member of the privileged
+'Administrators' group.
+
+SCRIPTING
+---------
+This command is intended to be used in scripts.
+
+OPTIONS
+-------
+<PROJECT>::
+	Required; name of the project for which the commit should be
+	banned.
+
+<COMMIT>::
+	Required; commit(s) that should be banned.
+
+--reason::
+	Reason for banning the commit.
+
+EXAMPLES
+--------
+Ban commit `421919d015c062fd28901fe144a78a555d0b5984` from project
+`myproject`:
+
+====
+	$ ssh -p 29418 review.example.com gerrit ban-commit myproject \
+	421919d015c062fd28901fe144a78a555d0b5984
+====
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/cmd-cherry-pick.txt b/Documentation/cmd-cherry-pick.txt
index 568c872..d051a9a 100644
--- a/Documentation/cmd-cherry-pick.txt
+++ b/Documentation/cmd-cherry-pick.txt
@@ -39,7 +39,7 @@
 ====
   $ scp -p -P 29418 john.doe@review.example.com:bin/gerrit-cherry-pick ~/bin/
 
-  $ curl http://review.example.com/tools/bin/gerrit-cherry-pick
+  $ curl -o ~/bin/gerrit-cherry-pick http://review.example.com/tools/bin/gerrit-cherry-pick
 ====
 
 GERRIT
diff --git a/Documentation/cmd-create-account.txt b/Documentation/cmd-create-account.txt
index 98f950f..16b2eb5 100644
--- a/Documentation/cmd-create-account.txt
+++ b/Documentation/cmd-create-account.txt
@@ -13,6 +13,7 @@
   [--full-name <FULLNAME>]
   [--email <EMAIL>]
   [--ssh-key - | <KEY>]
+  [--http-password <PASSWORD>]
   <USERNAME>
 
 DESCRIPTION
@@ -59,6 +60,9 @@
 --email::
 	Preferred email address for the user account.
 
+--http-password::
+    HTTP password for the user account.
+
 EXAMPLES
 --------
 Create a new user account called `watcher`:
diff --git a/Documentation/cmd-create-group.txt b/Documentation/cmd-create-group.txt
index 475d2c5..8dc6dcc 100644
--- a/Documentation/cmd-create-group.txt
+++ b/Documentation/cmd-create-group.txt
@@ -9,8 +9,8 @@
 --------
 [verse]
 'ssh' -p <port> <host> 'gerrit create-group'
-  [--owner <GROUP>]
-  [--description <DESC>]
+  [--owner <GROUP> | -o <GROUP>]
+  [--description <DESC> | -d <DESC>]
   [--member <USERNAME>]
   [--group <GROUP>]
   [--visible-to-all]
@@ -53,6 +53,13 @@
 --member::
 	User name to become initial member of the group.  Multiple --member
 	options may be specified to add more initial members.
++
+Trying to add a user that doesn't have an account in Gerrit fails,
+unless LDAP is used for authentication. If LDAP is used for
+authentication and the user is not found, Gerrit tries to authenticate
+the user against the LDAP backend. If the authentication is successful
+a user account is automatically created, so that the user can be added
+to the group.
 
 --group::
 	Group name to include in the group.  Multiple --group options may
diff --git a/Documentation/cmd-create-project.txt b/Documentation/cmd-create-project.txt
index f22141c..d0e56fd 100644
--- a/Documentation/cmd-create-project.txt
+++ b/Documentation/cmd-create-project.txt
@@ -14,12 +14,12 @@
   [--suggest-parents | -S ]
   [--permissions-only]
   [--description <DESC> | -d <DESC>]
-  [--submit-type <TYPE> |  -t <TYPE>]
+  [--submit-type <TYPE> | -t <TYPE>]
   [--use-contributor-agreements | --ca]
   [--use-signed-off-by | --so]
   [--use-content-merge]
   [--require-change-id | --id]
-  [--branch <REF> | -b <REF>]
+  [[--branch <REF> | -b <REF>] ...]
   [--empty-commit]
   { <NAME> | --name <NAME> }
 
@@ -59,8 +59,11 @@
 
 --branch::
 -b::
-	Name of the initial branch in the newly created project.
-	Defaults to 'master'.
+	Name of the initial branch(es) in the newly created project.
+	Several branches can be specified on the command line.
+	If several branches are specified then the first one becomes HEAD
+	of the project. If none branches are specified then default value
+	('master') is used.
 
 --owner::
 -o::
@@ -163,7 +166,8 @@
 
 REPLICATION
 -----------
-The remote repository creation is performed by a Bourne shell script:
+If the replication plugin is installed, the plugin will attempt to
+perform remote repository creation by a Bourne shell script:
 
 ====
   mkdir -p '/base/project.git' && cd '/base/project.git' && git init --bare && git update-ref HEAD refs/heads/master
@@ -174,10 +178,13 @@
 environment variable.  Administrators could also run this command line
 by hand to establish a new empty repository.
 
+A custom extension or plugin may also be developed to implement the
+NewProjectCreatedListener extension point and handle custom logic
+for remote repository creation.
+
 SEE ALSO
 --------
 
-* link:config-replication.html[Git Replication/Mirroring]
 * link:project-setup.html[Project Setup]
 
 GERRIT
diff --git a/Documentation/cmd-hook-commit-msg.txt b/Documentation/cmd-hook-commit-msg.txt
index 6318bba..bd602c1 100644
--- a/Documentation/cmd-hook-commit-msg.txt
+++ b/Documentation/cmd-hook-commit-msg.txt
@@ -54,7 +54,7 @@
 OBTAINING
 ---------
 To obtain the 'commit-msg' script use scp, wget or curl to download it
-to your local system from your gerrit server.
+to your local system from your Gerrit server.
 
 You can use either of the below commands:
 
@@ -73,6 +73,12 @@
   $ curl -o ~/duhproject/.git/hooks/commit-msg http://review.example.com/tools/hooks/commit-msg
 ====
 
+Make sure the hook file is executable:
+
+====
+  $ chmod u+x ~/duhproject/.git/hooks/commit-msg
+====
+
 SEE ALSO
 --------
 
diff --git a/Documentation/cmd-index.txt b/Documentation/cmd-index.txt
index b09c3b3..ccd9ffc 100644
--- a/Documentation/cmd-index.txt
+++ b/Documentation/cmd-index.txt
@@ -12,8 +12,8 @@
   $ scp -p -P 29418 john.doe@review.example.com:bin/gerrit-cherry-pick ~/bin/
   $ scp -p -P 29418 john.doe@review.example.com:hooks/commit-msg .git/hooks/
 
-  $ curl http://review.example.com/tools/bin/gerrit-cherry-pick
-  $ curl http://review.example.com/tools/hooks/commit-msg
+  $ curl -o ~/bin/gerrit-cherry-pick http://review.example.com/tools/bin/gerrit-cherry-pick
+  $ curl -o .git/hooks/commit-msg http://review.example.com/tools/hooks/commit-msg
 
 For more details on how to determine the correct SSH port number,
 see link:user-upload.html#test_ssh[Testing Your SSH Connection].
@@ -54,6 +54,9 @@
 'gerrit approve'::
 	'Deprecated alias for `gerrit review`.'
 
+link:cmd-ban-commit.html[gerrit ban-commit]::
+	Bans a commit from a project's repository.
+
 link:cmd-ls-groups.html[gerrit ls-groups]::
 	List groups visible to the caller.
 
@@ -93,21 +96,24 @@
 link:cmd-create-account.html[gerrit create-account]::
 	Create a new batch/role account.
 
+link:cmd-set-account.html[gerrit set-account]::
+	Change an account's settings.
+
 link:cmd-create-group.html[gerrit create-group]::
 	Create a new account group.
 
 link:cmd-create-project.html[gerrit create-project]::
 	Create a new project and associated Git repository.
 
+link:cmd-set-project.html[gerrit set-project]::
+    Change a project's settings.
+
 link:cmd-flush-caches.html[gerrit flush-caches]::
 	Flush some/all server caches from memory.
 
 link:cmd-gsql.html[gerrit gsql]::
 	Administrative interface to active database.
 
-link:cmd-replicate.html[gerrit replicate]::
-	Manually trigger replication, to recover a node.
-
 link:cmd-set-project-parent.html[gerrit set-project-parent]::
 	Change the project permissions are inherited from.
 
@@ -120,6 +126,30 @@
 link:cmd-show-queue.html[gerrit show-queue]::
 	Display the background work queues, including replication.
 
+link:cmd-plugin-install.html[gerrit plugin add]::
+    Alias for 'gerrit plugin install'.
+
+link:cmd-plugin-enable.html[gerrit plugin enable]::
+    Enable plugins.
+
+link:cmd-plugin-install.html[gerrit plugin install]::
+    Install/Add a plugin.
+
+link:cmd-plugin-ls.html[gerrit plugin ls]::
+    List the installed plugins.
+
+link:cmd-plugin-reload.html[gerrit plugin reload]::
+    Reload/Restart plugins.
+
+link:cmd-plugin-remove.html[gerrit plugin remove]::
+    Disable plugins.
+
+link:cmd-plugin-remove.html[gerrit plugin rm]::
+    Alias for 'gerrit plugin remove'.
+
+link:cmd-test-submit-rule.html[gerrit test-submit-rule]::
+	Test prolog submit rules.
+
 link:cmd-kill.html[kill]::
 	Kills a scheduled or running task.
 
diff --git a/Documentation/cmd-ls-groups.txt b/Documentation/cmd-ls-groups.txt
index 8564db2..306bb92 100644
--- a/Documentation/cmd-ls-groups.txt
+++ b/Documentation/cmd-ls-groups.txt
@@ -9,10 +9,11 @@
 --------
 [verse]
 'ssh' -p <port> <host> 'gerrit ls-groups'
-  [--project <NAME>]
-  [--user <NAME>]
+  [--project <NAME> | -p <NAME>]
+  [--user <NAME> | -u <NAME>]
   [--visible-to-all]
-  [--type {internal | ldap | system}]
+  [--type {internal | system}]
+  [--verbose | -v]
 
 DESCRIPTION
 -----------
@@ -30,6 +31,12 @@
 ---------
 This command is intended to be used in scripts.
 
+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.
+
 OPTIONS
 -------
 --project::
@@ -65,10 +72,19 @@
 +
 --
 `internal`:: Any group defined within Gerrit.
-`ldap`:: Any group defined by an external LDAP database.
 `system`:: Any system defined and managed group.
 --
 
+--verbose::
+-v::
+	Enable verbose output with tab-separated columns for the
+	group name, UUID, description, type (`SYSTEM` or `INTERNAL`),
+	owner group name, owner group UUID and whether the group is
+	visible to all (`true` or `false`).
++
+If a group has been "orphaned", i.e. its owner group UUID refers to a
+nonexistent group, the owner group name field will read `n/a`.
+
 EXAMPLES
 --------
 
@@ -91,6 +107,23 @@
 	Registered Users
 =====
 
+Extract the UUID of the 'Administrators' group:
+
+=====
+	$ ssh -p 29418 review.example.com gerrit ls-groups -v | awk '-F\t' '$1 == "Administrators" {print $2}'
+	ad463411db3eec4e1efb0d73f55183c1db2fd82a
+=====
+
+Extract and expand the multi-line description of the 'Administrators'
+group:
+
+=====
+	$ printf "$(ssh -p 29418 review.example.com gerrit ls-groups -v | awk '-F\t' '$1 == "Administrators" {print $3}')\n"
+	This is a
+	multi-line
+	description.
+=====
+
 GERRIT
 ------
 Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/cmd-ls-projects.txt b/Documentation/cmd-ls-projects.txt
index 7782aa8..d7d5aa5 100644
--- a/Documentation/cmd-ls-projects.txt
+++ b/Documentation/cmd-ls-projects.txt
@@ -10,8 +10,12 @@
 [verse]
 'ssh' -p <port> <host> 'gerrit ls-projects'
   [--show-branch <BRANCH> ...]
-  [--tree]
+  [--description | -d]
+  [--tree | -t]
   [--type {code | permissions | all}]
+  [--format {text | json | json_compact}]
+  [--all]
+  [--limit <N>]
 
 DESCRIPTION
 -----------
@@ -23,7 +27,7 @@
 
 ACCESS
 ------
-Any user who has configured an SSH key.
+Any user who has configured an SSH key, or by an user over HTTP.
 
 SCRIPTING
 ---------
@@ -42,12 +46,15 @@
 	whole project is not shown.
 
 --description::
---d::
+-d::
 	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::
@@ -64,6 +71,15 @@
 `all`:: Any type of project.
 --
 
+--format::
+	What output format to display the results in.
++
+--
+`text`:: Simple text based format.
+`json`:: Map of JSON objects describing each project.
+`json_compact`:: Minimized JSON output.
+--
+
 --all::
 	Display all projects that are accessible by the calling user
 	account. Besides the projects that the calling user account has
@@ -72,12 +88,43 @@
 	the 'READ' access right is not assigned to the calling user
 	account).
 
+--limit::
+	Cap the number of results to the first N matches.
+
+HTTP
+----
+This command is also available over HTTP, as `/projects/` for
+anonymous access and `/a/projects/` for authenticated access.
+Named options are available as query parameters. Results can
+be limited to projects matching a prefix by supplying the prefix
+as part of the URL, for example `/projects/external/` lists only
+projects whose name start with the string `external/`.
+
+Over HTTP the `json_compact` output format is assumed if the client
+explicitly asks for JSON using HTTP header `Accept: application/json`.
+When any JSON output format is used on HTTP, readers must skip the
+first line produced. The first line is a garbage JSON string crafted
+to prevent a browser from executing the response in a script tag.
+
+Output will be gzip compressed if `Accept-Encoding: gzip` was used
+by the client in the request headers.
+
 EXAMPLES
 --------
 
 List visible projects:
 =====
 	$ ssh -p 29418 review.example.com gerrit ls-projects
+	platform/manifest
+	tools/gerrit
+	tools/gwtorm
+
+	$ curl http://review.example.com/projects/
+	platform/manifest
+	tools/gerrit
+	tools/gwtorm
+
+	$ curl http://review.example.com/projects/tools/
 	tools/gerrit
 	tools/gwtorm
 =====
diff --git a/Documentation/cmd-plugin-enable.txt b/Documentation/cmd-plugin-enable.txt
new file mode 100644
index 0000000..da651ca
--- /dev/null
+++ b/Documentation/cmd-plugin-enable.txt
@@ -0,0 +1,44 @@
+plugin enable
+=============
+
+NAME
+----
+plugin enable - Enable plugins.
+
+SYNOPSIS
+--------
+[verse]
+'ssh' -p <port> <host> 'gerrit plugin enable'
+  <NAME> ...
+
+DESCRIPTION
+-----------
+Enable plugins currently disabled. The plugins will be enabled by renaming
+the plugin jars in the site path's `plugins` directory from
+`<plugin-jar-name>.disabled` to `<plugin-jar-name>`.
+
+ACCESS
+------
+Caller must be a member of the privileged 'Administrators' group.
+
+SCRIPTING
+---------
+This command is intended to be used in scripts.
+
+OPTIONS
+-------
+<NAME>::
+	Name of the plugin that should be enabled.  Multiple names of
+	plugins that should be enabled may be specified.
+
+EXAMPLES
+--------
+Enable a plugin:
+
+====
+	ssh -p 29418 localhost gerrit plugin enable my-plugin
+====
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/cmd-plugin-install.txt b/Documentation/cmd-plugin-install.txt
new file mode 100644
index 0000000..79d1f4a
--- /dev/null
+++ b/Documentation/cmd-plugin-install.txt
@@ -0,0 +1,71 @@
+plugin install
+==============
+
+NAME
+----
+plugin install - Install/Add a plugin.
+
+plugin add - Install/Add a plugin.
+
+SYNOPSIS
+--------
+[verse]
+'ssh' -p <port> <host> 'gerrit plugin install | add'
+  [--name <NAME> | -n <NAME>]
+  - | <URL> | <PATH>
+
+DESCRIPTION
+-----------
+Install/Add a plugin. The plugin will be copied into the site path's
+`plugins` directory.
+
+ACCESS
+------
+Caller must be a member of the privileged 'Administrators' group.
+
+SCRIPTING
+---------
+This command is intended to be used in scripts.
+
+OPTIONS
+-------
+-::
+	Plugin jar as piped input.
+
+<URL>::
+	URL from where the plugin should be downloaded. This can be an
+	HTTP or FTP site.
+
+<PATH>::
+	Absolute file path to the plugin jar.
+
+--name::
+-n::
+	The name under which the plugin should be installed.
+
+EXAMPLES
+--------
+Install a plugin from an absolute file path on the server's host:
+
+====
+	ssh -p 29418 localhost gerrit plugin install -n name \
+	  $(pwd)/my-plugin.jar
+====
+
+Install a plugin from an HTTP site:
+
+====
+	ssh -p 29418 localhost gerrit plugin install -n name \
+	  http://build-server/output/our-plugin.jar
+====
+
+Install a plugin from piped input:
+
+====
+	ssh -p 29418 localhost gerrit plugin install -n name \
+	  - <target/name-0.1.jar
+====
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/cmd-plugin-ls.txt b/Documentation/cmd-plugin-ls.txt
new file mode 100644
index 0000000..6cce83c
--- /dev/null
+++ b/Documentation/cmd-plugin-ls.txt
@@ -0,0 +1,44 @@
+plugin ls
+=========
+
+NAME
+----
+plugin ls - List the installed plugins.
+
+SYNOPSIS
+--------
+[verse]
+'ssh' -p <port> <host> 'gerrit plugin ls'
+  [--all | -a]
+  [--format {text | json | json_compact}]
+
+DESCRIPTION
+-----------
+List the installed plugins and show their version and status.
+
+ACCESS
+------
+Caller must be a member of the privileged 'Administrators' group.
+
+SCRIPTING
+---------
+This command is intended to be used in scripts.
+
+OPTIONS
+-------
+--all::
+-a::
+	List all plugins, including disabled plugins.
+
+--format::
+	What output format to display the results in.
++
+--
+`text`:: Simple text based format.
+`json`:: Map of JSON objects describing each project.
+`json_compact`:: Minimized JSON output.
+--
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/cmd-plugin-reload.txt b/Documentation/cmd-plugin-reload.txt
new file mode 100644
index 0000000..3932e30
--- /dev/null
+++ b/Documentation/cmd-plugin-reload.txt
@@ -0,0 +1,48 @@
+plugin reload
+=============
+
+NAME
+----
+plugin reload - Reload/Restart plugins.
+
+SYNOPSIS
+--------
+[verse]
+'ssh' -p <port> <host> 'gerrit plugin reload'
+  <NAME> ...
+
+DESCRIPTION
+-----------
+Reload/Restart plugins.
+
+Whether a plugin is reloaded or restarted is defined by the plugin's
+link:dev-plugins.html#reload_method[reload method].
+
+E.g. a plugin needs to be reloaded if its configuration is modified to
+make the new configuration data become active.
+
+ACCESS
+------
+Caller must be a member of the privileged 'Administrators' group.
+
+SCRIPTING
+---------
+This command is intended to be used in scripts.
+
+OPTIONS
+-------
+<NAME>::
+	Name of the plugin that should be reloaded.  Multiple names of
+	plugins that should be reloaded may be specified.
+
+EXAMPLES
+--------
+Reload a plugin:
+
+====
+	ssh -p 29418 localhost gerrit plugin reload my-plugin
+====
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/cmd-plugin-remove.txt b/Documentation/cmd-plugin-remove.txt
new file mode 100644
index 0000000..ab8f95b
--- /dev/null
+++ b/Documentation/cmd-plugin-remove.txt
@@ -0,0 +1,45 @@
+plugin remove
+=============
+
+NAME
+----
+plugin remove - Disable plugins.
+
+plugin rm - Disable plugins.
+
+SYNOPSIS
+--------
+[verse]
+'ssh' -p <port> <host> 'gerrit plugin remove | rm'
+  <NAME> ...
+
+DESCRIPTION
+-----------
+Disable plugins. The plugins will be disabled by renaming the plugin
+jars in the site path's `plugins` directory to `<plugin-jar-name>.disabled`.
+
+ACCESS
+------
+Caller must be a member of the privileged 'Administrators' group.
+
+SCRIPTING
+---------
+This command is intended to be used in scripts.
+
+OPTIONS
+-------
+<NAME>::
+	Name of the plugin that should be disabled.  Multiple names of
+	plugins that should be disabled may be specified.
+
+EXAMPLES
+--------
+Disable a plugin:
+
+====
+	ssh -p 29418 localhost gerrit plugin remove my-plugin
+====
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/cmd-query.txt b/Documentation/cmd-query.txt
index 253bed1..2feea11 100644
--- a/Documentation/cmd-query.txt
+++ b/Documentation/cmd-query.txt
@@ -16,6 +16,7 @@
   [--comments]
   [--commit-message]
   [--dependencies]
+  [--submit-records]
   [--]
   <query>
   [limit:<n>]
@@ -80,6 +81,11 @@
 	Show information about patch sets which depend on, or are needed by,
 	each patch set.
 
+--submit-records::
+	Show submit record information about the change, which
+	includes whether the change meets the criteria for submission
+	(including information for each review label).
+
 limit:<n>::
 	Maximum number of results to return.  This is actually a
 	query operator, and not a command line option.	If more
@@ -124,7 +130,7 @@
 ------
 The JSON messages consist of nested objects referencing the
 link:json.html#change[change],
-link:json.html#patchset[patchset],
+link:json.html#patchSet[patchset],
 link:json.html#[account]
 involved, and other attributes as appropriate.
 
diff --git a/Documentation/cmd-receive-pack.txt b/Documentation/cmd-receive-pack.txt
index 7e5ca09..68f686d 100644
--- a/Documentation/cmd-receive-pack.txt
+++ b/Documentation/cmd-receive-pack.txt
@@ -8,7 +8,10 @@
 SYNOPSIS
 --------
 [verse]
-'git receive-pack' [--reviewer <address>] [--cc <address>] <project>
+'git receive-pack'
+  [--reviewer <address> | --re <address>]
+  [--cc <address>]
+  <project>
 
 DESCRIPTION
 -----------
diff --git a/Documentation/cmd-replicate.txt b/Documentation/cmd-replicate.txt
deleted file mode 100644
index 7722027..0000000
--- a/Documentation/cmd-replicate.txt
+++ /dev/null
@@ -1,103 +0,0 @@
-gerrit replicate
-================
-
-NAME
-----
-gerrit replicate - Manually trigger replication, to recover a node
-
-SYNOPSIS
---------
-[verse]
-'ssh' -p <port> <host> 'gerrit replicate'
-  [--url <PATTERN>]
-  {--all | <PROJECT> ...}
-
-DESCRIPTION
------------
-Schedules replication of the specified projects to all configured
-replication destinations, or only those whose URLs match the pattern
-given on the command line.
-
-Normally Gerrit automatically schedules replication whenever it
-makes a change to a managed Git repository.  However, there are
-other reasons why an administrator may wish to trigger replication:
-
-* Destination disappears, then later comes back online.
-+
-If a destination went offline for a period of time, when it comes
-back, it may be missing commits that it should have.  Triggering a
-replication run for all projects against that URL will update it.
-
-* After repacking locally, and using `rsync` to distribute the new
-  pack files to the destinations.
-+
-If the local server is repacked, and then the resulting pack files
-are sent to remote peers using `rsync -a --delete-after`, there
-is a chance that the rsync missed a change that was added during
-the rsync data transfer, and the rsync will remove that changes's
-data from the remote, even though the automatic replication pushed
-it there in parallel to the rsync.
-+
-Its a good idea to run replicate with `--all` to ensure all
-projects are consistent after the rsync is complete.
-
-* After deleting a ref by hand.
-+
-If a ref must be removed (e.g. to purge a change or patch set
-that shouldn't have been created, and that must be eradicated)
-that delete must be done by direct git access on the local,
-managed repository.  Gerrit won't know about the delete, and is
-unable to replicate it automatically.  Triggering replication on
-just the affected project can update the mirrors.
-
-ACCESS
-------
-Caller must be a member of the privileged 'Administrators' group,
-or have been granted
-link:access-control.html#capability_startReplication[the 'Start Replication' global capability].
-
-SCRIPTING
----------
-This command is intended to be used in scripts.
-
-OPTIONS
--------
---all::
-	Schedule replicating for all projects.
-
---url <PATTERN>::
-	Replicate only to replication destinations whose URL
-	contains the substring <PATTERN>.  This can be useful to
-	replicate only to a previously down node, which has been
-	brought back online.
-
-EXAMPLES
---------
-Replicate every project, to every configured remote:
-
-====
-  $ ssh -p 29418 review.example.com gerrit replicate --all
-====
-
-Replicate only to `srv2` now that it is back online:
-
-====
-  $ ssh -p 29418 review.example.com gerrit replicate --url srv2 --all
-====
-
-Replicate only the `tools/gerrit` project, after deleting a ref
-locally by hand:
-
-====
-  $ git --git-dir=/home/git/tools/gerrit.git update-ref -d refs/changes/00/100/1
-  $ ssh -p 29418 review.example.com gerrit replicate tools/gerrit
-====
-
-SEE ALSO
---------
-
-* link:config-replication.html[Git Replication/Mirroring]
-
-GERRIT
-------
-Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/cmd-review.txt b/Documentation/cmd-review.txt
index ac613e5..513bc6e 100644
--- a/Documentation/cmd-review.txt
+++ b/Documentation/cmd-review.txt
@@ -9,10 +9,10 @@
 --------
 [verse]
 'ssh' -p <port> <host> 'gerrit review'
-  [--project <PROJECT>]
-  [--message <MESSAGE>]
+  [--project <PROJECT> | -p <PROJECT>]
+  [--message <MESSAGE> | -m <MESSAGE>]
   [--force-message]
-  [--submit]
+  [--submit | -s]
   [--abandon | --restore]
   [--publish]
   [--delete]
@@ -52,16 +52,19 @@
 
 --force-message::
 	Option which allows Gerrit to publish the --message, even
-	when the labels could not be applied due to change being
-	closed).
+	when the labels could not be applied due to the change being
+	closed.
 +
 Used by some scripts/CI-systems, where the results (or links
 to the result) are posted as a message after completion of a
 build (often together with a label-change, indicating the success
 of the build).
 +
-If the message is posted successfully, the cmd will return
+If the message is posted successfully, the command will return
 successfully, even if the label could not be changed.
++
+This option will not force the message to be posted if the command
+fails because the user is not permitted to change the label.
 
 --help::
 -h::
@@ -69,11 +72,11 @@
 	complete listing of supported approval categories and values.
 
 --abandon::
-	Abandon the specified patch set(s).
+	Abandon the specified change(s).
 	(option is mutually exclusive with --submit and --restore)
 
 --restore::
-	Restore the specified abandoned patch set(s).
+	Restore the specified abandoned change(s).
 	(option is mutually exclusive with --abandon)
 
 --submit::
diff --git a/Documentation/cmd-set-account.txt b/Documentation/cmd-set-account.txt
new file mode 100644
index 0000000..f9855cd
--- /dev/null
+++ b/Documentation/cmd-set-account.txt
@@ -0,0 +1,96 @@
+gerrit set-account
+==================
+
+NAME
+----
+gerrit set-account - Change an account's settings.
+
+SYNOPSIS
+--------
+[verse]
+set-account [--full-name <FULLNAME>] [--active|--inactive] \
+            [--add-email <EMAIL>] [--delete-email <EMAIL> | ALL] \
+            [--add-ssh-key - | <KEY>] \
+            [--delete-ssh-key - | <KEY> | ALL] \
+            [--http-password <PASSWORD>] <USER>
+
+DESCRIPTION
+-----------
+Modifies a given user's settings. This command can be useful to
+deactivate an account, set HTTP password, add/delete ssh keys without
+going through the UI.
+
+It also allows managing email addresses, which bypasses the
+verification step we force within the UI.
+
+ACCESS
+------
+Caller must be a member of the privileged 'Administrators' group.
+
+SCRIPTING
+---------
+This command is intended to be used in scripts.
+
+OPTIONS
+-------
+<USER>::
+    Required; Full name, email-address, SSH username or account id.
+
+--full-name::
+    Display name of the user account.
++
+Names containing spaces should be quoted in single quotes (').
+This most likely requires double quoting the value, for example
+`--full-name "'A description string'"`.
+
+--active::
+    Set the account state to be active.
+
+--inactive::
+    Set the account state to be inactive. This prevents the
+    user from logging in.
+
+--add-email::
+    Add another email to the user's account. This doesn't
+    trigger the mail validation and adds the email directly
+    to the user's account.
+    May be supplied more than once to add multiple emails to
+    an account in a single command execution.
+
+--delete-email::
+    Delete an email from this user's account if it exists.
+    If the email provided is 'ALL', all associated emails are
+    deleted from this account.
+    Maybe supplied more than once to remove multiple emails
+    from an account in a single command execution.
+
+--add-ssh-key::
+    Content of the public SSH key to add to the account's
+    keyring.  If `-` the key is read from stdin, rather than
+    from the command line.
+    May be supplied more than once to add multiple SSH keys
+    in a single command execution.
+
+--delete-ssh-key::
+    Content of the public SSH key to remove from the account's
+    keyring or the comment associated with this key.
+    If `-` the key is read from stdin, rather than from the
+    command line. If the key provided is 'ALL', all
+    associated SSH keys are removed from this account.
+    May be supplied more than once to delete multiple SSH
+    keys in a single command execution.
+
+--http-password::
+    Set the HTTP password for the user account.
+
+EXAMPLES
+--------
+Add an email and SSH key to `watcher`'s account:
+
+====
+    $ cat ~/.ssh/id_watcher.pub | ssh -p 29418 review.example.com gerrit set-account --add-ssh-key - --add-email mail@example.com watcher
+====
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/cmd-set-project.txt b/Documentation/cmd-set-project.txt
new file mode 100644
index 0000000..059f063
--- /dev/null
+++ b/Documentation/cmd-set-project.txt
@@ -0,0 +1,111 @@
+gerrit set-project
+==================
+
+NAME
+----
+gerrit set-project - Change a project's settings.
+
+SYNOPSIS
+--------
+[verse]
+'ssh' -p <port> <host> 'gerrit set-project'
+  [--description <DESC> | -d <DESC>]
+  [--submit-type <TYPE> | -t <TYPE>]
+  [--use|no-contributor-agreements | --ca|nca]
+  [--use|no-signed-off-by | --so|nso]
+  [--use|no-content-merge]
+  [--require|no-change-id | --id|nid]
+  [--project-state | --ps]
+  <NAME>
+
+DESCRIPTION
+-----------
+Modifies a given project's settings. This command can be useful to
+batch change projects.
+
+The command is argument-safe, that is, if no argument is given the
+previous settings are kept intact.
+
+ACCESS
+------
+Caller must be a member of the privileged 'Administrators' group.
+
+SCRIPTING
+---------
+This command is intended to be used in scripts.
+
+OPTIONS
+-------
+<NAME>::
+    Required; name of the project to edit.  If name ends
+    with `.git` the suffix will be automatically removed.
+
+--description::
+-d::
+    New description of the project.  If not specified,
+    the old description is kept.
++
+Description values containing spaces should be quoted in single quotes
+(').  This most likely requires double quoting the value, for example
+`--description "'A description string'"`.
+
+--submit-type::
+-t::
+    Action used by Gerrit to submit an approved change to its
+    destination branch.  Supported options are:
++
+* FAST_FORWARD_ONLY: produces a strictly linear history.
+* MERGE_IF_NECESSARY: create a merge commit when required.
+* MERGE_ALWAYS: always create a merge commit.
+* CHERRY_PICK: always cherry-pick the commit.
+
++
+For more details see
+link:project-setup.html#submit_type[Change Submit Actions].
+
+--use|no-content-merge::
+    If enabled, Gerrit will try to perform a 3-way merge of text
+    file content when a file has been modified by both the
+    destination branch and the change being submitted.  This
+    option only takes effect if submit type is not
+    FAST_FORWARD_ONLY.
+
+--use|no-contributor-agreements::
+--ca|nca::
+    If enabled, authors must complete a contributor agreement
+    on the site before pushing any commits or changes to this
+    project.
+
+--use|no-signed-off-by::
+--so|nso:
+    If enabled, each change must contain a Signed-off-by line
+    from either the author or the uploader in the commit message.
+
+--require|no-change-id::
+--id|nid::
+    Require a valid link:user-changeid.html[Change-Id] footer
+    in any commit uploaded for review. This does not apply to
+    commits pushed directly to a branch or tag.
+
+--project-state::
+--ps::
+    Set project's visibility.
++
+* ACTIVE: project is regular and is the default value.
+* READ_ONLY: users can see the project if read permission
+is granted, but all modification operations are disabled.
+* HIDDEN: the project is not visible for those who are not owners
+
+EXAMPLES
+--------
+Change project `example` to be hidden, require change id, don't use content merge
+and use 'merge if necessary' as merge strategy:
+
+====
+    $ ssh -p 29418 review.example.com gerrit set-project example --submit-type MERGE_IF_NECESSARY\
+    --require-change-id --no-content-merge --project-state HIDDEN
+====
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
\ No newline at end of file
diff --git a/Documentation/cmd-set-reviewers.txt b/Documentation/cmd-set-reviewers.txt
index 9e08e39..32fd35e 100644
--- a/Documentation/cmd-set-reviewers.txt
+++ b/Documentation/cmd-set-reviewers.txt
@@ -9,9 +9,9 @@
 --------
 [verse]
 'ssh' -p <port> <host> 'gerrit set-reviewers'
-  [--project <PROJECT>]
-  [--add REVIEWER ...]
-  [--remove REVIEWER ...]
+  [--project <PROJECT> | -p <PROJECT>]
+  [--add <REVIEWER> ... | -a <REVIEWER> ...]
+  [--remove <REVIEWER> ... | -r <REVIEWER> ...]
   [--]
   {COMMIT | CHANGE-ID}...
 
diff --git a/Documentation/cmd-show-connections.txt b/Documentation/cmd-show-connections.txt
index b5d41bd..8404a97 100644
--- a/Documentation/cmd-show-connections.txt
+++ b/Documentation/cmd-show-connections.txt
@@ -8,7 +8,7 @@
 SYNOPSIS
 --------
 [verse]
-'ssh' -p <port> <host> 'gerrit show-connections' [-n]
+'ssh' -p <port> <host> 'gerrit show-connections' [--numeric | -n]
 
 DESCRIPTION
 -----------
diff --git a/Documentation/cmd-stream-events.txt b/Documentation/cmd-stream-events.txt
index bf78051..a8cf3b0 100644
--- a/Documentation/cmd-stream-events.txt
+++ b/Documentation/cmd-stream-events.txt
@@ -41,9 +41,10 @@
 SCHEMA
 ------
 The JSON messages consist of nested objects referencing the *change*,
-*patchset*, *account* involved, and other attributes as appropriate.
+*patchSet*, *account* involved, and other attributes as appropriate.
 The currently supported message types are *patchset-created*,
-*comment-added*, *change-merged*, and *change-abandoned*.
+*draft-published*, *change-abandoned*, *change-restored*,
+*change-merged*, *comment-added* and *ref-updated*.
 
 Note that any field may be missing in the JSON messages, so consumers of
 this JSON stream should deal with that appropriately.
@@ -56,7 +57,17 @@
 
 change:: link:json.html#change[change attribute]
 
-patchset:: link:json.html#patchset[patchset attribute]
+patchSet:: link:json.html#patchSet[patchSet attribute]
+
+uploader:: link:json.html#account[account attribute]
+
+Draft Published
+^^^^^^^^^^^^^^^
+type:: "draft-published"
+
+change:: link:json.html#change[change attribute]
+
+patchset:: link:json.html#patchSet[patchset attribute]
 
 uploader:: link:json.html#account[account attribute]
 
@@ -66,27 +77,31 @@
 
 change:: link:json.html#change[change attribute]
 
-patchset:: link:json.html#patchset[patchset attribute]
+patchSet:: link:json.html#patchSet[patchSet attribute]
 
 abandoner:: link:json.html#account[account attribute]
 
+reason:: Reason for abandoning the change.
+
 Change Restored
-^^^^^^^^^^^^^^^^
+^^^^^^^^^^^^^^^
 type:: "change-restored"
 
 change:: link:json.html#change[change attribute]
 
-patchset:: link:json.html#patchset[patchset attribute]
+patchSet:: link:json.html#patchSet[patchSet attribute]
 
 restorer:: link:json.html#account[account attribute]
 
+reason:: Reason for restoring the change.
+
 Change Merged
 ^^^^^^^^^^^^^
 type:: "change-merged"
 
 change:: link:json.html#change[change attribute]
 
-patchset:: link:json.html#patchset[patchset attribute]
+patchSet:: link:json.html#patchSet[patchSet attribute]
 
 submitter:: link:json.html#account[account attribute]
 
@@ -96,10 +111,12 @@
 
 change:: link:json.html#change[change attribute]
 
-patchset:: link:json.html#patchset[patchset attribute]
+patchSet:: link:json.html#patchSet[patchSet attribute]
 
 author:: link:json.html#account[account attribute]
 
+approvals:: All link:json.html#approval[approval attributes] granted.
+
 comment:: Comment text author had written
 
 Ref Updated
@@ -108,7 +125,7 @@
 
 submitter:: link:json.html#account[account attribute]
 
-refUpdate:: link:json.html#refupdate[refupdate attribute]
+refUpdate:: link:json.html#refUpdate[refUpdate attribute]
 
 
 SEE ALSO
diff --git a/Documentation/cmd-test-submit-rule.txt b/Documentation/cmd-test-submit-rule.txt
new file mode 100644
index 0000000..5b70bd1
--- /dev/null
+++ b/Documentation/cmd-test-submit-rule.txt
@@ -0,0 +1,93 @@
+gerrit test-submit-rule
+=======================
+
+NAME
+----
+gerrit test-submit-rule - Test prolog submit rules with a chosen changeset.
+
+SYNOPSIS
+--------
+[verse]
+'ssh' -p <port> <host> 'gerrit test-submit-rule'
+  [-s]
+  [--no-filters]
+  [--format {TEXT | JSON}]
+  CHANGE
+
+DESCRIPTION
+-----------
+Provides a way to test prolog link:prolog-cookbook.html[submit rules].
+
+OPTIONS
+-------
+-s::
+	Reads a rules.pl file from stdin instead of rules.pl in refs/meta/config.
+
+--no-filters::
+	Don't run the submit_filter/2 from the parent projects of the specified change.
+
+--format::
+  What output format to display the results in.
++
+--
+`text`:: Simple text based format.
+`json`:: A JSON object described in link:json.html#submitRecord[submit record].
+`json_compact`:: Minimized JSON output.
+--
+
+ACCESS
+------
+Can be used by anyone that has permission to read the specified changeset.
+
+EXAMPLES
+--------
+
+
+Test submit_rule from stdin.
+====
+ $ cat non-author-codereview.pl | ssh -p 29418 review.example.com gerrit test-submit-rule -s I78f2c6673db24e4e92ed32f604c960dc952437d9
+ Non-Author-Code-Review: NOT_READY
+ Verified: NOT_READY
+ Code-Review: NOT_READY by Anonymous Coward <test@email.com>
+
+ NOT_READY
+====
+
+Test submit_rule from stdin and return the results as JSON.
+====
+ cat non-author-codereview.pl | ssh -p 29418 review.example.com gerrit test-submit-rule --format=JSON -s I78f2c6673db24e4e92ed32f604c960dc952437d9
+ {
+  "approvals": [
+    {
+      "type": "Verified",
+      "value": "NEED"
+    },
+    {
+      "type": "Code-Review",
+      "value": "OK",
+      "by": {
+        "email": "test@email.com",
+        "username": "test"
+      }
+    }
+  ],
+  "value": "NOT_READY"
+ }
+====
+
+Test the active submit_rule from the refs/meta/config branch, ignoring filters in the project parents.
+====
+ $ ssh -p 29418 review.example.com gerrit test-submit-rule I78f2c6673db24e4e92ed32f604c960dc952437d9 --no-filters
+ Verified: NOT_READY
+ Code-Review: NOT_READY by Anonymous Coward <test@email.com>
+
+ NOT_READY
+====
+
+SCRIPTING
+---------
+Can be used either interactively for testing new prolog submit rules, or from a script to check the submit status of a change.
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/config-contact.txt b/Documentation/config-contact.txt
index 5c633cf..4d8851f 100644
--- a/Documentation/config-contact.txt
+++ b/Documentation/config-contact.txt
@@ -48,7 +48,7 @@
 The actual values chosen don't matter later, and are only to help
 document the purpose of the key.
 
-Chose a fairly long expiration period, such as 20 years.  For most
+Choose a fairly long expiration period, such as 20 years.  For most
 Gerrit instances, contact data will be written once, and rarely,
 if ever, read back.
 
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 535aaa8..de2aa02 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -88,6 +88,12 @@
 provider chosen by the end-user.  For more information see
 http://openid.net/[openid.net].
 +
+* `OpenID_SSO`
++
+Supports OpenID from a single provider.  There is no registration
+link, and the "Sign In" link sends the user directly to the provider's
+SSO entry point.
++
 * `HTTP`
 +
 Gerrit relies upon data presented in the HTTP request.  This includes
@@ -107,7 +113,7 @@
 * `CLIENT_SSL_CERT_LDAP`
 +
 This authentication type is actually kind of SSO. Gerrit will configure
-Jetty's SSL channel to request client's SSL certificate. For this
+Jetty's SSL channel to request the client's SSL certificate. For this
 authentication to work a Gerrit administrator has to import the root
 certificate of the trust chain used to issue the client's certificate
 into the <review-site>/etc/keystore.
@@ -161,7 +167,7 @@
 +
 List of permitted OpenID providers.  A user may only authenticate
 with an OpenID that matches this list.  Only used if `auth.type`
-was set to OpenID (the default).
+is set to OpenID (the default).
 +
 Patterns may be either a
 link:http://download.oracle.com/javase/6/docs/api/java/util/regex/Pattern.html[standard
@@ -173,7 +179,7 @@
 
 [[auth.trustedOpenID]]auth.trustedOpenID::
 +
-List of trusted OpenID providers.  Only used if `auth.type` was
+List of trusted OpenID providers.  Only used if `auth.type` is
 set to OpenID (the default).
 +
 In order for a user to take advantage of permissions beyond those
@@ -229,10 +235,17 @@
 +
 Default is 12 hours.
 
+[[auth.openIdSsoUrl]]auth.openIdSsoUrl::
++
+The SSO entry point URL.  Only used if `auth.type` was set to
+OpenID_SSO.
++
+The "Sign In" link will send users directly to this URL.
+
 [[auth.httpHeader]]auth.httpHeader::
 +
 HTTP header to trust the username from, or unset to select HTTP basic
-or digest authentication.  Only used if `auth.type` was set to HTTP.
+or digest authentication.  Only used if `auth.type` is set to HTTP.
 
 [[auth.logoutUrl]]auth.logoutUrl::
 +
@@ -319,6 +332,18 @@
 +
 By default this is set to false.
 
+[[auth.gitBasicAuth]]auth.gitBasicAuth::
++
+If true then Git over HTTP and HTTP/S traffic is authenticated using
+standard BasicAuth and credentials validated using the same auth
+method configured for Gerrit Web UI.
++
+This parameter only affects git over http traffic. If set to false
+then Gerrit will authenticate through DIGEST authentication and
+the randomly generated HTTP password in Gerrit DB.
++
+By default this is set to false.
+
 [[auth.userNameToLowerCase]]auth.userNameToLowerCase::
 +
 If set the username that is received to authenticate a git operation
@@ -354,8 +379,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 +396,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 +404,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 +481,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 +492,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"`::
 +
@@ -504,6 +530,10 @@
 low maxAge setting, to ensure LDAP modifications are picked up in
 a timely fashion.
 
+cache `"ldap_groups_byinclude"`::
++
+Caches the hierarchical structure of LDAP groups.
+
 cache `"ldap_usernames"`::
 +
 Caches a mapping of LDAP username to Gerrit account identity.  The
@@ -512,11 +542,17 @@
 
 cache `"permission_sort"`::
 +
-Caches the order access control sections must be applied to a
+Caches the order in which access control sections must be applied to a
 reference.  Sorting the sections can be expensive when regular
 expressions are used, so this cache remembers the ordering for
 each branch.
 
+cache `"plugin_resources"`::
++
+Caches formatted plugin resources, such as plugin documentation that
+has been converted from Markdown to HTML. The memoryLimit refers to
+the bytes of memory dedicated to storing the documentation.
+
 cache `"projects"`::
 +
 Caches the project description records, from the `projects` table
@@ -550,8 +586,8 @@
 unable to persist the session information.  Enabling a disk cache
 is strongly recommended.
 +
-Session storage is relatively inexpensive, the average entry in
-this cache is approximately 248 bytes, depending on the JVM.
+Session storage is relatively inexpensive. The average entry in
+this cache is approximately 346 bytes.
 
 See also link:cmd-flush-caches.html[gerrit flush-caches].
 
@@ -563,7 +599,7 @@
 Number of idle worker threads to maintain for the intraline difference
 computations.  There is no upper bound on how many concurrent requests
 can occur at once, if additional threads are started to handle a peak
-load, only this many will remaining idle afterwards.
+load, only this many will remain idle afterwards.
 +
 Default is 1.5x number of available CPUs.
 
@@ -639,7 +675,7 @@
 to changes which reference it.  The second configuration 'bugzilla'
 will hyperlink terms such as 'bug 42' to an external bug tracker,
 supplying the argument record number '42' for display.  The third
-configuration 'tracker' uses raw HTML to more preciously control
+configuration 'tracker' uses raw HTML to more precisely control
 how the replacement is displayed to the user.
 
 ----
@@ -980,6 +1016,10 @@
 
 ----
 [download]
+  command = checkout
+  command = cherry_pick
+  command = pull
+  command = format_patch
   scheme = ssh
   scheme = http
   scheme = anon_http
@@ -989,6 +1029,34 @@
 
 The download section configures the allowed download methods.
 
+[[download.command]]download.command::
++
+Commands that should be offered to download changes.
++
+Multiple commands are supported:
++
+* `checkout`
++
+Command to fetch and checkout the patch set.
++
+* `cherry_pick`
++
+Command to fetch the patch set and to cherry-pick it onto the current
+commit.
++
+* `pull`
++
+Command to pull the patch set.
++
+* `format_patch`
++
+Command to fetch the patch set and to feed it into the `format-patch`
+command.
+
++
+If `download.command` is not specified, all download commands are
+offered.
+
 [[download.scheme]]download.scheme::
 +
 Schemes that should be used to download changes.
@@ -1021,7 +1089,7 @@
 not default, as not all instances will deploy repo.
 
 +
-If download.scheme is not specified, SSH, HTTP and Anonymous HTTP
+If `download.scheme` is not specified, SSH, HTTP and Anonymous HTTP
 downloads are allowed.
 
 [[gerrit]]Section gerrit
@@ -1078,10 +1146,12 @@
 by the system administrator, and might not even be running on the
 same host as Gerrit.
 
-[[gerrit.replicateOnStartup]]gerrit.replicateOnStartup::
+[[gerrit.reportBugUrl]]gerrit.reportBugUrl::
 +
-If true, replicates to all remotes on startup to ensure they are
-in-sync with this server.  By default, true.
+URL to direct users to when they need to report a bug about the
+Gerrit service. By default this links to the upstream Gerrit
+Code Review's own bug tracker but could be directed to the system
+administrator's ticket queue.
 
 [[gitweb]]Section gitweb
 ~~~~~~~~~~~~~~~~~~~~~~~~
@@ -1187,6 +1257,11 @@
 Optional filename for the patchset created hook, if not specified then
 `patchset-created` will be used.
 
+[[hooks.draftPublishedHook]]hooks.draftPublishedHook::
++
+Optional filename for the draft published hook, if not specified then
+`draft-published` will be used.
+
 [[hooks.commentAddedHook]]hooks.commentAddedHook::
 +
 Optional filename for the comment added hook, if not specified then
@@ -1202,6 +1277,21 @@
 Optional filename for the change abandoned hook, if not specified then
 `change-abandoned` will be used.
 
+[[hooks.changeRestoredHook]]hooks.changeRestoredHook::
++
+Optional filename for the change restored hook, if not specified then
+`change-restored` will be used.
+
+[[hooks.refUpdatedHook]]hooks.refUpdatedHook::
++
+Optional filename for the ref updated hook, if not specified then
+`ref-updated` will be used.
+
+[[hooks.claSignedHook]]hooks.claSignedHook::
++
+Optional filename for the CLA signed hook, if not specified then
+`cla-signed` will be used.
+
 [[http]]Section http
 ~~~~~~~~~~~~~~~~~~~~
 
@@ -1325,7 +1415,7 @@
 [[httpd.sslKeyPassword]]httpd.sslKeyPassword::
 +
 Password used to decrypt the private portion of the sslKeyStore.
-Java key stores require a password, even if the administrator
+Java keystores require a password, even if the administrator
 doesn't want to enable one.
 +
 If set to the empty string the embedded server will prompt for the
@@ -1345,7 +1435,7 @@
 [[httpd.acceptorThreads]]httpd.acceptorThreads::
 +
 Number of worker threads dedicated to accepting new incoming TCP
-connections and allocate them connection-specific resources.
+connections and allocating them connection-specific resources.
 +
 By default, 2, which should be suitable for most high-traffic sites.
 
@@ -1373,7 +1463,7 @@
 
 [[httpd.maxWait]]httpd.maxWait::
 +
-Maximum amount of time a client will wait to for an available
+Maximum amount of time a client will wait for an available
 thread to handle a project clone, fetch or push request over the
 smart HTTP transport.
 +
@@ -1398,7 +1488,7 @@
 [[ldap]]Section ldap
 ~~~~~~~~~~~~~~~~~~~~
 
-LDAP integration is only enabled if `auth.type` was set to
+LDAP integration is only enabled if `auth.type` is set to
 `HTTP_LDAP`, `LDAP` or `CLIENT_SSL_CERT_LDAP`.  See above for a
 detailed description of the auth.type settings and their
 implications.
@@ -1466,7 +1556,7 @@
 _(Optional)_ The read timeout for an LDAP operation. The value is
 in the usual time-unit format like "1 s", "100 ms", etc...
 A timeout can be used to avoid blocking all of the SSH command start
-threads in case when the LDAP server becomes slow.
+threads in case the LDAP server becomes slow.
 +
 By default there is no timeout and Gerrit will wait for the LDAP
 server to respond until the TCP connection times out.
@@ -1513,8 +1603,8 @@
 Typically this is the `displayName` property in LDAP, but could
 also be `legalName` or `cn`.
 +
-Attribute values may be concatenated with literal strings, for
-example to join given name and surname together use the pattern
+Attribute values may be concatenated with literal strings.  For
+example to join given name and surname together, use the pattern
 `${givenName} ${SN}`.
 +
 If set, users will be unable to modify their full name field, as
@@ -1535,7 +1625,7 @@
 `${sAMAccountName.toLowerCase}@example.com`.
 +
 If set, the preferred email address will be prefilled from LDAP,
-but users may still be able to register additional email address,
+but users may still be able to register additional email addresses,
 and select a different preferred email address.
 +
 Default is `mail`.
@@ -1625,7 +1715,7 @@
 +
 If set, it must be ensured that the local usernames for all existing
 accounts are converted to lower case, otherwise a user that has a
-local username that contains upper case characters cannot login
+local username that contains upper case characters will not be able to login
 anymore. The local usernames for the existing accounts can be
 converted to lower case by running the server program
 link:pgm-LocalUsernamesToLowerCase.html[LocalUsernamesToLowerCase].
@@ -1689,6 +1779,22 @@
 By default, 1.
 
 
+[[plugins]]Section plugins
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+[[plugins.checkFrequency]]plugins.checkFrequency::
++
+How often plugins should be examined for new plugins to load, removed
+plugins to be unloaded, or updated plugins to be reloaded.  Values can
+be specified using standard time unit abbreviations ('ms', 'sec',
+'min', etc.).
++
+If set to 0, automatic plugin reloading is disabled.  Administrators
+may force reloading with link:cmd-plugin.html[gerrit plugin reload].
++
+Default is 1 minute.
+
+
 [[receive]]Section receive
 ~~~~~~~~~~~~~~~~~~~~~~~~~~
 This section is used to set who can execute the 'receive-pack' and
@@ -1719,7 +1825,7 @@
 and the push operation will fail. If set to zero then there is no
 limit.
 +
-Gerrit administrator can use this setting to prevent developers
+Gerrit administrators can use this setting to prevent developers
 from pushing objects which are too large to Gerrit.
 +
 Default is zero.
@@ -1870,6 +1976,22 @@
 +
 By default, unset, permitting delivery to any email address.
 
+[[sendemail.includeDiff]]sendemail.includeDiff::
++
+If true, new change emails from Gerrit will include the complete
+unified diff of the change. Variable maxmimumDiffSize places an upper
+limit on how large the email can get when this option is enabled.
++
+By default, false.
+
+[[sendemail.maximumDiffSize]]sendemail.maximumDiffSize::
++
+Largest size of unified diff output to include in an email. When
+the diff exceeds this size the file paths will be listed instead.
+Standard byte unit suffixes are supported.
++
+By default, 256 KiB.
+
 [[sendemail.importance]]sendemail.importance::
 +
 If present, emails sent from Gerrit will have the given level
@@ -1907,6 +2029,45 @@
 updated versions. If false, a server restart is required to change
 any of these resources. Default is true, allowing automatic reloads.
 
+[[site.enableDeprecatedQuery]]site.enableDeprecatedQuery::
++
+If true the deprecated `/query` URL is available to return JSON
+and text results for changes. If false, the URL is disabled and
+returns 404 to clients. Default is true, enabling `/query`.
+
+[[site.upgradeSchemaOnStartup]]site.upgradeSchemaOnStartup::
++
+Control whether schema upgrade should be done on Gerrit startup. The following
+values are supported:
++
+* `OFF`
++
+No automatic schema upgrade on startup.
++
+* `AUTO`
++
+Perform schema migration on startup, if necessary.  If, as a result of
+schema migration, there would be any unused database objects they will
+be dropped automatically.
++
+* `AUTO_NO_PRUNE`
++
+Like `AUTO` but unused database objects will not be pruned.
+
++
+The default is `OFF`.
+
+[[ssh-alias]] Section ssh-alias
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Variables in section ssh-alias permit the site administrator to alias
+another command from Gerrit or a plugin into the `gerrit` command
+namespace. To alias `replication start` to `gerrit replicate`:
+
+----
+[ssh-alias]
+  replicate = replication start
+----
 
 [[sshd]] Section sshd
 ~~~~~~~~~~~~~~~~~~~~~
@@ -1969,7 +2130,7 @@
 +
 Number of threads to use when executing SSH command requests.
 If additional requests are received while all threads are busy they
-are queued and serviced in a first-come-first-serve order.
+are queued and serviced in a first-come-first-served order.
 +
 By default, 1.5x the number of CPUs available to the JVM.
 
@@ -2036,7 +2197,7 @@
 +
 Maximum number of concurrent SSH sessions that a user account
 may open at one time.  This is the number of distinct SSH logins
-the each user may have active at one time, and is not related to
+that each user may have active at one time, and is not related to
 the number of commands a user may issue over a single connection.
 If set to 0, there is no limit.
 +
@@ -2132,6 +2293,31 @@
 +
 By default a shade of yellow, `FFFFCC`.
 
+[[theme.changeTableOutdatedColor]]theme.changeTableOutdatedColor::
++
+Background color used for patch outdated messages.  The value must be
+a valid HTML hex color code, or standard color name.
++
+By default a shade of red, `F08080`.
+
+[[theme.tableOddRowColor]]theme.tableOddRowColor::
++
+Background color for tables such as lists of open reviews for odd
+rows.  This is so you can have a different color for odd and even
+rows of the table.  The value must be a valid HTML hex color code,
+or standard color name.
++
+By default transparent.
+
+[[theme.tableEvenRowColor]]theme.tableEvenRowColor::
++
+Background color for tables such as lists of open reviews for even
+rows.  This is so you can have a different color for odd and even
+rows of the table.  The value must be a valid HTML hex color code,
+or standard color name.
++
+By default transparent.
+
 A different theme may be used for signed-in vs. signed-out user status
 by using the "signed-in" and "signed-out" theme sections. Variables
 not specified in a section are inherited from the default theme.
@@ -2184,7 +2370,7 @@
 external tracking id part of the footer line. The match can
 result in several entries in the DB.  If grouping is used in the
 regex the first group will be interpreted as the tracking id.
-Tracking ids > 20 char will be ignored.
+Tracking ids longer than 20 characters will be ignored.
 +
 The configuration file parser eats one level of backslashes, so the
 character class `\s` requires `\\s` in the configuration file.  The
@@ -2193,7 +2379,7 @@
 
 [[trackingid.name.system]]trackingid.<name>.system::
 +
-The name of the external tracking system(max 10 char).
+The name of the external tracking system (maximum 10 characters).
 It is possible to have several trackingid entries for the same
 tracking system.
 
@@ -2276,6 +2462,7 @@
 ----
 [auth]
   registerEmailPrivateKey = 2zHNrXE2bsoylzUqDxZp0H1cqUmjgWb6
+  restTokenPrivateKey = 7e40PzCjlUKOnXATvcBNXH6oyiu+r0dFk2c=
 
 [database]
   username = webuser
@@ -2294,15 +2481,6 @@
   password = s3kr3t
 ----
 
-File `etc/replication.config`
------------------------------
-
-The optional file `'$site_path'/etc/replication.config` controls how
-Gerrit automatically replicates changes it makes to any of the Git
-repositories under its control.
-
-* link:config-replication.html[Git Replication/Mirroring]
-
 File `etc/peer_keys`
 --------------------
 
@@ -2338,7 +2516,6 @@
 Other files support site customization.
 +
 * link:config-headerfooter.html[Site Header/Footer]
-* link:config-replication.html[Git Replication/Mirroring]
 
 GERRIT
 ------
diff --git a/Documentation/config-gitweb.txt b/Documentation/config-gitweb.txt
index a08eb87..35d5c0d 100644
--- a/Documentation/config-gitweb.txt
+++ b/Documentation/config-gitweb.txt
@@ -27,7 +27,7 @@
 Alternatively, if Gerrit is served behind reverse proxy, it can
 generate different URLs for gitweb's links (they need to be
 rewritten to `<gerrit>/gitweb?args` on the web server). This allows
-for serving gitweb under different URL than the Gerrit instance.
+for serving gitweb under a different URL than the Gerrit instance.
 To enable this feature, set both: `gitweb.cgi` and `gitweb.url`.
 
 ====
diff --git a/Documentation/config-headerfooter.txt b/Documentation/config-headerfooter.txt
index c06080b..ae5d8f7 100644
--- a/Documentation/config-headerfooter.txt
+++ b/Documentation/config-headerfooter.txt
@@ -42,7 +42,7 @@
 or `GerritSite.css` by the relative URL `static/$name`
 (e.g. `static/logo.png`).
 
-To simplify security management, only files are served from
+To simplify security management, files are only served from
 `'$site_path'/static`.  Subdirectories are explicitly forbidden from
 being served from this location by enforcing the rule that file names
 cannot contain `/` or `\`.  (Client requests for `static/foo/bar`
diff --git a/Documentation/config-hooks.txt b/Documentation/config-hooks.txt
index ceb7c78..dfdba52 100644
--- a/Documentation/config-hooks.txt
+++ b/Documentation/config-hooks.txt
@@ -24,10 +24,19 @@
 ~~~~~~~~~~~~~~~~
 
 This is called whenever a patchset is created (this includes new
-changes)
+changes and drafts).
 
 ====
-  patchset-created --change <change id> --change-url <change url> --project <project name> --branch <branch> --uploader <uploader> --commit <sha1> --patchset <patchset id>
+  patchset-created --change <change id> --is-draft <boolean> --change-url <change url> --project <project name> --branch <branch> --topic <topic> --uploader <uploader> --commit <sha1> --patchset <patchset id>
+====
+
+draft-published
+~~~~~~~~~~~~~~~
+
+This is called whenever a draft change is published.
+
+====
+  draft-published --change <change id> --change-url <change url> --project <project name> --branch <branch> --topic <topic> --uploader <uploader> --commit <sha1> --patchset <patchset id>
 ====
 
 comment-added
@@ -36,7 +45,7 @@
 This is called whenever a comment is added to a change.
 
 ====
-  comment-added --change <change id> --change-url <change url> --project <project name> --branch <branch> --author <comment author> --commit <commit> --comment <comment> [--<approval category id> <score> --<approval category id> <score> ...]
+  comment-added --change <change id> --change-url <change url> --project <project name> --branch <branch> --topic <topic> --author <comment author> --commit <commit> --comment <comment> [--<approval category id> <score> --<approval category id> <score> ...]
 ====
 
 change-merged
@@ -45,7 +54,7 @@
 Called whenever a change has been merged.
 
 ====
-  change-merged --change <change id> --change-url <change url> --project <project name> --branch <branch> --submitter <submitter> --commit <sha1>
+  change-merged --change <change id> --change-url <change url> --project <project name> --branch <branch> --topic <topic> --submitter <submitter> --commit <sha1>
 ====
 
 change-abandoned
@@ -54,16 +63,16 @@
 Called whenever a change has been abandoned.
 
 ====
-  change-abandoned --change <change id> --change-url <change url> --project <project name> --branch <branch> --abandoner <abandoner> --reason <reason>
+  change-abandoned --change <change id> --change-url <change url> --project <project name> --branch <branch> --topic <topic> --abandoner <abandoner> --reason <reason>
 ====
 
 change-restored
-~~~~~~~~~~~~~~~~
+~~~~~~~~~~~~~~~
 
 Called whenever a change has been restored.
 
 ====
-  change-restored --change <change id> --change-url <change url> --project <project name> --branch <branch> --restorer <restorer> --reason <reason>
+  change-restored --change <change id> --change-url <change url> --project <project name> --branch <branch> --topic <topic> --restorer <restorer> --reason <reason>
 ====
 
 ref-updated
@@ -76,9 +85,9 @@
 ====
 
 cla-signed
-~~~~~~~~~~~
+~~~~~~~~~~
 
-Called whenever a user signs a contributor license agreement
+Called whenever a user signs a contributor license agreement.
 
 ====
   cla-signed --submitter <submitter> --user-id <user_id> --cla-id <cla_id>
@@ -88,13 +97,15 @@
 Configuration Settings
 ----------------------
 
-It is possible to change where gerrit looks for hooks, and what
-filenames it looks for by adding a [hooks] section to gerrit.config.
+It is possible to change where Gerrit looks for hooks, and what
+filenames it looks for, by adding a [hooks] section in gerrit.config.
 
-Gerrit will use the value of hooks.path for the hooks directory, and
-the values of hooks.patchsetCreatedHook, hooks.commentAddedHook,
-hooks.changeMergedHook and hooks.changeAbandonedHook for the
-filenames for the hooks.
+Gerrit will use the value of hooks.path for the hooks directory.
+
+For the hook filenames, Gerrit will use the values of hooks.patchsetCreatedHook,
+hooks.draftPublishedHook, hooks.commentAddedHook, hooks.changeMergedHook,
+hooks.changeAbandonedHook, hooks.changeRestoredHook, hooks.refUpdatedHook and
+hooks.claSignedHook.
 
 Missing Change URLs
 -------------------
diff --git a/Documentation/config-mail.txt b/Documentation/config-mail.txt
index 8aa7d08..ad0704f 100644
--- a/Documentation/config-mail.txt
+++ b/Documentation/config-mail.txt
@@ -20,7 +20,7 @@
 Supported Mail Templates:
 -------------------------
 
-Each mail that Gerrit sends out is controlled by at least one template, these
+Each mail that Gerrit sends out is controlled by at least one template.  These
 are listed below.  Change emails are influenced by two additional templates,
 one to set the subject line, and one to set the footer which gets appended to
 all the change emails (see `ChangeSubject.vm` and `ChangeFooter.vm` below.)
@@ -36,7 +36,7 @@
 ~~~~~~~~~~~~~~~
 
 The `ChangeFooter.vm` template will determine the contents of the footer
-text that will be appended to emails related to changes (all `ChangeEmails)`.
+text that will be appended to emails related to changes (all `ChangeEmail`s).
 
 ChangeSubject.vm
 ~~~~~~~~~~~~~~~~
@@ -49,6 +49,7 @@
 
 The `Comment.vm` template will determine the contents of the email related to
 a user submitting comments on changes.  It is a `ChangeEmail`: see
+`ChangeSubject.vm` and `ChangeFooter.vm`.
 
 Merged.vm
 ~~~~~~~~~
@@ -62,6 +63,7 @@
 
 The `MergeFail.vm` template will determine the contents of the email related
 to a failure upon attempting to merge a change to the head.  It is a
+`ChangeEmail`: see `ChangeSubject.vm` and `ChangeFooter.vm`.
 
 NewChange.vm
 ~~~~~~~~~~~~
@@ -70,6 +72,13 @@
 to a user submitting a new change for review. It is a `ChangeEmail`: see
 `ChangeSubject.vm` and `ChangeFooter.vm`.
 
+RebasedPatchSet.vm
+~~~~~~~~~~~~~~~~~~
+
+The `RebasedPatchSet.vm` template will determine the contents of the email
+related to a user rebasing a patchset for a change through the Gerrit UI.
+It is a `ChangeEmail`: see `ChangeSubject.vm` and `ChangeFooter.vm`.
+
 RegisterNewEmail.vm
 ~~~~~~~~~~~~~~~~~~~
 
@@ -90,6 +99,12 @@
 to a change being restored.  It is a `ChangeEmail`: see `ChangeSubject.vm` and
 `ChangeFooter.vm`.
 
+Reverted.vm
+~~~~~~~~~~~
+
+The `Reverted.vm` template will determine the contents of the email related
+to a change being reverted.  It is a `ChangeEmail`: see `ChangeSubject.vm` and
+`ChangeFooter.vm`.
 
 
 Mail Variables and Methods
@@ -107,7 +122,7 @@
 Warning
 ~~~~~~~
 
-Be aware that modifying templates can cause them to fail to parse and therefor
+Be aware that modifying templates can cause them to fail to parse and therefore
 not send out the actual email, or worse, calling methods on the available
 objects could have internal side effects which would adversely affect the
 health of your Gerrit server and/or data.
@@ -125,7 +140,7 @@
 
 $messageClass::
 +
-A String containing the messageClass
+A String containing the messageClass.
 
 $StringUtils::
 +
@@ -139,35 +154,35 @@
 
 $change::
 +
-A reference to the current `Change` object
+A reference to the current `Change` object.
 
 $changeId::
 +
-Id of the current change (a `Change.Key`)
+Id of the current change (a `Change.Key`).
 
 $coverLetter::
 +
-The text of the `ChangeMessage`
+The text of the `ChangeMessage`.
 
 $branch::
 +
-A reference to the branch of this change (a `Branch.NameKey`)
+A reference to the branch of this change (a `Branch.NameKey`).
 
 $fromName::
 +
-The name of the from user
+The name of the from user.
 
 $projectName::
 +
-The name of this change's project
+The name of this change's project.
 
 $patchSet::
 +
-A reference to the current `PatchSet`
+A reference to the current `PatchSet`.
 
 $patchSetInfo::
 +
-A reference to the current `PatchSetInfo`
+A reference to the current `PatchSetInfo`.
 
 
 See Also
diff --git a/Documentation/config-replication.txt b/Documentation/config-replication.txt
deleted file mode 100644
index 772282e..0000000
--- a/Documentation/config-replication.txt
+++ /dev/null
@@ -1,273 +0,0 @@
-Gerrit Code Review - Git Replication
-====================================
-
-Gerrit can automatically push any changes it makes to its managed Git
-repositories to another system.  Usually this would be configured to
-provide mirroring of changes, for warm-standby backups, or a
-load-balanced public mirror farm.
-
-The replication runs on a short delay.  This gives Gerrit a small
-time window to batch updates going to the same project, such as
-when a user uploads multiple changes at once.
-
-Typically replication should be done over SSH, with a passwordless
-public/private key pair.  On a trusted network it is also possible to
-use replication over the insecure (but much faster) git:// protocol,
-by enabling the `receive-pack` service on the receiving system, but
-this configuration is not recommended.  It is also possible to
-specify a local path as replication target. This makes e.g. sense if
-a network share is mounted to which the repositories should be
-replicated.
-
-Enabling Replication
---------------------
-
-If replicating over SSH (recommended), ensure the host key of the
-remote system(s) is already in the Gerrit user's `~/.ssh/known_hosts`
-file.  The easiest way to add the host key is to connect once by hand
-with the command line:
-
-====
-  sudo su -c 'ssh mirror1.us.some.org echo' gerrit2
-====
-
-Next, create `'$site_path'/etc/replication.config` as a Git-style
-config file, and restart Gerrit.
-
-Example `replication.config` to replicate in parallel to four
-different hosts:
-
-====
-  [remote "host-one"]
-    url = gerrit2@host-one.example.com:/some/path/${name}.git
-
-  [remote "pubmirror"]
-    url = mirror1.us.some.org:/pub/git/${name}.git
-    url = mirror2.us.some.org:/pub/git/${name}.git
-    url = mirror3.us.some.org:/pub/git/${name}.git
-    push = +refs/heads/*:refs/heads/*
-    push = +refs/tags/*:refs/tags/*
-    threads = 3
-    authGroup = Public Mirror Group
-    authGroup = Second Public Mirror Group
-====
-
-To manually trigger replication at runtime, see
-link:cmd-replicate.html[gerrit replicate].
-
-[[replication_config]]File `replication.config`
------------------------------------------------
-
-The optional file `'$site_path'/etc/replication.config` is a
-Git-style config file that controls the replication settings for
-Gerrit.
-
-The file is composed of one or more `remote` sections, each remote
-section provides common configuration settings for one or more
-destination URLs.
-
-Each remote section uses its own thread pool.  If pushing to
-multiple remotes, over differing types of network connections
-(e.g. LAN and also public Internet), its a good idea to put them
-into different remote sections, so that replication to the slower
-connection does not starve out the faster local one.  The example
-file above does this.
-
-[[remote]]Section remote
-~~~~~~~~~~~~~~~~~~~~~~~~
-
-In the keys below, the <name> portion is unused by Gerrit, but must be
-unique to distinguish the different sections if more than one remote
-section appears in the file.
-
-[[remote.name.url]]remote.<name>.url::
-+
-Address of the remote server to push to.  Multiple URLs may
-be specified within a single remote block, listing different
-destinations which share the same settings.  Assuming sufficient
-threads in the thread pool, Gerrit pushes to all URLs in parallel,
-using one thread per URL.
-+
-Within each URL value the magic placeholder `${name}` is replaced
-with the Gerrit project name.  This is a Gerrit specific extension
-to the otherwise standard Git URL syntax and it must be included
-in each URL so that Gerrit can figure out where each project needs
-to be replicated.
-+
-See link:http://www.kernel.org/pub/software/scm/git/docs/git-push.html#URLS[GIT URLS]
-for details on Git URL syntax.
-
-[[remote.name.url]]remote.<name>.adminUrl::
-+
-Address of the alternative remote server only for repository creation.  Multiple URLs may
-be specified within a single remote block, listing different
-destinations which share the same settings.
-+
-The adminUrl can be used as a ssh alternative to the url option, but only related to repository creation.
-If not specified, the repository creation tries to follow the default way through the url value specified.
-+
-It is useful when remote.<name>.url protocols does not allow repository creation
-although their usage are mandatory in the local environment.
-In that case, an alternative ssh url could be specified to repository creation.
-
-[[remote.name.receivepack]]remote.<name>.receivepack::
-+
-Path of the `git-receive-pack` executable on the remote system, if
-using the SSH transport.
-+
-Defaults to `git-receive-pack`.
-
-[[remote.name.uploadpack]]remote.<name>.uploadpack::
-+
-Path of the `git-upload-pack` executable on the remote system, if
-using the SSH transport.
-+
-Defaults to `git-upload-pack`.
-
-[[remote.name.push]]remote.<name>.push::
-+
-Standard Git refspec denoting what should be replicated.  Setting this
-to `+refs/heads/*:refs/heads/*` would mirror only the active
-branches, but not the change refs under `refs/changes/`, or the tags
-under `refs/tags/`.
-+
-Multiple push keys can be supplied, to specify multiple patterns
-to match against.  In the example file above, remote "pubmirror"
-uses two push keys to match both `refs/heads/*` and `refs/tags/*`,
-but excludes all others, including `refs/changes/*`.
-+
-Defaults to `+refs/*:refs/*` (all refs) if not specified.
-
-[[remote.name.timeout]]remote.<name>.timeout::
-+
-Number of seconds to wait for a network read or write to complete
-before giving up and declaring the remote side is not responding.
-If 0, there is no timeout, and the push client waits indefinitely.
-+
-A timeout should be large enough to mostly transfer the objects to
-the other side.  1 second may be too small for larger projects,
-especially over a WAN link, while 10-30 seconds is a much more
-reasonable timeout value.
-+
-Defaults to 0 seconds, wait indefinitely.
-
-[[remote.name.replicationDelay]]remote.<name>.replicationDelay::
-+
-Number of seconds to wait before scheduling a remote push operation.
-Setting the delay to 0 effectively disables the delay, causing the
-push to start as soon as possible.
-+
-This is a Gerrit specific extension to the Git remote block.
-+
-By default, 15 seconds.
-
-[[remote.name.replicationRetry]]remote.<name>.replicationRetry::
-+
-Number of minutes to wait before scheduling a remote push operation
-previously failed due to an offline remote server.
-+
-If a remote push operation fails because a remote server was
-offline, all push operations to the same destination URL are
-blocked, and the remote push is continuously retried.
-+
-This is a Gerrit specific extension to the Git remote block.
-+
-By default, 1 minute.
-
-[[remote.name.threads]]remote.<name>.threads::
-+
-Number of worker threads to dedicate to pushing to the repositories
-described by this remote.  Each thread can push one project at a
-time, to one destination URL.  Scheduling within the thread pool
-is done on a per-project basis.  If a remote block describes 4
-URLs, allocating 4 threads in the pool will permit some level of
-parallel pushing.
-+
-By default, 1 thread.
-
-[[remote.name.authGroup]]remote.<name>.authGroup::
-+
-Specifies the name of a group that the remote should use to access
-the repositories. Multiple authGroups may be specified within a
-single remote block to signify a wider access right. In the project
-administration web interface the read access can be specified for
-this group to control if a project should be replicated or not to the
-remote.
-+
-By default, replicates without group control, i.e replicates
-everything to all remotes.
-
-[[remote.name.replicatePermissions]]remote.<name>.replicatePermissions::
-+
-If true, permissions-only projects and the refs/meta/config branch
-will also be replicated to the remote site.  These projects and
-branches may be needed to keep a backup or slave server current.
-+
-By default, true, replicating everything.
-
-[[remote.name.mirror]]remote.<name>.mirror::
-+
-If true, replication will remove remote branches that absent locally
-or invisible to the replication (i.e. read access denied via 'authGroup'
-option).
-+
-By default, false, do not remove remote branches.
-
-
-[[secure_config]]File `secure.config`
------------------------------------------------
-
-The optional file `'$site_path'/secure.config` is a Git-style config
-file that provides secure values that should not be world-readable,
-such as passwords. Passwords for HTTP remotes can be obtained from
-this file.
-
-[[remote.name.username]]remote.<name>.username::
-+
-Username to use for HTTP authentication on this remote, if not given
-in the URL.
-
-[[remote.name.password]]remote.<name>.password::
-+
-Password to use for HTTP authentication on this remote.
-
-
-[[ssh_config]]File `~/.ssh/config`
-----------------------------------
-
-If present, Gerrit reads and caches `~/.ssh/config` at startup, and
-supports most SSH configuration options.  For example:
-
-====
-  Host host-one.example.com:
-    IdentityFile ~/.ssh/id_hostone
-    PreferredAuthentications publickey
-
-  Host mirror*.us.some.org:
-    User mirror-updater
-    IdentityFile ~/.ssh/id_pubmirror
-    PreferredAuthentications publickey
-====
-
-Supported options:
-
- * Host
- * Hostname
- * User
- * Port
- * IdentityFile
- * PreferredAuthentications
- * StrictHostKeyChecking
-
-SSH authentication must be by passwordless public key, as there is
-no facility to read passphases on startup or passwords during the
-SSH connection setup, and SSH agents are not supported from Java.
-
-Host keys for any destination SSH servers must appear in the user's
-`~/.ssh/known_hosts` file, and must be added in advance, before
-Gerrit starts.  If a host key is not listed, Gerrit will be unable to
-connect to that destination, and replication to that URL will fail.
-
-GERRIT
-------
-Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/config-reverseproxy.txt b/Documentation/config-reverseproxy.txt
index 4eecf67..7161c4a 100644
--- a/Documentation/config-reverseproxy.txt
+++ b/Documentation/config-reverseproxy.txt
@@ -5,7 +5,7 @@
 -----------
 
 Gerrit can be configured to run behind a third-party web server.
-This allows the other web server to bind to the privileged ports 80
+This allows the other web server to bind to the privileged port 80
 (or 443 for SSL), as well as offloads the SSL processing overhead
 from Java to optimized native C code.
 
diff --git a/Documentation/config-sso.txt b/Documentation/config-sso.txt
index 9aa06be..e915ffb 100644
--- a/Documentation/config-sso.txt
+++ b/Documentation/config-sso.txt
@@ -146,7 +146,7 @@
 The auth.type must always be HTTP, indicating the user identity
 will be obtained from the HTTP authorization data.
 
-The auth.httpHeader indicates which HTTP header field the Siteminder
+The auth.httpHeader indicates in which HTTP header field the Siteminder
 product has stored the username.  Usually this is "SM_USER", but
 may differ in your environment.  Please refer to your organization's
 single sign-on or security group to ensure the setting is correct.
diff --git a/Documentation/dev-contributing.txt b/Documentation/dev-contributing.txt
index 2609b05..065e9d1 100644
--- a/Documentation/dev-contributing.txt
+++ b/Documentation/dev-contributing.txt
@@ -150,7 +150,7 @@
     should be before the instance members.
   * Annotations should go before language keywords (final, private...) +
     Example: @Assisted @Nullable final type varName
-  * Imports should be mostly aphabetical (uppercase sorts before
+  * Imports should be mostly alphabetical (uppercase sorts before
     all lowercase, which means classes come before packages at the
     same level).
 
@@ -164,7 +164,7 @@
 Design
 ------
 
-Here are some design level ojectives that you should keep in mind
+Here are some design level objectives that you should keep in mind
 when coding:
 
   * ORM entity objects should match exactly one row in the database.
@@ -191,6 +191,7 @@
     on slow links.  If the action buttons are disabled, they cannot
     be resubmitted and the user can see that Gerrit is still busy.
   * GWT EventBus is the new way forward.
+  * ...and so is Guava (previously known as Google Collections).
 
 
 Tests
diff --git a/Documentation/dev-design.txt b/Documentation/dev-design.txt
index bf5ba73..ce2868c 100644
--- a/Documentation/dev-design.txt
+++ b/Documentation/dev-design.txt
@@ -37,7 +37,7 @@
 
 Git is a distributed version control system, wherein each repository
 is assumed to be owned/maintained by a single user.  There are no
-inherit security controls built into Git, so the ability to read
+inherent security controls built into Git, so the ability to read
 from or write to a repository is controlled entirely by the host's
 filesystem access controls.  When multiple maintainers collaborate
 on a single shared repository a high degree of trust is required,
@@ -87,7 +87,7 @@
 
 Each Git commit created on the client desktop system is converted
 into a unique change record which can be reviewed independently.
-Change records are stored in a database: PostgreSQL, MySql, or the
+Change records are stored in a database: PostgreSQL, MySQL, or the
 built-in H2, where they can be queried to present customized user
 dashboards, enumerating any pending changes.
 
@@ -191,7 +191,7 @@
 
 * link:http://code.google.com/p/gerrit/[Project Homepage]
 * link:http://code.google.com/p/gerrit/downloads/list[Release Versions]
-* link:http://code.google.com/p/gerrit/wiki/Source?tm=4[Source]
+* link:http://code.google.com/p/gerrit/source/checkout[Source]
 * link:http://code.google.com/p/gerrit/issues/list[Issue Tracking]
 * link:https://review.source.android.com/[Change Review]
 
@@ -669,17 +669,18 @@
 Backups
 ~~~~~~~
 
-PostgreSQL can be configured to save its write-ahead-log (WAL)
-and ship these logs to other systems, where they are applied to
-a warm-standby backup in real time.  Gerrit instances which care
-about reduduncy will setup this feature of PostgreSQL to ensure
-the warm-standby is reasonably current should the master go offline.
+PostgreSQL and MySQL can be configured to replicate their data to
+other systems, where they are applied to a warm-standby backup in
+real time.  Gerrit instances which care about reduduncy will setup
+this feature of PostgreSQL or MySQL to ensure the warm-standby is
+reasonably current should the master go offline.
 
-Gerrit can be configured to replicate changes made to the local
-Git repositories over any standard Git transports.  This can be
-configured in `'$site_path'/etc/replication.conf` to send copies
-of all changes over SSH to other servers, or to the Amazon S3 blob
-storage service.
+Using the standard replication plugin, Gerrit can be configured
+to replicate changes made to the local Git repositories over any
+standard Git transports. After the plugin is installed, remote
+destinations can be configured in `'$site_path'/etc/replication.conf`
+to send copies of all changes over SSH to other servers, or to the
+Amazon S3 blob storage service.
 
 
 Logging Plan
diff --git a/Documentation/dev-eclipse.txt b/Documentation/dev-eclipse.txt
index e239a63..b2bf011 100644
--- a/Documentation/dev-eclipse.txt
+++ b/Documentation/dev-eclipse.txt
@@ -70,12 +70,19 @@
 * Change Save as to be Local file.
 
 
+[[hosted-mode]]
 Running Hosted Mode
 ~~~~~~~~~~~~~~~~~~~
 
-Import the gerrit-gwtdebug project:
+To debug the GWT code executing in the web browser, three additional Git
+repositories need to be cloned.
 
-* Import gerrit-gwtdebug/pom.xml using General -> Maven Projects
+* https://gerrit.googlesource.com/gwtexpui
+* https://gerrit.googlesource.com/gwtjsonrpc
+* https://gerrit.googlesource.com/gwtorm
+
+In Eclipse, import the pom.xml file in the root directory of each of
+these cloned gits via General -> Maven Projects.
 
 Duplicate the existing `gwtui_dbg` launch configuration:
 
@@ -94,6 +101,22 @@
 * Change Save as to be Local file.
 
 
+[[known-problems]]
+Known problems
+--------------
+
+* When running Gerrit under the Eclipse debugger, code that attempts
+to load Prolog code may erroneously raise ClassNotFoundException,
+claiming that classes in the `Gerrit` package can't be found. The
+error can often be resolved by rebuilding Gerrit with `mvn package`
+and restarting the debug session.
+
+* OpenID authentication won't work in hosted mode, so you need to change
+the link:config-gerrit.html#auth.type[auth.type] configuration parameter
+to `DEVELOPMENT_BECOME_ANY_ACCOUNT` to disable OpenID and allow you to
+impersonate whatever account you otherwise would've used.
+
+
 GERRIT
 ------
 Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
new file mode 100644
index 0000000..dd0c44c
--- /dev/null
+++ b/Documentation/dev-plugins.txt
@@ -0,0 +1,420 @@
+Gerrit Code Review - Plugin Development
+=======================================
+
+The Gerrit server functionality can be extended by installing plugins.
+This page describes how plugins for Gerrit can be developed.
+
+Depending on how tightly the extension code is coupled with the Gerrit
+server code, there is a distinction between `plugins` and `extensions`.
+
+[[plugin]]
+A `plugin` in Gerrit is tightly coupled code that runs in the same
+JVM as Gerrit. It has full access to all server internals. Plugins
+are tightly coupled to a specific major.minor server version and
+may require source code changes to compile against a different
+server version.
+
+[[extension]]
+An `extension` in Gerrit runs inside of the same JVM as Gerrit
+in the same way as a plugin, but has limited visibility to the
+server's internals. The limited visibility reduces the extension's
+dependencies, enabling it to be compatible across a wider range
+of server versions.
+
+Most of this documentation refers to either type as a plugin.
+
+[[getting-started]]
+Getting started
+---------------
+
+To get started with the development of a plugin there are two
+recommended ways:
+
+. use the Gerrit Plugin Maven archetype to create a new plugin project:
++
+With the Gerrit Plugin Maven archetype you can create a skeleton for a
+plugin project.
++
+----
+mvn archetype:generate -DarchetypeGroupId=com.google.gerrit \
+    -DarchetypeArtifactId=gerrit-plugin-archetype \
+    -DarchetypeVersion=2.5-SNAPSHOT \
+    -DgroupId=com.google.gerrit \
+    -DartifactId=testPlugin
+----
++
+Maven will ask for additional properties and then create the plugin in
+the current directory. To change the default property values answer 'n'
+when Maven asks to confirm the properties configuration. It will then
+ask again for all properties including those with predefined default
+values.
+
+. clone the sample helloworld plugin:
++
+This is a Maven project that adds an SSH command to Gerrit to print
+out a hello world message. It can be taken as an example to develop
+an own plugin.
++
+----
+$ git clone https://gerrit.googlesource.com/plugins/helloworld
+----
++
+When starting from this example one should take care to adapt the
+`Gerrit-ApiVersion` in the `pom.xml` to the version of Gerrit for which
+the plugin is developed. If the plugin is developed for a released
+Gerrit version (no `SNAPSHOT` version) then the URL for the
+`gerrit-api-repository` in the `pom.xml` needs to be changed to
+`https://gerrit-api.commondatastorage.googleapis.com/release/`.
+
+[[API]]
+API
+---
+
+There are two different API formats offered against which plugins can
+be developed:
+
+gerrit-extension-api.jar::
+  A stable but thin interface. Suitable for extensions that need
+  to be notified of events, but do not require tight coupling to
+  the internals of Gerrit. Extensions built against this API can
+  expect to be binary compatible across a wide range of server
+  versions.
+
+gerrit-plugin-api.jar::
+  The complete internals of the Gerrit server, permitting a
+  plugin to tightly couple itself and provide additional
+  functionality that is not possible as an extension. Plugins
+  built against this API are expected to break at the source
+  code level between every major.minor Gerrit release. A plugin
+  that compiles against 2.5 will probably need source code level
+  changes to work with 2.6, 2.7, and so on.
+
+Manifest
+--------
+
+Plugins may provide optional description information with standard
+manifest fields:
+
+====
+  Implementation-Title: Example plugin showing examples
+  Implementation-Version: 1.0
+  Implementation-Vendor: Example, Inc.
+  Implementation-URL: http://example.com/opensource/plugin-foo/
+====
+
+ApiType
+~~~~~~~
+
+Plugins using the tightly coupled `gerrit-plugin-api.jar` must
+declare this API dependency in the manifest to gain access to server
+internals. If no `Gerrit-ApiType` is specified the stable `extension`
+API will be assumed. This may cause ClassNotFoundExceptions when
+loading a plugin that needs the plugin API.
+
+====
+  Gerrit-ApiType: plugin
+====
+
+Explicit Registration
+~~~~~~~~~~~~~~~~~~~~~
+
+Plugins that use explicit Guice registration must name the Guice
+modules in the manifest. Up to three modules can be named in the
+manifest. `Gerrit-Module` supplies bindings to the core server;
+`Gerrit-SshModule` supplies SSH commands to the SSH server (if
+enabled); `Gerrit-HttpModule` supplies servlets and filters to the HTTP
+server (if enabled). If no modules are named automatic registration
+will be performed by scanning all classes in the plugin JAR for
+`@Listen` and `@Export("")` annotations.
+
+====
+  Gerrit-Module:     tld.example.project.CoreModuleClassName
+  Gerrit-SshModule:  tld.example.project.SshModuleClassName
+  Gerrit-HttpModule: tld.example.project.HttpModuleClassName
+====
+
+[[reload_method]]
+Reload Method
+~~~~~~~~~~~~~
+
+If a plugin holds an exclusive resource that must be released before
+loading the plugin again (for example listening on a network port or
+acquiring a file lock) the manifest must declare `Gerrit-ReloadMode`
+to be `restart`. Otherwise the preferred method of `reload` will
+be used, as it enables the server to hot-patch an updated plugin
+with no down time.
+
+====
+  Gerrit-ReloadMode: restart
+====
+
+In either mode ('restart' or 'reload') any plugin or extension can
+be updated without restarting the Gerrit server. The difference is
+how Gerrit handles the upgrade:
+
+restart::
+  The old plugin is completely stopped. All registrations of SSH
+  commands and HTTP servlets are removed. All registrations of any
+  extension points are removed. All registered LifecycleListeners
+  have their `stop()` method invoked in reverse order. The new
+  plugin is started, and registrations are made from the new
+  plugin. There is a brief window where neither the old nor the
+  new plugin is connected to the server. This means SSH commands
+  and HTTP servlets will return not found errors, and the plugin
+  will not be notified of events that occurred during the restart.
+
+reload::
+  The new plugin is started. Its LifecycleListeners are permitted
+  to perform their `start()` methods. All SSH and HTTP registrations
+  are atomically swapped out from the old plugin to the new plugin,
+  ensuring the server never returns a not found error. All extension
+  point listeners are atomically swapped out from the old plugin to
+  the new plugin, ensuring no events are missed (however some events
+  may still route to the old plugin if the swap wasn't complete yet).
+  The old plugin is stopped.
+
+To reload/restart a plugin the link:cmd-plugin-reload.html[plugin reload]
+command can be used.
+
+[[classpath]]
+Classpath
+---------
+
+Each plugin is loaded into its own ClassLoader, isolating plugins
+from each other. A plugin or extension inherits the Java runtime
+and the Gerrit API chosen by `Gerrit-ApiType` (extension or plugin)
+from the hosting server.
+
+Plugins are loaded from a single JAR file. If a plugin needs
+additional libraries, it must include those dependencies within
+its own JAR. Plugins built using Maven may be able to use the
+link:http://maven.apache.org/plugins/maven-shade-plugin/[shade plugin]
+to package additional dependencies. Relocating (or renaming) classes
+should not be necessary due to the ClassLoader isolation.
+
+[[ssh]]
+SSH Commands
+------------
+
+Plugins may provide commands that can be accessed through the SSH
+interface (extensions do not have this option).
+
+Command implementations must extend the base class SshCommand:
+
+====
+  import com.google.gerrit.sshd.SshCommand;
+
+  class PrintHello extends SshCommand {
+    protected abstract void run() {
+      stdout.print("Hello\n");
+    }
+  }
+====
+
+If no Guice modules are declared in the manifest, SSH commands may
+use auto-registration by providing an `@Export` annotation:
+
+====
+  import com.google.gerrit.extensions.annotations.Export;
+  import com.google.gerrit.sshd.SshCommand;
+
+  @Export("print")
+  class PrintHello extends SshCommand {
+    protected abstract void run() {
+      stdout.print("Hello\n");
+    }
+  }
+====
+
+If explicit registration is being used, a Guice module must be
+supplied to register the SSH command and declared in the manifest
+with the `Gerrit-SshModule` attribute:
+
+====
+  import com.google.gerrit.sshd.PluginCommandModule;
+
+  class MyCommands extends PluginCommandModule {
+    protected void configureCommands() {
+      command("print").to(PrintHello.class);
+    }
+  }
+====
+
+For a plugin installed as name `helloworld`, the command implemented
+by PrintHello class will be available to users as:
+
+----
+$ ssh -p 29418 review.example.com helloworld print
+----
+
+[[http]]
+HTTP Servlets
+-------------
+
+Plugins or extensions may register additional HTTP servlets, and
+wrap them with HTTP filters.
+
+Servlets may use auto-registration to declare the URL they handle:
+
+====
+  import com.google.gerrit.extensions.annotations.Export;
+  import com.google.inject.Singleton;
+  import javax.servlet.http.HttpServlet;
+  import javax.servlet.http.HttpServletRequest;
+  import javax.servlet.http.HttpServletResponse;
+
+  @Export("/print")
+  @Singleton
+  class HelloServlet extends HttpServlet {
+    protected void doGet(HttpServletRequest req, HttpServletResponse res) throws IOException {
+      res.setContentType("text/plain");
+      res.setCharacterEncoding("UTF-8");
+      res.getWriter().write("Hello");
+    }
+  }
+====
+
+The auto registration only works for standard servlet mappings like
+`/foo` or `/foo/*`. Regex style bindings must use a Guice ServletModule
+to register the HTTP servlets and declare it explicitly in the manifest
+with the `Gerrit-HttpModule` attribute:
+
+====
+  import com.google.inject.servlet.ServletModule;
+
+  class MyWebUrls extends ServletModule {
+    protected void configureServlets() {
+      serve("/print").with(HelloServlet.class);
+    }
+  }
+====
+
+For a plugin installed as name `helloworld`, the servlet implemented
+by HelloServlet class will be available to users as:
+
+----
+$ curl http://review.example.com/plugins/helloworld/print
+----
+
+[[data-directory]]
+Data Directory
+--------------
+
+Plugins can request a data directory with a `@PluginData` File
+dependency. A data directory will be created automatically by the
+server in `$site_path/data/$plugin_name` and passed to the plugin.
+
+Plugins can use this to store any data they want.
+
+====
+  @Inject
+  MyType(@PluginData java.io.File myDir) {
+    new FileInputStream(new File(myDir, "my.config"));
+  }
+====
+
+[[documentation]]
+Documentation
+-------------
+
+If a plugin does not register a filter or servlet to handle URLs
+`/Documentation/*` or `/static/*`, the core Gerrit server will
+automatically export these resources over HTTP from the plugin JAR.
+
+Static resources under `static/` directory in the JAR will be
+available as `/plugins/helloworld/static/resource`.
+
+Documentation files under `Documentation/` directory in the JAR
+will be available as `/plugins/helloworld/Documentation/resource`.
+
+Documentation may be written in
+link:http://daringfireball.net/projects/markdown/[Markdown] style
+if the file name ends with `.md`. Gerrit will automatically convert
+Markdown to HTML if accessed with extension `.html`.
+
+[[macros]]
+Within the Markdown documentation files macros can be used that allow
+to write documentation with reasonably accurate examples that adjust
+automatically based on the installation.
+
+The following macros are supported:
+
+[width="40%",options="header"]
+|===================================================
+|Macro       | Replacement
+|@PLUGIN@    | name of the plugin
+|@URL@       | Gerrit Web URL
+|@SSH_HOST@  | SSH Host
+|@SSH_PORT@  | SSH Port
+|===================================================
+
+The macros will be replaced when the documentation files are rendered
+from Markdown to HTML.
+
+Macros that start with `\` such as `\@KEEP@` will render as `@KEEP@`
+even if there is an expansion for `KEEP` in the future.
+
+[[auto-index]]
+Automatic Index
+~~~~~~~~~~~~~~~
+
+If a plugin does not handle its `/` URL itself, Gerrit will
+redirect clients to the plugin's `/Documentation/index.html`.
+Requests for `/Documentation/` (bare directory) will also redirect
+to `/Documentation/index.html`.
+
+If neither resource `Documentation/index.html` or
+`Documentation/index.md` exists in the plugin JAR, Gerrit will
+automatically generate an index page for the plugin's documentation
+tree by scanning every `*.md` and `*.html` file in the Documentation/
+directory.
+
+For any discovered Markdown (`*.md`) file, Gerrit will parse the
+header of the file and extract the first level one title. This
+title text will be used as display text for a link to the HTML
+version of the page.
+
+For any discovered HTML (`*.html`) file, Gerrit will use the name
+of the file, minus the `*.html` extension, as the link text. Any
+hyphens in the file name will be replaced with spaces.
+
+If a discovered file name beings with `cmd-` it will be clustered
+into a 'Commands' section of the generated index page. All other
+files are clustered under a 'Documentation' section.
+
+Some optional information from the manifest is extracted and
+displayed as part of the index page, if present in the manifest:
+
+[width="40%",options="header"]
+|===================================================
+|Field       | Source Attribute
+|Name        | Implementation-Title
+|Vendor      | Implementation-Vendor
+|Version     | Implementation-Version
+|URL         | Implementation-URL
+|API Version | Gerrit-ApiVersion
+|===================================================
+
+[[deployment]]
+Deployment
+----------
+
+Compiled plugins and extensions can be deployed to a running Gerrit
+server using the link:cmd-plugin-install.html[plugin install] command.
+
+Plugins can also be copied directly into the server's
+directory at `$site_path/plugins/$name.jar`.  The name of
+the JAR file, minus the `.jar` extension, will be used as the
+plugin name. Unless disabled, servers periodically scan this
+directory for updated plugins. The time can be adjusted by
+link:config-gerrit.html#plugins.checkFrequency[plugins.checkFrequency].
+
+For disabling plugins the link:cmd-plugin-remove.html[plugin remove]
+command can be used.
+
+Disabled plugins can be re-enabled using the
+link:cmd-plugin-enable.html[plugin enable] command.
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/dev-release-subproject.txt b/Documentation/dev-release-subproject.txt
new file mode 100644
index 0000000..799ff2d
--- /dev/null
+++ b/Documentation/dev-release-subproject.txt
@@ -0,0 +1,99 @@
+Making a Release of a Gerrit Subproject / Core Plugin
+=====================================================
+
+Preparing a New Snapshot for Publishing
+---------------------------------------
+
+* You will need to have the following in the `pom.xml` to make it
+  deployable to the `gerrit-maven` storage bucket:
+
+----
+  <distributionManagement>
+    <repository>
+      <id>gerrit-maven</id>
+      <name>gerrit Maven Repository</name>
+      <url>s3://gerrit-maven@commondatastorage.googleapis.com</url>
+      <uniqueVersion>true</uniqueVersion>
+    </repository>
+  </distributionManagement>
+----
+
+
+* Add this to the `pom.xml` to enable the wagon provider:
+
+----
+  <build>
+    <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.
+  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"
+            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+            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</id>
+        <username>GOOG..EXAMPLE.....EXAMPLE</username>
+        <password>EXAMPLE..EXAMPLE..EXAMPLE</password>
+      </server>
+    </servers>
+  </settings>
+----
+
+
+Making a Snapshot
+-----------------
+
+* Only for plugins: in the `pom.xml` update the Gerrit version under
+`properties` > `Gerrit-ApiVersion` to the version of the new Gerrit
+release
+* First build and deploy the latest snapshot and ensure that Gerrit
+builds/runs with this snapshot
+
+* Deploy the snapshot:
+
+====
+  mvn deploy
+====
+
+
+Making a Release
+----------------
+
+* First deploy (and test) the latest snapshot for the subproject/plugin
+
+* Update the top level `pom.xml` in the subproject/plugin to reflect
+the new project version (the exact value of the tag you will create
+below)
+
+* Commit the pom change and push to the project's repo
+`refs/for/<master/stable>`
+
+* Tag the version you just pushed (and push the tag)
+
+====
+ git tag -a -m "prolog-cafe 1.3" v1.3
+ git push gerrit-review refs/tags/v1.3:refs/tags/v1.3
+====
+
+* Deploy the new release:
+
+====
+ mvn deploy
+====
+
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/dev-release.txt b/Documentation/dev-release.txt
new file mode 100644
index 0000000..5ea3042
--- /dev/null
+++ b/Documentation/dev-release.txt
@@ -0,0 +1,279 @@
+Making a Gerrit Release
+=======================
+
+[NOTE]
+========================================================================
+This document is meant primarily for Gerrit maintainers
+who have been given approval and submit status to the Gerrit
+projects.  Additionally, maintainers should be given owner
+status to the Gerrit web site.
+========================================================================
+
+To make a Gerrit release involves a great deal of complex
+tasks and it is easy to miss a step so this document should
+hopefuly serve as both a how to for those new to the process
+and as a checklist for those already familiar with these
+tasks.
+
+
+Gerrit Release Type
+-------------------
+
+Here are some guidelines on release approaches depending on the
+type of release you want to make (stable-fix, stable, RC0, RC1...).
+
+Stable
+~~~~~~
+
+A stable release is generally built from the master branch and may need to
+undergo some stabilization before releasing the final release.
+
+* Propose the release with any plans/objectives to the mailing list
+
+* Create a Gerrit RC0
+
+* If needed create a Gerrit RC1
+
+[NOTE]
+========================================================================
+You may let in a few features to this release
+========================================================================
+
+* If needed create a Gerrit RC2
+
+[NOTE]
+========================================================================
+There should be no new features in this release, only bug fixes
+========================================================================
+
+* Finally create the stable release (no RC)
+
+
+Stable-Fix
+~~~~~~~~~~
+
+Stable-fix releases should likely only contain bug fixes and doc updates.
+
+* Propose the release with any plans/objectives to the mailing list
+
+* This type of release does not need any RCs, release when the objectives
+  are met
+
+
+
+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
+~~~~~~~~~~~~~~~~~~~~~~~
+
+* Publish the latest snapshot for all subprojects
+* Freeze all subprojects and link:dev-release-subproject.html[publish]
+  them!
+
+
+Prepare Gerrit
+~~~~~~~~~~~~~~
+
+* Create a `stable-2.5` branch for making the new release
+
+* In the `master` branch: Update the poms for the Gerrit version, push for
+review, get merged
+
+====
+ tools/version.sh --snapshot=2.5
+====
+
+* Checkout the `stable-2.5` branch
+* Update the top level `pom.xml` in Gerrit to ensure that none of the
+Subprojects point to snapshot releases
+
+* Tag
+
+====
+ git tag -a -m "gerrit 2.5-rc0" v2.5-rc0
+ git tag -a -m "gerrit 2.5" v2.5
+====
+
+* Build (without plugins)
+
+====
+ ./tools/release.sh
+====
+
+[[plugin-api]]
+Publish the Plugin API JAR File
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+* Push JAR to `commondatastorage.googleapis.com`
+** Run `tools/deploy_api.sh`
+
+Prepare the Core Plugins
+~~~~~~~~~~~~~~~~~~~~~~~~
+* link:dev-release-subproject.html[Release and publish] the core plugins
+
+Package Gerrit with Plugins
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+* Ensure that the core plugins listed in `gerrit-package-plugins/pom.xml`
+point to the latest release version (no dependency to snapshot versions)
+* Include core plugins into WAR
+====
+ $ ./tools/version.sh --release && mvn clean package -f gerrit-package-plugins/pom.xml
+ $ ./tools/version.sh --reset
+====
+
+* Find WAR that includes the core plugins at
+`gerrit-package-plugins\target\gerrit-full-v2.5.war`
+* Sanity check WAR
+
+Publish to the Project Locations
+--------------------------------
+
+WAR File
+~~~~~~~~
+
+* Upload WAR to code.google.com/p/gerrit (manual web browser)
+** Go to http://code.google.com/p/gerrit/downloads/list
+** Use the "New Download" button
+
+* Update labels:
+** new war: [release-candidate], featured...
+** old war: deprecated
+
+Tag
+~~~
+
+* Push the New Tag
+
+====
+ git push gerrit-review refs/tags/v2.5-rc0:refs/tags/v2.5-rc0
+ git push gerrit-review refs/tags/v2.5:refs/tags/v2.5
+====
+
+
+Docs
+~~~~
+
+====
+ make -C Documentation PRIOR=2.4 update
+ make -C ReleaseNotes update
+====
+
+(no +PRIOR=+... if updating the same release again during RCs)
+
+* Update Google Code project links
+** Go to http://code.google.com/p/gerrit/admin
+** 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]
+========================================================================
+The docs makefile does an svn cp of the prior revision of the docs to branch
+the docs so you have less to upload on the new docs.
+
+User and password from here:
+
+    https://code.google.com/hosting/settings
+
+If subversion assumes a different username than your google one and asks for a
+password right away simply hit enter. Subversion will fail and then ask for
+another username and password. This time enter the username and password from
+the page linked above. After that subversion should save the username/password
+somewhere under `~/.subversion/auth` folder.
+========================================================================
+
+
+Issues
+~~~~~~
+
+====
+ How do the issues get updated?  Do you run a script to do
+ this?  When do you do it, after the final 2.2.2 is released?
+====
+
+By hand.
+
+Our current process is an issue should be updated to say Status =
+Submitted, FixedIn-2.2.2 once the change is submitted, but before the
+release.
+
+After the release is actually made, you can search in Google Code for
+``Status=Submitted FixedIn=2.2.2'' and then batch update these changes
+to say Status=Released. Make sure the pulldown says ``All Issues''
+because Status=Submitted is considered a closed issue.
+
+
+Mailing List
+~~~~~~~~~~~~
+
+* Send an email to the mailing list to announce the release, consider including some or all of the following in the email:
+** A link to the release and the release notes (if a final release)
+** A link to the docs
+** Describe the type of release (stable, bug fix, RC)
+
+----
+To: Repo and Gerrit Discussion <repo-discuss@googlegroups.com>
+Subject: Announce: Gerrit 2.2.2.1  (Stable bug fix update)
+
+I am pleased to announce Gerrit Code Review 2.2.2.1.
+
+Download:
+
+  http://code.google.com/p/gerrit/downloads/list
+
+
+This release is a stable bug fix release with some
+documentation updates including a new "Contributing to
+Gerrit" doc:
+
+  http://gerrit-documentation.googlecode.com/svn/Documentation/2.2.2/dev-contributing.html
+
+
+To read more about the bug fixes:
+
+  http://gerrit-documentation.googlecode.com/svn/ReleaseNotes/ReleaseNotes-2.2.2.1.html
+
+-Martin
+----
+
+* Add an entry to the NEWS section of the main Gerrit project web page
+** Go to: http://code.google.com/p/gerrit/admin
+** Add entry like:
+----
+ * Jun 14, 2012 - Gerrit 2.4.1 [https://groups.google.com/d/topic/repo-discuss/jHg43gixqzs/discussion Released]
+----
+
+* Update the new discussion group announcement to be sticky
+** Go to: http://groups.google.com/group/repo-discuss/topics
+** Click on the announcement thread
+** Near the top right, click on options
+** Under options, cick the "Display this top first" checkbox
+** and Save
+
+* Update the previous discussion group announcement to no longer be sticky
+** See above (unclick checkbox)
+
+
+Merging Stable Fixes to master
+------------------------------
+
+After every stable-fix release, stable should be merged to master to
+ensure that none of the fixes ever get lost.
+
+====
+ git config merge.summary true
+ git checkout master
+ git reset --hard origin/master
+ git branch -f stable origin/stable
+ git merge stable
+====
+
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/error-branch-not-found.txt b/Documentation/error-branch-not-found.txt
index e2dcff1..2aad0e1 100644
--- a/Documentation/error-branch-not-found.txt
+++ b/Documentation/error-branch-not-found.txt
@@ -7,8 +7,8 @@
 To push a change for code review the commit has to be pushed to the
 project's magical `refs/for/'branch'` ref (for details have a look at
 link:user-upload.html#push_create[Create Changes]).
-If you specify a non existing branch in the `refs/for/'branch'` ref
-the push is failing with the error message 'branch ... not found'.
+If you specify a non-existing branch in the `refs/for/'branch'` ref
+the push fails with the error message 'branch ... not found'.
 
 To fix this problem verify
 
diff --git a/Documentation/error-change-closed.txt b/Documentation/error-change-closed.txt
index 7170a65..3244fb3 100644
--- a/Documentation/error-change-closed.txt
+++ b/Documentation/error-change-closed.txt
@@ -1,8 +1,11 @@
 change ... closed
 =================
 
-With this error message Gerrit rejects to push a commit to a change
-that is already closed.
+With this error message Gerrit rejects to push a commit or submit a
+review label (approval) to a change that is already closed.
+
+When Pushing a Commit
+---------------------
 
 This error occurs if you are trying to push a commit that contains
 the Change-Id of a closed change in its commit message. A change can
@@ -14,7 +17,7 @@
 new change. To do this you have to remove the Change-Id from the
 commit message as explained link:error-push-fails-due-to-commit-message.html[here] and ideally generate a new Change-Id
 using the link:cmd-hook-commit-msg.html[commit hook] or EGit. Before pushing again it is also
-recommendable to do a link:http://www.kernel.org/pub/software/scm/git/docs/git-rebase.html[git rebase] to base your commit on the submitted
+recommended to do a link:http://www.kernel.org/pub/software/scm/git/docs/git-rebase.html[git rebase] to base your commit on the submitted
 change. Pushing again should now create a new change in Gerrit.
 
 If the change for which you wanted to upload a new patch set was
@@ -24,6 +27,14 @@
 'Restore Change' button). Afterwards the push should succeed and a
 new patch set for this change will be created.
 
+When Submitting a Review Label
+------------------------------
+
+This error occurs if you are trying to submit a review label (approval) using
+the link:cmd-review.html[ssh review command] after the change has been closed.
+A change can be closed because it was submitted and merged, because it was abandoned,
+or because the patchset to which you are submitting the review has been replaced
+by a newer patchset.
 
 GERRIT
 ------
diff --git a/Documentation/error-change-does-not-belong-to-project.txt b/Documentation/error-change-does-not-belong-to-project.txt
index 29957e1..e747881 100644
--- a/Documentation/error-change-does-not-belong-to-project.txt
+++ b/Documentation/error-change-does-not-belong-to-project.txt
@@ -7,7 +7,7 @@
 This error message means that the user explicitly pushed a commit to
 a change that belongs to another project by specifying it as target
 ref. This way of adding a new patch set to a change is deprecated as
-explained link:user-upload.html#manual_replacement_mapping[here]. It is recommended to only rely on Change-ID's for
+explained link:user-upload.html#manual_replacement_mapping[here]. It is recommended to only rely on Change-IDs for
 link:user-upload.html#push_replace[replacing changes].
 
 
diff --git a/Documentation/error-change-not-found.txt b/Documentation/error-change-not-found.txt
index c9ac0d8..b6df13b 100644
--- a/Documentation/error-change-not-found.txt
+++ b/Documentation/error-change-not-found.txt
@@ -7,7 +7,7 @@
 This error message means that the user explicitly pushed a commit to
 a non-existing change by specifying it as target ref. This way of
 adding a new patch set to a change is deprecated as explained link:user-upload.html#manual_replacement_mapping[here].
-It is recommended to only rely on Change-ID's for link:user-upload.html#push_replace[replacing changes].
+It is recommended to only rely on Change-IDs for link:user-upload.html#push_replace[replacing changes].
 
 
 GERRIT
diff --git a/Documentation/error-you-are-not-author.txt b/Documentation/error-invalid-author.txt
similarity index 88%
rename from Documentation/error-you-are-not-author.txt
rename to Documentation/error-invalid-author.txt
index a245252..c484776 100644
--- a/Documentation/error-you-are-not-author.txt
+++ b/Documentation/error-invalid-author.txt
@@ -1,10 +1,10 @@
-you are not author ...
-======================
+invalid author
+==============
 
-Gerrit verifies for every pushed commit that the e-mail address of
+For every pushed commit Gerrit verifies that the e-mail address of
 the author matches one of the registered e-mail addresses of the
 pushing user. If this is not the case pushing the commit fails with
-the error message "you are not author ...". This policy can be
+the error message "invalid author". This policy can be
 bypassed by having the access right
 link:access-control.html#category_forge_author['Forge Author'].
 
@@ -17,8 +17,8 @@
 Incorrect configuration of the e-mail address on client or server side
 ----------------------------------------------------------------------
 
-If pushing to Gerrit fails with the error message "you are not
-author ..." and you are the author of the commit for which the push
+If pushing to Gerrit fails with the error message "invalid author"
+and you are the author of the commit for which the push
 fails, then either you have not successfully registered this e-mail
 address for your Gerrit account or the author information of the
 pushed commit is incorrect.
@@ -27,7 +27,7 @@
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 Check in Gerrit under 'Settings -> Identities' which e-mail addresses
-you've configured for your Gerrit account, if no e-mail address is
+you've configured for your Gerrit account.  If no e-mail address is
 registered go to 'Settings -> Contact Information' and register a new
 e-mail address there. Make sure you confirm your e-mail address by
 clicking on the link in the e-mail verification mail sent by Gerrit.
@@ -92,7 +92,7 @@
 git rebase for the affected commits. While doing the interactive
 rebase you have to choose 'edit' for those commits for which the
 author should be rewritten. When the rebase stops at such a commit
-you have to amend the commit with explicitly setting the author
+you have to amend the commit, explicitly setting the author
 before continuing the rebase.
 
 Here is an example that shows how the interactive rebase is used to
@@ -131,8 +131,8 @@
 Missing privileges to push commits of other users
 -------------------------------------------------
 
-If pushing to Gerrit fails with the error message "you are not
-author ..." and somebody else is author of the commit for which the
+If pushing to Gerrit fails with the error message "invalid author"
+and somebody else is author of the commit for which the
 push fails, then you have no permissions to forge the author
 identity. In this case you may contact the project owner to request
 the access right '+1 Forge Author Identity' in the 'Forge Identity'
diff --git a/Documentation/error-you-are-not-committer.txt b/Documentation/error-invalid-committer.txt
similarity index 87%
rename from Documentation/error-you-are-not-committer.txt
rename to Documentation/error-invalid-committer.txt
index b5b8c44..447064e 100644
--- a/Documentation/error-you-are-not-committer.txt
+++ b/Documentation/error-invalid-committer.txt
@@ -1,10 +1,10 @@
-you are not committer ...
-=========================
+invalid committer
+=================
 
-Gerrit verifies for every pushed commit that the e-mail address of
+For every pushed commit Gerrit verifies that the e-mail address of
 the committer matches one of the registered e-mail addresses of the
 pushing user. If this is not the case pushing the commit fails with
-the error message "you are not committer ...". This policy can be
+the error message "invalid committer". This policy can be
 bypassed by having the access right
 link:access-control.html#category_forge_committer['Forge Committer'].
 
@@ -19,8 +19,8 @@
 Incorrect configuration of the e-mail address on client or server side
 ----------------------------------------------------------------------
 
-If pushing to Gerrit fails with the error message "you are not
-committer ..." and you committed the change for which the push fails,
+If pushing to Gerrit fails with the error message "invalid committer"
+and you committed the change for which the push fails,
 then either you have not successfully registered this e-mail address
 for your Gerrit account or the committer information of the pushed
 commit is incorrect.
@@ -29,7 +29,7 @@
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 Check in Gerrit under 'Settings -> Identities' which e-mail addresses
-you've configured for your Gerrit account, if no e-mail address is
+you've configured for your Gerrit account.  If no e-mail address is
 registered go to 'Settings -> Contact Information' and register a new
 e-mail address there. Make sure you confirm your e-mail address by
 clicking on the link in the e-mail verification mail sent by Gerrit.
@@ -96,8 +96,8 @@
 Missing privileges to push commits that were committed by other users
 ---------------------------------------------------------------------
 
-If pushing to Gerrit fails with the error message "you are not
-committer ..." and somebody else committed the change for which the
+If pushing to Gerrit fails with the error message "invalid committer"
+and somebody else committed the change for which the
 push fails, then you have no permissions to forge the committer
 identity. In this case you may contact the project owner to request
 the access right '+2 Forge Committer or Tagger Identity' in the
diff --git a/Documentation/error-messages.txt b/Documentation/error-messages.txt
index f915a14..c9df883 100644
--- a/Documentation/error-messages.txt
+++ b/Documentation/error-messages.txt
@@ -15,7 +15,9 @@
 * link:error-change-not-found.html[change ... not found]
 * link:error-contains-banned-commit.html[contains banned commit ...]
 * link:error-has-duplicates.html[... has duplicates]
+* link:error-invalid-author.html[invalid author]
 * link:error-invalid-changeid-line.html[invalid Change-Id line format in commit message]
+* link:error-invalid-committer.html[invalid committer]
 * link:error-missing-changeid.html[missing Change-Id in commit message]
 * link:error-multiple-changeid-lines.html[multiple Change-Id lines in commit message]
 * link:error-no-changes-made.html[no changes made]
@@ -33,8 +35,6 @@
 * link:error-squash-commits-first.html[squash commits first]
 * link:error-upload-denied.html[Upload denied for project \'...']
 * link:error-not-allowed-to-upload-merges.html[you are not allowed to upload merges]
-* link:error-you-are-not-author.html[you are not author ...]
-* link:error-you-are-not-committer.html[you are not committer ...]
 
 
 General Hints
diff --git a/Documentation/error-no-changes-made.txt b/Documentation/error-no-changes-made.txt
index 7ef7082..d0e1d4f 100644
--- a/Documentation/error-no-changes-made.txt
+++ b/Documentation/error-no-changes-made.txt
@@ -2,10 +2,10 @@
 ===============
 
 With this error message Gerrit rejects to push a commit as a new
-patch set for a change, if the pushed commit is identical with the
+patch set for a change, if the pushed commit is identical to the
 current patch set of this change.
 
-A pushed commit is considered to be identical with the current patch
+A pushed commit is considered to be identical to the current patch
 set if
 
 - the files in the commit,
diff --git a/Documentation/error-no-new-changes.txt b/Documentation/error-no-new-changes.txt
index 347c080..8e409ef 100644
--- a/Documentation/error-no-new-changes.txt
+++ b/Documentation/error-no-new-changes.txt
@@ -3,7 +3,7 @@
 
 With this error message Gerrit rejects to push a commit if the pushed
 commit was already successfully pushed to Gerrit. In this case there
-is no new change and consequently there is nothing to do for Gerrit.
+is no new change and consequently there is nothing for Gerrit to do.
 
 If your push is failing with this error message, you normally
 don't have to do anything since the commit was already successfully
diff --git a/Documentation/error-non-fast-forward.txt b/Documentation/error-non-fast-forward.txt
index 7dba51b..6604e10 100644
--- a/Documentation/error-non-fast-forward.txt
+++ b/Documentation/error-non-fast-forward.txt
@@ -1,15 +1,15 @@
 non-fast forward
 ================
 
-With this error message Git rejects a push if the remote branch can't
+With this error message Gerrit rejects a push if the remote branch can't
 be fast forwarded onto the pushed commit. This is the case if the
 pushed commit is not based on the current tip of the remote branch.
 
 If a non-fast forward update would be done, all commits from the
 remote branch that succeed the base commit of the pushed commit would
 be removed. This would be especially confusing for other users that
-have based their work on such a commit. Because of this Git is by
-default not allowing non-fast forward updates.
+have based their work on such a commit. Because of this Git by
+default does not allow non-fast forward updates.
 
 When working with Gerrit, this error can only occur if
 link:user-upload.html#bypass_review[code review is bypassed].
@@ -46,7 +46,7 @@
 the commit to the correct project.
 
 
-Although it is considered as bad practice, it is possible to allow
+Although it is considered bad practice, it is possible to allow
 non-fast forward updates with Git. For this the remote Git repository
 has to be configured to not deny non-fast forward updates (set the
 link:http://www.kernel.org/pub/software/scm/git/docs/git-config.html[Git configuration] parameter 'receive.denyNonFastForwards' to
diff --git a/Documentation/error-not-a-gerrit-administrator.txt b/Documentation/error-not-a-gerrit-administrator.txt
index 0468d83..b771af6 100644
--- a/Documentation/error-not-a-gerrit-administrator.txt
+++ b/Documentation/error-not-a-gerrit-administrator.txt
@@ -1,7 +1,7 @@
 Not a Gerrit administrator
 ==========================
 
-With this error message Gerrit rejects to execute a SSH command that
+With this error message Gerrit rejects to execute an SSH command that
 requires administrator privileges if the user is not a Gerrit
 administrator.
 
diff --git a/Documentation/error-not-a-gerrit-project.txt b/Documentation/error-not-a-gerrit-project.txt
index 368a102..dac98ae 100644
--- a/Documentation/error-not-a-gerrit-project.txt
+++ b/Documentation/error-not-a-gerrit-project.txt
@@ -18,7 +18,7 @@
   project is listed. If the project is not listed the project either
   does not exist or you don't have
   link:access-control.html#category_read['Read'] access for it. This
-  means if you certain that the project name is right you should
+  means if you are certain that the project name is right you should
   contact the Gerrit Administrator or project owner to request access
   to the project.
 
diff --git a/Documentation/error-not-allowed-to-upload-merges.txt b/Documentation/error-not-allowed-to-upload-merges.txt
index 981ba91c..515eef5 100644
--- a/Documentation/error-not-allowed-to-upload-merges.txt
+++ b/Documentation/error-not-allowed-to-upload-merges.txt
@@ -2,11 +2,11 @@
 ====================================
 
 With this error message Gerrit rejects to push a merge commit if the
-pushing user has no permissions to upload merge commits for the
+pushing user has no permission to upload merge commits for the
 project to which the push is done.
 
 If you need to upload merge commits, you can contact one of the
-project owners and request permissions to upload merge commits
+project owners and request permission to upload merge commits
 (access right link:access-control.html#category_push_merge['Push Merge Commit'])
 for this project.
 
diff --git a/Documentation/error-permission-denied.txt b/Documentation/error-permission-denied.txt
index 1cb5708..2ec0a3f 100644
--- a/Documentation/error-permission-denied.txt
+++ b/Documentation/error-permission-denied.txt
@@ -1,7 +1,7 @@
 Permission denied (publickey)
 =============================
 
-With this error message a SSH command to Gerrit is rejected if the
+With this error message an SSH command to Gerrit is rejected if the
 SSH authentication is not successful.
 
 The link:http://en.wikipedia.org/wiki/Secure_Shell[SSH] protocol uses link:http://en.wikipedia.org/wiki/Public-key_cryptography[Public-key Cryptography] for authentication.
diff --git a/Documentation/error-prohibited-by-gerrit.txt b/Documentation/error-prohibited-by-gerrit.txt
index 69f80c1..bad2b3c 100644
--- a/Documentation/error-prohibited-by-gerrit.txt
+++ b/Documentation/error-prohibited-by-gerrit.txt
@@ -17,10 +17,17 @@
 3. if you push an annotated tag without
    link:access-control.html#category_push_annotated['Push Annotated Tag']
    access right on 'refs/tags/*'
-4. if you push a lightweight tag without the access right link:access-control.html#category_create['Create
+4. if you push a signed tag without
+   link:access-control.html#category_push_signed['Push Signed Tag']
+   access right on 'refs/tags/*'
+5. if you push a lightweight tag without the access right link:access-control.html#category_create['Create
    Reference'] for the reference name 'refs/tags/*'
+6. if you push a tag with somebody else as tagger and you don't have the
+   link:access-control.html#category_forge_committer['Forge Committer']
+   access right for the reference name 'refs/tags/*'
+7. if you push to a project that is in state 'Read Only'
 
-For new users it happens often that they accidentally try to bypass
+For new users it often happens that they accidentally try to bypass
 code review. The push then fails with the error message 'prohibited
 by Gerrit' because the project didn't allow to bypass code review.
 Bypassing the code review is done by pushing directly to refs/heads/*
diff --git a/Documentation/error-push-fails-due-to-commit-message.txt b/Documentation/error-push-fails-due-to-commit-message.txt
index 01e0a8e..172d64f 100644
--- a/Documentation/error-push-fails-due-to-commit-message.txt
+++ b/Documentation/error-push-fails-due-to-commit-message.txt
@@ -3,7 +3,7 @@
 
 If Gerrit rejects pushing a commit it is often the case that there is
 an issue with the commit message of the pushed commit. In this case
-often the problem can be resolved by fixing the commit message.
+the problem can often be resolved by fixing the commit message.
 
 If the commit message of the last commit needs to be fixed you can
 simply amend the last commit (please find a detailed description in
diff --git a/Documentation/error-squash-commits-first.txt b/Documentation/error-squash-commits-first.txt
index 138ad98..2181c52 100644
--- a/Documentation/error-squash-commits-first.txt
+++ b/Documentation/error-squash-commits-first.txt
@@ -9,7 +9,7 @@
 prevents such dependencies between patch sets within the same change
 to keep the review process simple. Otherwise reviewers would not only
 have to review the latest patch set but also all the patch sets the
-latest one is depending on.
+latest one depends on.
 
 This error is quite common, it appears when a user tries to address
 review comments and creates a new commit instead of amending the
@@ -93,8 +93,8 @@
 ----
 
 If it was the intention to create a patch series with multiple
-changes to be reviewed each commit message should contain the
-Change-ID of the corresponding change in Gerrit, if a change in
+changes to be reviewed, each commit message should contain the
+Change-ID of the corresponding change in Gerrit.  If a change in
 Gerrit does not exist yet, the Change-ID should be generated (either
 by using a link:cmd-hook-commit-msg.html[commit hook] or by using EGit) or the Change-ID could be
 removed (not recommended since then amending this commit to create
diff --git a/Documentation/i18n-readme.txt b/Documentation/i18n-readme.txt
index 080ecb6..a84c3dc 100644
--- a/Documentation/i18n-readme.txt
+++ b/Documentation/i18n-readme.txt
@@ -1,7 +1,7 @@
 Gerrit Code Review - i18n
 =========================
 
-Aside from actually writing translations, there's some issues with
+Aside from actually writing translations, there are some issues with
 the way the code produces output.  Most of the UI should support
 right-to-left (RTL) languages.
 
diff --git a/Documentation/index.txt b/Documentation/index.txt
index 5143bf7..4c2335f 100644
--- a/Documentation/index.txt
+++ b/Documentation/index.txt
@@ -18,7 +18,13 @@
 * link:user-signedoffby.html[Signed-off-by Lines]
 * link:access-control.html[Access Controls]
 * link:error-messages.html[Error Messages]
+* link:rest-api.html[REST API]
+* link:user-custom-dashboards.html[Custom Dashboards]
+* link:user-notify.html[Subscribing to Email Notifications]
 * link:user-submodules.html[Subscribing to Git Submodules]
+* link:refs-notes-review.html[The `refs/notes/review` namespace]
+* link:prolog-cookbook.html[Prolog Cookbook]
+* link:prolog-change-facts.html[Prolog Facts for Gerrit Changes]
 
 Installation
 ------------
@@ -33,7 +39,6 @@
 
 * link:config-gerrit.html[System Settings]
 * link:config-contact.html[User Contact Information]
-* link:config-replication.html[Git Replication/Mirroring]
 * link:config-gitweb.html[Gitweb Integration]
 * link:config-headerfooter.html[Site Header/Footer]
 * link:config-sso.html[Single Sign-On Systems]
@@ -47,8 +52,11 @@
 * link:dev-readme.html[Developer Setup]
 * link:dev-eclipse.html[Eclipse Setup]
 * link:dev-contributing.html[Contributing to Gerrit]
+* link:dev-plugins.html[Developing Plugins]
 * link:dev-design.html[System Design]
 * link:i18n-readme.html[i18n Support]
+* link:dev-release.html[Developer Release]
+* link:dev-release-subproject.html[Developer Subproject Release]
 
 Resources
 ---------
@@ -56,5 +64,5 @@
 * link:http://code.google.com/p/gerrit/[Homepage]
 * link:http://code.google.com/p/gerrit/downloads/list[Downloads]
 * link:http://code.google.com/p/gerrit/issues/list[Issue Tracking]
-* link:http://code.google.com/p/gerrit/wiki/Source?tm=4[Source Code]
+* link:http://code.google.com/p/gerrit/source/checkout[Source Code]
 * link:http://code.google.com/p/gerrit/wiki/Background[A History of Gerrit Code Review]
diff --git a/Documentation/install-j2ee.txt b/Documentation/install-j2ee.txt
index 507d6c5..96814a0 100644
--- a/Documentation/install-j2ee.txt
+++ b/Documentation/install-j2ee.txt
@@ -44,7 +44,7 @@
 +
 If you enabled Bouncy Castle Crypto during 'init', copy the JAR
 from `'$site_path'/lib` into your servlet container's extensions
-directory so its available to Gerrit Code Review.
+directory so it's available to Gerrit Code Review.
 
 
 Jetty 7.x
diff --git a/Documentation/install-quick.txt b/Documentation/install-quick.txt
index 6bea7f8..c09c197 100644
--- a/Documentation/install-quick.txt
+++ b/Documentation/install-quick.txt
@@ -12,7 +12,7 @@
 flavors or BSD.
 
 It's also presumed that you have access to an OpenID enabled email address.
-Examples of OpenID enable email providers are gmail, yahoo and hotmail.
+Examples of OpenID enable email providers are Gmail, Yahoo! Mail and Hotmail.
 It's also possible to register a custom email address with OpenID, but that is
 outside the scope of this quick installation guide. For testing purposes one of
 the above providers should be fine. Please note that network access to the
@@ -42,7 +42,7 @@
 Create a user to host the Gerrit service
 ----------------------------------------
 
-We will run the service as a non privileged user on your system.
+We will run the service as a non-privileged user on your system.
 First create the user and then become the user:
 
 ----
@@ -50,7 +50,7 @@
   $ sudo su gerrit2
 ----
 
-If you don't have root privileges you could skip this step and run gerrit
+If you don't have root privileges you could skip this step and run Gerrit
 as your own user as well.
 
 
@@ -58,7 +58,7 @@
 Download Gerrit
 ---------------
 
-It's time to download the archive that contains the gerrit web and ssh service.
+It's time to download the archive that contains the Gerrit web and ssh service.
 
 You can choose from different versions to download from here:
 
@@ -87,14 +87,28 @@
 When the init is complete, you can review your settings in the
 file `'$site_path/etc/gerrit.config'`.
 
-An important setting will be the canonicalWebUrl which will
-be needed later to access gerrit's web interface.
+Note that initialization also starts the server.  If any settings changes are
+made, the server must be restarted before they will take effect.
 
 ----
-  gerrit2@host:~$ cat ~/gerrit_testsite/etc/gerrit.config | grep canonical
-  canonicalWebUrl = http://localhost:8080/
+  gerrit2@host:~$ ~/gerrit_testsite/bin/gerrit.sh restart
+  Stopping Gerrit Code Review: OK
+  Starting Gerrit Code Review: OK
   gerrit2@host:~$
 ----
+
+The server can be also stopped and started by passing the `stop` and `start`
+commands to gerrit.sh.
+
+----
+  gerrit2@host:~$ ~/gerrit_testsite/bin/gerrit.sh stop
+  Stopping Gerrit Code Review: OK
+  gerrit2@host:~$
+  gerrit2@host:~$ ~/gerrit_testsite/bin/gerrit.sh start
+  Starting Gerrit Code Review: OK
+  gerrit2@host:~$
+----
+
 [[usersetup]]
 The first user
 --------------
@@ -154,15 +168,32 @@
 Registering your key in Gerrit
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-Open a browser and enter the canonical url you got before when
-initializing Gerrit.
+Open a browser and enter the canonical url of your Gerrit server.  You can
+find the url in the settings file.
 
 ----
-  Canonical URL                [http://localhost:8080/]:
+  gerrit2@host:~$ git config -f ~/gerrit_testsite/etc/gerrit.config gerrit.canonicalWebUrl
+  http://localhost:8080/
+  gerrit2@host:~$
 ----
 
 Register a new account in Gerrit through the web interface with the
 email address of your choice.
+
+The default authentication type is OpenID.  If your Gerrit server is behind a
+proxy, and you are using an external OpenID provider, you will need to add the
+proxy settings in the configuration file.
+
+----
+  gerrit2@host:~$ git config -f ~/gerrit_testsite/etc/gerrit.config --add http.proxy http://proxy:8080
+  gerrit2@host:~$ git config -f ~/gerrit_testsite/etc/gerrit.config --add http.proxyUsername username
+  gerrit2@host:~$ git config -f ~/gerrit_testsite/etc/gerrit.config --add http.proxyPassword password
+----
+
+Refer to the Gerrit configuration guide for more detailed information about
+link:config-gerrit.html#auth[authentication] and
+link:config-gerrit.html#http.proxy[proxy] settings.
+
 The first user to sign-in and register an account will be
 automatically placed into the fully privileged Administrators group,
 permitting server management over the web and over SSH.  Subsequent
@@ -216,7 +247,7 @@
 Your base Gerrit server is now running and you have a user that's ready
 to interact with it.  You now have two options, either you create a new
 test project to work with or you already have a git with history that
-you would like to import into gerrit and try out code review on.
+you would like to import into Gerrit and try out code review on.
 
 New project from scratch
 ~~~~~~~~~~~~~~~~~~~~~~~~
@@ -231,14 +262,14 @@
   user@host:~$
 ----
 
-This will create a repository that you could clone to work with.
+This will create a repository that you can clone to work with.
 
 Already existing project
 ~~~~~~~~~~~~~~~~~~~~~~~~
 
 The other alternative is if you already have a git project that you
 want to try out Gerrit on.
-First you have to create the project, this is done via the SSH port:
+First you have to create the project.  This is done via the SSH port:
 
 ----
   user@host:~$ ssh -p 29418 user@localhost gerrit create-project --name demo-project
@@ -262,7 +293,7 @@
   user@host:~/my-project$
 ----
 
-This will create a repository that you could clone to work with.
+This will create a repository that you can clone to work with.
 
 
 My first change
@@ -294,7 +325,7 @@
 
 Usually when you push to a remote git, you push to the reference
 `'/refs/heads/branch'`, but when working with Gerrit you have to push to a
-virtual branch representing "code review before submittal to branch".
+virtual branch representing "code review before submission to branch".
 This virtual name space is known as /refs/for/<branch>
 
 ----
@@ -319,11 +350,11 @@
 ---------------------------
 
 This covers the scope of getting Gerrit started and your first change uploaded.
-It doesn't give any clue as to how the review workflow works, please find
+It doesn't give any clue as to how the review workflow works, please read
 link:http://source.android.com/submit-patches/workflow[Default Workflow] to
 learn more about the workflow of Gerrit.
 
-To read more on the installation of Gerrit please read link:install.html[the detailed
+To read more on the installation of Gerrit please see link:install.html[the detailed
 installation page].
 
 
diff --git a/Documentation/install.txt b/Documentation/install.txt
index b90bbce..9926b8f 100644
--- a/Documentation/install.txt
+++ b/Documentation/install.txt
@@ -3,7 +3,7 @@
 
 [[requirements]]
 Requirements
------------
+------------
 To run the Gerrit service, the following requirements must be met on
 the host:
 
@@ -199,11 +199,10 @@
 ------------------
 
 Gerrit Code Review supports some site-specific customization options.
-For more information, see the related topic in this manual:
+For more information, see the related topics in this manual:
 
 * link:config-reverseproxy.html[Reverse Proxy]
 * link:config-sso.html[Single Sign-On Systems]
-* link:config-replication.html[Git Replication/Mirroring]
 * link:config-headerfooter.html[Site Header/Footer]
 * link:config-gitweb.html[Gitweb Integration]
 * link:config-gerrit.html[Other System Settings]
@@ -222,6 +221,13 @@
 * http://www.kernel.org/pub/software/scm/git/docs/git-daemon.html[man git-daemon]
 
 
+[[plugins]]
+Plugins
+-------
+
+Place Gerrit plugins in the review_site/plugins directory to have them loaded on Gerrit startup.
+
+
 External Documentation Links
 ----------------------------
 
diff --git a/Documentation/intro-quick.txt b/Documentation/intro-quick.txt
index 3d5cbcb..25f5d5e 100644
--- a/Documentation/intro-quick.txt
+++ b/Documentation/intro-quick.txt
@@ -27,7 +27,7 @@
 simple for all committers on a project to ensure that changes are
 checked over before they're actually applied. Because of this Gerrit
 is equally useful where all users are trusted committers such as may
-the case with closed-source commercial development. Either way it's
+be the case with closed-source commercial development. Either way it's
 still desirable to have code reviewed to improve the quality and
 maintainability of the code. After all, if only one person has seen
 the code it may be a little difficult to maintain when that person
@@ -337,7 +337,7 @@
 Easy as that, we now have the change in our working copy to play with.
 You might be interested in what the numbers of the refspec mean.
 
-* The first *68* is the id if the change +mod 100+.  The only reason
+* The first *68* is the id of the change +mod 100+.  The only reason
 for this initial number is to reduce the number of files in any given
 directory within the git repository.
 * The second *68* is the full id of the change. You'll notice this in
@@ -379,7 +379,7 @@
 that can be done by different users, Submission is a third operation
 that can be limited down to another group of users.
 
-Activating the _Publish and Submit_ or _Submit Patch Set X_ button
+Clicking the _Publish and Submit_ or _Submit Patch Set X_ button
 will merge the change into the main part of the repository so that it
 becomes an accepted part of the project. After this anyone fetching
 the git repository will receive this change as a part of the master
diff --git a/Documentation/json.txt b/Documentation/json.txt
index b1dbc32..f0588ce 100644
--- a/Documentation/json.txt
+++ b/Documentation/json.txt
@@ -11,22 +11,22 @@
 ------
 The Gerrit change being reviewed, or that was already reviewed.
 
-project:: Project path in Gerrit
+project:: Project path in Gerrit.
 
-branch:: Branch name within project
+branch:: Branch name within project.
 
-topic:: Topic name specified by the uploader for this change series
+topic:: Topic name specified by the uploader for this change series.
 
 id:: Change identifier, as scraped out of the Change-Id field in
 the commit message, or as assigned by the server if it was missing.
 
-number:: Change number (deprecated)
+number:: Change number (deprecated).
 
-subject:: Description of change
+subject:: Description of change.
 
-owner:: Owner in <<account,account attribute>>
+owner:: Owner in <<account,account attribute>>.
 
-url:: Canonical URL to reach this change
+url:: Canonical URL to reach this change.
 
 commitMessage:: The full commit message for the change.
 
@@ -53,9 +53,12 @@
 message based on the server's
 link:config-gerrit.html#trackingid[trackingid] sections.
 
-currentPatchSet:: Current <<patchset,patchset attribute>>.
+currentPatchSet:: Current <<patchSet,patchSet attribute>>.
 
-patchSets:: All <<patchset,patchset attribute>> for this change.
+patchSets:: All <<patchSet,patchSet attribute>> for this change.
+
+submitRecords:: The <<submitRecord,submitRecord attribute>> contains
+information about whether this change has been or can be submitted.
 
 [[trackingid]]
 trackingid
@@ -76,8 +79,10 @@
 
 email:: User's preferred email address.
 
-[[patchset]]
-patchset
+username:: User's username, if configured.
+
+[[patchSet]]
+patchSet
 --------
 Refers to a specific patchset within a <<change,change>>.
 
@@ -109,8 +114,8 @@
 
 by:: Reviewer of the patch set in <<account,account attribute>>.
 
-[[refupdate]]
-refupdate
+[[refUpdate]]
+refUpdate
 --------
 Information about a ref that was updated.
 
@@ -118,10 +123,61 @@
 
 newRev:: The new value the ref was updated to.
 
-project:: Project path in Gerrit
+project:: Project path in Gerrit.
 
 refName:: Ref name within project.
 
+[[queryLimit]]
+queryLimit
+----------
+Information about the link:access-control.html#capability_queryLimit[queryLimit]
+of a user.
+
+min:: lower limit
+
+max:: upper limit
+
+[[submitRecord]]
+submitRecord
+------------
+Information about the submit status of a change.
+
+status:: Current submit status.
+
+  OK;; The change is ready for submission or already submitted.
+
+  NOT_READY;; The change is missing a required label.
+
+  RULE_ERROR;; An internal server error occurred preventing computation.
+
+labels:: This describes the state of each code review
+<<label,label attribute>>, unless the status is RULE_ERROR.
+
+[[label]]
+label
+-----
+Information about a code review label for a change.
+
+label:: The name of the label.
+
+status:: The status of the label.
+
+  OK;; This label provides what is necessary for submission.
+
+  REJECT;; This label prevents the change from being submitted.
+
+  NEED;; The label is required for submission, but has not
+  been satisfied.
+
+  MAY;; The label may be set, but it's neither necessary for
+  submission nor does it block submission if set.
+
+  IMPOSSIBLE;; 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.
+
+by:: The <<account,account>> that applied the label.
+
 SEE ALSO
 --------
 
diff --git a/Documentation/licenses.txt b/Documentation/licenses.txt
index 69018d8..7787fe8 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>>
@@ -27,13 +28,12 @@
 |Apache Commons Pool        | <<apache2,Apache License 2.0>>
 |Apache Log4J               | <<apache2,Apache License 2.0>>
 |Apache MINA                | <<apache2,Apache License 2.0>>
-|Apache Tomact Servlet API  | <<apache2,Apache License 2.0>>
+|Apache Tomcat Servlet API  | <<apache2,Apache License 2.0>>
 |Apache SSHD                | <<apache2,Apache License 2.0>>, see also <<sshd,NOTICE>>
 |Apache Velocity            | <<apache2,Apache License 2.0>>
 |Apache Xerces              | <<apache2,Apache License 2.0>>
 |OpenID4Java                | <<apache2,Apache License 2.0>>
 |Neko HTML                  | <<apache2,Apache License 2.0>>
-|Ehcache                    | <<apache2,Apache License 2.0>>
 |mime-util                  | <<apache2,Apache License 2.0>>
 |Jetty                      | <<apache2,Apache License 2.0>>, or link:http://www.eclipse.org/legal/epl-v10.html[EPL]
 |Prolog Cafe                | <<prolog_cafe,EPL or GPL>>
@@ -52,6 +52,7 @@
 |JSR 305                    | <<jsr305,New-Style BSD>>
 |dk.brics.automaton         | <<automaton,New-Style BSD>>
 |Java Concurrency in Practice Annotations | <<jcip,Create Commons Attribution License>>
+|pegdown                    | <<apache2,Apache License 2.0>>
 |======================================================================
 
 Cryptography Notice
diff --git a/Documentation/pgm-ExportReviewNotes.txt b/Documentation/pgm-ExportReviewNotes.txt
index 17cc862..1b00213 100644
--- a/Documentation/pgm-ExportReviewNotes.txt
+++ b/Documentation/pgm-ExportReviewNotes.txt
@@ -3,7 +3,7 @@
 
 NAME
 ----
-ExportReviewNotes - Export successful reviews to refs/notes/review
+ExportReviewNotes - Export successful reviews to link:refs-notes-review.html[refs/notes/review]
 
 SYNOPSIS
 --------
@@ -14,7 +14,7 @@
 -----------
 Scans every submitted change and creates an initial notes
 branch detailing the previous submission information for
-each merged changed.
+each merged change.
 
 This task can take quite some time, but can run in the background
 concurrently to the server if the database is MySQL or PostgreSQL.
diff --git a/Documentation/pgm-init.txt b/Documentation/pgm-init.txt
index c53c57d..57decdd 100644
--- a/Documentation/pgm-init.txt
+++ b/Documentation/pgm-init.txt
@@ -19,7 +19,7 @@
 for some basic setup prior to writing default configuration files
 into a newly created `$site_path`.
 
-If run an an existing `$site_path`, init will upgrade some resources
+If run in an existing `$site_path`, init will upgrade some resources
 as necessary.
 
 OPTIONS
@@ -28,6 +28,11 @@
 	Run in batch mode, skipping interactive prompts.  Reasonable
 	configuration defaults are chosen based on the whims of
 	the Gerrit developers.
++
+If during a schema migration unused objects (e.g. tables, columns)
+are detected they are *not* automatically dropped, but only a list of
+SQL statements to drop these objects is provided. To drop the unused
+objects these SQL statements have to be executed manually.
 
 \--no-auto-start::
 	Don't automatically start the daemon after initializing a
diff --git a/Documentation/prolog-change-facts.txt b/Documentation/prolog-change-facts.txt
new file mode 100644
index 0000000..d5f6174
--- /dev/null
+++ b/Documentation/prolog-change-facts.txt
@@ -0,0 +1,100 @@
+Prolog Facts for Gerrit Changes
+===============================
+
+Prior to invoking the `submit_rule(X)` query for a change, Gerrit initializes
+the Prolog engine with a set of facts (current data) about this change.
+The following table provides an overview of the provided facts.
+
+IMPORTANT: All the terms listed below are defined in the `gerrit` package. To use any
+of them we must use a qualified name like `gerrit:change_branch(X)`.
+
+.Prolog facts about the current change
+[grid="cols"]
+[options="header"]
+|=============================================================================
+|Fact                 |Example  |Description
+
+|`change_branch/1`    |`change_branch('refs/heads/master').`
+    |Destination Branch for the change as string atom
+
+|`change_owner/1`     |`change_owner(user(1000000)).`
+    |Owner of the change as `user(ID)` term. ID is the numeric account ID
+
+|`change_project/1`   |`change_project('full/project/name').`
+    |Name of the project as string atom
+
+|`change_topic/1`     |`change_topic('plugins').`
+    |Topic name as string atom
+
+|`commit_author/1`    |`commit_author(user(100000)).`
+    |Author of the commit as `user(ID)` term. ID is the numeric account ID
+
+|`commit_author/3`    |`commit_author(user(100000), 'John Doe', 'john.doe@example.com').`
+    |ID, full name and the email of the commit author.  The full name and the
+    email are string atoms
+
+|`commit_committer/1` |`commit_committer()`
+    |Committer of the commit as `user(ID)` term. ID is the numeric account ID
+
+|`commit_committer/3` |`commit_committer()`
+    |ID, full name and the email of the commit committer. The full name and the
+    email are string atoms
+
+.2+|`commit_label/2`  |`commit_label(label('Code-Review', 2), user(1000000)).`
+    .2+|Set of votes on the last patch-set
+
+                      |`commit_label(label('Verified', -1), user(1000001)).`
+
+|`commit_message/1`   |`commit_message('Fix bug X').`
+    |Commit message as string atom
+
+.4+|`current_user/1`  |`current_user(user(1000000)).`
+    .4+|Current user as one of the four given possibilities
+
+                      |`current_user(user(anonymous)).`
+                      |`current_user(user(peer_daemon)).`
+                      |`current_user(user(replication)).`
+|=============================================================================
+
+In addition Gerrit provides a set of built-in helper predicates that can be used
+when implementing the `submit_rule` predicate. The most common ones are listed in
+the following table.
+
+.Built-in Prolog helper predicates
+[grid="cols"]
+[options="header"]
+|=============================================================================
+|Predicate                  |Example usage  |Description
+
+|`commit_delta/1`           |`commit_delta('\\.java$').`
+    |True if any file name from the last patch set matches the given regex.
+
+|`commit_delta/3`           |`commit_delta('\\.java$', T, P)`
+    |Returns the change type (via `T`) and path (via `P`), if the change type
+    is `rename`, it also returns the old path. If the change type is `rename`, it
+    returns a delete for old path and an add for new path. If the change type
+    is `copy`, an add is returned along with new path.
+
+    Possible values for the change type are the following symbols: `add`,
+    `modify`, `delete`, `rename`, `copy`
+
+|`commit_delta/4`           |`commit_delta('\\.java$', T, P, O)`
+    |Like `commit_delta/3` plus the old path (via `O`) if applicable.
+
+|`commit_edits/2`           |`commit_edits('/pom.xml$', 'dependency')`
+    |True if any of the files matched by the file name regex (first parameter)
+    have edited lines that match the regex in the second parameter. This
+    example will be true if there is a modification of a `pom.xml` file such
+    that an edited line contains or contained the string `'dependency'`.
+
+|`commit_message_matches/1` |`commit_message_matches('^Bug fix')`
+    |True if the commit message matches the given regex.
+
+|=============================================================================
+
+NOTE: for a complete list of built-in helpers read the `gerrit_common.pl` and
+all Java classes whose name matches `PRED_*.java` from Gerrit's source code.
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/prolog-cookbook.txt b/Documentation/prolog-cookbook.txt
new file mode 100644
index 0000000..e660067
--- /dev/null
+++ b/Documentation/prolog-cookbook.txt
@@ -0,0 +1,750 @@
+Gerrit Code Review - Prolog Submit Rules Cookbook
+=================================================
+
+Submit Rule
+-----------
+A 'Submit Rule' in Gerrit is logic that defines when a change is submittable.
+By default, a change is submittable when it gets at least one
+highest vote in each voting category and has no lowest vote (aka veto vote) in
+any category.  Typically, this means that a change needs 'Code-Review+2',
+'Verified+1' and has neither 'Code-Review-2' nor 'Verified-1' to become
+submittable.
+
+While this rule is a good default, there are projects which need more
+flexibility for defining when a change is submittable.  In Gerrit, it is
+possible to use Prolog based rules to provide project specific submit rules and
+replace the default submit rules. Using Prolog based rules, project owners can
+define a set of criteria which must be fulfilled for a change to become
+submittable. For a change that is not submittable, the set of needed criteria
+is displayed in the Gerrit UI.
+
+NOTE: Loading and executing Prolog submit rules may be disabled by setting
+`rules.enabled=false` in the Gerrit config file (see
+link:config-gerrit.html#_a_id_rules_a_section_rules[rules section])
+
+link:https://groups.google.com/d/topic/repo-discuss/wJxTGhlHZMM/discussion[This
+discussion thread] explains why Prolog was chosen for the purpose of writing
+project specific submit rules.
+link:http://gerrit-documentation.googlecode.com/svn/ReleaseNotes/ReleaseNotes-2.2.2.html[Gerrit
+2.2.2 ReleaseNotes] introduces Prolog support in Gerrit.
+
+Prolog Language
+---------------
+This document is not a complete Prolog tutorial.
+link:http://en.wikipedia.org/wiki/Prolog[This Wikipedia page on Prolog] is a
+good starting point for learning the Prolog language. This document will only explain
+some elements of Prolog that are necessary to understand the provided examples.
+
+Prolog in Gerrit
+----------------
+Gerrit uses its own link:https://code.google.com/p/prolog-cafe/[fork] of the
+original link:http://kaminari.istc.kobe-u.ac.jp/PrologCafe/[prolog-cafe]
+project. Gerrit embeds the prolog-cafe library and can interpret Prolog programs at
+runtime.
+
+Interactive Prolog Cafe Shell
+-----------------------------
+For interactive testing and playing with Prolog, Gerrit provides the
+link:pgm-prolog-shell.html[prolog-shell] program which opens an interactive
+Prolog interpreter shell.
+
+NOTE: The interactive shell is just a prolog shell, it does not load
+a gerrit server environment and thus is not intended for xref:TestingSubmitRules[testing submit rules].
+
+SWI-Prolog
+----------
+Instead of using the link:pgm-prolog-shell.html[prolog-shell] program one can
+also use the link:http://www.swi-prolog.org/[SWI-Prolog] environment. It
+provides a better shell interface and a graphical source-level debugger.
+
+The rules.pl file
+-----------------
+This section explains how to create and edit project specific submit rules. How
+to actually write the submit rules is explained in the next section.
+
+Project specific submit rules are stored in the `rules.pl` file in the
+`refs/meta/config` branch of that project.  Therefore, we need to fetch and
+checkout the `refs/meta/config` branch in order to create or edit the `rules.pl`
+file:
+
+====
+  $ git fetch origin refs/meta/config:config
+  $ git checkout config
+  ... edit or create the rules.pl file
+  $ git add rules.pl
+  $ git commit -m "My submit rules"
+  $ git push origin HEAD:refs/meta/config
+====
+
+How to write submit rules
+-------------------------
+Whenever Gerrit needs to evaluate submit rules for a change `C` from project `P` it
+will first initialize the embedded Prolog interpreter by:
+
+* consulting a set of facts about the change `C`
+* consulting the `rules.pl` from the project `P`
+
+Conceptually we can imagine that Gerrit adds a set of facts about the change
+`C` on top of the `rules.pl` file and then consults it. The set of facts about
+the change `C` will look like:
+
+====
+  :- package gerrit.                                                   <1>
+
+  commit_author(user(1000000), 'John Doe', 'john.doe@example.com').    <2>
+  commit_committer(user(1000000), 'John Doe', 'john.doe@example.com'). <3>
+  commit_message('Add plugin support to Gerrit').                      <4>
+  ...
+====
+
+<1> Gerrit will provide its facts in a package named `gerrit`. This means we
+have to use qualified names when writing our code and referencing these facts.
+For example: `gerrit:commit_author(ID, N, M)`
+<2> user ID, full name and email address of the commit author
+<3> user ID, full name and email address of the commit committer
+<4> commit message
+
+A complete set of facts which Gerrit provides about the change is listed in the
+link:prolog-change-facts.html[Prolog Facts for Gerrit Change].
+
+By default, Gerrit will search for a `submit_rule/1` predicate in the `rules.pl`
+file, evaluate the `submit_rule(X)` and then inspect the value of `X` in order
+to decide whether the change is submittable or not and also to find the set of
+needed criteria for the change to become submittable. This means that Gerrit has an
+expectation on the format and value of the result of the `submit_rule` predicate
+which is expected to be a `submit` term of the following format:
+
+====
+  submit(label(label-name, status) [, label(label-name, status)]*)
+====
+
+where `label-name` is usually `'Code-Review'` or `'Verified'` but could also
+be any other string (see examples below). The `status` is one of:
+
+* `ok(user(ID))` or just `ok(_)` if user info is not important. This status is
+   used to tell that this label/category has been met.
+* `need(_)` is used to tell that this label/category is needed for change to
+   become submittable
+* `reject(user(ID))` or just `reject(_)`. This status is used to tell that label/category
+   is blocking change submission
+* `impossible(_)` is used when the logic knows that the change cannot be submitted as-is.
+   Administrative intervention is probably required. This is meant for cases
+   where the logic requires members of "FooEng" to score "Code-Review +2" on a
+   change, but nobody is in group "FooEng". It is to hint at permissions
+   misconfigurations.
+* `may(_)` allows expression of approval categories that are optional, i.e.
+   could either be set or unset without ever influencing whether the change
+   could be submitted.
+
+NOTE: For a change to be submittable all `label` terms contained in the returned
+`submit` term must have either `ok` or `may` status.
+
+IMPORTANT: Gerrit will let the Prolog engine continue searching for solutions of
+the `submit_rule(X)` query until it finds the first one where all labels in the
+return result have either status `ok` or `may` or there are no more solutions.
+If a solution where all labels have status `ok` is found then all previously
+found solutions are ignored. Otherwise, all labels names with status `need`
+from all solutions will be displayed in the UI indicating the set of conditions
+needed for the change to become submittable.
+
+Here some examples of possible return values from the `submit_rule` predicate:
+
+====
+  submit(label('Code-Review', ok(_)))                               <1>
+  submit(label('Code-Review', ok(_)), label('Verified', reject(_))) <2>
+  submit(label('Author-is-John-Doe', need(_))                       <3>
+====
+
+<1> label `'Code-Review'` is met. As there are no other labels in the
+    return result, the change is submittable.
+<2> label `'Verified'` is rejected. Change is not submittable.
+<3> label `'Author-is-John-Doe'` is needed for the change to become submittable.
+    Note that this tells nothing about how this criteria will be met. It is up
+    to the implementor of the `submit_rule` to return `label('Author-is-John-Doe',
+    ok(_))` when this criteria is met.  Most likely, it will have to match
+    against `gerrit:commit_author` in order to check if this criteria is met.
+    This will become clear through the examples below.
+
+Of course, when implementing the `submit_rule` we will use the facts about the
+change that are already provided by Gerrit.
+
+Another aspect of the return result from the `submit_rule` predicate is that
+Gerrit uses it to decide which set of labels to display on the change review
+screen for voting. If the return result contains label `'ABC'` and if the label
+`'ABC'` is one of the (global) voting categories then voting for the label
+`'ABC'` will be displayed. Otherwise, it is not displayed. Note that we don't
+need a (global) voting category for each label contained in the result of
+`submit_rule` predicate.  For example, the decision whether `'Author-is-John-Doe'`
+label is met will probably not be made by explicit voting but, instead, by
+inspecting the facts about the change.
+
+Submit Filter
+-------------
+Another mechanism of changing the default submit rules is to implement the
+`submit_filter/2` predicate. While Gerrit will search for the `submit_rule` only
+in the `rules.pl` file of the current project, the `submit_filter` will be
+searched for in the `rules.pl` of all parent projects of the current project,
+but not in the `rules.pl` of the current project. The search will start from the
+immediate parent of the current project, then in the parent project of that
+project and so on until, and including, the 'All-Projects' project.
+
+The purpose of the submit filter is, as its name says, to filter the results
+of the `submit_rule`. Therefore, the `submit_filter` predicate has two
+parameters:
+
+====
+  submit_filter(In, Out) :- ...
+====
+
+Gerrit will invoke `submit_filter` with the `In` parameter containing a `submit`
+structure produced by the `submit_rule` and will take the value of the `Out`
+parameter as the result.
+
+The `Out` value of a `submit_filter` will become the `In` value for the
+next `submit_filter` in the parent line. The value of the `Out` parameter
+of the top-most `submit_filter` is the final result of the submit rule that
+is used to decide whether a change is submittable or not.
+
+IMPORTANT: `submit_filter` is a mechanism for Gerrit administrators to implement
+and enforce submit rules that would apply to all projects while `submit_rule` is
+a mechanism for project owners to implement project specific submit rules.
+However, project owners who own several projects could also make use of
+`submit_filter` by using a common parent project for all their projects and
+implementing the `submit_filter` in this common parent project. This way they
+can avoid implementing the same `submit_rule` in all their projects.
+
+The following "drawing" illustrates the order of the invocation and the chaining
+of the results of the `submit_rule` and `submit_filter` predicates.
+
+====
+  All-Projects
+  ^   submit_filter(B, S) :- ...  <4>
+  |
+  Parent-3
+  ^   <no submit filter here>
+  |
+  Parent-2
+  ^   submit_filter(A, B) :- ...  <3>
+  |
+  Parent-1
+  ^   submit_filter(X, A) :- ...  <2>
+  |
+  MyProject
+      submit_rule(X) :- ...       <1>
+====
+
+<1> The `submit_rule` of `MyProject` is invoked first.
+<2> The result `X` is filtered through the `submit_filter` from the `Parent-1`
+project.
+<3> The result of `submit_filter` from `Parent-1` project is filtered by the
+`submit_filter` in the `Parent-2` project. Since `Parent-3` project doesn't have
+a `submit_filter` it is skipped.
+<4> The result of `submit_filter` from `Parent-2` project is filtered by the
+`submit_filter` in the `All-Projects` project. The value in `S` is the final
+value of the submit rule evaluation.
+
+NOTE: If `MyProject` doesn't define its own `submit_rule` Gerrit will invoke the
+default implementation of submit rule that is named `gerrit:default_submit` and
+its result will be filtered as described above.
+
+[[TestingSubmitRules]]
+Testing submit rules
+--------------------
+The prolog environment running the `submit_rule` is loaded with state describing the
+change that is being evaluated. The easiest way to load this state is to test your
+`submit_rule` against a real change on a running gerrit instance. The command
+link:cmd-test-submit-rule.html[test-submit-rule] loads a specific change and executes
+the `submit_rule`. It optionally reads the rule from from `stdin` to facilitate easy testing.
+
+====
+  cat rules.pl | ssh gerrit_srv gerrit test-submit-rule I45e080b105a50a625cc8e1fb5b357c0bfabe6d68 -s
+====
+
+Prolog vs Gerrit plugin for project specific submit rules
+---------------------------------------------------------
+Since version 2.5 Gerrit supports plugins and extension points. A plugin or an
+extension point could also be used as another means to provide custom submit
+rules. One could ask for a guideline when to use Prolog based submit rules and
+when to go for writing a new plugin. Writing a Prolog program is usually much
+faster than writing a Gerrit plugin. Prolog based submit rules can be pushed
+to a project by project owners while Gerrit plugins could only be installed by
+Gerrit administrators. In addition, Prolog based submit rules can be pushed
+for review by pushing to `refs/for/refs/meta/config` branch.
+
+On the other hand, Prolog based submit rules get a limited amount of facts about
+the change exposed to them. Gerrit plugins get full access to Gerrit internals
+and can potentially check more things than Prolog based rules.
+
+Examples
+--------
+The following examples should serve as a cookbook for developing own submit rules.
+Some of them are too trivial to be used in production and their only purpose is
+to provide step by step introduction and understanding.
+
+Some of the examples will implement the `submit_rule` and some will implement
+the `submit_filter` just to show both possibilities.  Remember that
+`submit_rule` is only invoked from the current project and `submit_filter` is
+invoked from all parent projects. This is the most important fact in deciding
+whether to implement `submit_rule` or `submit_filter`.
+
+Example 1: Make every change submittable
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Let's start with a most trivial example where we would make every change submittable
+regardless of the votes it has:
+
+.rules.pl
+[caption=""]
+====
+  submit_rule(submit(label('Any-Label-Name', ok(_)))).
+====
+
+In this case we make no use of facts about the change. We don't need it as we are simply
+making every change submittable. Note that, in this case, the Gerrit UI will not show
+the UI for voting for the standard `'Code-Review'` and `'Verified'` categories as labels
+with these names are not part of the return result. The `'Any-Label-Name'` could really
+be any string.
+
+Example 2: Every change submittable and voting in the standard categories possible
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+This is continuation of the previous example where, in addition, to making
+every change submittable we want to enable voting in the standard
+`'Code-Review'` and `'Verified'` categories.
+
+.rules.pl
+[caption=""]
+====
+  submit_rule(submit(label('Code-Review', ok(_)), label('Verified', ok(_)))).
+====
+
+Since for every change all label statuses are `'ok'` every change will be submittable.
+Voting in the standard labels will be shown in the UI as the standard label names are
+included in the return result.
+
+Example 3: Nothing is submittable
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+This example shows how to make all changes non-submittable regardless of the
+votes they have.
+
+.rules.pl
+[caption=""]
+====
+  submit_rule(submit(label('Any-Label-Name', reject(_)))).
+====
+
+Since for any change we return only one label with status `reject`, no change
+will be submittable. The UI will, however, not indicate what is needed for a
+change to become submittable as we return no labels with status `need`.
+
+Example 4: Nothing is submittable but UI shows several 'Need ...' criteria
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+In this example no change is submittable but here we show how to present 'Need
+<label>' information to the user in the UI.
+
+.rules.pl
+[caption=""]
+====
+  % In the UI this will show: Need Any-Label-Name
+  submit_rule(submit(label('Any-Label-Name', need(_)))).
+
+  % We could define more "need" labels by adding more rules
+  submit_rule(submit(label('Another-Label-Name', need(_)))).
+
+  % or by providing more than one need label in the same rule
+  submit_rule(submit(label('X-Label-Name', need(_)), label('Y-Label-Name', need(_)))).
+====
+
+In the UI this will show:
+****
+* Need Any-Label-Name
+* Need Another-Label-Name
+* Need X-Label-Name
+* Need Y-Label-Name
+****
+
+From the example above we can see a few more things:
+
+* comment in Prolog starts with the `%` character
+* there could be multiple `submit_rule` predicates. Since Prolog, by default, tries to find
+  all solutions for a query, the result will be union of all solutions.
+  Therefore, we see all 4 `need` labels in the UI.
+
+Example 5: The 'Need ...' labels not shown when change is submittable
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+This example shows that, when there is a solution for `submit_rule(X)` where all labels
+have status `ok` then Gerrit will not show any labels with the `need` status from
+any of the previous `submit_rule(X)` solutions.
+
+.rules.pl
+[caption=""]
+====
+  submit_rule(label('Some-Condition', need(_))).
+  submit_rule(label('Another-Condition', ok(_))).
+====
+
+The 'Need Some-Condition' will not be show in the UI because of the result of
+the second rule.
+
+The same is valid if the two rules are swapped:
+
+.rules.pl
+[caption=""]
+====
+  submit_rule(label('Another-Condition', ok(_))).
+  submit_rule(label('Some-Condition', need(_))).
+====
+
+The result of the first rule will stop search for any further solutions.
+
+Example 6: Make change submittable if commit author is "John Doe"
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+This is the first example where we will use the Prolog facts about a change that
+are automatically exposed by Gerrit. Our goal is to make any change submittable
+when the commit author is named `'John Doe'`. In the very first
+step let's make sure Gerrit UI shows 'Need Author-is-John-Doe' in
+the UI to clearly indicate to the user what is needed for a change to become
+submittable:
+
+.rules.pl
+[caption=""]
+====
+  submit_rule(submit(label('Author-is-John-Doe', need(_)))).
+====
+
+This will show:
+****
+* Need Author-is-John-Doe
+****
+
+in the UI but no change will be submittable yet. Let's add another rule:
+
+.rules.pl
+[caption=""]
+====
+  submit_rule(submit(label('Author-is-John-Doe', need(_)))).
+  submit_rule(submit(label('Author-is-John-Doe', ok(_))))
+    :- gerrit:commit_author(_, 'John Doe', _).
+====
+
+In the second rule we return `ok` status for the `'Author-is-John-Doe'` label
+if there is a `commit_author` fact where the full name is `'John Doe'`. If
+author of a change is `'John Doe'` then the second rule will return a solution
+where all labels have `ok` status and the change will become submittable. If
+author of a change is not `'John Doe'` then only the first rule will produce a
+solution. The UI will show 'Need Author-is-John-Doe' but, as expected, the
+change will not be submittable.
+
+Instead of checking by full name we could also check by the email address:
+
+.rules.pl
+[caption=""]
+====
+  submit_rule(submit(label('Author-is-John-Doe', need(_)))).
+  submit_rule(submit(label('Author-is-John-Doe', ok(_))))
+    :- gerrit:commit_author(_, _, 'john.doe@example.com').
+====
+
+or by user id (assuming it is 1000000):
+
+.rules.pl
+[caption=""]
+====
+  submit_rule(submit(label('Author-is-John-Doe', need(_)))).
+  submit_rule(submit(label('Author-is-John-Doe', ok(_))))
+    :- gerrit:commit_author(user(1000000), _, _).
+====
+
+or by a combination of these 3 attributes:
+
+.rules.pl
+[caption=""]
+====
+  submit_rule(submit(label('Author-is-John-Doe', need(_)))).
+  submit_rule(submit(label('Author-is-John-Doe', ok(_))))
+    :- gerrit:commit_author(_, 'John Doe', 'john.doe@example.com').
+====
+
+Example 7: Make change submittable if commit message starts with "Trivial fix"
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Besides showing how to make use of the commit message text the purpose of this
+example is also to show how to match only a part of a string symbol. Similarly
+like commit author the commit message is provided as a string symbol which is
+an atom in Prolog terms. When working with an atom we could only match against
+the whole value. To match only part of a string symbol we have, at least, two
+options:
+
+* convert the string symbol into a list of characters and then perform
+  the "classical" list matching
+* use the `regex_matches/2` or, even more convenient, the
+  `gerrit:commit_message_matches/1` predicate
+
+Let's implement both options:
+
+.rules.pl
+[caption=""]
+====
+  submit_rule(submit(label('Commit-Message-starts-with-Trivial-Fix', need(_)))).
+  submit_rule(submit(label('Commit-Message-starts-with-Trivial-Fix', ok(_))))
+    :- gerrit:commit_message(M), name(M, L), starts_with(L, "Trivial Fix").
+
+  starts_with(L, []).
+  starts_with([H|T1], [H|T2]) :- starts_with(T1, T2).
+====
+
+NOTE: The `name/2` embedded predicate is used to convert a string symbol into a
+list of characters. A string `abc` is converted into a list of characters `[97,
+98, 99]`.  A double quoted string in Prolog is just a shortcut for creating a
+list of characters. `"abc"` is a shortcut for `[97, 98, 99]`. This is why we use
+double quotes for the `"Trivial Fix"` in the example above.
+
+The `starts_with` predicate is self explaining.
+
+Using the `gerrit:commit_message_matches` predicate is probably more efficient:
+
+.rules.pl
+[caption=""]
+====
+  submit_rule(submit(label('Commit-Message-starts-with-Trivial-Fix', need(_)))).
+  submit_rule(submit(label('Commit-Message-starts-with-Trivial-Fix', ok(_))))
+    :- gerrit:commit_message_matches('^Trivial Fix').
+====
+
+Reusing the default submit policy
+---------------------------------
+All examples until now concentrate on one particular aspect of change data.
+However, in real-life scenarios we would rather want to reuse Gerrit's default
+submit policy and extend/change it for our specific purpose. In other words, we
+would like to keep all the default policies (like the `Verified` category,
+vetoing change, etc...) and only extend/change an aspect of it. For example, we
+may want to disable the ability for change authors to approve their own changes
+but keep all other policies the same.
+
+To get results of Gerrits default submit policy we use the
+`gerrit:default_submit` predicate. This means that if we write a submit rule like:
+
+.rules.pl
+[caption=""]
+====
+  submit_rule(X) :- gerrit:default_submit(X).
+====
+
+then this is equivalent to not using `rules.pl` at all. We just delegate to
+default logic. However, once we invoke the `gerrit:default_submit(X)` we can
+perform further actions on the return result `X` and apply our specific
+logic. The following pattern illustrates this technique:
+
+.rules.pl
+[caption=""]
+====
+  submit_rule(S) :- gerrit:default_submit(R), project_specific_policy(R, S).
+
+  project_specific_policy(R, S) :- ...
+====
+
+The following examples build on top of the default submit policy.
+
+Example 8: Make change submittable only if `Code-Review+2` is given by a non author
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+In this example we introduce a new label `Non-Author-Code-Review` and make it
+satisfied if there is at least one `Code-Review+2` from a non author. All other
+default policies like the `Verified` category and vetoing changes still apply.
+
+First, we invoke `gerrit:default_submit` to compute the result for the default
+submit policy and then add the `Non-Author-Code-Review` label to it.  The
+`Non-Author-Code-Review` label is added with status `ok` if such an approval
+exists or with status `need` if it doesn't exist.
+
+.rules.pl
+[caption=""]
+====
+  submit_rule(S) :-
+    gerrit:default_submit(X),
+    X =.. [submit | Ls],
+    add_non_author_approval(Ls, R),
+    S =.. [submit | R].
+
+  add_non_author_approval(S1, S2) :-
+    gerrit:commit_author(A), gerrit:commit_label(label('Code-Review', 2), R),
+    R \= A, !,
+    S2 = [label('Non-Author-Code-Review', ok(R)) | S1].
+  add_non_author_approval(S1, [label('Non-Author-Code-Review', need(_)) | S1]).
+====
+
+This example uses the `univ` operator `=..` to "unpack" the result of the
+default_submit, which is a structure of the form `submit(label('Code-Review',
+ok(_)), label('Verified', need(_)) ...)` into a list like `[submit,
+label('Code-Review', ok(_)), label('Verified', need(_)), ...]`.  Then we
+process the tail of the list (the list of labels) as a Prolog list, which is
+much easier than processing a structure. In the end we use the same `univ`
+operator to convert the resulting list of labels back into a `submit` structure
+which is expected as a return result. The `univ` operator works both ways.
+
+In `add_non_author_approval` we use the `cut` operator `!` to prevent Prolog
+from searching for more solutions once the `cut` point is reached. This is
+important because in the second `add_non_author_approval` rule we just add the
+`label('Non-Author-Code-Review', need(_))` without first checking that there
+is no non author `Code-Review+2`. The second rule will only be reached
+if the `cut` in the first rule is not reached and it only happens if a
+predicate before the `cut` fails.
+
+Example 9: Remove the `Verified` category
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+A project has no build and test. It consists of only text files and needs only
+code review.  We want to remove the `Verified` category from this project so
+that `Code-Review+2` is the only criteria for a change to become submittable.
+We also want the UI to not show the `Verified` category in the table with
+votes and on the voting screen.
+
+.rules.pl
+[caption=""]
+====
+  submit_rule(S) :-
+    gerrit:default_submit(X),
+    X =.. [submit | Ls],
+    remove_verified_category(Ls, R),
+    S =.. [submit | R].
+
+  remove_verified_category([], []).
+  remove_verified_category([label('Verified', _) | T], R) :- remove_verified_category(T, R), !.
+  remove_verified_category([H|T], [H|R]) :- remove_verified_category(T, R).
+====
+
+Example 10: Combine examples 8 and 9
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+In this example we want to both remove the verified and have the four eyes
+principle.  This means we want a combination of examples 7 and 8.
+
+.rules.pl
+[caption=""]
+====
+  submit_rule(S) :-
+    gerrit:default_submit(X),
+    X =.. [submit | Ls],
+    remove_verified_category(Ls, R1),
+    add_non_author_approval(R1, R),
+    S =.. [submit | R].
+====
+
+The `remove_verified_category` and `add_non_author_approval` predicates are the
+same as defined in the previous two examples.
+
+Example 11: Remove the `Verified` category from all projects
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Example 9, implements `submit_rule` that removes the `Verified` category from
+one project. In this example we do the same but we want to remove the `Verified`
+category from all projects. This means we have to implement `submit_filter` and
+we have to do that in the `rules.pl` of the `All-Projects` project.
+
+.rules.pl
+[caption=""]
+====
+  submit_filter(In, Out) :-
+    In =.. [submit | Ls],
+    remove_verified_category(Ls, R),
+    Out =.. [submit | R].
+
+  remove_verified_category([], []).
+  remove_verified_category([label('Verified', _) | T], R) :-
+    remove_verified_category(T, R), !.
+  remove_verified_category([H|T], [H|R]) :- remove_verified_category(T, R).
+====
+
+Example 12: 1+1=2 Code-Review
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+In this example we introduce accumulative voting to determine if a change is
+submittable or not. We modify the standard Code-Review to be accumulative, and make the
+change submittable if the total score is 2 or higher.
+
+The code in this example is very similar to Example 8, with the addition of findall/3
+and gerrit:remove_label.
+The findall/3 embedded predicate is used to form a list of all objects that satisfy a
+specified Goal. In this example it is used to get a list of all the 'Code-Review' scores.
+gerrit:remove_label is a built-in helper that is implemented similarly to the
+'remove_verified_category' as seen in the previous example.
+
+.rules.pl
+[caption=""]
+====
+  sum_list([], 0).
+  sum_list([H | Rest], Sum) :- sum_list(Rest,Tmp), Sum is H + Tmp.
+
+  add_category_min_score(In, Category, Min,  P) :-
+    findall(X, gerrit:commit_label(label(Category,X),R),Z),
+    sum_list(Z, Sum),
+    Sum >= Min, !,
+    P = [label(Category,ok(R)) | In].
+
+  add_category_min_score(In, Category,Min,P) :-
+    P = [label(Category,need(Min)) | In].
+
+  submit_rule(S) :-
+    gerrit:default_submit(X),
+    X =.. [submit | Ls],
+    gerrit:remove_label(Ls,label('Code-Review',_),NoCR),
+    add_category_min_score(NoCR,'Code-Review', 2, Labels),
+    S =.. [submit | Labels].
+====
+
+Example 13: Master and apprentice
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+The master and apprentice example allow you to specify a user (the `master`)
+that must approve all changes done by another user (the `apprentice`).
+
+The code first checks if the commit author is in the apprentice database.
+If the commit is done by an apprentice, it will check if there is a +2
+review by the associated `master`.
+
+.rules.pl
+[caption=""]
+====
+  % master_apprentice(Master, Apprentice).
+  % Extend this with appropriate user-id's for your master/apprentice setup.
+  master_apprentice(user(1000064), user(1000000)).
+
+  submit_rule(S) :-
+    gerrit:default_submit(In),
+    In =.. [submit | Ls],
+    add_apprentice_master(Ls, R),
+    S =.. [submit | R].
+
+  check_master_approval(S1, S2, Master) :-
+    gerrit:commit_label(label('Code-Review', 2), R),
+    R = Master, !,
+    S2 = [label('Master-Approval', ok(R)) | S1].
+  check_master_approval(S1, [label('Master-Approval', need(_)) | S1], _).
+
+  add_apprentice_master(S1, S2) :-
+    gerrit:commit_author(Id),
+    master_apprentice(Master, Id),
+    !,
+    check_master_approval(S1, S2, Master).
+
+  add_apprentice_master(S, S).
+====
+
+Example 14: Only allow Author to submit change
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+This example adds a new needed category `Patchset-Author` for any user that is
+not the author of the patch. This effectively blocks all users except the author
+from submitting the change. This could result in an impossible situation if the
+author does not have permissions for submitting the change.
+
+.rules.pl
+[caption=""]
+====
+  submit_rule(S) :-
+    gerrit:default_submit(In),
+    In =.. [submit | Ls],
+    only_allow_author_to_submit(Ls, R),
+    S =.. [submit | R].
+
+  only_allow_author_to_submit(S, S) :-
+    gerrit:commit_author(Id),
+    gerrit:current_user(Id),
+    !.
+
+  only_allow_author_to_submit(S1, [label('Patchset-Author', need(_)) | S1]).
+====
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/refs-notes-review.txt b/Documentation/refs-notes-review.txt
new file mode 100644
index 0000000..632f567
--- /dev/null
+++ b/Documentation/refs-notes-review.txt
@@ -0,0 +1,111 @@
+The refs/notes/review namespace
+===============================
+
+Summary
+-------
+
+`refs/notes/review` is a special reference that Gerrit creates on repositories
+to store information about code reviews.
+
+When a repository is cloned from Gerrit, the `refs/notes/review` reference is
+not included by default.  It has to be manually fetched:
+
+====
+  $ git fetch origin refs/notes/review:refs/notes/review
+====
+
+It is also possible to
+link:http://www.kernel.org/pub/software/scm/git/docs/git-config.html[configure git]
+to always fetch `refs/notes/review`:
+
+====
+  $ git config --add remote.origin.fetch refs/notes/review:refs/notes/review
+  $ git fetch
+====
+
+When `refs/notes/review` is fetched on a repository, the Gerrit review
+information can be included in the git log output:
+
+====
+   $ git log --show-notes=review
+====
+
+Content of refs/notes/review
+----------------------------
+
+For each commit, Gerrit stores the following review information in
+`refs/notes/review`:
+
+[[submitted_by]]
+Submitted-by
+~~~~~~~~~~~~
+
+The name and email address of the Gerrit user that submitted the change in
+link:http://www.ietf.org/rfc/rfc2822.txt[RFC 2822] format.
+
+====
+  Submitted-by: Random J Developer <random@developer.example.org>
+====
+
+[[submitted_at]]
+Submitted-at
+~~~~~~~~~~~~
+
+The time the commit was submitted in RFC 2822 time stamp format.
+
+====
+  Submitted-at: Mon, 25 Jun 2012 16:15:57 +0200
+====
+
+[[reviewed_on]]
+Reviewed-on
+~~~~~~~~~~~
+
+The URL to the change on the Gerrit server.
+
+====
+  Reviewed-on: http://path.to.gerrit/12345
+====
+
+[[review_scores]]
+Review Labels and Scores
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+Review label and score, and the name and email address of the Gerrit user that
+gave it in RFC 2822 format:
+
+====
+  Code-Review+2: A. N. Other <another@developer.example.org>
+  Verified+1: A. N. Other <another@developer.example.org>
+====
+
+Commonly used review labels are "Code-Review" and "Verified", but any label
+configured in Gerrit can be included.
+
+All review labels and scores present on the change at the time of submit are
+included.
+
+[[project]]
+Project
+~~~~~~~
+
+The name of the project in which the commit was made.
+
+====
+  Project: kernel/common
+====
+
+[[branch]]
+Branch
+~~~~~~
+
+The name of the branch on which the commit was made.
+
+====
+  Branch: refs/heads/master
+====
+
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/rest-api.txt b/Documentation/rest-api.txt
new file mode 100644
index 0000000..2f9d03f
--- /dev/null
+++ b/Documentation/rest-api.txt
@@ -0,0 +1,393 @@
+Gerrit Code Review - REST API
+=============================
+
+Gerrit Code Review comes with a REST like API available over HTTP.
+The API is suitable for automated tools to build upon, as well as
+supporting some ad-hoc scripting use cases.
+
+Protocol Details
+----------------
+
+[[authentication]]
+Authentication
+~~~~~~~~~~~~~~
+By default all REST endpoints assume anonymous access and filter
+results to correspond to what anonymous users can read (which may
+be nothing at all).
+
+Users (and programs) may authenticate using HTTP authentication by
+supplying the HTTP password from the user's account settings page.
+Gerrit by default uses HTTP digest authentication. To authenticate,
+prefix the endpoint URL with `/a/`. For example to authenticate to
+`/projects/` request URL `/a/projects/`.
+
+[[output]]
+Output Format
+~~~~~~~~~~~~~
+Most APIs return text format by default. JSON can be requested
+by setting the `Accept` HTTP request header to include
+`application/json`, for example:
+
+----
+  GET /projects/ HTTP/1.0
+  Accept: application/json
+----
+
+JSON responses are encoded using UTF-8 and use content type
+`application/json`. The JSON response body starts with a magic prefix
+line that must be stripped before feeding the rest of the response
+body to a JSON parser:
+
+----
+  )]}'
+  [ ... valid JSON ... ]
+----
+
+The default JSON format is `JSON_COMPACT`, which skips unnecessary
+whitespace. This is not the easiest format for a human to read. Many
+examples in this documentation use `format=JSON` as a query parameter
+to obtain pretty formatting in the response. Producing (and parsing)
+the compact format is more efficient, so most tools should prefer the
+default compact format.
+
+Responses will be gzip compressed by the server if the HTTP
+`Accept-Encoding` request header is set to `gzip`. This may
+save on network transfer time for larger responses.
+
+Endpoints
+---------
+
+[[accounts_self_capabilities]]
+/accounts/self/capabilities (Account Capabilities)
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Returns the global capabilities (such as `createProject` or
+`createGroup`) that are enabled for the calling user. This can be used
+by UI tools to discover if administrative features are available
+to the caller, so they can hide (or show) relevant UI actions.
+
+----
+  GET /accounts/self/capabilities?format=JSON HTTP/1.0
+
+  )]}'
+  {
+    "queryLimit": {
+      "min": 0,
+      "max": 500
+    }
+  }
+----
+
+Administrator that has authenticated with digest authentication:
+----
+  GET /a/accounts/self/capabilities?format=JSON HTTP/1.0
+  Authorization: Digest username="admin", realm="Gerrit Code Review", nonce="...
+
+  )]}'
+  {
+    "administrateServer": true,
+    "queryLimit": {
+      "min": 0,
+      "max": 500
+    },
+    "createAccount": true,
+    "createGroup": true,
+    "createProject": true,
+    "killTask": true,
+    "viewCaches": true,
+    "flushCaches": true,
+    "viewConnections": true,
+    "viewQueue": true,
+    "startReplication": true
+  }
+----
+
+To filter the set of global capabilities the `q` parameter can be used.
+Filtering may decrease the response time by avoiding looking at every
+possible alternative for the caller.
+
+----
+  GET /a/accounts/self/capabilities?format=JSON&q=createAccount&q=createGroup HTTP/1.0
+  Authorization: Digest username="admin", realm="Gerrit Code Review", nonce="...
+
+  )]}'
+  {
+    "createAccount": true,
+    "createGroup": true
+  }
+----
+
+Most results are boolean, and a field is only present when its value
+is `true`. link:json.html#queryLimit[`queryLimit`] is a range and is
+presented as a nested JSON object with `min` and `max` members.
+
+[[projects]]
+/projects/ (List Projects)
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+Lists the projects accessible by the caller. This is the same as
+using the link:cmd-ls-projects.html[ls-projects] command over SSH,
+and accepts the same options as query parameters.
+
+----
+  GET /projects/?format=JSON&d HTTP/1.0
+
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+   
+  )]}'
+  {
+    "external/bison": {
+      "description": "GNU parser generator"
+    },
+    "external/gcc": {},
+    "external/openssl": {
+      "description": "encryption\ncrypto routines"
+    },
+    "test": {
+      "description": "\u003chtml\u003e is escaped"
+    }
+  }
+----
+
+[[suggest-projects]]
+The `/projects/` URL also accepts a prefix string as part of the URL.
+This limits the results to those projects that start with the specified
+prefix.
+List all projects that start with `platform/`:
+----
+GET /projects/platform/?format=JSON HTTP/1.0
+HTTP/1.1 200 OK
+Content-Disposition: attachment
+Content-Type: application/json;charset=UTF-8
+)]}'
+{
+"platform/drivers": {},
+"platform/tools": {}
+}
+----
+E.g. this feature can be used by suggestion client UI's to limit results.
+
+[[changes]]
+/changes/ (Query Changes)
+~~~~~~~~~~~~~~~~~~~~~~~~~
+Queries changes visible to the caller. The query string must be
+provided by the `q` parameter. The `n` parameter can be used to limit
+the returned results.
+
+Query for open changes of watched projects:
+----
+  GET /changes/?format=JSON&q=status:open+is:watched&n=2 HTTP/1.0
+
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "project": "demo",
+    "branch": "master",
+    "id": "Idaf5e098d70898b7119f6f4af5a6c13343d64b57",
+    "subject": "One change",
+    "status": "NEW",
+    "created": "2012-07-17 07:18:30.854000000",
+    "updated": "2012-07-17 07:19:27.766000000",
+    "reviewed": true,
+    "_sortkey": "001e7057000006dc",
+    "_number": 1756,
+    "owner": {
+      "name": "John Doe"
+    },
+  },
+  {
+    "project": "demo",
+    "branch": "master",
+    "id": "I09c8041b5867d5b33170316e2abc34b79bbb8501",
+    "subject": "Another change",
+    "status": "NEW",
+    "created": "2012-07-17 07:18:30.884000000",
+    "updated": "2012-07-17 07:18:30.885000000",
+    "_sortkey": "001e7056000006dd",
+    "_number": 1757,
+    "owner": {
+      "name": "John Doe"
+    },
+    "_more_changes": true
+  }
+----
+
+The change output is sorted by the last update time, most recently
+updated to oldest update.
+
+If the `n` query parameter is supplied and additional changes exist
+that match the query beyond the end, the last change object has a
+`_more_changes: true` JSON field set. Callers can resume a query with
+the `n` query parameter, supplying the last change's `_sortkey` field
+as the value. When going in the reverse direction with the `p` query
+parameter a `_more_changes: true` is put in the first change object if
+there are results *before* the first change returned.
+
+Clients are allowed to specify more than one query by setting the `q`
+parameter multiple times. In this case the result is an array of
+arrays, one per query in the same order the queries were given in.
+
+Query that retrieves changes for a user's dashboard:
+----
+  GET /changes/?format=JSON&q=is:open+owner:self&q=is:open+reviewer:self+-owner:self&q=is:closed+owner:self+limit:5&o=LABELS HTTP/1.0
+
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  [
+    [
+      {
+        "project": "demo",
+        "branch": "master",
+        "id": "Idaf5e098d70898b7119f6f4af5a6c13343d64b57",
+        "subject": "One change",
+        "status": "NEW",
+        "created": "2012-07-17 07:18:30.854000000",
+        "updated": "2012-07-17 07:19:27.766000000",
+        "reviewed": true,
+        "_sortkey": "001e7057000006dc",
+        "_number": 1756,
+        "owner": {
+          "name": "John Doe"
+        },
+        "labels": {
+          "Verified": {},
+          "Code-Review": {}
+        }
+      }
+    ],
+    [],
+    []
+  ]
+----
+
+Additional fields can be obtained by adding `o` parameters, each
+option requires more database lookups and slows down the query
+response time to the client so they are generally disabled by
+default. Optional fields are:
+
+* `LABELS`: a summary of each label required for submit, and
+  approvers that have granted (or rejected) with that label.
+
+* `CURRENT_REVISION`: describe the current revision (patch set)
+  of the change, including the commit SHA-1 and URLs to fetch from.
+
+* `ALL_REVISIONS`: describe all revisions, not just current.
+
+* `CURRENT_COMMIT`: parse and output all header fields from the
+  commit object, including message. Only valid when the current
+  revision or all revisions are selected.
+
+* `ALL_COMMITS`: parse and output all header fields from the
+  output revisions. If only `CURRENT_REVISION` was requested
+  then only the current revision's commit data will be output.
+
+* `CURRENT_FILES`: list files modified by the commit, including
+  basic line counts inserted/deleted per file. Only valid when
+  the current revision or all revisions are selected.
+
+* `ALL_FILES`: list files modified by the commit, including
+  basic line counts inserted/deleted per file. If only the
+  `CURRENT_REVISION` was requested the only that commit's
+  modified files will be output.
+
+----
+  GET /changes/?q=97&format=JSON&o=CURRENT_REVISION&o=CURRENT_COMMIT&o=CURRENT_FILES HTTP/1.0
+
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  [
+    {
+      "project": "gerrit",
+      "branch": "master",
+      "id": "I7ea46d2e2ee5c64c0d807677859cfb7d90b8966a",
+      "subject": "Use an EventBus to manage star icons",
+      "status": "NEW",
+      "created": "2012-04-25 00:52:25.580000000",
+      "updated": "2012-04-25 00:52:25.586000000",
+      "_sortkey": "001c9bf400000061",
+      "_number": 97,
+      "owner": {
+        "name": "Shawn Pearce"
+      },
+      "current_revision": "184ebe53805e102605d11f6b143486d15c23a09c",
+      "revisions": {
+        "184ebe53805e102605d11f6b143486d15c23a09c": {
+          "_number": 1,
+          "fetch": {
+            "git": {
+              "url": "git://localhost/gerrit",
+              "ref": "refs/changes/97/97/1"
+            },
+            "http": {
+              "url": "http://127.0.0.1:8080/gerrit",
+              "ref": "refs/changes/97/97/1"
+            }
+          },
+          "commit": {
+            "parents": [
+              {
+                "commit": "1eee2c9d8f352483781e772f35dc586a69ff5646",
+                "subject": "Migrate contributor agreements to All-Projects."
+              }
+            ],
+            "author": {
+              "name": "Shawn O. Pearce",
+              "email": "sop@google.com",
+              "date": "2012-04-24 18:08:08.000000000",
+              "tz": -420
+            },
+            "committer": {
+              "name": "Shawn O. Pearce",
+              "email": "sop@google.com",
+              "date": "2012-04-24 18:08:08.000000000",
+              "tz": -420
+            },
+            "subject": "Use an EventBus to manage star icons",
+            "message": "Use an EventBus to manage star icons\n\nImage widgets that need to ..."
+          },
+          "files": {
+            "gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeCache.java": {
+              "lines_deleted": 8
+            },
+            "gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDetailCache.java": {
+              "lines_inserted": 1
+            },
+            "gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java": {
+              "lines_inserted": 11,
+              "lines_deleted": 19
+            },
+            "gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java": {
+              "lines_inserted": 23,
+              "lines_deleted": 20
+            },
+            "gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarCache.java": {
+              "status": "D",
+              "lines_deleted": 139
+            },
+            "gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarredChanges.java": {
+              "status": "A",
+              "lines_inserted": 204
+            },
+            "gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Screen.java": {
+              "lines_deleted": 9
+            }
+          }
+        }
+      }
+    }
+  ]
+----
+
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/user-changeid.txt b/Documentation/user-changeid.txt
index 124ec31..a3015e1 100644
--- a/Documentation/user-changeid.txt
+++ b/Documentation/user-changeid.txt
@@ -4,7 +4,7 @@
 Description
 -----------
 
-Gerrit Code Review sometimes relies upon Change-Id lines in the
+Gerrit Code Review sometimes relies upon a Change-Id line at the
 bottom of a commit message to uniquely identify a change across all
 drafts of it.  By including a unique Change-Id in the commit message,
 Gerrit can automatically associate a new version of a change back
@@ -37,7 +37,7 @@
 the commit name, `29a6...`, as the change may have been amended or
 rebased to address reviewer comments since its initial inception.
 
-To avoid confusion with commit names, Change-Ids typically are with
+To avoid confusion with commit names, Change-Ids are typically prefixed with
 an uppercase `I`.
 
 Creation
@@ -46,11 +46,13 @@
 Gerrit Code Review provides a standard 'commit-msg' hook which
 can be installed in the local Git repository to automatically
 create and insert a unique Change-Id line during `git commit`.
-To install the hook, copy it from Gerrit's daemon:
+To install the hook, copy it from Gerrit's daemon by executing
+one of the following commands while being in the root directory
+of the local Git repository:
 
   $ scp -p -P 29418 john.doe@review.example.com:hooks/commit-msg .git/hooks/
 
-  $ curl http://review.example.com/tools/hooks/commit-msg
+  $ curl -o .git/hooks/commit-msg http://review.example.com/tools/hooks/commit-msg
 
 For more details, see link:cmd-hook-commit-msg.html[commit-msg].
 
@@ -125,7 +127,7 @@
 already uploaded to Gerrit Code Review, and thus has a corresponding
 change that reviewers have already examined and left comments on.
 If you aren't sure which lines Gerrit knows about, try copying and
-pasting the lines into the search box in the top-right.
+pasting the lines into the search box at the top-right of the web interface.
 
 If Gerrit already knows about more than one Change-Id, pick one
 to keep in the squashed commit message, and manually abandon the
diff --git a/Documentation/user-custom-dashboards.txt b/Documentation/user-custom-dashboards.txt
new file mode 100644
index 0000000..a015e4c
--- /dev/null
+++ b/Documentation/user-custom-dashboards.txt
@@ -0,0 +1,48 @@
+Gerrit Code Review - Custom Dashboards
+======================================
+
+Description
+-----------
+
+A custom dashboard is shown in a layout similar to the per-user
+dashboard, but the sections are entirely configured from the URL.
+Because of this custom dashboards are stateless on the server side.
+Users or projects can simply trade URLs using an external system like
+a project wiki, or site administrators can put the links into the
+site's `GerritHeader.html` or `GerritFooter.html`.
+
+Dashboards are available via URLs like:
+----
+  /#/dashboard/?title=Custom+View&To+Review=reviewer:john.doe@example.com&Pending+In+myproject=project:myproject+is:open
+----
+This opens a view showing the title "Custom View" with two sections,
+"To Review" and "Pending in myproject":
+----
+  Custom View
+
+  To Review
+
+    Results of `reviewer:john.doe@example.com`
+
+  Pending In myproject
+
+    Results of `project:myproject is:open`
+----
+
+The dashboard URLs are easy to configure. All keys and values in the
+URL are encoded as query parameters. Set the page and window title
+using an optional `title=Text` parameter.
+
+Each section's title is defined by the parameter name, the section
+display order is defined by the order the parameters appear in the
+URL, and the query results are defined by the parameter value. To
+limit the number of rows in a query use `limit:N`, otherwise the
+entire result set will be shown (up to the user's query limit).
+
+Parameters may be separated from each other using any of the following
+characters, as some users may find one more readable than another:
+`&` or `;` or `,`
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/user-notify.txt b/Documentation/user-notify.txt
new file mode 100644
index 0000000..ae3c2d09
--- /dev/null
+++ b/Documentation/user-notify.txt
@@ -0,0 +1,127 @@
+Gerrit Code Review - Email Notifications
+========================================
+
+Description
+-----------
+
+Gerrit can automatically notify users by email when new changes are
+uploaded for review, after comments have been posted on a change,
+or after the change has been submitted to a branch.
+
+User Level Settings
+-------------------
+
+Individual users can configure email subscriptions by editing
+watched projects through Settings > Watched Projects with the web UI.
+
+Specific projects may be watched, or the special project
+`All-Projects` can be watched to watch all projects that
+are visible to the user.
+
+Change search expressions can be used to filter change notifications
+to specific subsets, for example `branch:master` to only see changes
+proposed for the master branch.
+
+Project Level Settings
+----------------------
+
+Project owners and site administrators can configure project level
+notifications, enabling Gerrit Code Review to automatically send
+emails to team mailing lists, or groups of users. Project settings
+are stored inside of the `refs/meta/config` branch of each Git
+repository, and are placed inside of the `project.config` file.
+
+To edit the project level notify settings, ensure the project owner
+has Push permission already granted for the `refs/meta/config`
+branch. Consult link:access-control.html[access controls] for
+details on how access permissions work.
+
+Initialize a temporary Git repository to edit the configuration:
+====
+  mkdir cfg_dir
+  cd cfg_dir
+  git init
+====
+
+Download the existing configuration from Gerrit:
+====
+  git fetch ssh://localhost:29418/project refs/meta/config
+  git checkout FETCH_HEAD
+====
+
+Enable notifications to an email address by adding to
+`project.config`, this can be done using the `git config` command:
+====
+  git config -f project.config --add notify.team.email team-address@example.com
+  git config -f project.config --add notify.team.email paranoid-manager@example.com
+====
+
+Examining the project.config file with any text editor should show
+a new notify section describing the email addresses to deliver to:
+----
+  [notify "team"]
+  	email = team-address@example.com
+  	email = paranoid-manager@example.com
+----
+
+Each notify section within a single project.config file must have a
+unique name. The section name itself does not matter and may later
+appear in the web UI. Naming a section after the email address or
+group it delivers to is typical. Multiple sections can be specified
+if different filters are needed.
+
+Commit the configuration change, and push it back:
+====
+  git commit -a -m "Notify team-address@example.com of changes"
+  git push ssh://localhost:29418/project HEAD:refs/meta/config
+====
+
+[[notify.name.email]]notify.<name>.email::
++
+List of email addresses to send matching notifications to. Each
+email address should be placed on its own line.
++
+Internal groups within Gerrit Code Review can also be named using
+`group NAME` syntax. If this format is used the group's UUID must
+also appear in the corresponding `groups` file. Gerrit will expand
+the group membership and BCC all current users.
+
+[[notify.name.type]]notify.<name>.type::
++
+Types of notifications to send. If not specified, all notifications
+are sent.
++
+* `new_changes`: Only newly created changes.
+* `all_comments`: Only comments on existing changes.
+* `submitted_changes`: Only changes that have been submitted.
+* `all`: All notifications.
+
++
+Like email, this variable may be a list of options.
+
+[[notify.name.filter]]notify.<name>.filter::
++
+link:user-search.html[Change search expression] to match changes that
+should be sent to the emails named in this section. Within a Git-style
+configuration file double quotes around complex operator values may
+need to be escaped, e.g. `filter = branch:\"^(maint|stable)-.*\"`.
+
+When sending email to a bare email address in a notify block, Gerrit
+Code Review ignores read access controls and assumes the administrator
+has set the filtering options correctly. Project owners can implement
+security filtering by adding the `visibleto:groupname` predicate to
+the filter expression, for example:
+
+====
+  [notify "Developers"]
+  	email = team-address@example.com
+  	filter = visibleto:Developers
+====
+
+When sending email to an internal group, the internal group's read
+access is automatically checked by Gerrit and therefore does not
+need to use the `visibleto:` operator in the filter.
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index 3aafe8c..8a231fe 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -16,9 +16,10 @@
 |All > Open           | status:open '(or is:open)'
 |All > Merged         | status:merged
 |All > Abandoned      | status:abandoned
-|My > Dafts           | has:draft
+|My > Drafts          | is:draft
 |My > Watched Changes | status:open is:watched
 |My > Starred Changes | is:starred
+|My > Draft Comments  | has:draft
 |Open changes in Foo  | status:open project:Foo
 |=================================================
 
@@ -75,7 +76,8 @@
 [[owner]]
 owner:'USER'::
 +
-Changes originally submitted by 'USER'.
+Changes originally submitted by 'USER'. The special case of
+`owner:self` will find changes owned by the caller.
 
 [[ownerin]]
 ownerin:'GROUP'::
@@ -85,7 +87,9 @@
 [[reviewer]]
 reviewer:'USER'::
 +
-Changes that have been, or need to be, reviewed by 'USER'.
+Changes that have been, or need to be, reviewed by 'USER'. The
+special case of `reviewer:self` will find changes where the caller
+has been added as a reviewer.
 
 [[reviewerin]]
 reviewerin:'GROUP'::
@@ -145,7 +149,7 @@
 [[tr,bug]]
 tr:'ID', bug:'ID'::
 +
-Search for changes whose commit message contains 'ID' and matched
+Search for changes whose commit message contains 'ID' and matches
 one or more of the
 link:config-gerrit.html#trackingid[trackingid sections]
 in the server's configuration file.  This is typically used to
@@ -162,7 +166,7 @@
 [[message]]
 message:'MESSAGE'::
 +
-Changes that matches 'MESSAGE' arbitrary string in body commit messages.
+Changes that match 'MESSAGE' arbitrary string in the commit message body.
 
 [[file]]
 file:^'REGEX'::
@@ -213,9 +217,23 @@
 True if there is at least one non-zero score on the change, in any
 approval category, by any user.
 
+is:owner::
++
+True on any change where the current user is the change owner.
+Same as `owner:self`.
+
+is:reviewer::
++
+True on any change where the current user is a reviewer.
+Same as `reviewer:self`.
+
 is:open::
 +
-True if the change is other open or submitted, merge pending.
+True if the change is either open or submitted, merge pending.
+
+is:draft::
++
+True if the change is a draft.
 
 is:closed::
 +
@@ -228,7 +246,7 @@
 [[status]]
 status:open::
 +
-True if the change state is other 'review in progress' or 'submitted,
+True if the change state is either 'review in progress' or 'submitted,
 merge pending'.
 
 status:reviewed::
@@ -250,7 +268,7 @@
 
 status:abandoned::
 +
-Change has been abandoned by the change owner, or administrator.
+Change has been abandoned.
 
 
 Boolean Operators
@@ -286,7 +304,7 @@
 [[labels]]
 Labels
 ------
-Label operators can be used to match approval score given during
+Label operators can be used to match approval scores given during
 a code review.  The specific set of supported labels depends on
 the server configuration, however `CodeReview` and `Verified`
 are the default labels provided out of the box.
@@ -305,7 +323,7 @@
   of change list pages.  Example: `label:R` or `label:V`.
 
 A label name must be followed by a score, or an operator and a score.
-The easiest way to explain these are by example.
+The easiest way to explain this is by example.
 
 `label:CodeReview=2`::
 `label:CodeReview=+2`::
@@ -373,16 +391,20 @@
 starredby:'USER'::
 +
 Matches changes that have been starred by 'USER'.
+The special case `starredby:self` applies to the caller.
 
 watchedby:'USER'::
 +
 Matches changes that 'USER' has configured watch filters for.
+The special case `watchedby:self` applies to the caller.
 
 draftby:'USER'::
 +
-Matches changes that 'USER' has left unpublished drafts on.
+Matches changes that 'USER' has left unpublished draft comments on.
 Since the drafts are unpublished, it is not possible to see the
-draft text, or even how many drafts there are.
+draft text, or even how many drafts there are. The special case
+of `draftby:self` will find changes where the caller has created
+a draft comment.
 
 limit:'CNT'::
 +
diff --git a/Documentation/user-submodules.txt b/Documentation/user-submodules.txt
index 3d14437..cfaf3e9 100644
--- a/Documentation/user-submodules.txt
+++ b/Documentation/user-submodules.txt
@@ -18,7 +18,7 @@
 any gitlinks and .gitmodules file with required info) and if so,
 a new submodule subscription is registered.
 
-When a new commit of a registered submodule is merged, gerrit
+When a new commit of a registered submodule is merged, Gerrit
 automatically updates the subscribers to the submodule with a new
 commit having the updated gitlinks.
 
@@ -31,7 +31,7 @@
 in the official git submodule command documentation.
 
 Imagine a repository called 'super' and another one called 'a'.
-Also consider 'a' available in a running gerrit instance on "server".
+Also consider 'a' available in a running Gerrit instance on "server".
 With this feature, one could attach 'a' inside of 'super' repository
 at path 'a' by executing the following command when being inside
 'super':
@@ -86,12 +86,12 @@
 gitlinks/.gitmodules file.
 
 The branch field of a submodule section is a custom git submodule
-feature for gerrit use. One should always be sure to fill it in
+feature for Gerrit use. One should always be sure to fill it in
 editing .gitmodules file after adding submodules to a super project,
-if it is the intention to make use of the gerrit feature introduced here.
+if it is the intention to make use of the Gerrit feature introduced here.
 
 Any git submodules which are added and not have the branch field
-available in the .gitmodules file will not be subscribed by gerrit
+available in the .gitmodules file will not be subscribed by Gerrit
 to automatically update the superproject.
 
 Detecting and Subscribing Submodules
@@ -114,7 +114,7 @@
 
 Imagine a superproject called 'super' having a branch called 'dev'
 having subscribed to a submodule 'a' on a branch 'dev-of-a'. When a commit
-is merged in branch 'dev-of-a' of 'a' project, gerrit automatically
+is merged in branch 'dev-of-a' of 'a' project, Gerrit automatically
 creates a new commit on branch 'dev' of 'super' updating the gitlink
 to point to the just merged commit.
 
@@ -123,11 +123,11 @@
 
 Gerrit will automatically update only the superprojects that added
 the submodules of urls of the running server (the one described in
-the canonical web url value in gerrit configuration file).
+the canonical web url value in Gerrit configuration file).
 
 The Gerrit instance administrator group should always certify to
 provide the canonical web url value in its configuration file. Users
-should certify to use the url value of the running gerrit instance to
+should certify to use the url value of the running Gerrit instance to
 add/subscribe submodules.
 
 Removing Subscriptions
diff --git a/Documentation/user-upload.txt b/Documentation/user-upload.txt
index 8e05e72..6bdff7a 100644
--- a/Documentation/user-upload.txt
+++ b/Documentation/user-upload.txt
@@ -46,7 +46,7 @@
 
 [TIP]
 Users who frequently upload changes will also want to consider
-starting a `ssh-agent`, and adding their private key to the list
+starting an `ssh-agent`, and adding their private key to the list
 managed by the agent, to reduce the frequency of entering the
 key's passphrase.  Consult `man ssh-agent`, or your SSH client's
 documentation, for more details on configuration of the agent
@@ -57,7 +57,7 @@
 ~~~~~~~~~~~~~~~~~~~
 
 To verify your SSH key is working correctly, try using an SSH client
-to connect to Gerrit's SSHD port.  By default Gerrit is running on
+to connect to Gerrit's SSHD port.  By default Gerrit runs on
 port 29418, using the same hostname as the web server:
 
 ====
@@ -104,7 +104,7 @@
 Create Changes
 ~~~~~~~~~~~~~~
 
-To create new changes for review, simply push into the project's
+To create new changes for review, simply push to the project's
 magical `refs/for/'branch'` ref using any Git client tool:
 
 ====
@@ -316,7 +316,7 @@
 
 repo is a multiple repository management tool, most commonly
 used by the Android Open Source Project.  For more details, see
-link:http://source.android.com/download/using-repo[using repo].
+link:http://source.android.com/source/using-repo.html[using repo].
 
 [[repo_create]]
 Create Changes
diff --git a/ReleaseNotes/ReleaseNotes-2.2.2.txt b/ReleaseNotes/ReleaseNotes-2.2.2.txt
index 3f1f76f..ddfe323 100644
--- a/ReleaseNotes/ReleaseNotes-2.2.2.txt
+++ b/ReleaseNotes/ReleaseNotes-2.2.2.txt
@@ -33,7 +33,7 @@
 +
 Projects now inherit the prolog rules defined in their parent
 project. Submit results from the child project are filtered by the
-parent project using the filter predicate defined the parent's
+parent project using the filter predicate defined in the parent's
 rules.pl. The results of the filtering are then passed up to the
 parent's parent and filtered, repeating this process up to the top
 level All-Projects.
@@ -56,7 +56,7 @@
 * prolog-shell: Simple command line Prolog interpreter
 +
 Define a small interactive interpreter that users or site
-administartors can play around with by downloading the Gerrit WAR
+administrators can play around with by downloading the Gerrit WAR
 file and executing: java -jar gerrit.war prolog-shell
 
 Prolog Predicates
diff --git a/ReleaseNotes/ReleaseNotes-2.5.1.txt b/ReleaseNotes/ReleaseNotes-2.5.1.txt
new file mode 100644
index 0000000..ba4e204
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.5.1.txt
@@ -0,0 +1,94 @@
+Release notes for Gerrit 2.5.1
+==============================
+
+Gerrit 2.5.1 is now available:
+
+link:http://code.google.com/p/gerrit/downloads/detail?name=gerrit-full-2.5.1.war[http://code.google.com/p/gerrit/downloads/detail?name=gerrit-full-2.5.1.war]
+
+There are no schema changes from 2.5, or 2.5.1.
+
+However, if upgrading from a version older than 2.5, follow the upgrade
+procedure in the 2.5 link:ReleaseNotes-2.5.html[Release Notes].
+
+Security Fixes
+--------------
+* Correctly identify Git-over-HTTP operations
++
+Git operations over HTTP should be classified as using AccessPath.GIT
+and not WEB_UI. This ensures RefControl will correctly test for Create,
+Push or Delete access on a reference instead of Owner.
++
+E.g. without this fix project owners are able to force push commits
+via HTTP that are already in the history of the target branch, even
+without having any Push access right assigned.
+
+* Make sure only Gerrit admins can change the parent of a project
++
+Only Gerrit administrators should be able to change the parent of a
+project because by changing the parent project access rights and BLOCK
+rules which are configured on a parent project can be avoided.
++
+The `set-project-parent` SSH command already verifies that the caller
+is a Gerrit administrator, however project owners can change the parent
+project by modifying the `project.config` file and pushing to the
+`refs/meta/config` branch.
++
+This fix ensures that changes to the `project.config` file that change
+the parent project can only be pushed/submitted by Gerrit
+administrators.
++
+In addition it is now no longer possible to
+- set a non-existing project as parent (as this would make the project
+  be orphaned)
+- set a parent project for the `All-Projects` root project (the root
+  project by definition has no parent)
+by pushing changes of the `project.config` file to `refs/meta/config`.
+
+Bug Fixes
+---------
+* Fix RequestCleanup bug with Git over HTTP
++
+Decide if a continuation is going to be used early, before the filter
+that will attempt to cleanup a RequestCleanup. If so don't allow
+entering the RequestCleanup part of the system until the request is
+actually going to be processed.
++
+This fixes the IllegalStateException `Request has already been cleaned
+up` that occurred when running on Jetty and pushing over HTTP for URLs
+where the path starts with `/p/`.
+
+* Match all git fetch/clone/push commands to the command executor
++
+Route not just `/p/` but any Git access to the same thread pool as the
+SSH server is using, allowing all requests to compete fairly for
+resources.
+
+* Fix auto closing of changes on direct push
++
+When a commit is directly pushed into a repository (bypassing code
+review) and this commit has a Change-Id in its commit message then the
+corresponding change is automatically closed if it is open.
+
+* Allow assigning `Push` for `refs/meta/config` on `All-Projects`
++
+The `refs/meta/config` branch of the `All-Projects project` should only
+be modified by Gerrit administrators because being able to do
+modifications on this branch means that the user could assign himself
+administrator permissions.
++
+In addition to being administrator we already require that the
+administrator has the `Push` access right for `refs/meta/config` in
+order to be able to modify it (just as with all other branches
+administrators do not have edit permissions by default).
++
+The problem was that assigning the `Push` access right for
+`refs/meta/config` on the `All-Projects` project was not allowed.
++
+Having the `Push` access right for `refs/meta/config` on the
+`All-Projects` project without being administrator already has no
+effect.
++
+Prohibiting to assign the Push access right for `refs/meta/config` on
+the `All-Project` project was anyway pointless since it was e.g.
+possible to assign the `Push` access right on `refs/meta/*`.
+
diff --git a/ReleaseNotes/ReleaseNotes-2.5.2.txt b/ReleaseNotes/ReleaseNotes-2.5.2.txt
new file mode 100644
index 0000000..ceb23c2
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.5.2.txt
@@ -0,0 +1,138 @@
+Release notes for Gerrit 2.5.2
+==============================
+
+Gerrit 2.5.2 is now available:
+
+link:http://code.google.com/p/gerrit/downloads/detail?name=gerrit-full-2.5.2.war[http://code.google.com/p/gerrit/downloads/detail?name=gerrit-full-2.5.2.war]
+
+There are no schema changes from 2.5, or 2.5.1.
+
+However, if upgrading from a version older than 2.5, follow the upgrade
+procedure in the 2.5 link:ReleaseNotes-2.5.html[Release Notes].
+
+Bug Fixes
+---------
+* Improve performance of ReceiveCommits for repos with many refs
++
+When validating the received commits all existing refs were added as
+uninteresting to the RevWalk. This resulted in bad performance when a
+repository had many refs (>100000). Putting existing 'refs/changes/'
+or 'refs/tags/' into the RevWalk is now avoided, which improves the
+performance.
+
+* Improve Push performance by discarding 'cache-automerge/*' refs
+  early in VisibleRefFilter
++
+For a typical large Git repository, with many refs and lots of cached
+merges, the push time goes down significantly.
+
+* Don't display all files from a merge-commit when auto-merge fails
++
+For merge commits Gerrit shows the difference to the automatic merge
+result. The creation of the auto-merge result may fail, e.g. when the
+merge commit has multiple merge bases (because JGit doesn't support
+this case yet). In this case Gerrit was showing all files from the
+merge commit. This caused several issues:
++
+--
+** the file list was too large for projects with a large number of
+   files
+** Gerrit would send too many false notification emails to users
+   watching changes under certain paths
+** both client and server needed a lot of resources in order to handle
+   such a large list of files
+--
++
+Now the file list for a merge commit will be empty when the creation
+of the auto-merge result fails.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=1726[issue 1726]:
+  Create ref for new patch set on direct push
++
+If a change is in review and a new commit that has the Change-Id of
+this change in its commit message is pushed directly, then a new patch
+set for this commit is created and the change gets automatically
+closed. The problem was that no change ref for this new patch set was
+created and as result the change ref that was shown for the new patch
+set in the WebUI, and which was contained in the patchset-created
+event, was invalid.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=1767[issue 1767]:
+  Remove wrong error message when pushing a new ref fails
++
+If pushing a new ref was rejected because the user was not allowed to
+create it the error message always told the user that he's missing the
+'Create Reference' access right. This message was incorrect in some
+cases. Users that have the 'Create Reference' access right assigned
+are e.g. not allowed to create the ref if:
++
+--
+** they are pushing an annotated tag without having the
+   'Push Annotated Tag' access right
+** they are pushing a signed tag without having the 'Push Signed Tag'
+   access right
+** the project state is set to 'Read Only'
+--
++
+Now the error message just says 'Prohibited by Gerrit'. This generic
+error message is better than a more concrete error message which is
+wrong in same cases because a wrong message is misleading and
+confuses the user.
++
+In addition the description of the 'Prohibited by Gerrit' error in the
+documentation has been updated to explain some additional cases in
+which the 'Prohibited by Gerrit' error occurs.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=1444[issue 1444]:
+  Remove 'Mailing-List' header from sent emails
++
+The non-standard 'Mailing-List' header that is included in the emails
+sent by Gerrit isn't allowed by the Amazon Simple Email Service and is
+now removed.
+
+* Improve SMTP client error messages
++
+The wording of the error messages in the SMTP client was changed to
+make it more clear at exactly what stage in the SMTP transaction the
+server returned an error. Also the server's response text is now
+always included.
++
+In addition it is now ensured that already rejected recipients are
+included in the error message when the server rejects the DATA
+command. Without this there is no way of debugging rejected
+recipients if all recipients are rejected since that typically
+results in a DATA command rejection. Because some SMTP servers (e.g.
+Postfix with the default configuration) delay rejection of HELO/EHLO
+and MAIL FROM commands to the RCPT TO stage, this can happen not only
+for bad recipients.
+
+* Allow time unit variables to be '0'
++
+link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/config-gerrit.html[
+Gerrit Configuration parameters] that expect a numerical time unit as
+value can now be set to '0'.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=1076[issue 1076]:
+  Fix CLA hyperlink on account registration page
++
+The New Contributor Agreement hyperlink on the Account Registration page
+was malformed.
+
+* Fix broken link to repo command reference
++
+The link to the repo command reference in the 'repo upload' section of
+the 'Uploading Changes' documentation was broken.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=1569[issue 1569]:
+Fix unexpected behaviour in the commit-msg hook caused by `GREP_OPTIONS`
++
+If `GREP_OPTIONS` was set, it caused unexpected behaviour in the
+commit-msg hook.  For example if it included a setting like
+`--exclude=".git/*"` it caused a new `Change-Id` line to be appended
+to the commit message on every amend.
++
+`GREP_OPTIONS` is now unset at the beginning of the commit-msg script
+to prevent such problems from occurring.
++
+The `GREP_OPTIONS` setting in the user's environment is unaffected
+by this change.
diff --git a/ReleaseNotes/ReleaseNotes-2.5.3.txt b/ReleaseNotes/ReleaseNotes-2.5.3.txt
new file mode 100644
index 0000000..60efa7a
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.5.3.txt
@@ -0,0 +1,22 @@
+Release notes for Gerrit 2.5.3
+==============================
+
+Gerrit 2.5.3 is now available:
+
+link:http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.5.3.war[http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.5.3.war]
+
+There are no schema changes from any of the 2.5.x versions.
+
+However, if upgrading from a version older than 2.5, follow the upgrade
+procedure in the 2.5 link:ReleaseNotes-2.5.html[Release Notes].
+
+Security Fixes
+--------------
+* Patch vulnerabilities in OpenID client library
++
+Installations using OpenID for authentication were vulnerable to a
+number of attacks over the network.  The openid4java client library
+was identified as the entry point.  In this release Gerrit updated to
+the latest 0.9.8 release, which patches the known attack vectors.
+
+No other changes since 2.5.2.
diff --git a/ReleaseNotes/ReleaseNotes-2.5.4.txt b/ReleaseNotes/ReleaseNotes-2.5.4.txt
new file mode 100644
index 0000000..1657d9b
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.5.4.txt
@@ -0,0 +1,22 @@
+Release notes for Gerrit 2.5.4
+==============================
+
+Gerrit 2.5.4 is now available:
+
+link:http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.5.4.war[http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.5.4.war]
+
+There are no schema changes from any of the 2.5.x versions.
+
+However, if upgrading from a version older than 2.5, follow the upgrade
+procedure in the 2.5 link:ReleaseNotes-2.5.html[Release Notes].
+
+Bug Fixes
+---------
+* Require preferred email to be verified
++
+Some users were able to select a preferred email address that was
+not previously verified. This may have allowed the server to send
+notifications to an invalid destination, resulting in higher than
+usual bounce rates.
+
+No other changes since 2.5.3.
diff --git a/ReleaseNotes/ReleaseNotes-2.5.txt b/ReleaseNotes/ReleaseNotes-2.5.txt
new file mode 100644
index 0000000..55fbb60
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.5.txt
@@ -0,0 +1,1950 @@
+Release notes for Gerrit 2.5
+============================
+
+Gerrit 2.5 is now available:
+
+link:http://code.google.com/p/gerrit/downloads/detail?name=gerrit-full-2.5.war[http://code.google.com/p/gerrit/downloads/detail?name=gerrit-full-2.5.war]
+
+Gerrit 2.5 includes the bug fixes done with
+link:ReleaseNotes-2.4.1.html[Gerrit 2.4.1] and
+link:ReleaseNotes-2.4.2.html[Gerrit 2.4.2]. These bug fixes are *not*
+listed in these release notes.
+
+Schema Change
+-------------
+*WARNING:* This release contains schema changes.  To upgrade:
+----
+  java -jar gerrit.war init -d site_path
+----
+
+*WARNING:* Upgrading to 2.5.x requires the server be first upgraded to 2.1.7 (or
+a later 2.1.x version), and then to 2.5.x.  If you are upgrading from 2.2.x.x or
+newer, you may ignore this warning and upgrade directly to 2.5.x.
+
+Warning on upgrade to schema version 68
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The migration to schema version 68, may result in a warning, which can
+be ignored when running init in the interactive mode.
+
+E.g. this warning may look like this:
+
+----
+Upgrading database schema from version 67 to 68 ...
+warning: Cannot create index for submodule subscriptions
+Duplicate key name 'submodule_subscriptions_access_bySubscription'
+Ignore warning and proceed with schema upgrade [y/N]?
+----
+
+This migration is creating an index for the
+link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/user-submodules.html[submodule feature] in
+Gerrit. When the submodule feature was introduced the index was only
+created when a new site was initialized, but not when Gerrit was
+upgraded. This migration tries to create the index, but it will only
+succeed if the index does not exist yet. If the index exists already,
+the creation of the index will fail. There was no database independent
+way to detect this case and this is why this migration leaves it to the
+user to decide if a failure should be ignored or not. If from the error
+message you can see that the migration failed because the index exists
+already (as in the example above), you can safely ignore this warning.
+
+Upgrade Warnings
+----------------
+
+[[replication]]
+Replication
+~~~~~~~~~~~
+
+Gerrit 2.5 no longer includes replication support out of the box.
+Servers that reply upon `replication.config` to copy Git repository
+data to other locations must also install the replication plugin.
+
+Cache Configuration
+~~~~~~~~~~~~~~~~~~~
+
+Disk caches are now backed by individual H2 databases, rather than
+Ehcache's own private format. Administrators are encouraged to clear
+the `'$site_path'/cache` directory before starting the new server.
+
+The `cache.NAME.diskLimit` configuration variable is now expressed in
+bytes of disk used. This is a change from previous versions of Gerrit,
+which expressed the limit as the number of entries rather than bytes.
+Bytes of disk is a more accurate way to size what is held. Admins that
+set this variable must update their configurations, as the old values
+are too small. For example a setting of `diskLimit = 65535` will only
+store 64 KiB worth of data on disk and can no longer hold 65,000 patch
+sets. It is recommended to delete the diskLimit variable (if set) and
+rely on the built-in default of `128m`.
+
+The `cache.diff.memoryLimit` and `cache.diff_intraline.memoryLimit`
+configuration variables are now expressed in bytes of memory used,
+rather than number of entries in the cache. This is a change from
+previous versions of Gerrit and gives administrators more control over
+how memory is partioned within a server. Admins that set this variable
+must update their configurations, as the old values are too small.
+For example a setting of `memoryLimit = 1024` now means only 1 KiB of
+data (which may not even hold 1 patch set), not 1024 patch sets.  It
+is recommended to set these to `10m` for 10 MiB of memory, and
+increase as necessary.
+
+The `cache.NAME.maxAge` variable now means the maximum amount of time
+that can elapse between reads of the source data into the cache, no
+matter how often it is being accessed. In prior versions it meant how
+long an item could be held without being requested by a client before
+it was discarded. The new meaning of elapsed time before consulting
+the source data is more useful, as it enables a strict bound on how
+stale the cached data can be. This is especially useful for slave
+servers account and permission data, or the `ldap_groups` cache, where
+updates are often made to the source without telling Gerrit to reload
+the cache.
+
+New Features
+------------
+
+Plugins
+~~~~~~~
+
+The Gerrit server functionality can be extended by installing plugins.
+Depending on how tightly the extension code is coupled with the Gerrit
+server code, there is a distinction between
+link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/dev-plugins.html#plugin[plugins] and
+link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/dev-plugins.html#extension[extensions].
+
+* link:#replication[Move replication logic to replication plugin]
++
+This splits all of the replication code out of the core server
+and moves it into a standard plugin.
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/dev-plugins.html[Documentation about
+  plugin development] including instructions for:
+** link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/dev-plugins.html#getting-started[how to get
+   started with plugin development]
+** link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/dev-plugins.html#deployment[plugin
+   deployment/installation]
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/dev-plugins.html#API[API for plugins and
+  extensions]
+
+* Support for link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/dev-plugins.html#ssh[SSH command
+  plugins]
++
+Allows plugin developers to declare additional SSH commands.
+
+* Enable link:#ssh-alias[aliases for SSH commands]
++
+Site administrators can alias SSH commands from a plugin into the
+`gerrit` namespace.
++
+The aliases are configured statically at server startup, but are
+resolved dynamically at invocation time to the currently loaded
+version of the plugin. If the plugin is not loaded, or does not
+define the command, "not found" is returned to the user.
+
+* Support for link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/dev-plugins.html#http[HTTP
+  plugins]
++
+Plugins may contribute to the /plugins/NAME/ URL space.
+
+* Automatic registration of plugin bindings
++
+If a plugin has no modules declared in the manifest, automatically
+generate the modules for the plugin based on the class files that
+appear in the plugin and the `@Export` annotations that appear on
+these concrete classes.
++
+For any non-abstract command that extends SshCommand, plugins may
+declare the command with `@Export("name")` to
+link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/dev-plugins.html#ssh[bind the implementation
+as that SSH command].
++
+Likewise link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/dev-plugins.html#http[HTTP servlets
+can also be bound to URLs].
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/dev-plugins.html#data-directory[Support a data
+  directory for plugins on demand]
+
+* Support serving static/ and Documentation/ from plugins
++
+The static/ and Documentation/ resource directories of a plugin can be
+served over HTTP for any loaded and running plugin, even if it has no
+other HTTP handlers. This permits a plugin to supply icons or other
+graphics for the web UI, or documentation content to help users learn
+how to use the plugin.
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/dev-plugins.html#documentation[Auto-formatting
+  of plugin HTTP pages from Markdown files]
++
+If Gerrit detects that a requested plugin resource does not exist, but
+instead a file with a `.md` extension does exist, Gerrit opens the
+`.md` file and reformats it as html.
+
+* Support of link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/dev-plugins.html#macros[macros in
+  Markdown plugin documentation]
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/dev-plugins.html#auto-index[Automatic
+  generation of an index for the plugin documentation]
+
+* Support for audit plugins
++
+Plugins can implement an `AuditListener` to be informed about auditable
+actions:
++
+----
+  @Listener
+  public class MyAuditTrail extends AuditListener
+----
++
+The plugin must define a plugin module that binds the implementation of
+the audit listener in the `configure()` method:
++
+----
+  DynamicSet.bind(binder(), AuditListener.class).to(MyAuditTrail.class);
+----
+
+* Web UI for plugins
++
+Administrators can see the list of installed plugins in the WebUI
+under `Admin` > `Plugins`. For each plugin the plugin status is shown
+and it is possible to navigate to the plugin documentation.
+
+* Servlet to list plugins
++
+Administrators can retrieve plugin information from a REST interface
+by loading `<server-url>/a/plugins/`.
+
+* Support SSH commands to
+** link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/cmd-plugin-ls.html[list the installed
+   plugins]
+** link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/cmd-plugin-install.html[install plugins]
+** link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/cmd-plugin-enable.html[enable plugins]
+** link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/cmd-plugin-remove.html[disable plugins]
+** link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/cmd-plugin-reload.html[reload plugins]
+
+* Support installation of core plugin on site initialization
+
+* Automatically load/unload/reload plugins
++
+The PluginScanner thread runs every 1 minute by default and loads any
+newly created plugins, unloads any deleted plugins, and reloads any
+plugins that have been modified.
++
+The check frequency can be configured by setting
+link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/config-gerrit.html#plugins.checkFrequency[
+plugins.checkFrequency] in the Gerrit config file. By configuration
+the scanner can also be disabled.
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/dev-plugins.html#classpath[Loading of plugins
+  in own ClassLoader]
+
+* Plugin cleanup in the background
++
+When a plugin is stopped, schedule a Plugin Cleaner task to run
+1 minute later to try and clean out the garbage and release the
+JAR from `$site_path/tmp`.
+
+* Export `LifecycleListener` as extension point
++
+Extensions may need to know when they are starting or stopping.
+Export the interface that they can use to learn this information.
+
+* Support injection of `ServerInformation` into extensions and plugins
++
+Plugins can take this value by injection and learn the current
+server state during their own LifecycleListener. This enables a
+plugin to determine if it is loading as part of server startup, or
+because it was dynamically installed or reloaded by an administrator.
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/dev-plugins.html#getting-started[Maven
+  archetype for creating gerrit plugin projects]
+
+* Enables the use of session management in Jetty
++
+This enables plugins to make use of servlet sessions.
+
+REST API
+~~~~~~~~
+Gerrit now supports a REST like API available over HTTP. The API is
+suitable for automated tools to build upon, as well as supporting some
+ad-hoc scripting use cases.
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/rest-api.html[Documentation of the REST API]
+
+* Support REST endpoints to
+** link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/rest-api.html#changes[query changes]
+** link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/rest-api.html#projects[list projects]
+** link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/rest-api.html#suggest-projects[suggest
+   projects]
+** link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/rest-api.html#accounts_self_capabilities[query
+   the global capabilities of the calling user]
+
+* Support link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/rest-api.html#authentication[anonymous
+  and authenticated access] to the REST endpoints
+
+* Support link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/rest-api.html#output[JSON output
+  format] for the REST endpoints
+
+The new REST API is used from the Gerrit WebUI.
+
+Some of the methods from the old internal JSON-RPC interface were
+completely replaced by the new REST API and got deleted:
+
+* `ProjectAdminService.visibleProjects(AsyncCallback<ProjectList>)`
+* `ProjectAdminService.suggestParentCandidates(AsyncCallback<List<Project>>)`
+* `ChangeListService.myStarredChangeIds(AsyncCallback<Set<Change.Id>>)`
+* `ChangeListService.allQueryNext(String, String, int, AsyncCallback<SingleListChangeInfo>)`
+* `ChangeListService.allQueryPrev(String, String, int, AsyncCallback<SingleListChangeInfo>)`
+* `ChangeListService.forAccount(Account.Id, AsyncCallback<AccountDashboardInfo>)`
+
+[[query-deprecation]]
+In addition the `/query` API has been deprecated. By default it is
+still available but server administrators may disable it by setting
+the link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/config-gerrit.html#site.enableDeprecatedQuery[
+`site.enableDeprecatedQuery`] parameter in the Gerrit config file. This
+allows to enforce tools to move to the new API.
+
+Web
+~~~
+
+Change Screen
+^^^^^^^^^^^^^
+
+* Display commit message in a box
++
+The commit message on the change screen is now placed in a box with a
+title and emphasis on the commit summary. The star icon and the
+permalink are displayed in the box header. The header from the change
+screen is removed as it only held duplicate information.
+
+* Open the dependency section automatically when the change is needed
+  by an open change
+
+* Only show a change as needed by if its current patch set depends on
+  the change
+
+* Show only changes of the same project in the 'Depends On' section
++
+If two projects share the same history it can happen that the same
+commit is pushed for both projects, resulting in two changes. If now
+a successor commit is pushed for one of the projects, the resulting
+successor change was wrongly listing both changes in the 'Depends On'
+section. Now only the predecessor change of the own project is listed.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=1383[issue 1383]:
+  Display the approval table on the PublishCommentsScreen.
++
+So far the approval table that shows the reviewers and their current
+votes was only shown on the ChangeScreen. Now it is also shown on the
+PublishCommentScreen. This allows the reviewer to see all existing
+votes and reviewers when doing their own voting and publishing of
+comments. Seeing the existing votes helps the reviewer in
+understanding which votes are still required before the change can be
+submitted.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=1380[issue 1380]:
+  Display time next to change comments
++
+When a comment was posted yesterday, or any time older than 1 day but
+less than 1 year ago, display the time too. Display "May 2 17:37" rather
+than just "May 2".
+
+* Only show "Can Merge" when the change is new or draft
+
+* Allow auto suggesting reviewers to draft changes
++
+Auto completing users for draft changes did't work as the other
+users didn't have access to the drafts. The visibility check for
+the reviewer suggestion is now skipped.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=1294[issue 1294]:
+  Shorten subject of parent commit for displaying in the UI
++
+If the parent commit has a very long subject (> 80 characters) shorten
+the subject for displaying it in the Gerrit web UI on the change screen.
+This avoids that the 'Parent(s)' cell for the patch set becomes very
+wide.
+
+* If subject is shortened for displaying in the UI indicate this by '...'
++
+If a commit has a very long subject line (> 80 characters) it is
+shortened when it is displayed in the Gerrit Web UI. Indicate to the
+user that the subject was shortened by appending '...' to the shortened
+subject.
++
+Also the subject is now cropped after a whitespace if possible.
+
+* Insert Change-Id for revert commits
++
+The 'Revert Change' action on a merged change allows to create a new
+change that reverts the merged change. The commit message of the revert
+commit now contains a Change-Id.
++
+It is convenient if a Change-Id is automatically created and inserted
+into the commit message of the revert commit since it makes rebasing of
+the revert commit easier.
+
+* Use more gentle shade of red to highlight outdated dependencies
+
+Patch Screens
+^^^^^^^^^^^^^
+
+* New patch screen header
++
+A new patch screen header was added that is displayed above both the
+side-by-side and unified views. The new header contains actual links to
+the available patchsets and shows which patchset is being currently
+displayed.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=1192[issue 1192]:
+  Add download links to the unified diff view
+
+* Improvement of the side-by-side viewer table
++
+The line number column for the right side was moved to be on the far
+right of the table, so that the layout now looks like this:
++
+----
+  1 |  foo       |       bar   | 1
+  2 |  hello     |       hello | 2
+----
++
+This looks nicer when reading a lot of code, as the line numbers are
+less relevant than the code itself which is now in the center of the
+UI.
++
+Line numbers are still links to create comment editors, but they
+use a light shade of gray and skip the underline decoration, making
+them less visually distracting.
++
+Skip lines now use a paler shade of blue and also hide the fact they
+contain anchors, until you hover over them and the anchor shows up.
++
+The expand before and after are changed to be arrows showing in
+which direction the lines will appear above or below the skip
+line.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=626[issue 626]:
+  Option to display line endings
++
+There is a new user preference that allows to display Windows EOL/Cr-Lf.
+'\r' is shown in a dotted-line box (similar to how '\r' is displayed in
+GitWeb).
+
+* Streamlined review workflow
++
+A link was added next to the "Reviewed" checkbox that marks the current
+patch as reviewed and goes to the next unreviewed patch.
+
+* Add key commands to mark a patch as reviewed
++
+Add key commands
++
+. to toggle the reviewed flag for a patch ('m')
++
+and
++
+. to mark the patch as reviewed and navigate to the next unreviewed
+patch ('M').
+
+* Use download icons instead of the `Download` text links
+
+User Dashboard
+^^^^^^^^^^^^^^
+* Support for link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/user-custom-dashboards.html[custom
+  dashboards]
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=1407[issue 1407]:
+  Improve highlighting of unreviewed changes in the user's dashboard
++
+A change will be highlighted as unreviewed if
++
+. the user is reviewer of the change but hasn't published any change
+  message for the current patch set
+. the user has published a change message for the current patch set,
+  but afterwards the change owner has published a change message on
+  the change
+
+* Sort outgoing reviews in the user dashboard by created date
+
+* Sort incoming reviews in the user dashboard by updated date
++
+Sorting the incoming reviews by last updated date, descending, places
+the most recently updated reviews at the top of the list for a user,
+and the oldest stale at the bottom. This may help users to identify
+items to take immediate action on, as they appear closer to the top.
+
+Access Rights Screen
+^^^^^^^^^^^^^^^^^^^^
+
+* Display error if modifying access rights for a ref is forbidden
++
+If a user is owner of at least one ref he is able to edit the access
+rights on a project. If he adds access rights for other refs, these
+access rights were silently ignored on save. Instead of this now an
+error message is displayed to inform the user that he doesn't have
+permissions to do the update for these refs.
++
+In case of such an error the project access screen stays in the edit
+mode so that the unsaved modifications are not lost. The user may now
+propose the changes to the access rights through code review.
+
+* Allow to propose changes to access rights through code review
++
+Users that are able to upload changes for code review for the
+`refs/meta/config` branch can now propose changes to the project access
+rights through code review directly from the ProjectAccessScreen.
++
+When editing the project access rights there is a new button
+'Save for Review' which will create a new change for the access
+rights modifications. Project owners are automatically added as
+reviewer to this change. If a project owner agrees to the access rights
+modifications he can simply approve and submit the change.
+
+* Show all access rights in WebUI if user can read `refs/meta/config`
++
+Users who can read the `refs/meta/config` branch, can see all access
+rights by fetching this branch and looking at the `project.config`
+file. Now they can see the same information in the web UI.
+
+* Allow extra group suggestions for project owners
++
+When suggesting groups to a user, only groups that are visible to the
+user are suggested. These are those group that the user is member of.
+For project owners now also groups to which they are not a member are
+suggested when editing the access rights of the project.
+
+Other
+^^^^^
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=1592[issue 1592]:
+  Ask user to login if change is not found
++
+Accessing a change URL was failing with 'Application Error - The page
+you requested was not found, or you do not have permission to view this
+page' if the user was not signed in and the change was not visible to
+`Anonymous Users`. Instead Gerrit now asks the user to login and
+afterwards shows the change to the user if it exists and is visible.
+If the change doesn't exist or is not visible, the user will still get
+the NotFoundScreen after sign in.
+
+* Link to owner query from user names
++
+Instead of linking from a user name to the user's dashboards, link to
+a search for changes owned by that user.
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/config-gerrit.html#gerrit.reportBugUrl[Allow
+  configuring the `Report Bug` URL]
++
+Let site administrators direct users to their own ticket queue, as for
+many servers most of the reported bugs are small internal problems like
+asking for a repository to be created or updating group memberships.
+
+* On project creation allow choosing the parent project from a popup
++
+In the create project UI a user can now browse all projects and select
+one as parent for the new project.
+
+* Check for open changes on branch deletion
++
+Check for open changes when deleting a branch in the Gerrit WebUI.
+Delete a branch only if there are no open changes for this branch.
+This makes users aware of open changes when deleting a branch.
+
+* Enable ProjectBranchesScreen for the `All-Projects` project
++
+This allows to see the branches of the `All-Projects` project in the
+web UI.
+
+* Show for each project in the project list a link to the repository
+  browser (e.g. GitWeb).
+
+* Move the project listing menu items to a new top-level item
++
+Finding the project listing was very opaque to end users. Nobody
+expected to look under `Admin` and furthermore, anonymous users were
+unable to find that link at all.
++
+Introduced a new top-level `Projects` menu that has `List` in it to
+take you to the project listing.
++
+In addition the `Create new project` link from the top of that listing
+was moved to this new menu.
+
+* Move the Groups and Plugins menu items to the top level
++
+The top-level Admin menu is removed as it is now unnecessary after the
+Projects, Groups and Plugins menu items were moved to the top-level.
+
+* Move form for group creation to own screen
++
+Move the form for the group creation from the GroupListScreen to an
+own new CreateGroupScreen and add a link to this screen at the
+beginning of the GroupListScreen. The link to the CreateGroupScreen is
+only visible if the user has the permission to create new groups.
+
+* Drop the `Owners` column from the group list screen
++
+The `Owners` column on the group list screen has been dropped in order
+to link:#performance-issue-on-showing-group-list[speed up the loading
+of the group list screen].
+
+* Drop the `Group Type` column from the group list screen
++
+Since link:#migrate-ldap-groups[the LDAP group type was removed] there
+is no need to display the group type on the group list screen anymore.
+There are only 3 `SYSTEM` groups using well known names, and everything
+else has the type `INTERNAL`.
+
+* When adding a user to a group create an account for the user if needed
++
+Trying to add a user to a group that doesn't have an account fails with
+'... is not a registered user.'. Now adding a user to a group does not
+immediately fail if there is no account for the user, but it tries to
+authenticate the user and if the authentication is successful a user
+account is automatically created, so that the user can be added to the
+group. This only works if LDAP is used as user backend.
++
+This allows to add users to groups that did not log in into Gerrit
+before.
+
+* Differentiate between draft changes and draft comments
++
+Show the draft changes of the user when he clicks on `My` > `Drafts`.
+The user's draft comments are now available under `My` >
+`Draft Comments`.
+
+* Show NotFoundScreen if a user that can't create projects tries to
+  access the ProjectCreationScreen
+
+* Add Edit, Reload next to non-editable Full Name field
++
+If the user database is actually an external system users might need go
+to another server to edit their account data, and then re-import their
+account data by going through a login cycle. This is highly similar to
+LDAP where the directory provides account data and its refreshed every
+time the user visits the `/login/` URL handler.
++
+The URL for the external system can be configured for the
+link:#custom-extension[`CUSTOM_EXTENSION`] auth type.
+
+Access Rights
+~~~~~~~~~~~~~
+
+* Restrict rebasing of a change in the web UI to the change owner and
+  the submitter
+
+* Add a new link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/access-control.html#category_rebase[
+  access right to permit rebasing changes in the web UI]
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=930[issue 930]:
+  Add new link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/access-control.html#category_abandon[
+  access right for abandoning changes]
+
+* Check if user can upload in order to restore
++
+Restoring a change is similar to uploading a new change. If a branch
+gets closed by removing the access rights to upload new changes it
+shouldn't be possible to restore changes for this branch.
+
+[[hide-config]]
+* Make read access to `refs/meta/config` by default exclusive to
+  project owners
++
+When initializing a new site a set of default access rights is
+configured on the `All-Projects` project. These default access rights
+include read access on `refs/*` for `Anonymous Users` and read access
+on `refs/meta/config` for `Project Owners`. Since the read access on
+`refs/meta/config` for `Project Owners` was not exclusive,
+`Anonymous users` were able to access the `refs/meta/config` branch
+which by default should only be accessible by the project owners.
+
+Search
+~~~~~~
+* Offer suggestions for the search operators in the search panel
++
+There are many search operators and it's difficult to remember all of
+them. Now the search operators are suggested as the user types the
+query.
+
+* Support alias `self` in queries
++
+Writing an expression like "owner:self status:open" will now identify
+changes that the caller owns and are still open. This `self` alias
+is valid in contexts where a user is expected as an argument to a
+query operator.
+
+* Add parent(s) revision information to output of query command
+
+* Add owner username to output of query command
+
+* `/query` API has been link:#query-deprecation[deprecated]
+
+SSH
+~~~
+* link:http://code.google.com/p/gerrit/issues/detail?id=1095[issue 1095]
+  link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/cmd-set-account.html[SSH command to manage
+  accounts]
+
+* On link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/cmd-create-account.html[account creation] a
+  password for HTTP can be specified.
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/cmd-set-project.html[SSH command to manage
+  project settings]
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/cmd-test-submit-rule.html[SSH command to test
+  submit rules]
++
+The command creates a fresh Prolog environment and loads a Prolog
+script from stdin. `can_submit` is then queried and the results are
+returned to the user.
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/cmd-ban-commit.html[SSH command to ban
+  commits]
+
+[[ssh-alias]]
+* Enable aliases for SSH commands
++
+Site administrators can define aliases for SSH commands in the
+link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/config-gerrit.html#ssh-alias[`ssh-alias` section]
+of the Gerrit configuration.
+
+* Add submit records to the output of the
+  link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/cmd-query.html[query] SSH command:
++
+Add a command line option to the `query` SSH command to include submit
+records in the output.
++
+This facilitates the querying of information relating to the submit
+status from the command line and by API clients, including information
+such as whether the change can be submitted as-is, and whether the
+submission criteria for each review label has been met.
+
+* Support JSON output format for the
+  link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/cmd-ls-projects.html[ls-projects] SSH command
+
+* Support creation of multiple branches in
+  link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/cmd-create-project.html[create-project] SSH
+  command
++
+In case if a project has some kind of waterfall automerging
+a->b->c it is convenient to create all these branches at the
+project creation time.
++
+e.g. '.. gerrit create-project -b master -b foo -b bar ...'
+
+* Add verbose output option to
+  link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/cmd-ls-groups.html[ls-groups] command
++
+The verbose mode enabled by the new option makes the ls-groups
+command output a tab-separated table containing all available
+information about each group (though not its members).
+
+Documentation
+~~~~~~~~~~~~~
+
+Commands
+^^^^^^^^
+
+* document for the link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/cmd-create-group.html[`create-group`]
+  command that for unknown users an account is automatically created if
+  the LDAP authentication succeeds
+
+* Update documentation and help text for the
+  link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/cmd-review.html[`review`] SSH command
++
+The review command can be applied to multiple changes, but the
+help text was written in singular tense.
++
+Add a paragraph in the documentation explaining that the
+`--force-message` option will not be effective if the `review` command
+fails because the user is not permitted to change the label.
+
+* Clarify that `init --batch` doesn't drop old database objects
+
+* Update the list of unsupported slave commands
+
+* Fix link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/cmd-stream-events.html[`stream-events`]
+  documentation
++
+Some attributes contained in the events were not described, for a few
+others the name was given in a wrong case.
+
+* Fix and complete synopsis of commands
+
+Access Control
+^^^^^^^^^^^^^^
+
+* Clarify the ref format for
+  link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/access-control.html#category_push_merge[`Push
+  Merge Commit`]
++
+Elaborate on the required format of the ref used for `Push Merge Commit`
+access right entries to avoid user confusion when granting access to
+`refs/heads/*` still doesn't allow them to push any merge commits.
+
+* Document the
+  link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/access-control.html#capability_emailReviewers[
+  `emailReviewers`] capability
+
+Error
+^^^^^
+* Improve documentation of link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/error-change-closed.html[
+  `change closed` error]
++
+The `change closed` error can also occur when trying to submit a
+review label with the SSH review command onto a change that has
+been closed (submitted and merged, or abandoned) or onto a patchset
+that has been replaced by a newer patchset.
+
+* Correct documentation of `invalid author` and `invalid committer`
+  errors
++
+The error messages `you are not committer ...` and `you are not
+author ...` were replaced with `invalid author` and `invalid
+committer`.
+
+* Describe that the `prohibited by Gerrit` error is returned if pushing
+  a tag fails because the tagger is somebody else and the `Forge
+  Committer` access right is not assigned.
+
+Dev
+^^^
+
+* Update push URL in link:../SUBMITTING_PATCHES[SUBMITTING_PATCHES]
++
+Pushes are now accepted at the same address as clone/fetch/pull.
+
+* Update link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/dev-contributing.html[contributor
+  document]
++
+We now prefer to use Guava (previously known as Google Collections).
+
+* Fixed broken link to source code
++
+Updated the documentation source code links to point to:
+http://code.google.com/p/gerrit/source/checkout
+
+* State link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/dev-eclipse.html#known-problems[known issues]
+  when debugging Gerrit with Eclipse
+
+* Improved the section on
+  link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/dev-eclipse.html#hosted-mode[hosted mode
+  debugging]
++
+The existing section on hosted mode debugging left out a couple of
+steps, and the requirement to use `DEVELOPMENT_BECOME_ANY_ACCOUNT`
+instead of `OpenID` was not mentioned anywhere.
+
+* Add a link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/dev-release.html[release preparation
+  document]
++
+Document what it takes to make a Gerrit stable or stable-fix release,
+and how to release Gerrit subprojects.
+
+Other
+^^^^^
+* Add link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/prolog-cookbook.html[Cookbook for Prolog
+  submit rules]
++
+A new document providing a step by step introduction into implementing
+specific submit policies using Prolog based submit rules was added.
+
+* Describe link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/refs-notes-review.html[
+  `refs/notes/review` and its contents]
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/config-mail.html[Document `RebasedPatchSet.vm`
+  and `Reverted.vm` mail templates]
+
+* Specify output file for curl commands in documentation
++
+For downloading the `commit-msg` hook and the `gerrit-cherry-pick`
+script users can either use scp or curl. Specify the output file for
+each curl command so that the result is equal to the matching scp
+command.
+
+* Document that user must be in repository root to install `commit-msg`
+  hook
+
+* Add some clarifications to the
+  link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/install-quick.html[quick installation guide]
+
+* Add missing documentation about
+  link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/config-gerrit.html#hooks[hook configuration]
++
+Add documentation of hook config for `change-restored`, `ref-updated`
+and `cla-signed` hooks.
+
+* Document that the commit message hook file should be executable
+
+* Mention that also MySQL supports replication, not just Postgres
+
+* Make sorting of release notes consistent so that the release notes
+  for the newest release is always on top
+
+* Various corrections
++
+Correct typos, spelling mistakes, and grammatical errors.
+
+Dev
+~~~
+* Add link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/dev-release.html#plugin-api[script for
+  releasing plugin API jars]
+
+* Pushes are now accepted at the same address as clone/fetch/pull
++
+To submit patches commits can be pushed to
+https://gerrit.googlesource.com/gerrit
+
+* Add `-Pchrome`, `-Pwebkit`, `-Pfirefox` aliases for building
++
+This makes it easier to build for the browser you want to
+test on, rather than remembering what its GWT name is.
+
+* Disable assertions for KeyCommandSet when running in gwtdebug mode
++
+The assertions in the KeyCommandSet class cause exceptions when a
+KeyCommand is registered several times.
+
+* Add the run profiles to the favorites menu
+
+* Add Intellij IDEA files to ignore list
+
+* Move local Maven repository to Google Cloud Storage
+
+* Make sure asciidoc uses unix line endings in generated HTML.
++
+Use an explicit asciidoc attribute to make sure the produced HTML will
+always contain unix line endings.  This will help in producing build
+results that are better comparable by size.
+
+* Remove timestamp from all `org.eclipse.core.resources.prefs` files
++
+Eclipse overwrites these files when we import projects using m2e.
+Eclipse 3 writes a timestamp at the top of these files making the Git
+working tree dirty.  Eclipse 4 (Juno) still overwrites these files but
+doesn't write the timestamp.  This should help to keep the working tree
+clean.  However, since the timestamp is currently present in these
+files, Eclispe 4 would still make them dirty by overwriting and
+effectively removing the timestamp.
++
+This change removes the timestamp from these files. This helps those
+using Eclipse 4 and doesn't make it worse for those still using Eclispe
+3.
+
+* Add Maven profile to skip build of plugin modules
++
+Building the plugin modules ('Plugin API' and 'Plugin Archetype') may
+take a significant amount of time (since many jars are downloaded).
+During development it is not needed to build the plugin modules. A new
+Maven profile was added that skips the build of the plugin modules,
+so that developers have a faster turnaround. This profile is called
+`no-plugins` and it's active by default. To include the plugin modules
+into the build activate the `all` profile:
++
+----
+  mvn clean package -P all
+----
++
+The script to make release builds has been adapted to activate the
+`all` profile so that the plugin modules are always built for release
+builds.
+
+Mail
+~~~~
+
+* Add unified diff to newchange mail template
++
+Add `$email.UnifiedDiff` as new macro to the `NewChange.vm` mail
+template. This macro is expanded to a unified diff of the patch.
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/config-gerrit.html#sendemail.includeDiff[
+  sendemail.includeDiff]: Enable `$email.UnifiedDiff` in `NewChange.vm`
++
+Instead of making site administrators hack the email template, allow
+admins to enable the diff feature by setting a configuration variable
+in `gerrit.config`.
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/config-gerrit.html#sendemail.maximumDiffSize[
+  sendemail.maximumDiffSize]: Limit the size of diffs sent by email
++
+If a unified diff included in an email will exceed the limit configured
+by the system administrator, only the affected file paths are listed in
+the email instead. This gives interested parties some context on the
+size and scope of the change, without killing their inbox.
+
+* Catch all exceptions when emailing change update
+
+* Allow unique from address generation
++
+Allow the from email address to be a ParameterizedString that handles
+the `${userHash}` variable. The value of the variable is the md5 hash
+of the user name. This allows unique generation of email addresses, so
+GMAIL threads names of users in conversations correctly. For example,
+the from pattern for gerrit-review defined in the Gerrit configuration
+looks like this:
++
+----
+  [sendemail]
+    from = ${user} <noreply-gerritcodereview+${userHash}@google.com>
+----
+
+* Show new change URLs in the body of the new change email
++
+Some email clients hide the signature section of an email
+automatically.  If there are no reviewers listed on a new change,
+such as when a change is pushed over HTTP and a notification is
+automatically sent out to any subscribed watchers, the URL was
+hidden inside of the signature and not readily available.
++
+Show the URL right away in the body.
+
+Miscellaneous
+~~~~~~~~~~~~~
+* Back in-memory caches with Guava, disk caches with H2
++
+Instead of using Ehcache for in-memory caches, use Guava. The Guava
+cache code has been more completely tested by Google in high load
+production environments, and it tends to have fewer bugs. It enables
+caches to be built at any time, rather than only at server startup.
++
+By creating a Guava cache as soon as it is declared, rather than
+during the LifecycleListener.start() for the CachePool, we can promise
+any downstream consumer of the cache that the cache is ready to
+execute requests the moment it is supplied by Guice. This fixes a
+startup ordering problem in the GroupCache and the ProjectCache, where
+code wants to use one of these caches during startup to resolve a
+group or project by name.
++
+Tracking the Gauva backend caches with a DynamicMap makes it possible
+for plugins to define their own in-memory caches using CacheModule's
+cache() function to declare the cache. It allows the core server to
+make the cache available to administrators over SSH with the gerrit
+show-caches and gerrit `flush-caches` commands.
++
+Persistent caches store in a private H2 database per cache, with a
+simple one-table schema that stores each entry in a table row as a
+pair of serialized objects (key and value). Database reads are gated
+by a BloomFilter, to reduce the number of calls made to H2 during
+cache misses. In theory less than 3% of cache misses will reach H2 and
+find nothing. Stores happen on a background thread quickly after the
+put is made to the cache, reducing the risk that a diff or web_session
+record is lost during an ungraceful shutdown.
++
+Cache databases are capped around 128M worth of stored data by running
+a prune cycle each day at 1 AM local server time. Records are removed
+from the database by ordering on the last access time, where last
+accessed is the last time the record was moved from disk to memory.
+
+* Add OpenID SSO support.
++
+Setting `OPENID_SSO` for
+link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/config-gerrit.html#auth.type[`auth.type`] in the
+`gerrit.config` will allow the admin to specify an SSO entry point URL
+so that users clicking on "Sign In" are sent directly to that URL.
+
+* Git over HTTP BasicAuth against Gerrit basic auth.
++
+Allows the configuration of native Gerrit username/password
+authentication scheme used for Git over HTTP BasicAuth, as alternative
+of the default DigestAuth scheme against the random generated password
+on Gerrit DB.
++
+Example setting for link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/config-gerrit.html#auth.type[
+`auth.type`] and link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/config-gerrit.html#auth.gitBasicAuth[
+`auth.gitBasicAuth`]:
++
+----
+  [auth]
+    type = LDAP
+    gitBasicAuth = true
+----
++
+With this configuration Git over HTTP protocol will be authenticated
+using `HTTP-BasicAuth` and credentials checked on LDAP.
+
+* Abstract group systems into GroupBackend interface
++
+Group backends are supposed to use unique prefixes to isolate the
+namespaces. E.g. the group backend for LDAP is using `ldap/` as prefix
+for the group names.
++
+This means that to refer to an LDAP group in the WebUI the group name
+needs to be prefixed with the `ldap/` string. E.g. if there is a group
+in LDAP which is called "Developers", Gerrit will suggest this group
+when the user types `ldap/De`.
++
+WARNING: External groups are not anymore allowed to be members of
+internal groups.
+
+[[migrate-ldap-groups]]
+* Migrate existing internal LDAP groups
++
+Previously, LDAP groups were mirrored in the AccountGroup table and
+given an Id and UUID the same as internal groups. Update these groups
+to be backed by only a GroupReference, with a special "ldap:" UUID
+prefix. Migrate all existing references to the UUID in ownerGroupUUID
+and any `project.config`.
++
+This made the LDAP group type obsolete and it was removed.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=548[issue 548]:
+  Make commands to download patch sets
+  link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/config-gerrit.html#download.command[configurable]
++
+For patch sets on the ChangeScreen different commands for downloading
+the patch sets are offered. For some installations not all commands are
+needed. Allow Gerrit administrators to configure which download
+commands should be offered.
+
+* Add more link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/config-gerrit.html#theme[theme color
+  options]
++
+** Add a theme option to change outdated background color
+** Add odd/even row background color for tables such as list of open
+reviews.  This makes them more visible without clicking on them.
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/user-notify.html[Add `notify` section in
+  `project.config`]
++
+The notify section allows project owners to include emails to users
+directly from `project.config`. This removes the need to create fake
+user accounts to always BCC a group mailing list.
+
+* Include the contributor agreements in the `project.config` and
+  migrate contributor agreements to `All-Projects`
++
+Update the parsing of `project.config` to support the contributor
+agreements.
++
+Add a new schema to move the ContributorAgreement, AccountAgreement,
+and AccountGroupAgreement information into the `All-Projects`
+`project.config`.
+
+* Add `sameGroupVisibility` to `All-Projects` `project.config`
++
+The `sameGroupVisiblity` is needed to restrict the visibility of
+accounts when `accountVisibility` is `SAME_GROUP`. Namely, this is a
+way to make sure the `autoVerify` group in a `contributor-agreements`
+section is never suggested.
+
+* Add change topic in hook arguments
++
+It was not possible for hook scripts to include topic-specific
+behaviour because the topic name was not included in the arguments.
+
+* Add `--is-draft` argument on `patchset-created` hook
++
+The `--is-draft` argument will be passed with either `true` if
+the patchset is a draft, or `false` otherwise.
++
+This can be used by hooks that need to behave differently if the
+change is a draft.
+
+* Log sign in failures on info level
++
+If for a user signing in into the Gerrit web UI fails, this can have
+many reasons, e.g. username is wrong, password is wrong, user is marked
+as inactive, user is locked in the user backend etc. In all cases the
+user just gets a generic error message 'Incorrect username or
+password.'. Gerrit administrators had trouble to find the exact reason
+for the sign in problem because the corresponding AccountException was
+not logged.
+
+* Do not log 'Object too large' as error with full stacktrace
++
+If a user pushes an object which is larger than the configured
+`receive.maxObjectSizeLimit` parameter, the push is rejected with an
+'Object too large' error. In addition an error log entry with the full
+stacktrace was written into the error log.
++
+This is not really a server error, but just a user doing something that
+is not allowed, and thus it should not be logged as error. For a Gerrit
+administrator it might still be interesting how often the limit is hit.
+This is why it makes sense to still log this on info level.
++
+For the user pushing a too large object we now do not print the
+'fatal: Unpack error, check server log' message anymore, but only the
+'Object too large' error message.
+
+* Add better explanations to rejection messages
++
+Provide information to the user why a certain push was rejected.
+
+* Automatic schema upgrade on Gerrit startup
++
+In case when Gerrit administrator(s) don't have a direct access to the
+file system where the review site is located it gets difficult to
+perform a schema upgrade (run the init program). For such cases it is
+convenient if Gerrit performs schema upgrade automatically on its
+startup.
++
+Since this is a potentially dangerous operation, by default it will not
+be performed. The configuration parameter
+link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/config-gerrit.html#site.upgradeSchemaOnStartup[
+site.upgradeSchemaOnStartup] is used to switch on automatic schema
+upgrade.
+
+* Shorten column names that are longer than 30 characters
++
+Some databases can't deal with column names that are longer than 30
+characters. Examples are MaxDB and
+link:http://groups.google.com/group/repo-discuss/browse_thread/thread/ecb713d42c04ae8a/cc963525d8247a17?lnk=gst#cc963525d8247a17[Oracle].
++
+Gerrit had two column names in the `accounts` table that exceeded the
+30 characters: `displayPatchSetsInReverseOrder`,
+`displayPersonNameInReviewCategory`
++
+These 2 columns were renamed so that their names fit within the 30
+character range.
+
+* Increase the maximum length for tracking ID's to 32 characters
++
+So far tracking ID's had a maximum length of only 20 characters.
+
+* Set `GERRIT_SITE` in Gerrit hooks as environment variable
++
+Allows development of hooks parametrised on Gerrit location. This can
+be useful to allow hooks to load the Gerrit configuration when needed
+(from `$GERRIT_SITE`) or even store their additional config files under
+`$GERRIT_SITE/etc` and retrieve them at startup.
+
+* Add an exponentially rolling garbage collection script
++
+`git-exproll.sh` is a git garbage collection script aimed specifically
+at reducing exccessive garbage collection and particularly large
+packfile churn for Gerrit installations.
++
+Excessive garbage collection on "dormant" repos is wasteful of both CPU
+and disk IO.  Large packfile churn can lead to heavy RAM and FS usage
+on Gerrit servers when the Gerrit process continues to hold open the
+old delete packfiles.  This situation is most detrimental when jgit is
+configured with large caching parameters.  Aside from these downsides,
+running git gc often can be very beneficial to performance on servers.
+This script attempts to implement a git gc policy which avoids the
+downsides mentioned above so that git gc can be comfortably run very
+regularly.
++
+`git-exproll.sh` uses keep files to manage which files will get
+repacked.  It also uses timestamps on the repos to detect dormant repos
+to avoid repacking them at all.  The primary packfile objective is to
+keep around a series of packfiles with sizes spaced out exponentially
+from each other, and to roll smaller packfiles into larger ones once
+the smaller ones have grown.  This strategy attempts to balance disk
+space usage with avoiding rewriting large packfiles most of the time.
++
+The exponential packing objective above does not save a large amount of
+time or CPU, but it does prevent the packfile churn.  Depending on repo
+usage, however the dormant repo detection and avoidance can result in a
+very large time savings.
+
+* Automatically flush persistent H2 cache if the existing cache entries
+  are incompatible with the cache entry class and thus can't be
+  deserialized
+
+* Unpack JARs for running servers in `$site_path/tmp`
++
+Instead of unpacking a running server into `~/.gerritcodereview/tmp`
+only use that location for commands like init where there is no active
+site. From gerrit.sh always use `$site_path/tmp` for the JARs to
+isolate servers that run on the same host under the same UNIX user
+account.
+
+[[custom-extension]]
+* Allow for the `CUSTOM_EXTENSION` `auth.type` to configure URLs for
+  editing the user name and obtaining an HTTP password
++
+Allow `CUSTOM_EXTENSION` auth type to supply by `auth.editFullNameUrl`
+a URL in the web UI that links users to the other account system,
+where they can edit their name, and then use another reload URL to
+cycle through the `/login/` step and refresh the data cached by Gerrit.
++
+Allow `CUSTOM_EXTENSION` auth type to supply by `auth.httpPasswordUrl`
+a URL in the web UI that allows users to obtain an HTTP password.
++
+Like the rest of the `CUSTOM_EXTENSION` stuff, this is hack that will
+eventually go away when there is proper support for authentication
+plugins.
+
+Performance
+~~~~~~~~~~~
+[[performance-issue-on-showing-group-list]]
+* Fix performance issues on showing the list of groups in the Gerrit
+  WebUI
++
+Loading `Admin` > `Groups` on large servers was very slow. The entire
+group membership database was downloaded to the browser when showing
+just the list of groups.
++
+Now the amount of data that needs to be downloaded to the browser is
+reduced by using the more leightweight `AccountGroup` type instead of
+the `GroupDetail` type when showing the groups in a list format. As a
+consequence the `Owners` column that showed the name of the owner group
+had been dropped.
+
+* Add LDAP-cache to minimize number of queries when unnesting groups
++
+A new cache named "ldap_groups_byinclude" is introduced to help lessen
+the number of queries needed to resolve nested LDAP-groups.
+
+* Add index for accessing change messages by patch set
++
+This improves the performance of loading the dashboards.
+
+* Add a fast path to avoid checking every commit on push
++
+If a user can forge author, committer and gerrit server identity, and
+can upload merges, don't bother checking the commit history of what is
+being uploaded. This can save time on servers that are trying to accept
+a large project import using the push permission.
+
+* Improve performance of `ReceiveCommits` by reducing `RevWalk` load
++
+JGit RevWalk does not perform well when a large number of objects are
+added to the start set by `markStart` or `markUninteresting`. Avoid
+putting existing `refs/changes/` or `refs/tags/` into the `RevWalk` and
+instead use only the `refs/heads` namespace and the name of the branch
+used in the `refs/for/` push line.
++
+Catch existing changes by looking for their exact commit SHA-1, rather
+than complete ancestory. This should have roughly the same outcome for
+anyone pushing a new commit on top of an existing open change, but
+with lower computional cost at the server.
+
+* Lookup changes in parallel during `ReceiveCommits`
++
+If the database has high query latency, the loop that locates existing
+changes on the destination branch given Change-Id can be slow. Start
+all of the queries as commits are discovered, but don't block on
+results until all queries were started.
++
+If the database can build the `ResultSet` in the background, this may
+hide some of the query latency by allowing the queries to overlap when
+more than one lookup must be performed for a push.
+
+* Perform change update on multiple threads
++
+When multiple changes need to be created or updated for a single push
+operation they are now inserted into the database by parallel threads,
+up to the maximum allowed thread count. The current thread is used
+when the thread pool is already fully in use, falling back to the
+prior behavior where each concurrent push operation can do its own
+concurrent database update. The thread pool exists to reduce latency
+so long as there are sufficient threads available.
++
+This helps push times on databases that are high latency, such as
+database servers that are running on a different machine from the
+Gerrit server itself, e.g. gerrit.googlesource.com.
++
+The new thread pool is
+link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/config-gerrit.html#receive.changeUpdateThreads[
+disabled by default], limiting the overhead to servers that have good
+latency with their database, such as using in-process H2 database, or
+a MySQL or PostgreSQL on the same host.
+
+* Use `BatchRefUpdate` to execute reference changes
++
+Some storage backends for JGit are able to update multiple references
+in a single pass efficiently. Take advantage of this by pushing
+any normal reference updates (such as direct push or branch create)
+into a single `BatchRefUpdate` object.
+
+* Assume labels are correct in ListChanges
++
+To reduce end-user latency when displaying changes in a search result
+or user dashboard, assume the labels are accurate in the database at
+display time and don't recompute the access privileges of a reviewer.
+
+* Notify the cache that the git_tags was modified
++
+The tag cache was updated in-place, which prevented the H2 based
+storage from writing out the updated tag information. This meant
+servers almost never had the right data stored on disk and had to
+recompute it at startup.
++
+Anytime the value is now modified in place, put it back into the
+cache so it can be saved for use on the next startup.
+
+* Special case hiding `refs/meta/config` from Git clients
++
+VisibleRefFilter requires a lot of server CPU to accurately provide
+the correct listing to clients when they cannot read `refs/*`.
++
+Since the default configuration is now to link:#hide-config[
+hide `refs/meta/config`], use a special case in VisibleRefFilter that
+permits showing every reference except `refs/meta/config` if a user can
+read every other reference in the repository.
+
+* Avoid second remote call to lookup approvals when loading change
+  results
++
+By using the new link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/rest-api.html#changes[`/changes/`]
+REST endpoint the web UI client now obtains the label information
+during the query and avoids a second round trip to lookup the current
+approvals for each displayed change. For most users this should improve
+the way the page renders. The verified and code review columns will be
+populated before the table is made visible, preventing the layout from
+"jumping" the way the old UI did when the 2nd RPC finally finished and
+supplied the label data.
+
+* Load patch set approvals in parallel
++
+ResultSet is a future-like interface, the database system is free to
+execute each result set asynchronously in the background if it
+supports that. gwtorm's default SQL backend always runs queries
+immediately and then returns a ListResultSet, so for most installs this
+has no real impact in ordering.
++
+For the system that runs gerrit-review, each query has a high cost in
+network latency, the system treats ResultSet as a future promise to
+supply the matching rows. Getting all of the necessary ResultSets up
+front allows the database to send all requests to the backend as early
+as possible, allowing the network latency to overlap.
+
+Upgrades
+--------
+* Update Gson to 2.1
+* Update GWT to 2.4.0
+* Update JGit to 2.0.0.201206130900-r.23-gb3dbf19
+
+* Use gwtexpui 1.2.6
++
+** Hide superfluous status text from clippy flash widget
+** Fix diappearance of text in CopyableLabel when clicking on it
+
+* Update Guava to 12.0.1
++
+This fixes a performance problem with LoadingCache where the cache's
+inner table did not dynamically resize to handle a larger number
+of cached items, causing O(N) lookup performance for most objects.
+
+Bug Fixes
+---------
+
+Security
+~~~~~~~~
+* Ensure that only administrators can change the global capabilities
++
+Only Gerrit server administrators (members of the groups that have
+the `administrateServer` capability) should be able to edit the
+global capabilities because being able to edit the global capabilities
+means being able to assign the `administrateServer` capability.
++
+Because of this on the `All-Projects` project it is disallowed to assign
++
+. the `owner` access rights on `refs/*`
++
+Project owners (members of groups to which the `owner` access right
+is assigned) are able to edit the access control list of the projects
+they own. Hence being owner of the `All-Projects` project would allow
+to edit the global capabilities and assign the `administrateServer`
+capabilitiy without being Gerrit administrator.
++
+In earlier Gerrit versions (2.1.x) it was already implemented like
+this but the corresponding checks got lost.
++
+. the 'push' access right on `refs/meta/config`
++
+Being able to push configuration changes to the `All-Projects` project
+allows to edit the global capabilities and hence a user with this
+access right could assign the `administrateServer` capability without
+being Gerrit administrator.
++
+From the Gerrit WebUI (ProjectAccessScreen) it is not possible anymore
+to assign on the `All-Projects` project the `owner` access right on
+`refs/*` and the `push` access right on `refs/meta/config`.
++
+In addition it is ensured that an `owner` access right that is assigned
+for `refs/*` on the `All-Projects` project has no effect and that only
+Gerrit administrators with the `push` access right can push
+configuration changes to the `All-Projects` project.
++
+It is still possible to assign both access rights (`owner` on `refs/*`
+and `push` on `refs/meta/config`) on the `All-Projects` project by directly
+editing its `project.config` file and pushing to `refs/meta/config`.
+To fix this it would be needed to reject assigning these access rights
+on the `All-Projects` project as invalid configuration, however doing this
+would mean to break existing configurations of the `All-Projects` project
+that assign these access rights. At the moment there is no migration
+framework in place that would allow to migrate `project.config` files.
+Hence this check is currently not done and these access rights in this
+case have simply no effect.
+
+Web
+~~~
+
+* Do not show "Session cookie not available" on sign in
++
+When LDAP is used for authentication, clicking on the 'Sign In' link
+opens a user/password dialog. In this dialog the "Session cookie not
+available." message was always shown as warning. This warning was
+pretty useless since the user was about to sign in because he had no
+current session.
++
+This problem was discussed on the
+link:https://groups.google.com/forum/#!topic/repo-discuss/j-t77m8-7I0/discussion[
+Gerrit mailing list].
+
+* Reject restoring a change if its destination branch does not exist
+  anymore
+
+* Reject submitting a change if its destination branch does not exist
+  anymore
++
+If a branch got deleted and there was an open change for this branch,
+it was still possible to submit this open change. As result the
+destination branch was implicitly recreated, even if the user
+submitting the change had no privileges to create branches.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=1352[issue 1352]:
+  Don't display "Download" link for `/COMMIT_MSG`
++
+The commit message file is special, it doesn't actually exist and
+cannot be downloaded. Don't offer the download link in the side by
+side viewer.
+
+* Dependencies were lost in the ChangeScreen's "Needed By" table
++
+Older patchsets are now iterated for decendents, so that the dependency
+chain does not break on new upstream patchsets.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=1442[issue 1442]:
+  Only show draft change dependency if current user is owner or reviewer
++
+In the change screen, the dependencies panel was showing draft changes
+in the "Depends On" and "Needed By" lists for all users, and when there
+was no user logged in.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=1558[issue 1558]:
+  Create a draft patch set when a draft patch set is rebased
++
+Rebasing a draft patch set created a non-draft patch set. It was
+unexpected that rebasing a draft patch set published the modifications
+done in the draft patch set.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=1176[issue 1176]:
+  Fix disappearance of download command in Firefox
++
+Clicking on the download command for a patch set in Firefox made the
+download command disappear.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=1587[issue 1587]:
+  Fix disappearance of action buttons when selecting the last patch set
+  as `Old Version History`
+
+* Fix updating patch list when `Old Version History` is changed
++
+If a collapsed patch set panel was expanded and re-closed it's patch
+list wasn't updated anymore when the selection for `Old Version History`
+was changed.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=1523[issue 1523]:
+  Update diff base to match old version history
++
+When changing the diff base in the `Old Version History` on the change
+screen and then entering the Side-By-Side view for a file, clicking on
+the back button in the browser (reentering the change screen) was
+causing the files to be wrongly compared with `Base` again.
+
+* Don't NPE if current patch set is not available
++
+Broken changes may have the current patch set field incorrectly
+specified, causing currentPatchSet to be unable to locate the
+correct data and return it. When this happens don't NPE, just
+claim the change is not reviewed.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=1555[issue 1555]:
+  Fix displaying of file diff if draft patch has been deleted
++
+Displaying any file diff for a patch set failed if the change had any
+gaps in its patch set history. Patch sets can be missing, if they
+have been drafts and were deleted.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=856[issue 856]:
+  Fix displaying of comments on deleted files
++
+Published and draft comments that are posted on deleted files were not
+loaded and displayed.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=735[issue 735]:
+  Fix `ArrayIndexOutOfBoundsException` on navigation to next/previous
+  patch
++
+An `ArrayIndexOutOfBoundsException` could occur when navigating from
+one patch to the next/previous patch if the next/previous patch was a
+newly added binary file. The exception occurred if the user was not
+signed in or if the user was signed in and had `Syntax Coloring` in the
+preferences enabled.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=816[issue 816]:
+  Fix wrong file indention in Side-by-Sie diff viewer on right side
+
+* Only set reviewed attribute on open changes
++
+If a change is merged or abandoned, do not consider the reviewed
+property for the calling user, so that the change is not highlighted
+as unreviewed on the user's dashboard.
+
+* Change PatchTable pointer when loading patch
++
+This patch fixes an issue with the "file list" table displayed by
+clicking on the "Files" sub-menu when viewing a diff.
++
+Originally when navigating between patch screens the highlighted row
+(pointer) of the file list table would not change when not directly
+interacting with the table e.g. by clicking on the previous or next
+file link.
++
+This patch updates the file list table whenever a new patch screen is loaded
+so that the pointer corresponds to the current patch being displayed.
+
+* Don't hyperlink non-internal groups
++
+When an external group (such as LDAP) is used in a permission rule,
+don't attempt to link to the group in the internal account system UI.
+The group won't load successfully. Instead just display the name and
+put the UUID into a tooltip to show the full DN.
+
+* Fix: Popup jumps back to original position when resizing screen
++
+On 'Watched Projects' screen, the 'Browse' button displays a popup
+window. If the user moves it and then resizes the screen, it won't snap
+back to the original position.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=1457[issue 1457]:
+  Prevent groups from being renamed to empty string
+
+* Fixed AccountGroupInfoScreen search callback
++
+If the search returned no results, the search button would not be
+enabled and the status panel was not shown. Fixed the panel and button
+to always be enabled.
+
+* Fix NullPointerException on `/p/`
++
+Requesting just `/p/` caused a NullPointerException as the redirection
+logic had no project name to form a URL from. Detect requests for `/p/`
+and redirect to 'Admin' > 'Projects' to show the projects the caller
+has access to.
+
+Mail
+~~~~
+
+* Fix: Rebase did not mail all reviewers
+
+* Fix email showing in AccountLink instead of names
++
+Prefer the full name for the display text of the link.
+
+* Fix signature delimiter for e-mail messages
++
+Make sure the signature delimiter is "-- " (two dashes and a space).
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=1397[issue 1397]:
+  Don't wait for banner message from SMTP server after STARTTLS
+  negotiation
++
+According to RFC 2847 section 5.2, SMTP server won't send the banner
+message again after STARTTLS negotiation. The original code will hang
+until SMTP server kicks it off due to timeout and can't send email with
+STARTTLS enabled, aka. `sendemail.smtpEncryption = tls`.
+
+* Extract all mail templates during site init
++
+The example mail templates `RebasedPatchSet.vm`, `Restored.vm` and
+`Reverted.vm` were not extracted during the initialization of a new
+site.
+
+SSH
+~~~
+* Fix reject message if bypassing code review is not allowed
++
+If a user is not allowed to bypass code review, but tries to push a
+commit directly, Gerrit rejected this push with the error message
+"can not update the reference as a fast forward". This message was
+confusing to the user since the push only failed due to missing
+access rights. Go back to the old message that says "prohibited
+by Gerrit".
+
+* Fix reject message if pushing tag is rejected because tagger is
+  somebody else
++
+Pushing a tag that has somebody else as tagger requires the `Forge
+Committer` access right. If this access right was missing Gerrit
+was rejecting the push with "can not create new references". This error
+message was misleading because the user may have thought that the
+`Create Reference` access right was missing which was actually assigned.
++
+The same reject message was also returned on push of an annotated tag
+if the `Push Annotated Tag` access right was missing. Also in this case
+the error message was not ideal.
++
+Go back to the old more generic message which says `prohibited by
+Gerrit`.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=1437[issue 1437]:
+  Send event to stream when draft change is published
++
+When a change is uploaded as a draft, a `patchset-created` event is
+sent to the event stream, but since drafts are private to the owner,
+the event is not publicly visible.  When the draft is later published,
+no publicly visible event was sent. As result of this external tools
+that rely on the event stream to detect new changes didn't receive
+events for any changes that were first uploaded as draft.
++
+There is now a new event, `draft-published`, which is sent to the
+event stream when a draft change is published.  The content of this
+event is the same as `patchset-created`.
+
+* Fix: Wrong ps/rev in `change-merged` stream-event
++
+When using cherry-pick as merge strategy, the wrong ref was set in the
+`change-merged` stream-event.
++
+The issue stems from Gerrit would not acknowledge the resulting new
+pachset (the actual cherry-pick).
+
+* Fix NullPointerException in `query` SSH command
++
+Running the `query` SSH command with the options `--comments` and
+`--format=JSON` failed with a NullPointerException if a change had a
+message without author. Change messages have no author if they were
+created by Gerrit. For such messages now the Gerrit Server identity is
+returned as author.
+
+* Fix the `export-review-notes` command's Guice bindings
++
+The `export-review-notes` command was broken becasue of the CachePool
+class being bound twice. The startup of the command failed because of
+that.
+
+* Fix sorting of SSH help text
++
+Commands were displaying in random order, sort commands before output.
+
+* `replicate` command: Do not log errors for wrong user input
++
+If the user provided an invalid combination of command options or an
+non existing project name this was logged in the `error.log` but
+printing the error out to the user is sufficient.
+
+Authentication
+~~~~~~~~~~~~~~
+
+* Fix NPE in LdapRealm caused by non-LDAP users
++
+Servers that are connected to LDAP but have non-LDAP user accounts
+created by `gerrit create-account` (e.g. batch role accounts for
+build systems) were crashing with a NullPointerException when the
+LdapRealm tried to discover which LDAP groups the non-LDAP user
+was a member of in the directory.
+
+* Fix domain field of HTTP digest authentication
++
+Per RFC 2617 the domain field is optional. If it is not present,
+the digest token is valid on any URL on the server. When set it
+must be a path prefix describing the URLs that the password would
+be valid against.
++
+When a canonical URL is known, supply that as the only domain that
+is valid. When the URL is missing (e.g. because the provider is
+still broken) rely on the context path of the application instead.
+
+Replication
+~~~~~~~~~~~
+
+* Fix inconsistent behaviour when replicating `refs/meta/config`
++
+In `replication.config`, if `authGroup` is set to be used together with
+`mirror = true`, refs blocked through the `authGroup` are deleted from
+the slave/mirror. The same correctly applies if the `authGroup` is used
+to block `refs/meta/config`.
++
+However, if `replicatePermission` was set to `false`, Gerrit was
+refusing to clean up `refs/meta/config` on the slave/mirror.
+
+* Fix bug with member assignment order in PushReplication.
++
+The groupCache was being used before it was set in the class. Fix the
+ordering of the assignment.
+
+Approval Categories
+~~~~~~~~~~~~~~~~~~~
+
+* Make `NoBlock` and `NoOp` approval category functions work
++
+The approval category functions `NoBlock` and `NoOp` have not worked
+since the integration of Prolog.
++
+`MAY` was introduced as a new submit record status to complement `OK`,
+`REJECT`, `NEED`, and `IMPOSSIBLE`. This allows the expression of
+approval categories (labels) that are optional, i.e. could either be
+set or unset without ever influencing whether the change could be
+submitted. Previously there was no way to express this property in
+the submit record.
++
+This enables the `NoBlock` and `NoOp` approval category functions to
+work as they now emit may() terms from the Prolog rules. Previously
+they returned ok() terms lacking a nested user term, leading to
+exceptions in code that expected a user context if the label was `OK`.
+
+* Fix category block status without negative score
++
+Categories without blocking or approval scores will result in the
+blocking/approved image appearing in the category column after changes
+are merged should the score by the reviewer match the minimum or
+maximum value respectively.
++
+A check to ignore "No Score" values of 0 was added.
+
+* Don't remove dashes from approval category name
++
+If an approval category name contained a dash, it was removed by
+Gerrit. On the other side a space in an approval category name is
+converted to a dash. This was confusing for writing Prolog submit
+rules. If, for example, one defined a new category named `X-Y`, then in
+the Prolog code the proper name for that category would have been `XY`
+which was unintuitive.
+
+* Fix NPE in `PRED__load_commit_labels_1`
++
+If a change query uses reviewer information and loads the approvals
+map, but there are no approvals for a given patch set available, the
+collection came out null, which cannot be iterated. Make it always be
+an empty list.
+
+Other
+~~~~~
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=1554[issue 1554]:
+  Fix cloning of new projects from slave servers
++
+If a new project is created in Gerrit the replication creates the
+repository for this new project directly in the filesystem of the slave
+server. The slave server was not discovering this new repository and as
+result any attempt to clone the corresponding project from the slave
+server failed.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=1548[issue 1548]:
+  Create a ref for the patch set that is created when a change is
+  cherry-picked and trigger the replication for it:
++
+If Cherry Pick is chosen as submit type, on submit a new commit is
+created by the cherry-pick. For this commit a new patch set is created
+which is added to the change. Using any of the download commands to
+fetch this new patch set failed with 'Couldn't find remote ref' because
+no ref for the new patch set was created.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=1626[issue 1626]:
+  Fix NullPointerException on cherry-pick if `changeMerge.test` is enabled
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=1491[issue 1491]:
+  Fix nested submodule updates
+
+* Set link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/config-gerrit.html#transfer.timeout[transfer
+  timeout] for pushes through HTTP
++
+The transfer timeout was only set when pushing via SSH.
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/config-gerrit.html#receive.maxObjectSizeLimit[
+  Limit maximum Git object size] when pushing through HTTP
++
+The limit for the maximum object size was only set when pushing via SSH.
+
+* Fix units of `httpd.maxwait`
++
+The default unit here is minutes, but Jetty wants to get milliseconds
+from the maxWait field. Convert the minutes returned by getTimeUnit to
+be milliseconds, matching what Jetty expects.
++
+This should resolve a large number of 503 errors for Git over HTTP.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=1493[issue 1493]:
+  Fix wrong "change ... closed" message on direct push
++
+Pushing a commit directly into the central repository with bypassing
+code review wrongly resulted in a "change ... closed" message if the
+commit was already pushed for review and if a Change-Id was included in
+the commit message. Despite of the error message the push succeeded and
+the corresponding change got closed. Now the message is not printed
+anymore.
+
+* Fix NPE that can hide guice CreationException on site init
++
+Note that the `--show-stack-trace` option is needed to print the stack
+trace when a program stops with a Die exception.
+
+* Do not automatically add author/committer as reviewer to drafts
+
+* Do not automatically add reviewers from footer lines to drafts
+
+* Fix NullPointerException in MergeOp
++
+The body of the commit object may have been discarded earlier to
+save memory, so ensure it exists before asking for the author.
+
+* link:http://code.google.com/p/gerrit/issues/detail?id=1396[issue 1396]:
+  Initialize the submodule commit message buffer
+
+* Fix file name matching in `commit_delta` to perform substring
+  matching
++
+The `commit_delta` predicate was matching the entire file name against
+the given regular expression while other predicates (`commit_edits`,
+`commit_message_matches`) performed substring matching. It was
+inconsistent that for `commit_delta` it was needed to write something
+like:
++
+----
+  commit_delta('.*\.java')
+----
++
+to match all `*.java` files, while for `commit_edits` it was:
++
+----
+  commit_edits('\.java$', '...')
+----
++
+to match the same set of (Java) files.
+
+* Create index for submodule subscriptions on site upgrade
+
+* Fix URL to Jetty XML DTDs so they can be properly validated
+
+* Fix resource leak when `changeMerge.test` is `true`
+
+* Fix possible synchronization issue in TaskThunk
+
+* Fix possible NPEs in `ReplaceRequest.cmd` usage in `ReceiveCommits`
++
+The `cmd` field is populated by `validate(boolean)`. If this method
+fails, results on some `ReplaceRequests` may not be set. Guard the
+attempt to access the field with a null check.
+
+* Match no labels if current patch set is not available
++
+If the current patch set cannot be loaded from `ChangeData`, assume no
+label information. This works around an NullPointerException inside of
+`ChangeControl` where the `PatchSet` is otherwise required.
+
+* Create new patch set references before database records
++
+Ensure the commit used by a new change or replacement patch set
+always exists in the Git repository by writing the reference first
+as part of the overall `BatchRefUpdate`, then inserting the database
+records if all of the references stored successfully.
+
+* Fix rebase patch set and revert change to update Git first
++
+Update the Git reference before writing to the database. This way the
+repository cannot be corrupted if the server goes down between the two
+actions.
+
+* Make sure we use only one type of NoteMerger for review notes creation
+
+* Fix generation of owner group in GroupDetail
++
+Set the GroupDetail.ownerGroup to the AccountGroup.ownerGroupUUID
+instead of the groupUUID.
+
+* Ensure that ObjectOutputStream in H2CacheImpl is closed
+
+* Ensure that RevWalk in SubmoduleOp is released
diff --git a/ReleaseNotes/index.txt b/ReleaseNotes/index.txt
index 5f8de28..07e6aa5 100644
--- a/ReleaseNotes/index.txt
+++ b/ReleaseNotes/index.txt
@@ -1,6 +1,15 @@
 Gerrit Code Review - Release Notes
 ==================================
 
+[[2_5]]
+Version 2.5.x
+-------------
+* link:ReleaseNotes-2.5.4.html[2.5.4]
+* link:ReleaseNotes-2.5.3.html[2.5.3]
+* link:ReleaseNotes-2.5.2.html[2.5.2]
+* link:ReleaseNotes-2.5.1.html[2.5.1]
+* link:ReleaseNotes-2.5.html[2.5]
+
 [[2_4]]
 Version 2.4.x
 -------------
@@ -11,26 +20,26 @@
 [[2_3]]
 Version 2.3.x
 -------------
-* link:ReleaseNotes-2.3.html[2.3]
 * link:ReleaseNotes-2.3.1.html[2.3.1]
+* link:ReleaseNotes-2.3.html[2.3]
 
 [[2_2]]
 Version 2.2.x
 -------------
-* link:ReleaseNotes-2.2.2.html[2.2.2],
-* link:ReleaseNotes-2.2.2.2.html[2.2.2.2],
-* link:ReleaseNotes-2.2.2.1.html[2.2.2.1],
-* link:ReleaseNotes-2.2.1.html[2.2.1],
+* link:ReleaseNotes-2.2.2.2.html[2.2.2.2]
+* link:ReleaseNotes-2.2.2.1.html[2.2.2.1]
+* link:ReleaseNotes-2.2.2.html[2.2.2]
+* link:ReleaseNotes-2.2.1.html[2.2.1]
 * link:ReleaseNotes-2.2.0.html[2.2.0]
 
 [[2_1]]
 Version 2.1.x
 -------------
-* link:ReleaseNotes-2.1.8.html[2.1.8],
-* link:ReleaseNotes-2.1.7.2.html[2.1.7.2],
-* link:ReleaseNotes-2.1.7.html[2.1.7],
-* link:ReleaseNotes-2.1.6.html[2.1.6],
-  link:ReleaseNotes-2.1.6.1.html[2.1.6.1]
+* link:ReleaseNotes-2.1.8.html[2.1.8]
+* link:ReleaseNotes-2.1.7.2.html[2.1.7.2]
+* link:ReleaseNotes-2.1.7.html[2.1.7]
+* link:ReleaseNotes-2.1.6.1.html[2.1.6.1]
+* link:ReleaseNotes-2.1.6.html[2.1.6]
 * link:ReleaseNotes-2.1.5.html[2.1.5]
 * link:ReleaseNotes-2.1.4.html[2.1.4]
 * link:ReleaseNotes-2.1.3.html[2.1.3]
@@ -40,31 +49,31 @@
 * link:ReleaseNotes-2.1.2.2.html[2.1.2.2]
 * link:ReleaseNotes-2.1.2.1.html[2.1.2.1]
 * link:ReleaseNotes-2.1.2.html[2.1.2]
-* link:ReleaseNotes-2.1.1.html[2.1.1],
-  link:ReleaseNotes-2.1.1.html[2.1.1.1]
+* link:ReleaseNotes-2.1.1.html[2.1.1.1]
+* link:ReleaseNotes-2.1.1.html[2.1.1]
 * link:ReleaseNotes-2.1.html[2.1]
 
 [[2_0]]
 Version 2.0.x
 -------------
-* link:ReleaseNotes-2.0.24.html[2.0.24],
-  link:ReleaseNotes-2.0.24.html[2.0.24.1],
-  link:ReleaseNotes-2.0.24.html[2.0.24.2]
+* link:ReleaseNotes-2.0.24.html[2.0.24.2]
+* link:ReleaseNotes-2.0.24.html[2.0.24.1]
+* link:ReleaseNotes-2.0.24.html[2.0.24]
 * link:ReleaseNotes-2.0.23.html[2.0.23]
 * link:ReleaseNotes-2.0.22.html[2.0.22]
 * link:ReleaseNotes-2.0.21.html[2.0.21]
 * link:ReleaseNotes-2.0.20.html[2.0.20]
-* link:ReleaseNotes-2.0.19.html[2.0.19],
-  link:ReleaseNotes-2.0.19.html[2.0.19.1],
-  link:ReleaseNotes-2.0.19.html[2.0.19.2]
+* link:ReleaseNotes-2.0.19.html[2.0.19.2]
+* link:ReleaseNotes-2.0.19.html[2.0.19.1]
+* link:ReleaseNotes-2.0.19.html[2.0.19]
 * link:ReleaseNotes-2.0.18.html[2.0.18]
 * link:ReleaseNotes-2.0.17.html[2.0.17]
 * link:ReleaseNotes-2.0.16.html[2.0.16]
 * link:ReleaseNotes-2.0.15.html[2.0.15]
-* link:ReleaseNotes-2.0.14.html[2.0.14],
-  link:ReleaseNotes-2.0.14.html[2.0.14.1]
-* link:ReleaseNotes-2.0.13.html[2.0.13],
-  link:ReleaseNotes-2.0.13.html[2.0.13.1]
+* link:ReleaseNotes-2.0.14.html[2.0.14.1]
+* link:ReleaseNotes-2.0.14.html[2.0.14]
+* link:ReleaseNotes-2.0.13.html[2.0.13.1]
+* link:ReleaseNotes-2.0.13.html[2.0.13]
 * link:ReleaseNotes-2.0.12.html[2.0.12]
 * link:ReleaseNotes-2.0.11.html[2.0.11]
 * link:ReleaseNotes-2.0.10.html[2.0.10]
diff --git a/SUBMITTING_PATCHES b/SUBMITTING_PATCHES
index e766ef1..553ab34 100644
--- a/SUBMITTING_PATCHES
+++ b/SUBMITTING_PATCHES
@@ -5,7 +5,7 @@
  - Make sure all code is under the Apache License, 2.0.
  - Publish your changes for review:
 
-   git push https://gerrit-review.googlesource.com/gerrit HEAD:refs/for/master
+   git push https://gerrit.googlesource.com/gerrit HEAD:refs/for/master
 
 
 Long Version:
@@ -70,7 +70,7 @@
 Push your patches over HTTPS to the review server, possibly through
 a remembered remote to make this easier in the future:
 
-   git config remote.review.url https://google-review.googlesource.com/gerrit
+   git config remote.review.url https://gerrit.googlesource.com/gerrit
    git config remote.review.push HEAD:refs/for/master
 
    git push review
diff --git a/contrib/git-exproll.sh b/contrib/git-exproll.sh
new file mode 100644
index 0000000..9526d9f
--- /dev/null
+++ b/contrib/git-exproll.sh
@@ -0,0 +1,566 @@
+#!/bin/bash
+# Copyright (c) 2012, Code Aurora Forum. All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#    # Redistributions of source code must retain the above copyright
+#       notice, this list of conditions and the following disclaimer.
+#    # Redistributions in binary form must reproduce the above
+#       copyright notice, this list of conditions and the following
+#       disclaimer in the documentation and/or other materials provided
+#       with the distribution.
+#    # Neither the name of Code Aurora Forum, Inc. nor the names of its
+#       contributors may be used to endorse or promote products derived
+#       from this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED "AS IS" AND ANY EXPRESS OR IMPLIED
+# WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
+# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NON-INFRINGEMENT
+# ARE DISCLAIMED.  IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS
+# BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR
+# BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
+# WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
+# OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
+# IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+usage() { # error_message
+
+    cat <<-EOF
+		usage: $(basename $0) [-unvt] [--noref] [--nolosse] [-r|--ratio number]
+		                      [git gc option...] git.repo
+
+		-u|-h                usage/help
+		-v verbose
+		-n dry-run           don't actually repack anything
+		-t touch             treat repo as if it had been touched
+		--noref              avoid extra ref packing timestamp checking
+		--noloose            do not run just because there are loose object dirs
+		                     (repacking may still run if they are referenced)
+		-r ratio <number>    packfile ratio to aim for (default 10)
+
+		git gc option        will be passed as args to git gc
+
+		git.repo             to run gc against
+
+		Garbage collect using a pseudo logarithmic packfile maintenance
+		approach.  This approach attempts to minimize packfile churn
+		by keeping several generations of varying sized packfiles around
+		and only consolidating packfiles (or loose objects) which are
+		either new packfiles, or packfiles close to the same size as
+		another packfile.
+
+		An estimate is used to predict when rollups (one consolidation
+		would cause another consolidation) would occur so that this
+		rollup can be done all at once via a single repack.  This reduces
+		both the runtime and the pack file churn in rollup cases.
+
+		Approach: plan each consolidation by creating a table like this:
+
+		Id Keep Size           Sha1(or consolidation list)      Actions(repack down up note)
+		1     - 11356          9052edfb7392646cd4e5f362b953675985f01f96 y - - New
+		2     - 429088         010904d5c11cd26a79fda91b01ab454d1001b402 y - - New
+		c1    - 440444         [1,2]                                    - - -
+
+		Id:    numbers preceded by a c are estimated "c pack" files
+		Keep:  - none, k private keep, o our keep
+		Size:  in disk blocks (default du output)
+		Sha1:  of packfile, or consolidation list of packfile ids
+		Actions
+		repack: - n no, y yes
+		down:   - noop, ^ consolidate with a file above
+		up:     - noop, v consolidate with a file below
+		note:   Human description of script decisions:
+		         New (file is a new packfile)
+		         Consolidate with:<list of packfile ids>
+		         (too far from:<list of packfile ids>)
+
+		On the first pass, always consolidate any new packfiles along
+		with loose objects and along with any packfiles which are within
+		the ratio size of their predecessors (note, the list is ordered
+		by increasing size).  After each consolidation, insert a fake
+		consolidation, or "c pack", to naively represent the size and
+		ordered positioning of the anticipated new consolidated pack.
+		Every time a new pack is planned, rescan the list in case the
+		new "c pack" would cause more consolidation...
+
+		Once the packfiles which need consolidation are determined, the
+		packfiles which will not be consolidated are marked with a .keep
+		file, and those which will be consolidated will have their .keep
+		removed if they have one.  Thus, the packfiles with a .keep will
+		not get repacked.
+
+		Packfile consolidation is determined by the --ratio parameter
+		(default is 10).  This ratio is somewhat of a tradeoff.  The
+		smaller the number, the more packfiles will be kept on average;
+		this increases disk utilization somewhat.  However, a larger
+		ratio causes greater churn and may increase disk utilization due
+		to deleted packfiles not being reclaimed since they may still be
+		kept open by long running applications such as Gerrit.  Sane
+		ratio values are probably between 2 and 10.  Since most
+		consolidations actually end up smaller than the estimated
+		consolidated packfile size (due to compression), the true ratio
+		achieved will likely be 1 to 2 greater than the target ratio.
+		The smaller the target ratio, the greater this discrepancy.
+
+		Finally, attempt to skip garbage collection entirely on untouched
+		repos.  In order to determine if a repo has been touched, use the
+		timestamp on the script's keep files, if any relevant file/dir
+		is newer than a keep marker file, assume that the repo has been
+		touched and gc needs to run.  Also assume gc needs to run whenever
+		there are loose object dirs since they may contain untouched
+		unreferenced loose objects which need to be pruned (once they
+		expire).
+
+		In order to allow the keep files to be an effective timestamp
+		marker to detect relevant changes in a repo since the last run,
+		all relevant files and directories which may be modified during a
+		gc run (even during a noop gc run), must have their timestamps
+		reset to the same time as the keep files or gc will always run
+		even on untouched repos.  The relevant files/dirs are all those
+		files and directories which garbage collection, object packing,
+		ref packing and pruning might change during noop actions.
+EOF
+
+    [ -n "$1" ] && info "ERROR $1"
+
+    exit
+}
+
+debug() { [ -n "$SW_V" ] && info "$1" ; }
+info() { echo "$1" >&2 ; }
+
+array_copy() { #v2 # array_src array_dst
+    local src=$1 dst=$2
+    local s i=0
+    eval s=\${#$src[@]}
+    while [ $i -lt $s ] ; do
+        eval $dst[$i]=\"\${$src[$i]}\"
+        i=$(($i + 1))
+    done
+}
+
+array_equals() { #v2 # array_name [vals...]
+    local a=$1 ; shift
+    local s=0 t=() val
+    array_copy "$a" t
+    for s in "${!t[@]}" ; do s=$((s+1)) ; done
+    [ "$s" -ne "$#" ] && return 1
+    for val in "${t[@]}" ; do
+        [ "$val" = "$1" ] || return 2
+        shift
+    done
+    return 0
+}
+
+packs_sizes() { # git.repo > "size pack"...
+    du -s "$1"/objects/pack/pack-$SHA1.pack | sort -n 2> /dev/null
+}
+
+is_ourkeep() { grep -q "$KEEP" "$1" 2> /dev/null ; } # keep
+has_ourkeep() { is_ourkeep "$(keep_for "$1")" ; } # pack
+has_keep() { [ -f "$(keep_for "$1")" ] ; } # pack
+is_repo() { [ -d "$1/objects" ] && [ -d "$1/refs/heads" ] ; } # git.repo
+
+keep() { # pack   # returns true if we added our keep
+    keep=$(keep_for "$1")
+    [ -f "$keep" ] && return 1
+    echo "$KEEP" > "$keep"
+    return 0
+}
+
+keep_for() { # packfile > keepfile
+    local keep=$(echo "$1" | sed -es'/\.pack$/.keep/')
+    [ "${keep/.keep}" = "$keep" ] && return 1
+    echo "$keep"
+}
+
+idx_for() { # packfile > idxfile
+    local idx=$(echo "$1" | sed -es'/\.pack$/.idx/')
+    [ "${idx/.idx}" = "$idx" ] && return 1
+    echo "$idx"
+}
+
+# pack_or_keep_file > sha
+sha_for() { echo "$1" | sed -es'|\(.*/\)*pack-\([^.]*\)\..*$|\2|' ; }
+
+private_keeps() { # git.repo -> sets pkeeps
+    local repo=$1 ary=$2
+    local keep keeps=("$repo"/objects/pack/pack-$SHA1.keep)
+    pkeeps=()
+    for keep in "${keeps[@]}" ; do
+        is_ourkeep "$keep" || pkeeps=("${pkeeps[@]}" "$keep")
+    done
+}
+
+is_tooclose() { [ "$(($1 * $RATIO))" -gt "$2" ] ; } # smaller larger
+
+unique() { # [args...] > unique_words
+    local lines=$(while [ $# -gt 0 ] ; do echo "$1" ; shift ; done)
+    lines=$(echo "$lines" | sort -u)
+    echo $lines  # as words
+}
+
+outfs() { # fs [args...] > argfs...
+    local fs=$1 ; shift
+    [ $# -gt 0 ] && echo -n "$1" ; shift
+    while [ $# -gt 0 ] ; do echo -n "$fs$1" ; shift ; done
+}
+
+sort_list() { # < list > formatted_list
+    # n has_keep size sha repack down up note
+    awk '{ note=$8; for(i=8;i<NF;i++) note=note " "$(i+1)
+           printf("%-5s %s %-14s %-40s %s %s %s %s\n", \
+                     $1,$2,   $3,  $4, $5,$6,$7,note)}' |\
+        sort -k 3,3n -k 1,1n
+}
+
+is_touched() { # git.repo
+    local repo=$1
+    local loose keep ours newer
+    [ -n "$SW_T" ] && { debug "$SW_T -> treat as touched" ; return 0 ; }
+
+    if [ -z "$SW_LOOSE" ] ; then
+        # If there are loose objects, they may need to be pruned,
+        # run even if nothing has really been touched.
+        loose=$(find "$repo/objects" -type d \
+                      -wholename "$repo/objects/[0-9][0-9]"
+                      -print -quit 2>/dev/null)
+        [ -n "$loose" ] && { info "There are loose object directories" ; return 0 ; }
+    fi
+
+    # If we don't have a keep, the current packfiles may not have been
+    # compressed with the current gc policy (gc may never have been run),
+    # so run at least once to repack everything.  Also, we need a marker
+    # file for timestamp tracking (a dir needs to detect changes within
+    # it, so it cannot be a marker) and our keeps are something we control,
+    # use them.
+    for keep in "$repo"/objects/pack/pack-$SHA1.keep ; do
+        is_ourkeep "$keep" && { ours=$keep ; break ; }
+    done
+    [ -z "$ours" ] && { info 'We have no keep (we have never run?): run' ; return 0 ; }
+
+    debug "Our timestamp keep: $ours"
+    # The wholename stuff seems to get touched by a noop git gc
+    newer=$(find "$repo/objects" "$repo/refs" "$repo/packed-refs" \
+                  '!' -wholename "$repo/objects/info" \
+                  '!' -wholename "$repo/objects/info/*" \
+                  -newer "$ours" \
+                  -print -quit 2>/dev/null)
+    [ -z "$newer" ] && return 1
+
+    info "Touched since last run: $newer"
+    return 0
+}
+
+touch_refs() { # git.repo start_date refs
+    local repo=$1 start_date=$2 refs=$3
+    (
+        debug "Setting start date($start_date) on unpacked refs:"
+        debug "$refs"
+        cd "$repo/refs" || return
+        # safe to assume no newlines in a ref name
+        echo "$refs" | xargs -d '\n' -n 1 touch -c -d "$start_date"
+    )
+}
+
+set_start_date() { # git.repo start_date refs refdirs packedrefs [packs]
+    local repo=$1 start_date=$2 refs=$3 refdirs=$4 packedrefs=$5 ; shift 5
+    local pack keep idx repacked
+
+    # This stuff is touched during object packs
+    while [ $# -gt 0 ] ; do
+        pack=$1 ; shift
+        keep="$(keep_for "$pack")"
+        idx="$(idx_for "$pack")"
+        touch -c -d "$start_date" "$pack" "$keep" "$idx"
+        debug "Setting start date on: $pack $keep $idx"
+    done
+    # This will prevent us from detecting any deletes in the pack dir
+    # since gc ran, except for private keeps which we are checking
+    # manually.  But there really shouldn't be any other relevant deletes
+    # in this dir which should cause us to rerun next time, deleting a
+    # pack or index file by anything but gc would be bad!
+    debug "Setting start date on pack dir: $start_date"
+    touch -c -d "$start_date" "$repo/objects/pack"
+
+
+    if [ -z "$SW_REFS" ] ; then
+        repacked=$(find "$repo/packed-refs" -newer "$repo/objects/pack"
+                      -print -quit 2>/dev/null)
+        if [ -n "$repacked" ] ; then
+            # The ref dirs and packed-ref files seem to get touched even on
+            # a noop refpacking
+            debug "Setting start date on packed-refs"
+            touch -c -d "$start_date" "$repo/packed-refs"
+            touch_refs "$repo" "$start_date" "$refdirs"
+
+            # A ref repack does not imply a ref change, but since it is
+            # hard to tell, simply assume so
+            if [ "$refs" != "$(cd "$repo/refs" ; find -depth)" ] || \
+               [ "$packedrefs" != "$(<"$repo/packed-refs")" ] ; then
+                # We retouch if needed (instead of simply checking then
+                # touching) to avoid a race between the check and the set.
+                debug "  but refs actually got packed, so retouch packed-refs"
+                touch -c "$repo/packed-refs"
+            fi
+        fi
+    fi
+}
+
+note_consolidate() { # note entry > note (no duplicated consolidated entries)
+    local note=$1 entry=$2
+    local entries=() ifs=$IFS
+    if  echo "$note" | grep -q 'Consolidate with:[0-9,c]' ; then
+        IFS=,
+        entries=( $(echo "$note" | sed -es'/^.*Consolidate with:\([0-9,c]*\).*$/\1/') )
+        note=( $(echo "$note" | sed -es'/Consolidate with:[0-9,c]*//') )
+        IFS=$ifs
+    fi
+    entries=( $(unique "${entries[@]}" "$entry") )
+    echo "$note Consolidate with:$(outfs , "${entries[@]}")"
+}
+
+note_toofar() { # note entry > note (no duplicated "too far" entries)
+    local note=$1 entry=$2
+    local entries=() ifs=$IFS
+    if  echo "$note" | grep -q '(too far from:[0-9,c]*)' ; then
+        IFS=,
+        entries=( $(echo "$note" | sed -es'/^.*(too far from:\([0-9,c]*\)).*$/\1/') )
+        note=( $(echo "$note" | sed -es'/(too far from:[0-9,c]*)//') )
+        IFS=$ifs
+    fi
+    entries=( $(unique "${entries[@]}" "$entry") )
+    echo "$note (too far from:$(outfs , "${entries[@]}"))"
+}
+
+last_entry() { # isRepack pline repackline > last_rows_entry
+    local size_hit=$1 pline=$2 repackline=$3
+    if [ -n "$pline" ] ; then
+        if [ -n "$size_hit" ] ; then
+            echo "$repack_line"
+        else
+            echo "$pline"
+        fi
+    fi
+}
+
+init_list() { # git.repo > shortlist
+    local repo=$1
+    local file
+    local n has_keep size sha repack
+
+    packs_sizes "$1" | {
+        while read size file ; do
+            n=$((n+1))
+            repack=n
+            has_keep=-
+            if has_keep "$file" ; then
+                has_keep=k
+                has_ourkeep "$file" && has_keep=o
+            fi
+            sha=$(sha_for "$file")
+            echo "$n $has_keep $size $sha $repack"
+        done
+    } | sort_list
+}
+
+consolidate_list() { # run < list > list
+    local run=$1
+    local sum=0 psize=0 sum_size=0 size_hit pn clist pline repackline
+    local n has_keep size sha repack down up note
+
+    {
+        while read n has_keep size sha repack down up note; do
+            [ -z "$up" ] && up='-'
+            [ -z "$down" ] && down="-"
+
+            if [ "$has_keep" = "k" ] ; then
+                echo "$n $has_keep $size $sha $repack - - Private"
+                continue
+            fi
+
+            if [ "$repack" = "n" ] ; then
+                if is_tooclose $psize $size ; then
+                    size_hit=y
+                    repack=y
+                    sum=$(($sum + $sum_size + $size))
+                    sum_size=0 # Prevents double summing this entry
+                    clist=($(unique "${clist[@]}" $pn $n))
+                    down="^"
+                    [ "$has_keep" = "-" ] && note="$note New +"
+                    note=$(note_consolidate "$note" "$pn")
+                elif [ "$has_keep" = "-" ] ; then
+                    repack=y
+                    sum=$(($sum + $size))
+                    sum_size=0 # Prevents double summing this entry
+                    clist=($(unique "${clist[@]}" $n))
+                    note="$note New"
+                elif [ $psize -ne 0 ] ; then
+                    sum_size=$size
+                    down="!"
+                    note=$(note_toofar "$note" "$pn")
+                else
+                    sum_size=$size
+                fi
+            else
+                sum_size=$size
+            fi
+
+            # By preventing "c files" (consolidated) from being marked
+            # "repack" they won't get keeps
+            repack2=y
+            [ "${n/c}" != "$n" ] && { repack=- ; repack2=- ; }
+
+            last_entry "$size_hit" "$pline" "$repack_line"
+            # Delay the printout until we know whether we are
+            # being consolidated with the entry following us
+            # (we won't know until the next iteration).
+            # size_hit is used to determine which of the lines
+            # below will actually get printed above on the next
+            # iteration.
+            pline="$n $has_keep $size $sha $repack $down $up $note"
+            repack_line="$n $has_keep $size $sha $repack2 $down v $note"
+
+            pn=$n ; psize=$size # previous entry data
+            size_hit='' # will not be consolidated up
+
+        done
+        last_entry "$size_hit" "$pline" "$repack_line"
+
+        [ $sum -gt 0 ] && echo "c$run - $sum [$(outfs , "${clist[@]}")] - - -"
+
+    } | sort_list
+}
+
+process_list() { # git.repo > list
+    local list=$(init_list "$1")  plist run=0
+
+    while true ; do
+        plist=$list
+        run=$((run +1))
+        list=$(echo "$list" | consolidate_list "$run")
+        if [ "$plist" != "$list" ] ; then
+            debug "------------------------------------------------------------------------------------"
+            debug "$HEADER"
+            debug "$list"
+        else
+            break
+        fi
+    done
+    debug "------------------------------------------------------------------------------------"
+    echo "$list"
+}
+
+repack_list() { # git.repo < list
+    local repo=$1
+    local start_date newpacks=0 pkeeps keeps=1 refs refdirs rtn
+    local packedrefs=$(<"$repo/packed-refs")
+
+    # so they don't appear touched after a noop refpacking
+    if [ -z "$SW_REFS" ] ; then
+        refs=$(cd "$repo/refs" ; find -depth)
+        refdirs=$(cd "$repo/refs" ; find -type d -depth)
+        debug "Before refs:"
+        debug "$refs"
+    fi
+
+    # Find a private keep snapshot which has not changed from
+    # before our start_date so private keep deletions during gc
+    # can be detected
+    while ! array_equals pkeeps "${keeps[@]}" ; do
+       debug "Getting a private keep snapshot"
+       private_keeps "$repo"
+       keeps=("${pkeeps[@]}")
+       debug "before keeps: ${keeps[*]}"
+       start_date=$(date)
+       private_keeps "$repo"
+       debug "after keeps: ${pkeeps[*]}"
+    done
+
+    while read n has_keep size sha repack down up note; do
+        if [ "$repack" = "y" ] ; then
+            keep="$repo/objects/pack/pack-$sha.keep"
+            info "Repacking $repo/objects/pack/pack-$sha.pack"
+            [ -f "$keep" ] && rm -f "$keep"
+        fi
+    done
+
+    ( cd "$repo" && git gc "${GC_OPTS[@]}" ) ; rtn=$?
+
+    # Mark any files withoug a .keep with our .keep
+    packs=("$repo"/objects/pack/pack-$SHA1.pack)
+    for pack in "${packs[@]}" ; do
+        if keep "$pack" ; then
+            info "New pack: $pack"
+            newpacks=$((newpacks+1))
+        fi
+    done
+
+    # Record start_time.  If there is more than 1 new packfile, we
+    # don't want to risk touching it with an older date since that
+    # would prevent consolidation on the next run.  If the private
+    # keeps have changed, then we should run next time no matter what.
+    if [ $newpacks -le 1 ] || ! array_equals pkeeps "${keeps[@]}" ; then
+        set_start_date "$repo" "$start_date" "$refs" "$refdirs" "$packedrefs" "${packs[@]}"
+    fi
+
+    return $rtn # we really only care about the gc error code
+}
+
+git_gc() { # git.repo
+    local list=$(process_list "$1")
+    if [ -z "$SW_V" ] ; then
+        info "Running $PROG on $1.  git gc options: ${GC_OPTS[@]}"
+        echo "$HEADER" >&2
+        echo "$list" >&2 ;
+    fi
+    echo "$list" | repack_list "$1"
+}
+
+
+PROG=$(basename "$0")
+HEADER="Id Keep Size           Sha1(or consolidation list)      Actions(repack down up note)"
+KEEP=git-exproll
+HEX='[0-9a-f]'
+HEX10=$HEX$HEX$HEX$HEX$HEX$HEX$HEX$HEX$HEX$HEX
+SHA1=$HEX10$HEX10$HEX10$HEX10
+
+RATIO=10
+SW_N='' ; SW_V='' ; SW_T='' ; SW_REFS='' ; SW_LOOSE='' ; GC_OPTS=()
+while [ $# -gt 0 ] ; do
+    case "$1" in
+        -u|-h)  usage ;;
+        -n)  SW_N="$1" ;;
+        -v)  SW_V="$1" ;;
+
+        -t)  SW_T="$1" ;;
+        --norefs)  SW_REFS="$1" ;;
+        --noloose) SW_LOOSE="$1" ;;
+
+        -r|--ratio)  shift ; RATIO="$1" ;;
+
+        *)  [ $# -le 1 ] && break
+            GC_OPTS=( "${GC_OPTS[@]}" "$1" )
+            ;;
+    esac
+    shift
+done
+
+
+REPO="$1"
+if ! is_repo "$REPO" ; then
+    REPO=$REPO/.git
+    is_repo "$REPO" || usage "($1) is not likely a git repo"
+fi
+
+
+if [ -z "$SW_N" ] ; then
+    is_touched "$REPO" || { info "Repo untouched since last run" ; exit ; }
+    git_gc "$REPO"
+else
+    is_touched "$REPO" || info "Repo untouched since last run, analyze anyway."
+    process_list "$REPO" >&2
+fi
diff --git a/gerrit-antlr/.gitignore b/gerrit-antlr/.gitignore
index 194bedc..fb047af 100644
--- a/gerrit-antlr/.gitignore
+++ b/gerrit-antlr/.gitignore
@@ -2,4 +2,5 @@
 /.classpath
 /.project
 /.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-antlr.iml
\ No newline at end of file
diff --git a/gerrit-antlr/.settings/org.eclipse.core.resources.prefs b/gerrit-antlr/.settings/org.eclipse.core.resources.prefs
index 589908f..e9441bb 100644
--- a/gerrit-antlr/.settings/org.eclipse.core.resources.prefs
+++ b/gerrit-antlr/.settings/org.eclipse.core.resources.prefs
@@ -1,4 +1,3 @@
-#Thu Jul 28 11:02:35 PDT 2011
 eclipse.preferences.version=1
 encoding//src/main/java=UTF-8
 encoding/<project>=UTF-8
diff --git a/gerrit-antlr/pom.xml b/gerrit-antlr/pom.xml
index aa0d7fd..34cb46f 100644
--- a/gerrit-antlr/pom.xml
+++ b/gerrit-antlr/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.4-SNAPSHOT</version>
+    <version>2.5-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-antlr</artifactId>
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 20251d4..cb430b8 100644
--- a/gerrit-ehcache/.gitignore
+++ b/gerrit-cache-h2/.gitignore
@@ -1,5 +1,6 @@
 /target
 /.classpath
 /.project
-/.settings/org.eclipse.m2e.core.prefs
 /.settings/org.maven.ide.eclipse.prefs
+/.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..f9fe345
--- /dev/null
+++ b/gerrit-cache-h2/.settings/org.eclipse.core.resources.prefs
@@ -0,0 +1,4 @@
+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 73%
rename from gerrit-ehcache/pom.xml
rename to gerrit-cache-h2/pom.xml
index 839c52b0..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.
@@ -22,26 +22,31 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.4-SNAPSHOT</version>
+    <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..5a600c0
--- /dev/null
+++ b/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/DefaultCacheFactory.java
@@ -0,0 +1,135 @@
+// 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.ForwardingRemovalListener;
+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.FactoryModule;
+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() {
+      install(new FactoryModule() {
+        @Override
+        protected void configure() {
+          factory(ForwardingRemovalListener.Factory.class);
+        }
+      });
+
+      bind(DefaultCacheFactory.class);
+      bind(MemoryCacheFactory.class).to(DefaultCacheFactory.class);
+      bind(PersistentCacheFactory.class).to(H2CacheFactory.class);
+      listener().to(H2CacheFactory.class);
+    }
+  }
+
+  private final Config cfg;
+  private final ForwardingRemovalListener.Factory forwardingRemovalListenerFactory;
+
+  @Inject
+  public DefaultCacheFactory(@GerritServerConfig Config config,
+      ForwardingRemovalListener.Factory forwardingRemovalListenerFactory) {
+    this.cfg = config;
+    this.forwardingRemovalListenerFactory = forwardingRemovalListenerFactory;
+  }
+
+  @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()));
+
+    builder.removalListener(forwardingRemovalListenerFactory.create(def.name()));
+
+    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..a196b07
--- /dev/null
+++ b/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheImpl.java
@@ -0,0 +1,726 @@
+// 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.h2.jdbc.JdbcSQLException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.io.InvalidClassException;
+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>() {
+        private static final long serialVersionUID = 1L;
+
+        @Override
+        public void funnel(K from, PrimitiveSink into) {
+          try {
+            ObjectOutputStream ser =
+                new ObjectOutputStream(new SinkOutputStream(into));
+            try {
+              ser.writeObject(from);
+              ser.flush();
+            } finally {
+              ser.close();
+            }
+          } 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));
+            }
+          } catch (JdbcSQLException e) {
+            if (e.getCause() instanceof InvalidClassException) {
+              log.warn("Entries cached for " + url
+                  + " have an incompatible class and can't be deserialized. "
+                  + "Cache is flushed.");
+              invalidateAll();
+            } else {
+              throw e;
+            }
+          } 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/.gitignore b/gerrit-common/.gitignore
index 194bedc..759f12c 100644
--- a/gerrit-common/.gitignore
+++ b/gerrit-common/.gitignore
@@ -2,4 +2,5 @@
 /.classpath
 /.project
 /.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-common.iml
\ No newline at end of file
diff --git a/gerrit-common/.settings/org.eclipse.core.resources.prefs b/gerrit-common/.settings/org.eclipse.core.resources.prefs
index fc11c3f..f9fe345 100644
--- a/gerrit-common/.settings/org.eclipse.core.resources.prefs
+++ b/gerrit-common/.settings/org.eclipse.core.resources.prefs
@@ -1,4 +1,3 @@
-#Thu Jul 28 11:02:36 PDT 2011
 eclipse.preferences.version=1
 encoding//src/main/java=UTF-8
 encoding//src/test/java=UTF-8
diff --git a/gerrit-common/pom.xml b/gerrit-common/pom.xml
index e7933ea..9b3fe5f 100644
--- a/gerrit-common/pom.xml
+++ b/gerrit-common/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.4-SNAPSHOT</version>
+    <version>2.5-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-common</artifactId>
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 71df400..2b9b72a 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/PageLinks.java
@@ -14,13 +14,11 @@
 
 package com.google.gerrit.common;
 
-import com.google.gerrit.common.data.AccountInfo;
 import com.google.gerrit.common.data.ChangeInfo;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Change.Status;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.Change.Status;
 import com.google.gwtorm.client.KeyUtil;
 
 public class PageLinks {
@@ -40,8 +38,10 @@
 
   public static final String MINE = "/";
   public static final String ADMIN_GROUPS = "/admin/groups/";
+  public static final String ADMIN_CREATE_GROUP = "/admin/create-group/";
   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());
@@ -59,12 +59,12 @@
     return "/admin/projects/" + p.get() + ",access";
   }
 
-  public static String toAccountDashboard(final AccountInfo acct) {
-    return toAccountDashboard(acct.getId());
+  public static String toAccountQuery(final String fullname) {
+    return toAccountQuery(fullname, Status.NEW);
   }
 
-  public static String toAccountDashboard(final Account.Id acct) {
-    return "/dashboard/" + acct.toString();
+  public static String toAccountQuery(String fullname, Status status) {
+    return toChangeQuery(op("owner", fullname) + " " + status(status), TOP);
   }
 
   public static String toChangeQuery(final String query) {
@@ -72,30 +72,38 @@
   }
 
   public static String toChangeQuery(String query, String page) {
-    query = KeyUtil.encode(query).replaceAll("%3[Aa]", ":");
-    return "/q/" + query + "," + page;
+    return "/q/" + KeyUtil.encode(query) + "," + page;
   }
 
   public static String projectQuery(Project.NameKey proj, Status status) {
+      return status(status) + " " + op("project", proj.get());
+  }
+
+  private static String status(Status status) {
     switch (status) {
       case ABANDONED:
-        return "status:abandoned " + op("project", proj.get());
-
+        return "status:abandoned";
       case MERGED:
-        return "status:merged " + op("project", proj.get());
-
+        return "status:merged";
       case NEW:
       case SUBMITTED:
       default:
-        return "status:open " + op("project", proj.get());
+        return "status:open";
     }
   }
 
-  public static String op(String name, String value) {
-    if (value.indexOf(' ') >= 0) {
-      return name + ":\"" + value + "\"";
+  public static String op(String op, String value) {
+    if (isSingleWord(value)) {
+      return op + ":" + value;
     }
-    return name + ":" + value;
+    return op + ":\"" + value + "\"";
+  }
+
+  private static boolean isSingleWord(String value) {
+    if (value.startsWith("-")) {
+      return false;
+    }
+    return value.matches("[^\u0000-\u0020!\"#$%&'():;?\\[\\]{}~]+");
   }
 
   protected PageLinks() {
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/audit/Audit.java b/gerrit-common/src/main/java/com/google/gerrit/common/audit/Audit.java
new file mode 100644
index 0000000..90c7f75
--- /dev/null
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/audit/Audit.java
@@ -0,0 +1,36 @@
+// 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.audit;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+/**
+ * Audit annotation for JSON/RPC interfaces.
+ *
+ * Flag with @Audit all the JSON/RPC methods to
+ * be traced in audit-trail and submitted to the
+ * AuditService.
+ */
+@Retention(RetentionPolicy.RUNTIME)
+@Target({ElementType.METHOD})
+public @interface Audit {
+  String action() default "";
+
+  /** List of positions of parameters to be obfuscated in audit-trail (i.e. passwords) */
+  int[] obfuscate() default {};
+}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/auth/userpass/UserPassAuthService.java b/gerrit-common/src/main/java/com/google/gerrit/common/auth/userpass/UserPassAuthService.java
index 1d25a3d..0936d23 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/auth/userpass/UserPassAuthService.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/auth/userpass/UserPassAuthService.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.common.auth.userpass;
 
+import com.google.gerrit.common.audit.Audit;
 import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.gwtjsonrpc.common.AllowCrossSiteRequest;
 import com.google.gwtjsonrpc.common.RemoteJsonService;
@@ -22,6 +23,7 @@
 
 @RpcImpl(version = Version.V2_0)
 public interface UserPassAuthService extends RemoteJsonService {
+  @Audit(action = "sign in", obfuscate={1})
   @AllowCrossSiteRequest
   void authenticate(String username, String password,
       AsyncCallback<LoginResult> callback);
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/changes/ListChangesOption.java b/gerrit-common/src/main/java/com/google/gerrit/common/changes/ListChangesOption.java
new file mode 100644
index 0000000..a5ab851
--- /dev/null
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/changes/ListChangesOption.java
@@ -0,0 +1,73 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.common.changes;
+
+import java.util.EnumSet;
+
+/** Output options available when using {@code /changes/} RPCs. */
+public enum ListChangesOption {
+  LABELS(0),
+
+  /** Return information on the current patch set of the change. */
+  CURRENT_REVISION(1),
+  ALL_REVISIONS(2),
+
+  /** If revisions are included, parse the commit object. */
+  CURRENT_COMMIT(3),
+  ALL_COMMITS(4),
+
+  /** If a patch set is included, include the files of the patch set. */
+  CURRENT_FILES(5),
+  ALL_FILES(6);
+
+  private final int value;
+
+  private ListChangesOption(int v) {
+    this.value = v;
+  }
+
+  public int getValue() {
+    return value;
+  }
+
+  public static ListChangesOption fromValue(int value) {
+    return ListChangesOption.values()[value];
+  }
+
+  public static EnumSet<ListChangesOption> fromBits(int v) {
+    EnumSet<ListChangesOption> r = EnumSet.noneOf(ListChangesOption.class);
+    for (ListChangesOption o : ListChangesOption.values()) {
+      if ((v & (1 << o.value)) != 0) {
+        r.add(o);
+        v &= ~(1 << o.value);
+      }
+      if (v == 0) {
+        return r;
+      }
+    }
+    if (v != 0) {
+      throw new IllegalArgumentException("unknown " + Integer.toHexString(v));
+    }
+    return r;
+  }
+
+  public static int toBits(EnumSet<ListChangesOption> set) {
+    int r = 0;
+    for (ListChangesOption o : set) {
+      r |= 1 << o.value;
+    }
+    return r;
+  }
+}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccessSection.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccessSection.java
index cd64b0a..a9b5e85 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccessSection.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccessSection.java
@@ -118,4 +118,13 @@
   public String toString() {
     return "AccessSection[" + getName() + "]";
   }
+
+  @Override
+  public boolean equals(final Object obj) {
+    if (!super.equals(obj) || !(obj instanceof AccessSection)) {
+      return false;
+    }
+    return new HashSet<Permission>(permissions).equals(new HashSet<Permission>(
+        ((AccessSection) obj).permissions));
+  }
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountInfo.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountInfo.java
index 9a4d9fb..92c2d6c 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountInfo.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountInfo.java
@@ -65,4 +65,51 @@
   public void setPreferredEmail(final String email) {
     preferredEmail = email;
   }
+
+  /**
+   * Formats an account name.
+   * <p>
+   * If the account has a full name, it returns only the full name. Otherwise it
+   * returns a longer form that includes the email address.
+   */
+  public String getName(String anonymousCowardName) {
+    if (getFullName() != null) {
+      return getFullName();
+    }
+    if (getPreferredEmail() != null) {
+      return getPreferredEmail();
+    }
+    return getNameEmail(anonymousCowardName);
+  }
+
+  /**
+   * Formats an account as an name and an email address.
+   * <p>
+   * Example output:
+   * <ul>
+   * <li><code>A U. Thor &lt;author@example.com&gt;</code>: full populated</li>
+   * <li><code>A U. Thor (12)</code>: missing email address</li>
+   * <li><code>Anonymous Coward &lt;author@example.com&gt;</code>: missing name</li>
+   * <li><code>Anonymous Coward (12)</code>: missing name and email address</li>
+   * </ul>
+   */
+  public String getNameEmail(String anonymousCowardName) {
+    String name = getFullName();
+    if (name == null) {
+      name = anonymousCowardName;
+    }
+
+    final StringBuilder b = new StringBuilder();
+    b.append(name);
+    if (getPreferredEmail() != null) {
+      b.append(" <");
+      b.append(getPreferredEmail());
+      b.append(">");
+    } else if (getId() != null) {
+      b.append(" (");
+      b.append(getId().get());
+      b.append(")");
+    }
+    return b.toString();
+  }
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountSecurity.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountSecurity.java
index 21aca69..aa212f9 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountSecurity.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountSecurity.java
@@ -14,17 +14,18 @@
 
 package com.google.gerrit.common.data;
 
+import com.google.gerrit.common.audit.Audit;
 import com.google.gerrit.common.auth.SignInRequired;
 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.AccountSshKey;
 import com.google.gerrit.reviewdb.client.ContactInformation;
-import com.google.gerrit.reviewdb.client.ContributorAgreement;
 import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.gwtjsonrpc.common.RemoteJsonService;
 import com.google.gwtjsonrpc.common.RpcImpl;
-import com.google.gwtjsonrpc.common.VoidResult;
 import com.google.gwtjsonrpc.common.RpcImpl.Version;
+import com.google.gwtjsonrpc.common.VoidResult;
 
 import java.util.List;
 import java.util.Set;
@@ -34,20 +35,25 @@
   @SignInRequired
   void mySshKeys(AsyncCallback<List<AccountSshKey>> callback);
 
+  @Audit
   @SignInRequired
   void addSshKey(String keyText, AsyncCallback<AccountSshKey> callback);
 
+  @Audit
   @SignInRequired
   void deleteSshKeys(Set<AccountSshKey.Id> ids,
       AsyncCallback<VoidResult> callback);
 
+  @Audit
   @SignInRequired
   void changeUserName(String newName, AsyncCallback<VoidResult> callback);
 
+  @Audit
   @SignInRequired
   void generatePassword(AccountExternalId.Key key,
       AsyncCallback<AccountExternalId> callback);
 
+  @Audit
   @SignInRequired
   void clearPassword(AccountExternalId.Key key,
       AsyncCallback<AccountExternalId> gerritCallback);
@@ -56,23 +62,28 @@
   void myExternalIds(AsyncCallback<List<AccountExternalId>> callback);
 
   @SignInRequired
-  void myGroups(AsyncCallback<List<GroupDetail>> callback);
+  void myGroups(AsyncCallback<List<AccountGroup>> callback);
 
+  @Audit
   @SignInRequired
   void deleteExternalIds(Set<AccountExternalId.Key> keys,
       AsyncCallback<Set<AccountExternalId.Key>> callback);
 
+  @Audit
   @SignInRequired
   void updateContact(String fullName, String emailAddr,
       ContactInformation info, AsyncCallback<Account> callback);
 
+  @Audit
   @SignInRequired
-  void enterAgreement(ContributorAgreement.Id id,
+  void enterAgreement(String agreementName,
       AsyncCallback<VoidResult> callback);
 
+  @Audit
   @SignInRequired
   void registerEmail(String address, AsyncCallback<Account> callback);
 
+  @Audit
   @SignInRequired
   void validateEmail(String token, AsyncCallback<VoidResult> callback);
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountService.java
index 7377d7e..18cf657 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountService.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountService.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.common.data;
 
+import com.google.gerrit.common.audit.Audit;
 import com.google.gerrit.common.auth.SignInRequired;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountDiffPreference;
@@ -36,10 +37,12 @@
   @SignInRequired
   void myDiffPreferences(AsyncCallback<AccountDiffPreference> callback);
 
+  @Audit
   @SignInRequired
   void changePreferences(AccountGeneralPreferences pref,
       AsyncCallback<VoidResult> gerritCallback);
 
+  @Audit
   @SignInRequired
   void changeDiffPreferences(AccountDiffPreference diffPref,
       AsyncCallback<VoidResult> callback);
@@ -47,14 +50,17 @@
   @SignInRequired
   void myProjectWatch(AsyncCallback<List<AccountProjectWatchInfo>> callback);
 
+  @Audit
   @SignInRequired
   void addProjectWatch(String projectName, String filter,
       AsyncCallback<AccountProjectWatchInfo> callback);
 
+  @Audit
   @SignInRequired
   void updateProjectWatch(AccountProjectWatch watch,
       AsyncCallback<VoidResult> callback);
 
+  @Audit
   @SignInRequired
   void deleteProjectWatches(Set<AccountProjectWatch.Key> keys,
       AsyncCallback<VoidResult> callback);
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/AgreementInfo.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/AgreementInfo.java
index 0c6f6b7..7464bd1 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/AgreementInfo.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/AgreementInfo.java
@@ -14,30 +14,21 @@
 
 package com.google.gerrit.common.data;
 
-import com.google.gerrit.reviewdb.client.AccountAgreement;
-import com.google.gerrit.reviewdb.client.AccountGroupAgreement;
-import com.google.gerrit.reviewdb.client.ContributorAgreement;
-
 import java.util.List;
 import java.util.Map;
 
 public class AgreementInfo {
-  public List<AccountAgreement> userAccepted;
-  public List<AccountGroupAgreement> groupAccepted;
-  public Map<ContributorAgreement.Id, ContributorAgreement> agreements;
+  public List<String> accepted;
+  public Map<String, ContributorAgreement> agreements;
 
   public AgreementInfo() {
   }
 
-  public void setUserAccepted(List<AccountAgreement> a) {
-    userAccepted = a;
+  public void setAccepted(List<String> a) {
+    accepted = a;
   }
 
-  public void setGroupAccepted(List<AccountGroupAgreement> a) {
-    groupAccepted = a;
-  }
-
-  public void setAgreements(Map<ContributorAgreement.Id, ContributorAgreement> a) {
+  public void setAgreements(Map<String, ContributorAgreement> a) {
     agreements = a;
   }
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetailService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetailService.java
index 8b43624..c50d2e3 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetailService.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetailService.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.common.data;
 
+import com.google.gerrit.common.audit.Audit;
 import com.google.gerrit.common.auth.SignInRequired;
 import com.google.gerrit.reviewdb.client.AccountDiffPreference;
 import com.google.gerrit.reviewdb.client.Change;
@@ -25,12 +26,16 @@
 
 @RpcImpl(version = Version.V2_0)
 public interface ChangeDetailService extends RemoteJsonService {
+  @Audit
   void changeDetail(Change.Id id, AsyncCallback<ChangeDetail> callback);
 
+  @Audit
   void includedInDetail(Change.Id id, AsyncCallback<IncludedInDetail> callback);
 
+  @Audit
   void patchSetDetail(PatchSet.Id key, AsyncCallback<PatchSetDetail> callback);
 
+  @Audit
   void patchSetDetail2(PatchSet.Id baseId, PatchSet.Id key,
       AccountDiffPreference diffPrefs, AsyncCallback<PatchSetDetail> callback);
 
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..0c466497 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
@@ -14,39 +14,22 @@
 
 package com.google.gerrit.common.data;
 
+import com.google.gerrit.common.audit.Audit;
 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.
    *
    * @param req the add and remove cluster.
    */
+  @Audit
   @SignInRequired
   void toggleStars(ToggleStarRequest req, AsyncCallback<VoidResult> callback);
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeManageService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeManageService.java
index 872bddc..4ef6b3e 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeManageService.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeManageService.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.common.data;
 
+import com.google.gerrit.common.audit.Audit;
 import com.google.gerrit.common.auth.SignInRequired;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gwtjsonrpc.common.AsyncCallback;
@@ -24,27 +25,34 @@
 
 @RpcImpl(version = Version.V2_0)
 public interface ChangeManageService extends RemoteJsonService {
+  @Audit
   @SignInRequired
   void submit(PatchSet.Id patchSetId, AsyncCallback<ChangeDetail> callback);
 
+  @Audit
   @SignInRequired
   void abandonChange(PatchSet.Id patchSetId, String message,
       AsyncCallback<ChangeDetail> callback);
 
+  @Audit
   @SignInRequired
   void revertChange(PatchSet.Id patchSetId, String message,
       AsyncCallback<ChangeDetail> callback);
 
+  @Audit
   @SignInRequired
   void restoreChange(PatchSet.Id patchSetId, String message,
       AsyncCallback<ChangeDetail> callback);
 
+  @Audit
   @SignInRequired
   void publish(PatchSet.Id patchSetId, AsyncCallback<ChangeDetail> callback);
 
+  @Audit
   @SignInRequired
   void deleteDraftChange(PatchSet.Id patchSetId, AsyncCallback<VoidResult> callback);
 
+  @Audit
   @SignInRequired
   void rebaseChange(PatchSet.Id patchSetId, AsyncCallback<ChangeDetail> callback);
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ContributorAgreement.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ContributorAgreement.java
new file mode 100644
index 0000000..e02d9d3
--- /dev/null
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ContributorAgreement.java
@@ -0,0 +1,111 @@
+// 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.Project;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/** Portion of a {@link Project} describing a single contributor agreement. */
+public class ContributorAgreement implements Comparable<ContributorAgreement> {
+  protected String name;
+  protected String description;
+  protected List<PermissionRule> accepted;
+  protected boolean requireContactInformation;
+  protected GroupReference autoVerify;
+  protected String agreementUrl;
+
+  protected ContributorAgreement() {
+  }
+
+  public ContributorAgreement(String name) {
+    setName(name);
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  public void setName(String name) {
+    this.name = name;
+  }
+
+  public String getDescription() {
+    return description;
+  }
+
+  public void setDescription(String description) {
+    this.description = description;
+  }
+
+  public List<PermissionRule> getAccepted() {
+    if (accepted == null) {
+      accepted = new ArrayList<PermissionRule>();
+    }
+    return accepted;
+  }
+
+  public void setAccepted(List<PermissionRule> accepted) {
+    this.accepted = accepted;
+  }
+
+  public boolean isRequireContactInformation() {
+    return requireContactInformation;
+  }
+
+  public void setRequireContactInformation(boolean requireContactInformation) {
+    this.requireContactInformation = requireContactInformation;
+  }
+
+  public GroupReference getAutoVerify() {
+    return autoVerify;
+  }
+
+  public void setAutoVerify(GroupReference autoVerify) {
+    this.autoVerify = autoVerify;
+  }
+
+  public String getAgreementUrl() {
+    return agreementUrl;
+  }
+
+  public void setAgreementUrl(String agreementUrl) {
+    this.agreementUrl = agreementUrl;
+  }
+
+  @Override
+  public int compareTo(ContributorAgreement o) {
+    return getName().compareTo(o.getName());
+  }
+
+  @Override
+  public String toString() {
+    return "ContributorAgreement[" + getName() + "]";
+  }
+
+  public ContributorAgreement forUi() {
+    ContributorAgreement ca = new ContributorAgreement(name);
+    ca.description = description;
+    ca.accepted = Collections.emptyList();
+    ca.requireContactInformation = requireContactInformation;
+    if (autoVerify != null) {
+      ca.autoVerify = new GroupReference();
+    }
+    ca.agreementUrl = agreementUrl ;
+    return ca;
+  }
+}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GerritConfig.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GerritConfig.java
index 456ffb4..7c16129 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GerritConfig.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GerritConfig.java
@@ -16,9 +16,11 @@
 
 import com.google.gerrit.common.auth.openid.OpenIdProviderPattern;
 import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Account.FieldName;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadCommand;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme;
 import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme;
 import com.google.gwtexpui.safehtml.client.RegexFindReplace;
 
 import java.util.List;
@@ -27,6 +29,8 @@
 public class GerritConfig implements Cloneable {
   protected String registerUrl;
   protected String httpPasswordUrl;
+  protected String reportBugUrl;
+  protected String openIdSsoUrl;
   protected List<OpenIdProviderPattern> allowedOpenIDs;
 
   protected GitwebConfig gitweb;
@@ -35,9 +39,11 @@
   protected boolean allowRegisterNewEmail;
   protected AuthType authType;
   protected Set<DownloadScheme> downloadSchemes;
+  protected Set<DownloadCommand> downloadCommands;
   protected String gitDaemonUrl;
   protected String gitHttpUrl;
   protected String sshdAddress;
+  protected String editFullNameUrl;
   protected Project.NameKey wildProject;
   protected ApprovalTypes approvalTypes;
   protected Set<Account.FieldName> editableAccountFields;
@@ -54,6 +60,22 @@
     registerUrl = u;
   }
 
+  public String getReportBugUrl() {
+    return reportBugUrl;
+  }
+
+  public void setReportBugUrl(String u) {
+    reportBugUrl = u;
+  }
+
+  public String getEditFullNameUrl() {
+    return editFullNameUrl;
+  }
+
+  public void setEditFullNameUrl(String u) {
+    editFullNameUrl = u;
+  }
+
   public String getHttpPasswordUrl() {
     return httpPasswordUrl;
   }
@@ -62,6 +84,14 @@
     httpPasswordUrl = url;
   }
 
+  public String getOpenIdSsoUrl() {
+      return openIdSsoUrl;
+  }
+
+  public void setOpenIdSsoUrl(final String u) {
+    openIdSsoUrl = u;
+  }
+
   public List<OpenIdProviderPattern> getAllowedOpenIDs() {
     return allowedOpenIDs;
   }
@@ -86,6 +116,14 @@
     downloadSchemes = s;
   }
 
+  public Set<DownloadCommand> getDownloadCommands() {
+    return downloadCommands;
+  }
+
+  public void setDownloadCommands(final Set<DownloadCommand> downloadCommands) {
+    this.downloadCommands = downloadCommands;
+  }
+
   public GitwebConfig getGitwebLink() {
     return gitweb;
   }
@@ -199,4 +237,13 @@
   public void setAnonymousCowardName(final String anonymousCowardName) {
     this.anonymousCowardName = anonymousCowardName;
   }
+
+  public boolean siteHasUsernames() {
+    if (getAuthType() == AuthType.CUSTOM_EXTENSION
+        && getHttpPasswordUrl() != null
+        && !canEdit(FieldName.USER_NAME)) {
+      return false;
+    }
+    return true;
+  }
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java
index d3d2a4d..81d4fc9 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java
@@ -15,6 +15,8 @@
 package com.google.gerrit.common.data;
 
 import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
 
 /** Server wide capabilities. Represented as {@link Permission} objects. */
@@ -40,12 +42,13 @@
   public static final String CREATE_PROJECT = "createProject";
 
   /**
-   * Denotes who may email change reviewers.
+   * Denotes who may email change reviewers and watchers.
    * <p>
    * This can be used to deny build bots from emailing reviewers and people who
-   * have starred the changed. Instead, only the authors of the change will be
-   * emailed. The allow rules are evaluated before deny rules, however the
-   * default is to allow emailing, if no explicit rule is matched.
+   * watch the change. Instead, only the authors of the change and those who
+   * starred it will be emailed. The allow rules are evaluated before deny
+   * rules, however the default is to allow emailing, if no explicit rule is
+   * matched.
    */
   public static final String EMAIL_REVIEWERS = "emailReviewers";
 
@@ -73,23 +76,34 @@
   /** Can view all pending tasks in the queue (not just the filtered set). */
   public static final String VIEW_QUEUE = "viewQueue";
 
+  private static final List<String> NAMES_ALL;
   private static final List<String> NAMES_LC;
 
   static {
-    NAMES_LC = new ArrayList<String>();
-    NAMES_LC.add(ADMINISTRATE_SERVER.toLowerCase());
-    NAMES_LC.add(CREATE_ACCOUNT.toLowerCase());
-    NAMES_LC.add(CREATE_GROUP.toLowerCase());
-    NAMES_LC.add(CREATE_PROJECT.toLowerCase());
-    NAMES_LC.add(EMAIL_REVIEWERS.toLowerCase());
-    NAMES_LC.add(FLUSH_CACHES.toLowerCase());
-    NAMES_LC.add(KILL_TASK.toLowerCase());
-    NAMES_LC.add(PRIORITY.toLowerCase());
-    NAMES_LC.add(QUERY_LIMIT.toLowerCase());
-    NAMES_LC.add(START_REPLICATION.toLowerCase());
-    NAMES_LC.add(VIEW_CACHES.toLowerCase());
-    NAMES_LC.add(VIEW_CONNECTIONS.toLowerCase());
-    NAMES_LC.add(VIEW_QUEUE.toLowerCase());
+    NAMES_ALL = new ArrayList<String>();
+    NAMES_ALL.add(ADMINISTRATE_SERVER);
+    NAMES_ALL.add(CREATE_ACCOUNT);
+    NAMES_ALL.add(CREATE_GROUP);
+    NAMES_ALL.add(CREATE_PROJECT);
+    NAMES_ALL.add(EMAIL_REVIEWERS);
+    NAMES_ALL.add(FLUSH_CACHES);
+    NAMES_ALL.add(KILL_TASK);
+    NAMES_ALL.add(PRIORITY);
+    NAMES_ALL.add(QUERY_LIMIT);
+    NAMES_ALL.add(START_REPLICATION);
+    NAMES_ALL.add(VIEW_CACHES);
+    NAMES_ALL.add(VIEW_CONNECTIONS);
+    NAMES_ALL.add(VIEW_QUEUE);
+
+    NAMES_LC = new ArrayList<String>(NAMES_ALL.size());
+    for (String name : NAMES_ALL) {
+      NAMES_LC.add(name.toLowerCase());
+    }
+  }
+
+  /** @return all valid capability names. */
+  public static Collection<String> getAllNames() {
+    return Collections.unmodifiableList(NAMES_ALL);
   }
 
   /** @return true if the name is recognized as a capability name. */
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..5cb7fa2 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
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.common.data;
 
+import com.google.gerrit.common.audit.Audit;
 import com.google.gerrit.common.auth.SignInRequired;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountGroupInclude;
@@ -24,61 +25,64 @@
 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)
 public interface GroupAdminService extends RemoteJsonService {
+  @Audit
   @SignInRequired
   void visibleGroups(AsyncCallback<GroupList> callback);
 
+  @Audit
   @SignInRequired
   void createGroup(String newName, AsyncCallback<AccountGroup.Id> callback);
 
+  @Audit
   @SignInRequired
   void groupDetail(AccountGroup.Id groupId, AccountGroup.UUID uuid,
       AsyncCallback<GroupDetail> callback);
 
+  @Audit
   @SignInRequired
   void changeGroupDescription(AccountGroup.Id groupId, String description,
       AsyncCallback<VoidResult> callback);
 
+  @Audit
   @SignInRequired
   void changeGroupOptions(AccountGroup.Id groupId, GroupOptions groupOptions,
       AsyncCallback<VoidResult> callback);
 
+  @Audit
   @SignInRequired
   void changeGroupOwner(AccountGroup.Id groupId, String newOwnerName,
       AsyncCallback<VoidResult> callback);
 
+  @Audit
   @SignInRequired
   void renameGroup(AccountGroup.Id groupId, String newName,
       AsyncCallback<GroupDetail> callback);
 
+  @Audit
   @SignInRequired
   void changeGroupType(AccountGroup.Id groupId, AccountGroup.Type newType,
       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);
-
+  @Audit
   @SignInRequired
   void addGroupMember(AccountGroup.Id groupId, String nameOrEmail,
       AsyncCallback<GroupDetail> callback);
 
+  @Audit
   @SignInRequired
   void addGroupInclude(AccountGroup.Id groupId, String groupName,
       AsyncCallback<GroupDetail> callback);
 
+  @Audit
   @SignInRequired
   void deleteGroupMembers(AccountGroup.Id groupId,
       Set<AccountGroupMember.Key> keys, AsyncCallback<VoidResult> callback);
 
+  @Audit
   @SignInRequired
   void deleteGroupIncludes(AccountGroup.Id groupId,
       Set<AccountGroupInclude.Key> keys, AsyncCallback<VoidResult> callback);
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDescription.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDescription.java
new file mode 100644
index 0000000..828bf24
--- /dev/null
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDescription.java
@@ -0,0 +1,48 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.common.data;
+
+import com.google.gerrit.reviewdb.client.AccountGroup;
+
+/**
+ * Group methods exposed by the GroupBackend.
+ */
+public class GroupDescription {
+  /**
+   * The Basic information required to be exposed by any Group.
+   */
+  public interface Basic {
+    /** @return the non-null UUID of the group. */
+    AccountGroup.UUID getGroupUUID();
+
+    /** @return the non-null name of the group. */
+    String getName();
+
+    /** @return whether the group is visible to all accounts. */
+    boolean isVisibleToAll();
+  }
+
+  /**
+   * The extended information exposed by internal groups backed by an
+   * AccountGroup.
+   */
+  public interface Internal extends Basic {
+    /** @return the backing AccountGroup. */
+    AccountGroup getAccountGroup();
+  }
+
+  private GroupDescription() {
+  }
+}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDescriptions.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDescriptions.java
new file mode 100644
index 0000000..e0bc7d8
--- /dev/null
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDescriptions.java
@@ -0,0 +1,60 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.common.data;
+
+import com.google.gerrit.reviewdb.client.AccountGroup;
+
+import javax.annotation.Nullable;
+
+/**
+ * Utility class for building GroupDescription objects.
+ */
+public class GroupDescriptions {
+
+  @Nullable
+  public static AccountGroup toAccountGroup(GroupDescription.Basic group) {
+    if (group instanceof GroupDescription.Internal) {
+      return ((GroupDescription.Internal) group).getAccountGroup();
+    }
+    return null;
+  }
+
+  public static GroupDescription.Internal forAccountGroup(final AccountGroup group) {
+    return new GroupDescription.Internal() {
+      @Override
+      public AccountGroup.UUID getGroupUUID() {
+        return group.getGroupUUID();
+      }
+
+      @Override
+      public String getName() {
+        return group.getName();
+      }
+
+      @Override
+      public boolean isVisibleToAll() {
+        return group.isVisibleToAll();
+      }
+
+      @Override
+      public AccountGroup getAccountGroup() {
+        return group;
+      }
+    };
+  }
+
+  private GroupDescriptions() {
+  }
+}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDetail.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDetail.java
index 65723f7..01c7985 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDetail.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDetail.java
@@ -26,7 +26,7 @@
   public AccountGroup group;
   public List<AccountGroupMember> members;
   public List<AccountGroupInclude> includes;
-  public AccountGroup ownerGroup;
+  public GroupReference ownerGroup;
   public boolean canModify;
 
   public GroupDetail() {
@@ -52,7 +52,7 @@
     includes = i;
   }
 
-  public void setOwnerGroup(AccountGroup g) {
+  public void setOwnerGroup(GroupReference g) {
     ownerGroup = g;
   }
 
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupList.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupList.java
index 6352461..b3095cd 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupList.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupList.java
@@ -14,25 +14,27 @@
 
 package com.google.gerrit.common.data;
 
+import com.google.gerrit.reviewdb.client.AccountGroup;
+
 import java.util.List;
 
 public class GroupList {
-  protected List<GroupDetail> groups;
+  protected List<AccountGroup> groups;
   protected boolean canCreateGroup;
 
   protected GroupList() {
   }
 
-  public GroupList(final List<GroupDetail> groups, final boolean canCreateGroup) {
+  public GroupList(final List<AccountGroup> groups, final boolean canCreateGroup) {
     this.groups = groups;
     this.canCreateGroup = canCreateGroup;
   }
 
-  public List<GroupDetail> getGroups() {
+  public List<AccountGroup> getGroups() {
     return groups;
   }
 
-  public void setGroups(List<GroupDetail> groups) {
+  public void setGroups(List<AccountGroup> groups) {
     this.groups = groups;
   }
 
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/HostPageData.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/HostPageData.java
index c3d3f1e..f991f4c 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/HostPageData.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/HostPageData.java
@@ -31,5 +31,8 @@
     public String textColor;
     public String trimColor;
     public String selectionColor;
+    public String changeTableOutdatedColor;
+    public String tableOddRowColor;
+    public String tableEvenRowColor;
   }
 }
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/PatchDetailService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchDetailService.java
index 0191544..91ecb92 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchDetailService.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchDetailService.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.common.data;
 
+import com.google.gerrit.common.audit.Audit;
 import com.google.gerrit.common.auth.SignInRequired;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountDiffPreference;
@@ -34,13 +35,16 @@
 
 @RpcImpl(version = Version.V2_0)
 public interface PatchDetailService extends RemoteJsonService {
+  @Audit
   void patchScript(Patch.Key key, PatchSet.Id a, PatchSet.Id b,
       AccountDiffPreference diffPrefs, AsyncCallback<PatchScript> callback);
 
+  @Audit
   @SignInRequired
   void saveDraft(PatchLineComment comment,
       AsyncCallback<PatchLineComment> callback);
 
+  @Audit
   @SignInRequired
   void deleteDraft(PatchLineComment.Key key, AsyncCallback<VoidResult> callback);
 
@@ -57,18 +61,22 @@
    *        change, then <code>null</code> is passed as result to
    *        {@link AsyncCallback#onSuccess(Object)}
    */
+  @Audit
   @SignInRequired
   void deleteDraftPatchSet(PatchSet.Id psid, AsyncCallback<ChangeDetail> callback);
 
+  @Audit
   @SignInRequired
   void publishComments(PatchSet.Id psid, String message,
       Set<ApprovalCategoryValue.Id> approvals,
       AsyncCallback<VoidResult> callback);
 
+  @Audit
   @SignInRequired
   void addReviewers(Change.Id id, List<String> reviewers, boolean confirmed,
       AsyncCallback<ReviewerResult> callback);
 
+  @Audit
   @SignInRequired
   void removeReviewer(Change.Id id, Account.Id reviewerId,
       AsyncCallback<ReviewerResult> callback);
@@ -82,6 +90,7 @@
   /**
    * Update the reviewed status for the patch.
    */
+  @Audit
   @SignInRequired
   void setReviewedByCurrentUser(Key patchKey, boolean reviewed, AsyncCallback<VoidResult> callback);
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchSetPublishDetail.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchSetPublishDetail.java
index 075d558..3c5c688 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchSetPublishDetail.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchSetPublishDetail.java
@@ -20,6 +20,9 @@
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.client.PatchSetInfo;
 
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
 import java.util.List;
 
 public class PatchSetPublishDetail {
@@ -28,6 +31,8 @@
   protected Change change;
   protected List<PatchLineComment> drafts;
   protected List<PermissionRange> labels;
+  protected List<ApprovalDetail> approvals;
+  protected List<SubmitRecord> submitRecords;
   protected List<PatchSetApproval> given;
   protected boolean canSubmit;
 
@@ -39,6 +44,23 @@
     this.labels = labels;
   }
 
+  public List<ApprovalDetail> getApprovals() {
+    return approvals;
+  }
+
+  public void setApprovals(Collection<ApprovalDetail> list) {
+    approvals = new ArrayList<ApprovalDetail>(list);
+    Collections.sort(approvals, ApprovalDetail.SORT);
+  }
+
+  public void setSubmitRecords(List<SubmitRecord> all) {
+    submitRecords = all;
+  }
+
+  public List<SubmitRecord> getSubmitRecords() {
+    return submitRecords;
+  }
+
   public List<PatchSetApproval> getGiven() {
     return given;
   }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/Permission.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/Permission.java
index 20261de..fd40888 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/Permission.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/Permission.java
@@ -15,11 +15,13 @@
 package com.google.gerrit.common.data;
 
 import java.util.ArrayList;
+import java.util.HashSet;
 import java.util.Iterator;
 import java.util.List;
 
 /** A single permission within an {@link AccessSection} of a project. */
 public class Permission implements Comparable<Permission> {
+  public static final String ABANDON = "abandon";
   public static final String CREATE = "create";
   public static final String FORGE_AUTHOR = "forgeAuthor";
   public static final String FORGE_COMMITTER = "forgeCommitter";
@@ -40,6 +42,7 @@
     NAMES_LC = new ArrayList<String>();
     NAMES_LC.add(OWNER.toLowerCase());
     NAMES_LC.add(READ.toLowerCase());
+    NAMES_LC.add(ABANDON.toLowerCase());
     NAMES_LC.add(CREATE.toLowerCase());
     NAMES_LC.add(FORGE_AUTHOR.toLowerCase());
     NAMES_LC.add(FORGE_COMMITTER.toLowerCase());
@@ -73,6 +76,13 @@
     return LABEL + labelName;
   }
 
+  public static boolean canBeOnAllProjects(String ref, String permissionName) {
+    if (AccessSection.ALL.equals(ref)) {
+      return !OWNER.equals(permissionName);
+    }
+    return true;
+  }
+
   protected String name;
   protected boolean exclusiveGroup;
   protected List<PermissionRule> rules;
@@ -208,4 +218,23 @@
     int index = NAMES_LC.indexOf(a.getName().toLowerCase());
     return 0 <= index ? index : NAMES_LC.size();
   }
+
+  @Override
+  public boolean equals(final Object obj) {
+    if (!(obj instanceof Permission)) {
+      return false;
+    }
+
+    final Permission other = (Permission) obj;
+    if (!name.equals(other.name) || exclusiveGroup != other.exclusiveGroup) {
+      return false;
+    }
+    return new HashSet<PermissionRule>(rules)
+        .equals(new HashSet<PermissionRule>(other.rules));
+  }
+
+  @Override
+  public int hashCode() {
+    return name.hashCode();
+  }
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/PermissionRule.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/PermissionRule.java
index 9b6695e..5960165 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/PermissionRule.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/PermissionRule.java
@@ -257,4 +257,19 @@
     }
     return Integer.parseInt(value);
   }
+
+  @Override
+  public boolean equals(final Object obj) {
+    if (!(obj instanceof PermissionRule)) {
+      return false;
+    }
+    final PermissionRule other = (PermissionRule)obj;
+    return action.equals(other.action) && force == other.force
+        && min == other.min && max == other.max && group.equals(other.group);
+  }
+
+  @Override
+  public int hashCode() {
+    return group.hashCode();
+  }
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectAccess.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectAccess.java
index f935c03..1893843 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectAccess.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectAccess.java
@@ -26,6 +26,7 @@
   protected List<AccessSection> local;
   protected Set<String> ownerOf;
   protected boolean isConfigVisible;
+  protected boolean canUpload;
 
   public ProjectAccess() {
   }
@@ -94,4 +95,12 @@
   public void setConfigVisible(boolean isConfigVisible) {
     this.isConfigVisible = isConfigVisible;
   }
+
+  public boolean canUpload() {
+    return canUpload;
+  }
+
+  public void setCanUpload(boolean canUpload) {
+    this.canUpload = canUpload;
+  }
 }
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..13c0a48 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
@@ -14,8 +14,10 @@
 
 package com.google.gerrit.common.data;
 
+import com.google.gerrit.common.audit.Audit;
 import com.google.gerrit.common.auth.SignInRequired;
 import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.gwtjsonrpc.common.RemoteJsonService;
@@ -28,14 +30,12 @@
 
 @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);
 
+  @Audit
   @SignInRequired
   void createNewProject(String projectName, String parentName,
       boolean emptyCommit, boolean permissionsOnly,
@@ -44,22 +44,31 @@
   void projectAccess(Project.NameKey projectName,
       AsyncCallback<ProjectAccess> callback);
 
+  @Audit
   @SignInRequired
   void changeProjectSettings(Project update,
       AsyncCallback<ProjectDetail> callback);
 
+  @Audit
   @SignInRequired
   void changeProjectAccess(Project.NameKey projectName, String baseRevision,
       String message, List<AccessSection> sections,
       AsyncCallback<ProjectAccess> callback);
 
+  @SignInRequired
+  void reviewProjectAccess(Project.NameKey projectName, String baseRevision,
+      String message, List<AccessSection> sections,
+      AsyncCallback<Change.Id> callback);
+
   void listBranches(Project.NameKey projectName,
       AsyncCallback<ListBranchesResult> callback);
 
+  @Audit
   @SignInRequired
   void addBranch(Project.NameKey projectName, String branchName,
       String startingRevision, AsyncCallback<ListBranchesResult> callback);
 
+  @Audit
   @SignInRequired
   void deleteBranch(Project.NameKey projectName, Set<Branch.NameKey> ids,
       AsyncCallback<Set<Branch.NameKey>> 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/RefConfigSection.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/RefConfigSection.java
index 490378e..810e906 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/RefConfigSection.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/RefConfigSection.java
@@ -21,6 +21,9 @@
   /** Pattern that matches all branches in a project. */
   public static final String HEADS = "refs/heads/*";
 
+  /** Configuration settings for a project {@code refs/meta/config} */
+  public static final String REF_CONFIG = "refs/meta/config";
+
   /** Prefix that triggers a regular expression pattern. */
   public static final String REGEX_PREFIX = "^";
 
@@ -45,4 +48,17 @@
   public void setName(String name) {
     this.name = name;
   }
+
+  @Override
+  public boolean equals(final Object obj) {
+    if (!(obj instanceof RefConfigSection)) {
+      return false;
+    }
+    return name.equals(((RefConfigSection) obj).name);
+  }
+
+  @Override
+  public int hashCode() {
+    return name.hashCode();
+  }
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ReviewResult.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ReviewResult.java
index 001f9b4..c0bf818 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ReviewResult.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ReviewResult.java
@@ -63,6 +63,9 @@
       /** Review operation invalid because change is closed. */
       CHANGE_IS_CLOSED,
 
+      /** Review operation invalid because change is not abandoned. */
+      CHANGE_NOT_ABANDONED,
+
       /** Not permitted to publish this draft patch set */
       PUBLISH_NOT_PERMITTED,
 
@@ -76,7 +79,10 @@
       NOT_A_DRAFT,
 
       /** Error writing change to git repository */
-      GIT_ERROR
+      GIT_ERROR,
+
+      /** The destination branch does not exist */
+      DEST_BRANCH_NOT_FOUND
     }
 
     protected Type type;
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 85518b2..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
@@ -26,15 +26,19 @@
 
 @RpcImpl(version = Version.V2_0)
 public interface SuggestService extends RemoteJsonService {
-  void suggestProjectNameKey(String query, int limit,
-      AsyncCallback<List<Project.NameKey>> callback);
-
   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-common/src/main/java/com/google/gerrit/common/data/SystemInfoService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/SystemInfoService.java
index 78ccca1..4a45350 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/SystemInfoService.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/SystemInfoService.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.common.data;
 
 import com.google.gerrit.common.auth.SignInRequired;
-import com.google.gerrit.reviewdb.client.ContributorAgreement;
 import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.gwtjsonrpc.common.AllowCrossSiteRequest;
 import com.google.gwtjsonrpc.common.RemoteJsonService;
diff --git a/gerrit-ehcache/.settings/org.eclipse.core.resources.prefs b/gerrit-ehcache/.settings/org.eclipse.core.resources.prefs
deleted file mode 100644
index 82eb859..0000000
--- a/gerrit-ehcache/.settings/org.eclipse.core.resources.prefs
+++ /dev/null
@@ -1,3 +0,0 @@
-#Tue Sep 02 16:59:24 PDT 2008
-eclipse.preferences.version=1
-encoding/<project>=UTF-8
diff --git a/gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/EhcachePoolImpl.java b/gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/EhcachePoolImpl.java
deleted file mode 100644
index c25c381..0000000
--- a/gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/EhcachePoolImpl.java
+++ /dev/null
@@ -1,271 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.ehcache;
-
-import static java.util.concurrent.TimeUnit.MINUTES;
-import static java.util.concurrent.TimeUnit.SECONDS;
-
-import com.google.gerrit.lifecycle.LifecycleListener;
-import com.google.gerrit.lifecycle.LifecycleModule;
-import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.cache.CachePool;
-import com.google.gerrit.server.cache.CacheProvider;
-import com.google.gerrit.server.cache.EntryCreator;
-import com.google.gerrit.server.cache.EvictionPolicy;
-import com.google.gerrit.server.cache.ProxyCache;
-import com.google.gerrit.server.config.ConfigUtil;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.config.SitePaths;
-import com.google.inject.Inject;
-import com.google.inject.Singleton;
-
-import net.sf.ehcache.CacheManager;
-import net.sf.ehcache.Ehcache;
-import net.sf.ehcache.config.CacheConfiguration;
-import net.sf.ehcache.config.Configuration;
-import net.sf.ehcache.config.DiskStoreConfiguration;
-import net.sf.ehcache.store.MemoryStoreEvictionPolicy;
-
-import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.File;
-import java.util.HashMap;
-import java.util.Map;
-
-/** Pool of all declared caches created by {@link CacheModule}s. */
-@Singleton
-public class EhcachePoolImpl implements CachePool {
-  private static final Logger log =
-      LoggerFactory.getLogger(EhcachePoolImpl.class);
-
-  public static class Module extends LifecycleModule {
-    @Override
-    protected void configure() {
-      bind(CachePool.class).to(EhcachePoolImpl.class);
-      bind(EhcachePoolImpl.class);
-      listener().to(EhcachePoolImpl.Lifecycle.class);
-    }
-  }
-
-  public static class Lifecycle implements LifecycleListener {
-    private final EhcachePoolImpl cachePool;
-
-    @Inject
-    Lifecycle(final EhcachePoolImpl cachePool) {
-      this.cachePool = cachePool;
-    }
-
-    @Override
-    public void start() {
-      cachePool.start();
-    }
-
-    @Override
-    public void stop() {
-      cachePool.stop();
-    }
-  }
-
-  private final Config config;
-  private final SitePaths site;
-
-  private final Object lock = new Object();
-  private final Map<String, CacheProvider<?, ?>> caches;
-  private CacheManager manager;
-
-  @Inject
-  EhcachePoolImpl(@GerritServerConfig final Config cfg, final SitePaths site) {
-    this.config = cfg;
-    this.site = site;
-    this.caches = new HashMap<String, CacheProvider<?, ?>>();
-  }
-
-  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-ehcache/.gitignore b/gerrit-extension-api/.gitignore
similarity index 80%
copy from gerrit-ehcache/.gitignore
copy to gerrit-extension-api/.gitignore
index 20251d4..4e1ec9c 100644
--- a/gerrit-ehcache/.gitignore
+++ b/gerrit-extension-api/.gitignore
@@ -1,5 +1,6 @@
 /target
 /.classpath
 /.project
-/.settings/org.eclipse.m2e.core.prefs
 /.settings/org.maven.ide.eclipse.prefs
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-extension-api.iml
diff --git a/gerrit-extension-api/.settings/org.eclipse.core.resources.prefs b/gerrit-extension-api/.settings/org.eclipse.core.resources.prefs
new file mode 100644
index 0000000..f9fe345
--- /dev/null
+++ b/gerrit-extension-api/.settings/org.eclipse.core.resources.prefs
@@ -0,0 +1,4 @@
+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-extension-api/.settings/org.eclipse.core.runtime.prefs
similarity index 100%
copy from gerrit-ehcache/.settings/org.eclipse.core.runtime.prefs
copy to gerrit-extension-api/.settings/org.eclipse.core.runtime.prefs
diff --git a/gerrit-ehcache/.settings/org.eclipse.jdt.core.prefs b/gerrit-extension-api/.settings/org.eclipse.jdt.core.prefs
similarity index 99%
copy from gerrit-ehcache/.settings/org.eclipse.jdt.core.prefs
copy to gerrit-extension-api/.settings/org.eclipse.jdt.core.prefs
index e89c048..470942d 100644
--- a/gerrit-ehcache/.settings/org.eclipse.jdt.core.prefs
+++ b/gerrit-extension-api/.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-extension-api/.settings/org.eclipse.jdt.ui.prefs
similarity index 100%
copy from gerrit-ehcache/.settings/org.eclipse.jdt.ui.prefs
copy to gerrit-extension-api/.settings/org.eclipse.jdt.ui.prefs
diff --git a/gerrit-extension-api/pom.xml b/gerrit-extension-api/pom.xml
new file mode 100644
index 0000000..ff672d5
--- /dev/null
+++ b/gerrit-extension-api/pom.xml
@@ -0,0 +1,73 @@
+<?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-extension-api</artifactId>
+  <name>Gerrit Code Review - Extension API</name>
+
+  <description>
+    Interfaces describing the extension API
+  </description>
+
+  <dependencies>
+    <dependency>
+      <groupId>com.google.inject</groupId>
+      <artifactId>guice</artifactId>
+    </dependency>
+
+    <dependency>
+      <groupId>com.google.inject.extensions</groupId>
+      <artifactId>guice-servlet</artifactId>
+    </dependency>
+
+    <dependency>
+      <groupId>org.apache.tomcat</groupId>
+      <artifactId>servlet-api</artifactId>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-shade-plugin</artifactId>
+        <configuration>
+          <createSourcesJar>true</createSourcesJar>
+          <shadedArtifactAttached>true</shadedArtifactAttached>
+          <shadedClassifierName>all</shadedClassifierName>
+        </configuration>
+        <executions>
+          <execution>
+            <phase>package</phase>
+            <goals>
+              <goal>shade</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+</project>
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/Export.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/Export.java
new file mode 100644
index 0000000..4811e407
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/Export.java
@@ -0,0 +1,52 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.annotations;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.inject.BindingAnnotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation applied to auto-registered, exported types.
+ * <p>
+ * Plugins or extensions using auto-registration should apply this annotation to
+ * any non-abstract class they want exported for access.
+ * <p>
+ * For SSH commands the @Export annotation names the subcommand:
+ *
+ * <pre>
+ *   @Export("print")
+ *   class MyCommand extends SshCommand {
+ * </pre>
+ *
+ * For HTTP servlets, the @Export annotation names the URL the servlet is bound
+ * to, relative to the plugin or extension's namespace within the Gerrit
+ * container.
+ *
+ * <pre>
+ *  @Export("/index.html")
+ *  class ShowIndexHtml extends HttpServlet {
+ * </pre>
+ */
+@Target({ElementType.TYPE})
+@Retention(RUNTIME)
+@BindingAnnotation
+public @interface Export {
+  String value();
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/ExportImpl.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/ExportImpl.java
new file mode 100644
index 0000000..a3e72bc
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/ExportImpl.java
@@ -0,0 +1,52 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.annotations;
+
+import java.io.Serializable;
+import java.lang.annotation.Annotation;
+
+final class ExportImpl implements Export, Serializable {
+  private static final long serialVersionUID = 0;
+  private final String value;
+
+  ExportImpl(String value) {
+    this.value = value;
+  }
+
+  @Override
+  public Class<? extends Annotation> annotationType() {
+    return Export.class;
+  }
+
+  @Override
+  public String value() {
+    return value;
+  }
+
+  @Override
+  public int hashCode() {
+    return (127 * "value".hashCode()) ^ value.hashCode();
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    return o instanceof Export && value.equals(((Export) o).value());
+  }
+
+  @Override
+  public String toString() {
+    return "@" + Export.class.getName() + "(value=" + value + ")";
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeRestoreEvent.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/Exports.java
similarity index 60%
copy from gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeRestoreEvent.java
copy to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/Exports.java
index 1a2922b..c48bcfb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeRestoreEvent.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/Exports.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,12 +12,15 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.events;
+package com.google.gerrit.extensions.annotations;
 
-public class ChangeRestoreEvent extends ChangeEvent {
-    public final String type = "change-restored";
-    public ChangeAttribute change;
-    public PatchSetAttribute patchSet;
-    public AccountAttribute restorer;
-    public String reason;
+/** Static constructors for {@link Export} annotations. */
+public final class Exports {
+  /** Create an annotation to export under a specific name. */
+  public static Export named(String name) {
+    return new ExportImpl(name);
+  }
+
+  private Exports() {
+  }
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/ExtensionPoint.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/ExtensionPoint.java
new file mode 100644
index 0000000..4799f5e
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/ExtensionPoint.java
@@ -0,0 +1,41 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.annotations;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.inject.BindingAnnotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation for interfaces that accept auto-registered implementations.
+ * <p>
+ * Interfaces that accept automatically registered implementations into their
+ * {@link DynamicSet} must be tagged with this annotation.
+ * <p>
+ * Plugins or extensions that implement an {@code @ExtensionPoint} interface
+ * should use the {@link Listen} annotation to automatically register.
+ *
+ * @see Listen
+ */
+@Target({ElementType.TYPE})
+@Retention(RUNTIME)
+@BindingAnnotation
+public @interface ExtensionPoint {
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/Listen.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/Listen.java
new file mode 100644
index 0000000..e4ba931
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/Listen.java
@@ -0,0 +1,39 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.annotations;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.inject.BindingAnnotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation for auto-registered extension point implementations.
+ * <p>
+ * Plugins or extensions using auto-registration should apply this annotation to
+ * any non-abstract class that implements an unnamed extension point, such as a
+ * notification listener. Gerrit will automatically determine which extension
+ * points to apply based on the interfaces the type implements.
+ *
+ * @see Export
+ */
+@Target({ElementType.TYPE})
+@Retention(RUNTIME)
+@BindingAnnotation
+public @interface Listen {
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/PluginData.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/PluginData.java
new file mode 100644
index 0000000..bf2b09a
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/PluginData.java
@@ -0,0 +1,42 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.annotations;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.inject.BindingAnnotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * Local path where a plugin can store its own private data.
+ * <p>
+ * A plugin or extension may receive this string by Guice injection to discover
+ * a directory where it can store configuration or other data that is private:
+ *
+ * <pre>
+ * @Inject
+ * MyType(@PluginData java.io.File myDir) {
+ *   new FileInputStream(new File(myDir, &quot;my.config&quot;));
+ * }
+ * </pre>
+ */
+@Target({ElementType.PARAMETER, ElementType.FIELD})
+@Retention(RUNTIME)
+@BindingAnnotation
+public @interface PluginData {
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/PluginName.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/PluginName.java
new file mode 100644
index 0000000..672bab2
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/PluginName.java
@@ -0,0 +1,42 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.annotations;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.inject.BindingAnnotation;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation applied to a String containing the plugin or extension name.
+ * <p>
+ * A plugin or extension may receive this string by Guice injection to discover
+ * the name that an administrator has installed the plugin or extension under:
+ *
+ * <pre>
+ *  @Inject
+ *  MyType(@PluginName String myName) {
+ *  ...
+ *  }
+ * </pre>
+ */
+@Target({ElementType.PARAMETER, ElementType.FIELD})
+@Retention(RUNTIME)
+@BindingAnnotation
+public @interface PluginName {
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AdminCommand.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/RequiresCapability.java
similarity index 65%
rename from gerrit-sshd/src/main/java/com/google/gerrit/sshd/AdminCommand.java
rename to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/RequiresCapability.java
index adaf646..382f4ea 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AdminCommand.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/annotations/RequiresCapability.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.
@@ -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,12 +21,11 @@
 import java.lang.annotation.Target;
 
 /**
- * Annotation tagged on a concrete Command that requires administrator access.
- * <p>
- * Currently this annotation is only enforced by DispatchCommand after it has
- * created the command object, but before it populates it or starts execution.
+ * Annotation on {@link SshCommand} or {@link RestApiServlet} declaring a
+ * capability must be granted.
  */
-@Target( {ElementType.TYPE})
+@Target({ElementType.TYPE})
 @Retention(RUNTIME)
-public @interface AdminCommand {
+public @interface RequiresCapability {
+  String value();
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/GitReferenceUpdatedListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/GitReferenceUpdatedListener.java
new file mode 100644
index 0000000..438500d
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/GitReferenceUpdatedListener.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.events;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+
+import java.util.List;
+
+/** Notified when one or more references are modified. */
+@ExtensionPoint
+public interface GitReferenceUpdatedListener {
+  public interface Update {
+    String getRefName();
+  }
+
+  public interface Event {
+    String getProjectName();
+    List<Update> getUpdates();
+  }
+
+  void onGitReferenceUpdated(Event event);
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/lifecycle/LifecycleListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/LifecycleListener.java
similarity index 87%
rename from gerrit-server/src/main/java/com/google/gerrit/lifecycle/LifecycleListener.java
rename to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/LifecycleListener.java
index e6b06ef..93da347 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/lifecycle/LifecycleListener.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/LifecycleListener.java
@@ -12,11 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.lifecycle;
+package com.google.gerrit.extensions.events;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
 
 import java.util.EventListener;
 
 /** Listener interested in server startup and shutdown events. */
+@ExtensionPoint
 public interface LifecycleListener extends EventListener {
   /** Invoke when the server is starting. */
   public void start();
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/NewProjectCreatedListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/NewProjectCreatedListener.java
new file mode 100644
index 0000000..7eed7d4
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/NewProjectCreatedListener.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.events;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+
+
+/** Notified whenever a project is created on the master. */
+@ExtensionPoint
+public interface NewProjectCreatedListener {
+  public interface Event {
+    String getProjectName();
+    String getHeadName();
+  }
+
+  void onNewProjectCreated(Event event);
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMap.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMap.java
new file mode 100644
index 0000000..40bbb80
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMap.java
@@ -0,0 +1,163 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.registration;
+
+import com.google.inject.Binder;
+import com.google.inject.Key;
+import com.google.inject.Provider;
+import com.google.inject.Scopes;
+import com.google.inject.TypeLiteral;
+import com.google.inject.util.Types;
+
+import java.util.Collections;
+import java.util.Map;
+import java.util.SortedMap;
+import java.util.SortedSet;
+import java.util.TreeMap;
+import java.util.TreeSet;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ConcurrentMap;
+
+/**
+ * A map of members that can be modified as plugins reload.
+ * <p>
+ * Maps index their members by plugin name and export name.
+ * <p>
+ * DynamicMaps are always mapped as singletons in Guice. Maps store Providers
+ * internally, and resolve the provider to an instance on demand. This enables
+ * registrations to decide between singleton and non-singleton members.
+ */
+public abstract class DynamicMap<T> {
+  /**
+   * Declare a singleton {@code DynamicMap<T>} with a binder.
+   * <p>
+   * Maps must be defined in a Guice module before they can be bound:
+   *
+   * <pre>
+   * DynamicMap.mapOf(binder(), Interface.class);
+   * bind(Interface.class)
+   *   .annotatedWith(Exports.named(&quot;foo&quot;))
+   *   .to(Impl.class);
+   * </pre>
+   *
+   * @param binder a new binder created in the module.
+   * @param member type of value in the map.
+   */
+  public static <T> void mapOf(Binder binder, Class<T> member) {
+    mapOf(binder, TypeLiteral.get(member));
+  }
+
+  /**
+   * Declare a singleton {@code DynamicMap<T>} with a binder.
+   * <p>
+   * Maps must be defined in a Guice module before they can be bound:
+   *
+   * <pre>
+   * DynamicMap.mapOf(binder(), new TypeLiteral<Thing<Bar>>(){});
+   * bind(new TypeLiteral<Thing<Bar>>() {})
+   *   .annotatedWith(Exports.named(&quot;foo&quot;))
+   *   .to(Impl.class);
+   * </pre>
+   *
+   * @param binder a new binder created in the module.
+   * @param member type of value in the map.
+   */
+  public static <T> void mapOf(Binder binder, TypeLiteral<T> member) {
+    @SuppressWarnings("unchecked")
+    Key<DynamicMap<T>> key = (Key<DynamicMap<T>>) Key.get(
+        Types.newParameterizedType(DynamicMap.class, member.getType()));
+    binder.bind(key)
+        .toProvider(new DynamicMapProvider<T>(member))
+        .in(Scopes.SINGLETON);
+  }
+
+  final ConcurrentMap<NamePair, Provider<T>> items;
+
+  DynamicMap() {
+    items = new ConcurrentHashMap<NamePair, Provider<T>>(
+        16 /* initial size */,
+        0.75f /* load factor */,
+        1 /* concurrency level of 1, load/unload is single threaded */);
+  }
+
+  /**
+   * Lookup an implementation by name.
+   *
+   * @param pluginName local name of the plugin providing the item.
+   * @param exportName name the plugin exports the item as.
+   * @return the implementation. Null if the plugin is not running, or if the
+   *         plugin does not export this name.
+   * @throws ProvisionException if the registered provider is unable to obtain
+   *         an instance of the requested implementation.
+   */
+  public T get(String pluginName, String exportName) {
+    Provider<T> p = items.get(new NamePair(pluginName, exportName));
+    return p != null ? p.get() : null;
+  }
+
+  /**
+   * Get the names of all running plugins supplying this type.
+   *
+   * @return sorted set of active plugins that supply at least one item.
+   */
+  public SortedSet<String> plugins() {
+    SortedSet<String> r = new TreeSet<String>();
+    for (NamePair p : items.keySet()) {
+      r.add(p.pluginName);
+    }
+    return Collections.unmodifiableSortedSet(r);
+  }
+
+  /**
+   * Get the items exported by a single plugin.
+   *
+   * @param pluginName name of the plugin.
+   * @return items exported by a plugin, keyed by the export name.
+   */
+  public SortedMap<String, Provider<T>> byPlugin(String pluginName) {
+    SortedMap<String, Provider<T>> r = new TreeMap<String, Provider<T>>();
+    for (Map.Entry<NamePair, Provider<T>> e : items.entrySet()) {
+      if (e.getKey().pluginName.equals(pluginName)) {
+        r.put(e.getKey().exportName, e.getValue());
+      }
+    }
+    return Collections.unmodifiableSortedMap(r);
+  }
+
+  static class NamePair {
+    private final String pluginName;
+    private final String exportName;
+
+    NamePair(String pn, String en) {
+      this.pluginName = pn;
+      this.exportName = en;
+    }
+
+    @Override
+    public int hashCode() {
+      return pluginName.hashCode() * 31 + exportName.hashCode();
+    }
+
+    @Override
+    public boolean equals(Object other) {
+      if (other instanceof NamePair) {
+        NamePair np = (NamePair) other;
+        return pluginName.equals(np.pluginName)
+            && exportName.equals(np.exportName);
+      }
+      return false;
+    }
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMapProvider.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMapProvider.java
new file mode 100644
index 0000000..c6e4701
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicMapProvider.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.extensions.registration;
+
+import com.google.inject.Binding;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.Provider;
+import com.google.inject.TypeLiteral;
+
+import java.util.List;
+
+class DynamicMapProvider<T> implements Provider<DynamicMap<T>> {
+  private final TypeLiteral<T> type;
+
+  @Inject
+  private Injector injector;
+
+  DynamicMapProvider(TypeLiteral<T> type) {
+    this.type = type;
+  }
+
+  public DynamicMap<T> get() {
+    PrivateInternals_DynamicMapImpl<T> m =
+        new PrivateInternals_DynamicMapImpl<T>();
+    List<Binding<T>> bindings = injector.findBindingsByType(type);
+    if (bindings != null) {
+      for (Binding<T> b : bindings) {
+        if (b.getKey().getAnnotation() != null) {
+          m.put("gerrit", b.getKey(), b.getProvider());
+        }
+      }
+    }
+    return m;
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSet.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSet.java
new file mode 100644
index 0000000..ec34887
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSet.java
@@ -0,0 +1,253 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.registration;
+
+import com.google.inject.Binder;
+import com.google.inject.Key;
+import com.google.inject.Provider;
+import com.google.inject.Scopes;
+import com.google.inject.TypeLiteral;
+import com.google.inject.binder.LinkedBindingBuilder;
+import com.google.inject.internal.UniqueAnnotations;
+import com.google.inject.name.Named;
+import com.google.inject.util.Providers;
+import com.google.inject.util.Types;
+
+import java.util.Collection;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * A set of members that can be modified as plugins reload.
+ * <p>
+ * DynamicSets are always mapped as singletons in Guice. Sets store Providers
+ * internally, and resolve the provider to an instance on demand. This enables
+ * registrations to decide between singleton and non-singleton members.
+ */
+public class DynamicSet<T> implements Iterable<T> {
+  /**
+   * Declare a singleton {@code DynamicSet<T>} with a binder.
+   * <p>
+   * Sets must be defined in a Guice module before they can be bound:
+   * <pre>
+   *   DynamicSet.setOf(binder(), Interface.class);
+   *   DynamicSet.bind(binder(), Interface.class).to(Impl.class);
+   * </pre>
+   *
+   * @param binder a new binder created in the module.
+   * @param member type of entry in the set.
+   */
+  public static <T> void setOf(Binder binder, Class<T> member) {
+    setOf(binder, TypeLiteral.get(member));
+  }
+
+  /**
+   * Declare a singleton {@code DynamicSet<T>} with a binder.
+   * <p>
+   * Sets must be defined in a Guice module before they can be bound:
+   * <pre>
+   *   DynamicSet.setOf(binder(), new TypeLiteral<Thing<Foo>>() {});
+   * </pre>
+   *
+   * @param binder a new binder created in the module.
+   * @param member type of entry in the set.
+   */
+  public static <T> void setOf(Binder binder, TypeLiteral<T> member) {
+    @SuppressWarnings("unchecked")
+    Key<DynamicSet<T>> key = (Key<DynamicSet<T>>) Key.get(
+        Types.newParameterizedType(DynamicSet.class, member.getType()));
+    binder.bind(key)
+      .toProvider(new DynamicSetProvider<T>(member))
+      .in(Scopes.SINGLETON);
+  }
+
+  /**
+   * Bind one implementation into the set using a unique annotation.
+   *
+   * @param binder a new binder created in the module.
+   * @param type type of entries in the set.
+   * @return a binder to continue configuring the new set member.
+   */
+  public static <T> LinkedBindingBuilder<T> bind(Binder binder, Class<T> type) {
+    return bind(binder, TypeLiteral.get(type));
+  }
+
+  /**
+   * Bind one implementation into the set using a unique annotation.
+   *
+   * @param binder a new binder created in the module.
+   * @param type type of entries in the set.
+   * @return a binder to continue configuring the new set member.
+   */
+  public static <T> LinkedBindingBuilder<T> bind(Binder binder, TypeLiteral<T> type) {
+    return binder.bind(type).annotatedWith(UniqueAnnotations.create());
+  }
+
+  /**
+   * Bind a named implementation into the set.
+   *
+   * @param binder a new binder created in the module.
+   * @param type type of entries in the set.
+   * @param name {@code @Named} annotation to apply instead of a unique
+   *        annotation.
+   * @return a binder to continue configuring the new set member.
+   */
+  public static <T> LinkedBindingBuilder<T> bind(Binder binder,
+      Class<T> type,
+      Named name) {
+    return bind(binder, TypeLiteral.get(type));
+  }
+
+  /**
+   * Bind a named implementation into the set.
+   *
+   * @param binder a new binder created in the module.
+   * @param type type of entries in the set.
+   * @param name {@code @Named} annotation to apply instead of a unique
+   *        annotation.
+   * @return a binder to continue configuring the new set member.
+   */
+  public static <T> LinkedBindingBuilder<T> bind(Binder binder,
+      TypeLiteral<T> type,
+      Named name) {
+    return binder.bind(type).annotatedWith(name);
+  }
+
+  private final CopyOnWriteArrayList<AtomicReference<Provider<T>>> items;
+
+  DynamicSet(Collection<AtomicReference<Provider<T>>> base) {
+    items = new CopyOnWriteArrayList<AtomicReference<Provider<T>>>(base);
+  }
+
+  @Override
+  public Iterator<T> iterator() {
+    final Iterator<AtomicReference<Provider<T>>> itr = items.iterator();
+    return new Iterator<T>() {
+      private T next;
+
+      @Override
+      public boolean hasNext() {
+        while (next == null && itr.hasNext()) {
+          Provider<T> p = itr.next().get();
+          if (p != null) {
+            try {
+              next = p.get();
+            } catch (RuntimeException e) {
+              // TODO Log failed member of DynamicSet.
+            }
+          }
+        }
+        return next != null;
+      }
+
+      @Override
+      public T next() {
+        if (hasNext()) {
+          T result = next;
+          next = null;
+          return result;
+        }
+        throw new NoSuchElementException();
+      }
+
+      @Override
+      public void remove() {
+        throw new UnsupportedOperationException();
+      }
+    };
+  }
+
+  /**
+   * Add one new element to the set.
+   *
+   * @param item the item to add to the collection. Must not be null.
+   * @return handle to remove the item at a later point in time.
+   */
+  public RegistrationHandle add(final T item) {
+    return add(Providers.of(item));
+  }
+
+  /**
+   * Add one new element to the set.
+   *
+   * @param item the item to add to the collection. Must not be null.
+   * @return handle to remove the item at a later point in time.
+   */
+  public RegistrationHandle add(final Provider<T> item) {
+    final AtomicReference<Provider<T>> ref =
+        new AtomicReference<Provider<T>>(item);
+    items.add(ref);
+    return new RegistrationHandle() {
+      @Override
+      public void remove() {
+        if (ref.compareAndSet(item, null)) {
+          items.remove(ref);
+        }
+      }
+    };
+  }
+
+  /**
+   * Add one new element that may be hot-replaceable in the future.
+   *
+   * @param key unique description from the item's Guice binding. This can be
+   *        later obtained from the registration handle to facilitate matching
+   *        with the new equivalent instance during a hot reload.
+   * @param item the item to add to the collection right now. Must not be null.
+   * @return a handle that can remove this item later, or hot-swap the item
+   *         without it ever leaving the collection.
+   */
+  public ReloadableRegistrationHandle<T> add(Key<T> key, Provider<T> item) {
+    AtomicReference<Provider<T>> ref = new AtomicReference<Provider<T>>(item);
+    items.add(ref);
+    return new ReloadableHandle(ref, key, item);
+  }
+
+  private class ReloadableHandle implements ReloadableRegistrationHandle<T> {
+    private final AtomicReference<Provider<T>> ref;
+    private final Key<T> key;
+    private final Provider<T> item;
+
+    ReloadableHandle(AtomicReference<Provider<T>> ref,
+        Key<T> key,
+        Provider<T> item) {
+      this.ref = ref;
+      this.key = key;
+      this.item = item;
+    }
+
+    @Override
+    public void remove() {
+      if (ref.compareAndSet(item, null)) {
+        items.remove(ref);
+      }
+    }
+
+    @Override
+    public Key<T> getKey() {
+      return key;
+    }
+
+    @Override
+    public ReloadableHandle replace(Key<T> newKey, Provider<T> newItem) {
+      if (ref.compareAndSet(item, newItem)) {
+        return new ReloadableHandle(ref, newKey, newItem);
+      }
+      return null;
+    }
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSetProvider.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSetProvider.java
new file mode 100644
index 0000000..6c21553
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSetProvider.java
@@ -0,0 +1,65 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.registration;
+
+import com.google.inject.Binding;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.Provider;
+import com.google.inject.TypeLiteral;
+import com.google.inject.internal.UniqueAnnotations;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
+
+class DynamicSetProvider<T> implements Provider<DynamicSet<T>> {
+  private static final Class<?> UNIQUE_ANNOTATION =
+      UniqueAnnotations.create().getClass();
+  private final TypeLiteral<T> type;
+
+  @Inject
+  private Injector injector;
+
+  DynamicSetProvider(TypeLiteral<T> type) {
+    this.type = type;
+  }
+
+  public DynamicSet<T> get() {
+    return new DynamicSet<T>(find(injector, type));
+  }
+
+  private static <T> List<AtomicReference<Provider<T>>> find(
+      Injector src,
+      TypeLiteral<T> type) {
+    List<Binding<T>> bindings = src.findBindingsByType(type);
+    int cnt = bindings != null ? bindings.size() : 0;
+    if (cnt == 0) {
+      return Collections.emptyList();
+    }
+    List<AtomicReference<Provider<T>>> r = newList(cnt);
+    for (Binding<T> b : bindings) {
+      if (b.getKey().getAnnotation() != null) {
+        r.add(new AtomicReference<Provider<T>>(b.getProvider()));
+      }
+    }
+    return r;
+  }
+
+  private static <T> List<AtomicReference<Provider<T>>> newList(int cnt) {
+    return new ArrayList<AtomicReference<Provider<T>>>(cnt);
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicMapImpl.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicMapImpl.java
new file mode 100644
index 0000000..3558794
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicMapImpl.java
@@ -0,0 +1,97 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.registration;
+
+import com.google.gerrit.extensions.annotations.Export;
+import com.google.inject.Key;
+import com.google.inject.Provider;
+
+/** <b>DO NOT USE</b> */
+public class PrivateInternals_DynamicMapImpl<T> extends DynamicMap<T> {
+  PrivateInternals_DynamicMapImpl() {
+  }
+
+  /**
+   * Store one new element into the map.
+   *
+   * @param pluginName unique name of the plugin providing the export.
+   * @param exportName name the plugin has exported the item as.
+   * @param item the item to add to the collection. Must not be null.
+   * @return handle to remove the item at a later point in time.
+   */
+  public RegistrationHandle put(
+      String pluginName, String exportName,
+      final Provider<T> item) {
+    final NamePair key = new NamePair(pluginName, exportName);
+    items.put(key, item);
+    return new RegistrationHandle() {
+      @Override
+      public void remove() {
+        items.remove(key, item);
+      }
+    };
+  }
+
+  /**
+   * Store one new element that may be hot-replaceable in the future.
+   *
+   * @param pluginName unique name of the plugin providing the export.
+   * @param key unique description from the item's Guice binding. This can be
+   *        later obtained from the registration handle to facilitate matching
+   *        with the new equivalent instance during a hot reload. The key must
+   *        use an {@link @Export} annotation.
+   * @param item the item to add to the collection right now. Must not be null.
+   * @return a handle that can remove this item later, or hot-swap the item
+   *         without it ever leaving the collection.
+   */
+  public ReloadableRegistrationHandle<T> put(
+      String pluginName, Key<T> key,
+      Provider<T> item) {
+    String exportName = ((Export) key.getAnnotation()).value();
+    NamePair np = new NamePair(pluginName, exportName);
+    items.put(np, item);
+    return new ReloadableHandle(np, key, item);
+  }
+
+  private class ReloadableHandle implements ReloadableRegistrationHandle<T> {
+    private final NamePair np;
+    private final Key<T> key;
+    private final Provider<T> item;
+
+    ReloadableHandle(NamePair np, Key<T> key, Provider<T> item) {
+      this.np = np;
+      this.key = key;
+      this.item = item;
+    }
+
+    @Override
+    public void remove() {
+      items.remove(np, item);
+    }
+
+    @Override
+    public Key<T> getKey() {
+      return key;
+    }
+
+    @Override
+    public ReloadableHandle replace(Key<T> newKey, Provider<T> newItem) {
+      if (items.replace(np, item, newItem)) {
+        return new ReloadableHandle(np, newKey, newItem);
+      }
+      return null;
+    }
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicTypes.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicTypes.java
new file mode 100644
index 0000000..66dd45d
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicTypes.java
@@ -0,0 +1,175 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.registration;
+
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.inject.Binding;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.Key;
+import com.google.inject.TypeLiteral;
+
+import java.lang.reflect.ParameterizedType;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/** <b>DO NOT USE</b> */
+public class PrivateInternals_DynamicTypes {
+  public static Map<TypeLiteral<?>, DynamicSet<?>> dynamicSetsOf(Injector src) {
+    Map<TypeLiteral<?>, DynamicSet<?>> m = newHashMap();
+    for (Map.Entry<Key<?>, Binding<?>> e : src.getBindings().entrySet()) {
+      TypeLiteral<?> type = e.getKey().getTypeLiteral();
+      if (type.getRawType() == DynamicSet.class) {
+        ParameterizedType p = (ParameterizedType) type.getType();
+        m.put(TypeLiteral.get(p.getActualTypeArguments()[0]),
+            (DynamicSet<?>) e.getValue().getProvider().get());
+      }
+    }
+    if (m.isEmpty()) {
+      return Collections.emptyMap();
+    }
+    return Collections.unmodifiableMap(m);
+  }
+
+  public static Map<TypeLiteral<?>, DynamicMap<?>> dynamicMapsOf(Injector src) {
+    Map<TypeLiteral<?>, DynamicMap<?>> m = newHashMap();
+    for (Map.Entry<Key<?>, Binding<?>> e : src.getBindings().entrySet()) {
+      TypeLiteral<?> type = e.getKey().getTypeLiteral();
+      if (type.getRawType() == DynamicMap.class) {
+        ParameterizedType p = (ParameterizedType) type.getType();
+        m.put(TypeLiteral.get(p.getActualTypeArguments()[0]),
+            (DynamicMap<?>) e.getValue().getProvider().get());
+      }
+    }
+    if (m.isEmpty()) {
+      return Collections.emptyMap();
+    }
+    return Collections.unmodifiableMap(m);
+  }
+
+  public static List<RegistrationHandle> attachSets(
+      Injector src,
+      Map<TypeLiteral<?>, DynamicSet<?>> sets) {
+    if (src == null || sets == null || sets.isEmpty()) {
+      return Collections.emptyList();
+    }
+
+    List<RegistrationHandle> handles = new ArrayList<RegistrationHandle>(4);
+    try {
+      for (Map.Entry<TypeLiteral<?>, DynamicSet<?>> e : sets.entrySet()) {
+        @SuppressWarnings("unchecked")
+        TypeLiteral<Object> type = (TypeLiteral<Object>) e.getKey();
+
+        @SuppressWarnings("unchecked")
+        DynamicSet<Object> set = (DynamicSet<Object>) e.getValue();
+
+        for (Binding<Object> b : bindings(src, type)) {
+          if (b.getKey().getAnnotation() != null) {
+            handles.add(set.add(b.getKey(), b.getProvider()));
+          }
+        }
+      }
+    } catch (RuntimeException e) {
+      remove(handles);
+      throw e;
+    } catch (Error e) {
+      remove(handles);
+      throw e;
+    }
+    return handles;
+  }
+
+  public static List<RegistrationHandle> attachMaps(
+      Injector src,
+      String groupName,
+      Map<TypeLiteral<?>, DynamicMap<?>> maps) {
+    if (src == null || maps == null || maps.isEmpty()) {
+      return Collections.emptyList();
+    }
+
+    List<RegistrationHandle> handles = new ArrayList<RegistrationHandle>(4);
+    try {
+      for (Map.Entry<TypeLiteral<?>, DynamicMap<?>> e : maps.entrySet()) {
+        @SuppressWarnings("unchecked")
+        TypeLiteral<Object> type = (TypeLiteral<Object>) e.getKey();
+
+        @SuppressWarnings("unchecked")
+        PrivateInternals_DynamicMapImpl<Object> set =
+            (PrivateInternals_DynamicMapImpl<Object>) e.getValue();
+
+        for (Binding<Object> b : bindings(src, type)) {
+          if (b.getKey().getAnnotation() != null) {
+            handles.add(set.put(groupName, b.getKey(), b.getProvider()));
+          }
+        }
+      }
+    } catch (RuntimeException e) {
+      remove(handles);
+      throw e;
+    } catch (Error e) {
+      remove(handles);
+      throw e;
+    }
+    return handles;
+  }
+
+  public static LifecycleListener registerInParentInjectors() {
+    return new LifecycleListener() {
+      private List<RegistrationHandle> handles;
+
+      @Inject
+      private Injector self;
+
+      @Override
+      public void start() {
+        handles = new ArrayList<RegistrationHandle>(4);
+        Injector parent = self.getParent();
+        while (parent != null) {
+          handles.addAll(attachSets(self, dynamicSetsOf(parent)));
+          handles.addAll(attachMaps(self, "gerrit", dynamicMapsOf(parent)));
+          parent = parent.getParent();
+        }
+        if (handles.isEmpty()) {
+          handles = null;
+        }
+      }
+
+      @Override
+      public void stop() {
+        remove(handles);
+        handles = null;
+      }
+    };
+  }
+
+  private static void remove(List<RegistrationHandle> handles) {
+    if (handles != null) {
+      for (RegistrationHandle handle : handles) {
+        handle.remove();
+      }
+    }
+  }
+
+  private static <K,V> Map<K, V> newHashMap() {
+    return new HashMap<K,V>();
+  }
+
+  private static <T> List<Binding<T>> bindings(Injector src, TypeLiteral<T> type) {
+    return src.findBindingsByType(type);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CachePool.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/RegistrationHandle.java
similarity index 69%
copy from gerrit-server/src/main/java/com/google/gerrit/server/cache/CachePool.java
copy to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/RegistrationHandle.java
index 3370b08..2243786 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CachePool.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/RegistrationHandle.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,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.cache;
+package com.google.gerrit.extensions.registration;
 
-public interface CachePool {
-  public <K, V> ProxyCache<K, V> register(CacheProvider<K, V> provider);
+/** Handle for registered information. */
+public interface RegistrationHandle {
+  /** Delete this registration. */
+  public void remove();
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeRestoreEvent.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/ReloadableRegistrationHandle.java
similarity index 61%
copy from gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeRestoreEvent.java
copy to gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/ReloadableRegistrationHandle.java
index 1a2922b..7284296 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeRestoreEvent.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/ReloadableRegistrationHandle.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,12 +12,13 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.events;
+package com.google.gerrit.extensions.registration;
 
-public class ChangeRestoreEvent extends ChangeEvent {
-    public final String type = "change-restored";
-    public ChangeAttribute change;
-    public PatchSetAttribute patchSet;
-    public AccountAttribute restorer;
-    public String reason;
+import com.google.inject.Key;
+import com.google.inject.Provider;
+
+public interface ReloadableRegistrationHandle<T> extends RegistrationHandle {
+  public Key<T> getKey();
+
+  public RegistrationHandle replace(Key<T> key, Provider<T> item);
 }
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/systemstatus/ServerInformation.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/systemstatus/ServerInformation.java
new file mode 100644
index 0000000..3d2df21
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/systemstatus/ServerInformation.java
@@ -0,0 +1,42 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.systemstatus;
+
+/** Exports current server information to an extension. */
+public interface ServerInformation {
+  /** Current state of the server. */
+  public enum State {
+    /**
+     * The server is starting up, and network connections are not yet being
+     * accepted. Plugins or extensions starting during this time are starting
+     * for the first time in this process.
+     */
+    STARTUP,
+
+    /**
+     * The server is running and handling requests. Plugins starting during this
+     * state may be reloading, or being installed into a running system.
+     */
+    RUNNING,
+
+    /**
+     * The server is attempting a graceful halt of operations and will exit (or
+     * be killed by the operating system) soon.
+     */
+    SHUTDOWN;
+  }
+
+  State getState();
+}
diff --git a/gerrit-gwtdebug/.gitignore b/gerrit-gwtdebug/.gitignore
index 194bedc..4207862 100644
--- a/gerrit-gwtdebug/.gitignore
+++ b/gerrit-gwtdebug/.gitignore
@@ -2,4 +2,5 @@
 /.classpath
 /.project
 /.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-gwtdebug.iml
\ No newline at end of file
diff --git a/gerrit-gwtdebug/.settings/org.eclipse.core.resources.prefs b/gerrit-gwtdebug/.settings/org.eclipse.core.resources.prefs
index 36e1448..e9441bb 100644
--- a/gerrit-gwtdebug/.settings/org.eclipse.core.resources.prefs
+++ b/gerrit-gwtdebug/.settings/org.eclipse.core.resources.prefs
@@ -1,4 +1,3 @@
-#Thu Jul 28 11:02:38 PDT 2011
 eclipse.preferences.version=1
 encoding//src/main/java=UTF-8
 encoding/<project>=UTF-8
diff --git a/gerrit-gwtdebug/pom.xml b/gerrit-gwtdebug/pom.xml
index 734f645..01b93a6 100644
--- a/gerrit-gwtdebug/pom.xml
+++ b/gerrit-gwtdebug/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.4-SNAPSHOT</version>
+    <version>2.5-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-gwtdebug</artifactId>
diff --git a/gerrit-gwtui/.gitignore b/gerrit-gwtui/.gitignore
index 194bedc..53d46b3 100644
--- a/gerrit-gwtui/.gitignore
+++ b/gerrit-gwtui/.gitignore
@@ -2,4 +2,5 @@
 /.classpath
 /.project
 /.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-gwtui.iml
\ No newline at end of file
diff --git a/gerrit-gwtui/.settings/org.eclipse.core.resources.prefs b/gerrit-gwtui/.settings/org.eclipse.core.resources.prefs
index c780f44..e9441bb 100644
--- a/gerrit-gwtui/.settings/org.eclipse.core.resources.prefs
+++ b/gerrit-gwtui/.settings/org.eclipse.core.resources.prefs
@@ -1,4 +1,3 @@
-#Thu Jul 28 11:02:36 PDT 2011
 eclipse.preferences.version=1
 encoding//src/main/java=UTF-8
 encoding/<project>=UTF-8
diff --git a/gerrit-gwtui/pom.xml b/gerrit-gwtui/pom.xml
index f14daf6..b3291d1 100644
--- a/gerrit-gwtui/pom.xml
+++ b/gerrit-gwtui/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.4-SNAPSHOT</version>
+    <version>2.5-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-gwtui</artifactId>
@@ -168,12 +168,33 @@
       </properties>
     </profile>
     <profile>
+      <id>chrome</id>
+      <properties>
+        <GerritGwtUI.browserType>com.google.gerrit.GerritGwtUIsafari</GerritGwtUI.browserType>
+        <GerritGwtUI.draftCompile>true</GerritGwtUI.draftCompile>
+      </properties>
+    </profile>
+    <profile>
+      <id>webkit</id>
+      <properties>
+        <GerritGwtUI.browserType>com.google.gerrit.GerritGwtUIsafari</GerritGwtUI.browserType>
+        <GerritGwtUI.draftCompile>true</GerritGwtUI.draftCompile>
+      </properties>
+    </profile>
+    <profile>
       <id>gecko1_8</id>
       <properties>
         <GerritGwtUI.browserType>com.google.gerrit.GerritGwtUIgecko1_8</GerritGwtUI.browserType>
         <GerritGwtUI.draftCompile>true</GerritGwtUI.draftCompile>
       </properties>
     </profile>
+    <profile>
+      <id>firefox</id>
+      <properties>
+        <GerritGwtUI.browserType>com.google.gerrit.GerritGwtUIgecko1_8</GerritGwtUI.browserType>
+        <GerritGwtUI.draftCompile>true</GerritGwtUI.draftCompile>
+      </properties>
+    </profile>
   </profiles>
 
   <build>
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 3aee0e2..aefad27 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
@@ -14,9 +14,11 @@
 
 package com.google.gerrit.client;
 
+import static com.google.gerrit.common.PageLinks.ADMIN_CREATE_GROUP;
 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;
@@ -46,8 +48,10 @@
 import com.google.gerrit.client.admin.AccountGroupInfoScreen;
 import com.google.gerrit.client.admin.AccountGroupMembersScreen;
 import com.google.gerrit.client.admin.AccountGroupScreen;
+import com.google.gerrit.client.admin.CreateGroupScreen;
 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;
@@ -58,6 +62,7 @@
 import com.google.gerrit.client.auth.userpass.UserPassSignInDialog;
 import com.google.gerrit.client.changes.AccountDashboardScreen;
 import com.google.gerrit.client.changes.ChangeScreen;
+import com.google.gerrit.client.changes.CustomDashboardScreen;
 import com.google.gerrit.client.changes.PatchTable;
 import com.google.gerrit.client.changes.PublishCommentScreen;
 import com.google.gerrit.client.changes.QueryScreen;
@@ -234,6 +239,10 @@
     }
 
     if (matchExact("mine,drafts", token)) {
+      return PageLinks.toChangeQuery("is:draft");
+    }
+
+    if (matchExact("mine,comments", token)) {
       return PageLinks.toChangeQuery("has:draft");
     }
 
@@ -361,8 +370,18 @@
   }
 
   private static void dashboard(final String token) {
-    Gerrit.display(token, //
-        new AccountDashboardScreen(Account.Id.parse(skip(token))));
+    String rest = skip(token);
+    if (rest.matches("[0-9]+")) {
+      Gerrit.display(token, new AccountDashboardScreen(Account.Id.parse(rest)));
+      return;
+    }
+
+    if (rest.startsWith("?")) {
+      Gerrit.display(token, new CustomDashboardScreen(rest.substring(1)));
+      return;
+    }
+
+    Gerrit.display(token, new NotFoundScreen());
   }
 
   private static void change(final String token) {
@@ -606,10 +625,18 @@
         } 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());
 
+        } else if (matchExact(ADMIN_CREATE_GROUP, token)
+            || matchExact("/admin/create-group", token)) {
+          Gerrit.display(token, new CreateGroupScreen());
+
         } else {
           Gerrit.display(token, new NotFoundScreen());
         }
@@ -695,8 +722,7 @@
             return new ProjectInfoScreen(k);
           }
 
-          if (ProjectScreen.BRANCH.equals(panel)
-              && !k.equals(Gerrit.getConfig().getWildProject())) {
+          if (ProjectScreen.BRANCH.equals(panel)) {
             return new ProjectBranchesScreen(k);
           }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ErrorDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ErrorDialog.java
index 74a2678..13bba12 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ErrorDialog.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ErrorDialog.java
@@ -25,6 +25,7 @@
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.Label;
+import com.google.gwt.user.client.ui.Widget;
 import com.google.gwtexpui.safehtml.client.SafeHtml;
 import com.google.gwtexpui.user.client.PluginSafePopupPanel;
 import com.google.gwtjsonrpc.client.RemoteJsonException;
@@ -94,6 +95,11 @@
     body.add(message.toBlockWidget());
   }
 
+  public ErrorDialog(final Widget w) {
+    this();
+    body.add(w);
+  }
+
   /** Create a dialog box to nicely format an exception. */
   public ErrorDialog(final Throwable what) {
     this();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java
index e578eae..f10762a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/FormatUtil.java
@@ -25,9 +25,10 @@
 public class FormatUtil {
   private static final long ONE_YEAR = 182L * 24 * 60 * 60 * 1000;
 
-  private static DateTimeFormat sTime = DateTimeFormat.getFormat(DateTimeFormat.PredefinedFormat.TIME_SHORT);
-  private static DateTimeFormat sDate = DateTimeFormat.getFormat("MMM d");
-  private static DateTimeFormat mDate = DateTimeFormat.getFormat(DateTimeFormat.PredefinedFormat.DATE_MEDIUM);
+  private static DateTimeFormat sTime;
+  private static DateTimeFormat sDate;
+  private static DateTimeFormat sdtFmt;
+  private static DateTimeFormat mDate;
   private static DateTimeFormat dtfmt;
 
   public static void setPreferences(AccountGeneralPreferences pref) {
@@ -41,10 +42,12 @@
     }
 
     String fmt_sTime = pref.getTimeFormat().getFormat();
+    String fmt_sDate = pref.getDateFormat().getShortFormat();
     String fmt_mDate = pref.getDateFormat().getLongFormat();
 
     sTime = DateTimeFormat.getFormat(fmt_sTime);
-    sDate = DateTimeFormat.getFormat(pref.getDateFormat().getShortFormat());
+    sDate = DateTimeFormat.getFormat(fmt_sDate);
+    sdtFmt = DateTimeFormat.getFormat(fmt_sDate + " " + fmt_sTime);
     mDate = DateTimeFormat.getFormat(fmt_mDate);
     dtfmt = DateTimeFormat.getFormat(fmt_mDate + " " + fmt_sTime);
   }
@@ -75,6 +78,32 @@
     }
   }
 
+  /** Format a date using a really short format. */
+  public static String shortFormatDayTime(Date dt) {
+    if (dt == null) {
+      return "";
+    }
+
+    ensureInited();
+    final Date now = new Date();
+    dt = new Date(dt.getTime());
+    if (mDate.format(now).equals(mDate.format(dt))) {
+      // Same day as today, report only the time.
+      //
+      return sTime.format(dt);
+
+    } else if (Math.abs(now.getTime() - dt.getTime()) < ONE_YEAR) {
+      // Within the last year, show a shorter date.
+      //
+      return sdtFmt.format(dt);
+
+    } else {
+      // Report only date and year, its far away from now.
+      //
+      return mDate.format(dt);
+    }
+  }
+
   /** Format a date using the locale's medium length format. */
   public static String mediumFormat(final Date dt) {
     if (dt == null) {
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 6dbfeee..8fe658b 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
@@ -14,7 +14,13 @@
 
 package com.google.gerrit.client;
 
+import static com.google.gerrit.common.data.GlobalCapability.ADMINISTRATE_SERVER;
+import static com.google.gerrit.common.data.GlobalCapability.CREATE_PROJECT;
+import static com.google.gerrit.common.data.GlobalCapability.CREATE_GROUP;
+
+import com.google.gerrit.client.account.AccountCapabilities;
 import com.google.gerrit.client.auth.openid.OpenIdSignInDialog;
+import com.google.gerrit.client.auth.openid.OpenIdSsoPanel;
 import com.google.gerrit.client.auth.userpass.UserPassSignInDialog;
 import com.google.gerrit.client.changes.ChangeConstants;
 import com.google.gerrit.client.changes.ChangeListScreen;
@@ -53,8 +59,8 @@
 import com.google.gwt.user.client.ui.Anchor;
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.Grid;
-import com.google.gwt.user.client.ui.HTML;
 import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
+import com.google.gwt.user.client.ui.InlineHTML;
 import com.google.gwt.user.client.ui.InlineLabel;
 import com.google.gwt.user.client.ui.Label;
 import com.google.gwt.user.client.ui.RootPanel;
@@ -255,6 +261,13 @@
         Location.assign(selfRedirect("/become"));
         break;
 
+      case OPENID_SSO:
+        final RootPanel gBody = RootPanel.get("gerrit_body");
+        OpenIdSsoPanel singleSignOnPanel = new OpenIdSsoPanel();
+        gBody.add(singleSignOnPanel);
+        singleSignOnPanel.authenticate(SignInMode.SIGN_IN, token);
+        break;
+
       case OPENID:
         new OpenIdSignInDialog(SignInMode.SIGN_IN, token, null).center();
         break;
@@ -266,7 +279,7 @@
     }
   }
 
-  private static String loginRedirect(String token) {
+  public static String loginRedirect(String token) {
     if (token == null) {
       token = "";
     } else if (token.startsWith("/")) {
@@ -275,7 +288,7 @@
     return selfRedirect("/login/" + token);
   }
 
-  private static String selfRedirect(String suffix) {
+  public static String selfRedirect(String suffix) {
     // Clean up the path. Users seem to like putting extra slashes into the URL
     // which can break redirections by misinterpreting at either client or server.
     String path = Location.getPath();
@@ -323,6 +336,7 @@
     Cookies.removeCookie("GerritAccount");
   }
 
+  @Override
   public void onModuleLoad() {
     UserAgent.assertNotInIFrame();
 
@@ -332,6 +346,7 @@
         e = URL.encodeQueryString(e);
         e = fixPathImpl(e);
         e = fixColonImpl(e);
+        e = fixDoubleQuote(e);
         return e;
       }
 
@@ -345,6 +360,9 @@
 
       private native String fixColonImpl(String path)
       /*-{ return path.replace(/%3A/g, ":"); }-*/;
+
+      private native String fixDoubleQuote(String path)
+      /*-{ return path.replace(/%22/g, '"'); }-*/;
     });
 
     initHostname();
@@ -430,9 +448,19 @@
       vs = "dev";
     }
 
-    final HTML version = new HTML(M.poweredBy(vs));
-    version.setStyleName(RESOURCES.css().version());
-    btmmenu.add(version);
+    FlowPanel poweredBy = new FlowPanel();
+    poweredBy.setStyleName(RESOURCES.css().version());
+    poweredBy.add(new InlineHTML(M.poweredBy(vs)));
+    if (getConfig().getReportBugUrl() != null) {
+      poweredBy.add(new InlineLabel(" | "));
+      Anchor a = new Anchor(
+          C.reportBug(),
+          getConfig().getReportBugUrl());
+      a.setTarget("_blank");
+      a.setStyleName("");
+      poweredBy.add(a);
+    }
+    btmmenu.add(poweredBy);
   }
 
   private void onModuleLoad2() {
@@ -548,9 +576,10 @@
     if (signedIn) {
       m = new LinkMenuBar();
       addLink(m, C.menuMyChanges(), PageLinks.MINE);
-      addLink(m, C.menuMyDrafts(), PageLinks.toChangeQuery("has:draft"));
+      addLink(m, C.menuMyDrafts(), PageLinks.toChangeQuery("is:draft"));
       addLink(m, C.menuMyWatchedChanges(), PageLinks.toChangeQuery("is:watched status:open"));
       addLink(m, C.menuMyStarredChanges(), PageLinks.toChangeQuery("is:starred"));
+      addLink(m, C.menuMyDraftComments(), PageLinks.toChangeQuery("has:draft"));
       menuLeft.add(m, C.menuMine());
       menuLeft.selectTab(1);
     } else {
@@ -567,11 +596,44 @@
     addDiffLink(diffBar, C.menuDiffPatchSets(), PatchScreen.TopView.PATCH_SETS);
     addDiffLink(diffBar, C.menuDiffFiles(), PatchScreen.TopView.FILES);
 
+    final LinkMenuBar projectsBar = new LinkMenuBar();
+    addLink(projectsBar, C.menuProjectsList(), PageLinks.ADMIN_PROJECTS);
+    if(signedIn) {
+      AccountCapabilities.all(new GerritCallback<AccountCapabilities>() {
+        @Override
+        public void onSuccess(AccountCapabilities result) {
+          if (result.canPerform(CREATE_PROJECT)) {
+            addLink(projectsBar, C.menuProjectsCreate(), PageLinks.ADMIN_CREATE_PROJECT);
+          }
+        }
+      }, CREATE_PROJECT);
+    }
+    menuLeft.add(projectsBar, C.menuProjects());
+
     if (signedIn) {
-      m = new LinkMenuBar();
-      addLink(m, C.menuGroups(), PageLinks.ADMIN_GROUPS);
-      addLink(m, C.menuProjects(), PageLinks.ADMIN_PROJECTS);
-      menuLeft.add(m, C.menuAdmin());
+      final LinkMenuBar groupsBar = new LinkMenuBar();
+      addLink(groupsBar, C.menuGroupsList(), PageLinks.ADMIN_GROUPS);
+      AccountCapabilities.all(new GerritCallback<AccountCapabilities>() {
+        @Override
+        public void onSuccess(AccountCapabilities result) {
+          if (result.canPerform(CREATE_GROUP)) {
+            addLink(groupsBar, C.menuGroupsCreate(), PageLinks.ADMIN_CREATE_GROUP);
+          }
+        }
+      }, CREATE_GROUP);
+      menuLeft.add(groupsBar, C.menuGroups());
+
+      final LinkMenuBar pluginsBar = new LinkMenuBar();
+      AccountCapabilities.all(new GerritCallback<AccountCapabilities>() {
+        @Override
+        public void onSuccess(AccountCapabilities result) {
+          if (result.canPerform(ADMINISTRATE_SERVER)) {
+            addLink(pluginsBar, C.menuPluginsInstalled(), PageLinks.ADMIN_PLUGINS);
+            menuLeft.insert(pluginsBar, C.menuPlugins(),
+                menuLeft.getWidgetIndex(groupsBar) + 1);
+          }
+        }
+      }, ADMINISTRATE_SERVER);
     }
 
     if (getConfig().isDocumentationAvailable()) {
@@ -610,14 +672,25 @@
           });
           break;
 
+        case OPENID_SSO:
+          menuRight.addItem(C.menuSignIn(), new Command() {
+            public void execute() {
+              doSignIn(History.getToken());
+            }
+          });
+          break;
+
         case LDAP:
         case LDAP_BIND:
         case CUSTOM_EXTENSION:
           if (cfg.getRegisterUrl() != null) {
             menuRight.add(anchor(C.menuRegister(), cfg.getRegisterUrl()));
           }
-          signInAnchor = anchor(C.menuSignIn(), loginRedirect(History.getToken()));
-          menuRight.add(signInAnchor);
+          menuRight.addItem(C.menuSignIn(), new Command() {
+            public void execute() {
+              doSignIn(History.getToken());
+            }
+          });
           break;
 
         case DEVELOPMENT_BECOME_ANY_ACCOUNT:
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 ee107d0..87c01cc 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
@@ -21,6 +21,7 @@
   String menuSignOut();
   String menuRegister();
   String menuSettings();
+  String reportBug();
 
   String signInDialogTitle();
   String signInDialogClose();
@@ -60,6 +61,7 @@
   String menuMyDrafts();
   String menuMyWatchedChanges();
   String menuMyStarredChanges();
+  String menuMyDraftComments();
 
   String menuDiff();
   String menuDiffCommit();
@@ -67,10 +69,16 @@
   String menuDiffPatchSets();
   String menuDiffFiles();
 
-  String menuAdmin();
+  String menuProjects();
+  String menuProjectsList();
+  String menuProjectsCreate();
+
   String menuPeople();
   String menuGroups();
-  String menuProjects();
+  String menuGroupsList();
+  String menuGroupsCreate();
+  String menuPlugins();
+  String menuPluginsInstalled();
 
   String menuDocumentation();
   String menuDocumentationIndex();
@@ -96,4 +104,8 @@
   String jumpMineDrafts();
   String jumpMineWatched();
   String jumpMineStarred();
+  String jumpMineDraftComments();
+
+  String projectAccessError();
+  String projectAccessProposeForReviewHint();
 }
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 41db3d5..596b3ad 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
@@ -2,6 +2,7 @@
 menuSignOut = Sign Out
 menuRegister = Register
 menuSettings = Settings
+reportBug = Report Bug
 
 signInDialogTitle = Code Review - Sign In
 signInDialogClose = Close
@@ -43,6 +44,7 @@
 menuMyDrafts = Drafts
 menuMyStarredChanges = Starred Changes
 menuMyWatchedChanges = Watched Changes
+menuMyDraftComments = Draft Comments
 
 menuDiff = Differences
 menuDiffCommit = Commit Message
@@ -50,10 +52,17 @@
 menuDiffPatchSets = Patch Sets
 menuDiffFiles = Files
 
-menuAdmin = Admin
+menuProjects = Projects
+menuProjectsList = List
+menuProjectsCreate = Create New Project
+
 menuPeople = People
 menuGroups = Groups
-menuProjects = Projects
+menuGroupsList = List
+menuGroupsCreate = Create New Group
+
+menuPlugins = Plugins
+menuPluginsInstalled = Installed
 
 menuDocumentation = Documentation
 menuDocumentationIndex = Index
@@ -79,3 +88,7 @@
 jumpMineWatched = Go to watched changes
 jumpMineDrafts = Go to drafts
 jumpMineStarred = Go to starred changes
+jumpMineDraftComments = Go to draft comments
+
+projectAccessError = You don't have permissions to modify the access rights for the following refs:
+projectAccessProposeForReviewHint = You may propose these modifications to the project owners by clicking on 'Save for Review'.
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 a8315d8..9cbf5cd 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
@@ -78,7 +78,7 @@
   String contributorAgreementLegal();
   String contributorAgreementShortDescription();
   String coverMessage();
-  String createProjectLink();
+  String createGroupLink();
   String createProjectPanel();
   String dataCell();
   String dataHeader();
@@ -104,6 +104,7 @@
   String errorDialogTitle();
   String errorDialogButtons();
   String errorDialogErrorType();
+  String errorDialogText();
   String fileColumnHeader();
   String fileLine();
   String fileLineCONTEXT();
@@ -167,7 +168,7 @@
   String patchSetRevision();
   String patchSetUserIdentity();
   String patchSizeCell();
-  String permalink();
+  String pluginsTable();
   String posscore();
   String projectAdminApprovalCategoryRangeLine();
   String projectAdminApprovalCategoryValue();
@@ -181,6 +182,7 @@
   String rpcStatusPanel();
   String screen();
   String screenHeader();
+  String screenNoHeader();
   String searchPanel();
   String sectionHeader();
   String sideBySideScreenLinkTable();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritMessages.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritMessages.properties
index a91df3c..79772bd 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritMessages.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritMessages.properties
@@ -1,7 +1,6 @@
 windowTitle1 = {0} Code Review
 windowTitle2 = {0} | {1} Code Review
-poweredBy = Powered by <a href="http://code.google.com/p/gerrit/" target="_blank">Gerrit Code Review</a> ({0}) \
-| <a href="http://code.google.com/p/gerrit/issues/list" target="_blank">Report Bug</a>
+poweredBy = Powered by <a href="http://code.google.com/p/gerrit/" target="_blank">Gerrit Code Review</a> ({0})
 
 noSuchAccountMessage = {0} is not a registered user.
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritResources.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritResources.java
index b5c0900..d763ff1 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritResources.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritResources.java
@@ -39,4 +39,7 @@
 
   @Source("redNot.png")
   public ImageResource redNot();
+
+  @Source("downloadIcon.png")
+  public ImageResource downloadIcon();
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/JumpKeys.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/JumpKeys.java
index 873045d..a41ff02 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/JumpKeys.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/JumpKeys.java
@@ -55,6 +55,12 @@
       jumps.add(new KeyCommand(0, 'd', Gerrit.C.jumpMineDrafts()) {
         @Override
         public void onKeyPress(final KeyPressEvent event) {
+          Gerrit.display(PageLinks.toChangeQuery("is:draft"));
+        }
+      });
+      jumps.add(new KeyCommand(0, 'c', Gerrit.C.jumpMineDraftComments()) {
+        @Override
+        public void onKeyPress(final KeyPressEvent event) {
           Gerrit.display(PageLinks.toChangeQuery("has:draft"));
         }
       });
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/RpcStatus.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/RpcStatus.java
index 76ce384..955c8e2 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/RpcStatus.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/RpcStatus.java
@@ -56,6 +56,10 @@
 
   @Override
   public void onRpcStart(final RpcStartEvent event) {
+    onRpcStart();
+  }
+
+  public void onRpcStart() {
     if (++activeCalls == 1) {
       if (hideDepth == 0) {
         loading.setVisible(true);
@@ -65,6 +69,10 @@
 
   @Override
   public void onRpcComplete(final RpcCompleteEvent event) {
+    onRpcComplete();
+  }
+
+  public void onRpcComplete() {
     if (--activeCalls == 0) {
       loading.setVisible(false);
     }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchPanel.java
index e3d8468..7e7b927 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchPanel.java
@@ -27,6 +27,8 @@
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.Composite;
 import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.SuggestBox;
+import com.google.gwt.user.client.ui.SuggestOracle.Suggestion;
 import com.google.gwtexpui.globalkey.client.GlobalKey;
 import com.google.gwtexpui.globalkey.client.KeyCommand;
 
@@ -42,15 +44,22 @@
     searchBox = new HintTextBox();
     searchBox.setVisibleLength(70);
     searchBox.setHintText(Gerrit.C.searchHint());
+    final MySuggestionDisplay suggestionDisplay = new MySuggestionDisplay();
     searchBox.addKeyPressHandler(new KeyPressHandler() {
       @Override
       public void onKeyPress(final KeyPressEvent event) {
         if (event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER) {
-          doSearch();
+          if (!suggestionDisplay.isSuggestionSelected) {
+            doSearch();
+          }
         }
       }
     });
 
+    final SuggestBox suggestBox =
+        new SuggestBox(new SearchSuggestOracle(), searchBox, suggestionDisplay);
+    searchBox.setStyleName("gwt-TextBox");
+
     final Button searchButton = new Button(Gerrit.C.searchButton());
     searchButton.addClickHandler(new ClickHandler() {
       @Override
@@ -59,7 +68,7 @@
       }
     });
 
-    body.add(searchBox);
+    body.add(suggestBox);
     body.add(searchButton);
   }
 
@@ -106,4 +115,15 @@
       Gerrit.display(PageLinks.toChangeQuery(query), QueryScreen.forQuery(query));
     }
   }
+
+  private static class MySuggestionDisplay extends SuggestBox.DefaultSuggestionDisplay {
+    private boolean isSuggestionSelected;
+
+    @Override
+    protected Suggestion getCurrentSelection() {
+      Suggestion currentSelection = super.getCurrentSelection();
+      isSuggestionSelected = currentSelection != null;
+      return currentSelection;
+    }
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java
new file mode 100644
index 0000000..172a2af
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java
@@ -0,0 +1,140 @@
+// 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;
+
+import com.google.gwt.user.client.ui.SuggestOracle;
+import com.google.gwtexpui.safehtml.client.HighlightSuggestOracle;
+
+import java.util.ArrayList;
+import java.util.TreeSet;
+
+public class SearchSuggestOracle extends HighlightSuggestOracle {
+  private static final TreeSet<String> suggestions = new TreeSet<String>();
+
+  static {
+    suggestions.add("age:");
+    suggestions.add("age:1week"); // Give an example age
+
+    suggestions.add("change:");
+
+    suggestions.add("owner:");
+    suggestions.add("owner:self");
+    suggestions.add("ownerin:");
+
+    suggestions.add("reviewer:");
+    suggestions.add("reviewer:self");
+    suggestions.add("reviewerin:");
+
+    suggestions.add("commit:");
+    suggestions.add("project:");
+    suggestions.add("branch:");
+    suggestions.add("topic:");
+    suggestions.add("ref:");
+    suggestions.add("tr:");
+    suggestions.add("bug:");
+    suggestions.add("label:");
+    suggestions.add("message:");
+    suggestions.add("file:");
+
+    suggestions.add("has:");
+    suggestions.add("has:draft");
+    suggestions.add("has:star");
+
+    suggestions.add("is:");
+    suggestions.add("is:starred");
+    suggestions.add("is:watched");
+    suggestions.add("is:reviewed");
+    suggestions.add("is:owner");
+    suggestions.add("is:reviewer");
+    suggestions.add("is:open");
+    suggestions.add("is:draft");
+    suggestions.add("is:closed");
+    suggestions.add("is:submitted");
+    suggestions.add("is:merged");
+    suggestions.add("is:abandoned");
+
+    suggestions.add("status:");
+    suggestions.add("status:open");
+    suggestions.add("status:reviewed");
+    suggestions.add("status:submitted");
+    suggestions.add("status:closed");
+    suggestions.add("status:merged");
+    suggestions.add("status:abandoned");
+
+    suggestions.add("AND");
+    suggestions.add("OR");
+    suggestions.add("NOT");
+  }
+
+  @Override
+  public void requestDefaultSuggestions(Request request, Callback done) {
+    final ArrayList<SearchSuggestion> r = new ArrayList<SearchSuggestOracle.SearchSuggestion>();
+    // No text - show some default suggestions.
+    r.add(new SearchSuggestion("status:open", "status:open"));
+    r.add(new SearchSuggestion("age:1week", "age:1week"));
+    if (Gerrit.isSignedIn()) {
+      r.add(new SearchSuggestion("owner:self", "owner:self"));
+    }
+    done.onSuggestionsReady(request, new Response(r));
+  }
+
+  @Override
+  protected void onRequestSuggestions(Request request, Callback done) {
+    final String query = request.getQuery();
+    int lastSpace = query.lastIndexOf(' ');
+    final String lastWord;
+    // NOTE: this method is not called if the query is empty.
+    if (lastSpace == query.length() - 1) {
+      // Starting a new word - don't show suggestions yet.
+      done.onSuggestionsReady(request, null);
+      return;
+    } else if (lastSpace == -1) {
+      lastWord = query;
+    } else {
+      lastWord = query.substring(lastSpace + 1);
+    }
+
+    final ArrayList<SearchSuggestion> r = new ArrayList<SearchSuggestOracle.SearchSuggestion>();
+    for (String suggestion : suggestions.tailSet(lastWord)) {
+      if ((lastWord.length() < suggestion.length()) && suggestion.startsWith(lastWord)) {
+        if (suggestion.contains("self") && !Gerrit.isSignedIn()) {
+          continue;
+        }
+        r.add(new SearchSuggestion(suggestion, query + suggestion.substring(lastWord.length())));
+      }
+    }
+    done.onSuggestionsReady(request, new Response(r));
+  }
+
+  private static class SearchSuggestion implements SuggestOracle.Suggestion {
+    private final String suggestion;
+    private final String fullQuery;
+    public SearchSuggestion(String suggestion, String fullQuery) {
+      this.suggestion = suggestion;
+      // Add a space to the query if it is a complete operation (e.g.
+      // "status:open") so the user can keep on typing.
+      this.fullQuery = fullQuery.endsWith(":") ? fullQuery : fullQuery + " ";
+    }
+    @Override
+    public String getDisplayString() {
+      return suggestion;
+    }
+
+    @Override
+    public String getReplacementString() {
+      return fullQuery;
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountCapabilities.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountCapabilities.java
new file mode 100644
index 0000000..0565d3e
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountCapabilities.java
@@ -0,0 +1,36 @@
+// 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.account;
+
+import com.google.gerrit.client.rpc.RestApi;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwtjsonrpc.common.AsyncCallback;
+
+/** Capabilities the caller has from {@code /accounts/self/capabilities}.  */
+public class AccountCapabilities extends JavaScriptObject {
+  public static void all(AsyncCallback<AccountCapabilities> cb, String... filter) {
+    RestApi api = new RestApi("/accounts/self/capabilities");
+    for (String name : filter) {
+      api.addParameter("q", name);
+    }
+    api.send(cb);
+  }
+
+  protected AccountCapabilities() {
+  }
+
+  public final native boolean canPerform(String name)
+  /*-{ return this[name] ? true : false; }-*/;
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java
index c886216..d374d35 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.java
@@ -30,8 +30,8 @@
   String showSiteHeader();
   String useFlashClipboard();
   String copySelfOnEmails();
-  String displayPatchSetsInReverseOrder();
-  String displayPersonNameInReviewCategory();
+  String reversePatchSetOrder();
+  String showUsernameInReviewCategory();
   String buttonSaveChanges();
 
   String tabAccountSummary();
@@ -57,6 +57,8 @@
   String buttonClearPassword();
   String buttonGeneratePassword();
   String linkObtainPassword();
+  String linkEditFullName();
+  String linkReloadContact();
   String invalidUserName();
   String invalidUserEmail();
 
@@ -113,10 +115,7 @@
   String agreementStatus();
   String agreementName();
   String agreementDescription();
-  String agreementAccepted();
   String agreementStatus_EXPIRED();
-  String agreementStatus_NEW();
-  String agreementStatus_REJECTED();
   String agreementStatus_VERIFIED();
 
   String newAgreementSelectTypeHeading();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
index 499f051..8b32174 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
@@ -7,8 +7,8 @@
 showSiteHeader = Show Site Header
 useFlashClipboard = Use Flash Clipboard Widget
 copySelfOnEmails = CC Me On Comments I Write
-displayPatchSetsInReverseOrder = Display Patch Sets In Reverse Order
-displayPersonNameInReviewCategory = Display Person Name In Review Category
+reversePatchSetOrder = Display Patch Sets In Reverse Order
+showUsernameInReviewCategory = Display Person Name In Review Category
 defaultContextFieldLabel = Default Context:
 maximumPageSizeFieldLabel = Maximum Page Size:
 dateFormatLabel = Date/Time Format:
@@ -38,6 +38,8 @@
 buttonClearPassword = Clear Password
 buttonGeneratePassword = Generate Password
 linkObtainPassword = Obtain Password
+linkEditFullName = Edit
+linkReloadContact = Reload
 invalidUserName = Username must contain only letters, numbers, _, - or .
 invalidUserEmail = Email format is wrong.
 sshKeyInvalid = Invalid Key
@@ -103,11 +105,8 @@
 agreementStatus = Status
 agreementName = Name
 agreementStatus_EXPIRED = Expired
-agreementStatus_NEW = Pending
-agreementStatus_REJECTED = Rejected
 agreementStatus_VERIFIED = Verified
 agreementDescription = Description
-agreementAccepted = Accepted
 
 newAgreementSelectTypeHeading = Select an agreement type:
 newAgreementNoneAvailable = No contributor agreements are configured.
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java
index 4e0b3b2..4fbe7a0 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java
@@ -18,17 +18,18 @@
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.ui.OnEditEnabler;
+import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Account.FieldName;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.reviewdb.client.ContactInformation;
-import com.google.gerrit.reviewdb.client.Account.FieldName;
 import com.google.gwt.event.dom.client.ChangeEvent;
 import com.google.gwt.event.dom.client.ChangeHandler;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.i18n.client.LocaleInfo;
-import com.google.gwtjsonrpc.common.AsyncCallback;
+import com.google.gwt.user.client.Window;
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.Composite;
 import com.google.gwt.user.client.ui.FlowPanel;
@@ -41,6 +42,7 @@
 import com.google.gwt.user.client.ui.Widget;
 import com.google.gwtexpui.globalkey.client.NpTextBox;
 import com.google.gwtexpui.user.client.AutoCenterDialogBox;
+import com.google.gwtjsonrpc.common.AsyncCallback;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -102,12 +104,37 @@
     }
 
     int row = 0;
-    if (!Gerrit.getConfig().canEdit(FieldName.USER_NAME)) {
+    if (!Gerrit.getConfig().canEdit(FieldName.USER_NAME)
+        && Gerrit.getConfig().siteHasUsernames()) {
       infoPlainText.resizeRows(infoPlainText.getRowCount() + 1);
       row(infoPlainText, row++, Util.C.userName(), new UsernameField());
     }
 
-    row(infoPlainText, row++, Util.C.contactFieldFullName(), nameTxt);
+    if (!canEditFullName()) {
+      FlowPanel nameLine = new FlowPanel();
+      nameLine.add(nameTxt);
+      if (Gerrit.getConfig().getEditFullNameUrl() != null) {
+        Button edit = new Button(Util.C.linkEditFullName());
+        edit.addClickHandler(new ClickHandler() {
+          @Override
+          public void onClick(ClickEvent event) {
+            Window.open(Gerrit.getConfig().getEditFullNameUrl(), "_blank", null);
+          }
+        });
+        nameLine.add(edit);
+      }
+      Button reload = new Button(Util.C.linkReloadContact());
+      reload.addClickHandler(new ClickHandler() {
+        @Override
+        public void onClick(ClickEvent event) {
+          Window.Location.replace(Gerrit.loginRedirect(PageLinks.SETTINGS_CONTACT));
+        }
+      });
+      nameLine.add(reload);
+      row(infoPlainText, row++, Util.C.contactFieldFullName(), nameLine);
+    } else {
+      row(infoPlainText, row++, Util.C.contactFieldFullName(), nameTxt);
+    }
     row(infoPlainText, row++, Util.C.contactFieldEmail(), emailLine);
 
     infoPlainText.getCellFormatter().addStyleName(0, 0, Gerrit.RESOURCES.css().topmost());
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyAgreementsScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyAgreementsScreen.java
index af619a8..3bd2e77 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyAgreementsScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyAgreementsScreen.java
@@ -14,21 +14,15 @@
 
 package com.google.gerrit.client.account;
 
-import com.google.gerrit.client.FormatUtil;
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
 import com.google.gerrit.client.ui.FancyFlexTable;
 import com.google.gerrit.client.ui.Hyperlink;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.data.AgreementInfo;
-import com.google.gerrit.reviewdb.client.AbstractAgreement;
-import com.google.gerrit.reviewdb.client.AccountAgreement;
-import com.google.gerrit.reviewdb.client.AccountGroupAgreement;
-import com.google.gerrit.reviewdb.client.ContributorAgreement;
+import com.google.gerrit.common.data.ContributorAgreement;
 import com.google.gwt.user.client.ui.Anchor;
 import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
-import com.google.gwtexpui.safehtml.client.SafeHtml;
-import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
 
 public class MyAgreementsScreen extends SettingsScreen {
   private AgreementTable agreements;
@@ -52,16 +46,15 @@
     });
   }
 
-  private class AgreementTable extends FancyFlexTable<AbstractAgreement> {
+  private class AgreementTable extends FancyFlexTable<ContributorAgreement> {
     AgreementTable() {
       table.setWidth("");
       table.setText(0, 1, Util.C.agreementStatus());
       table.setText(0, 2, Util.C.agreementName());
       table.setText(0, 3, Util.C.agreementDescription());
-      table.setText(0, 4, Util.C.agreementAccepted());
 
       final FlexCellFormatter fmt = table.getFlexCellFormatter();
-      for (int c = 1; c <= 4; c++) {
+      for (int c = 1; c < 4; c++) {
         fmt.addStyleName(0, c, Gerrit.RESOURCES.css().dataHeader());
       }
     }
@@ -70,37 +63,22 @@
       while (1 < table.getRowCount())
         table.removeRow(table.getRowCount() - 1);
 
-      for (final AccountAgreement k : result.userAccepted) {
-        addOne(result, k);
-      }
-      for (final AccountGroupAgreement k : result.groupAccepted) {
+      for (final String k : result.accepted) {
         addOne(result, k);
       }
     }
 
-    void addOne(final AgreementInfo info, final AbstractAgreement k) {
+    void addOne(final AgreementInfo info, final String k) {
       final int row = table.getRowCount();
       table.insertRow(row);
       applyDataRowStyle(row);
 
-      final ContributorAgreement cla = info.agreements.get(k.getAgreementId());
+      final ContributorAgreement cla = info.agreements.get(k);
       final String statusName;
-      if (cla == null || !cla.isActive()) {
+      if (cla == null) {
         statusName = Util.C.agreementStatus_EXPIRED();
       } else {
-        switch (k.getStatus()) {
-          case NEW:
-            statusName = Util.C.agreementStatus_NEW();
-            break;
-          case REJECTED:
-            statusName = Util.C.agreementStatus_REJECTED();
-            break;
-          case VERIFIED:
-            statusName = Util.C.agreementStatus_VERIFIED();
-            break;
-          default:
-            statusName = k.getStatus().name();
-        }
+        statusName = Util.C.agreementStatus_VERIFIED();
       }
       table.setText(row, 1, statusName);
 
@@ -110,28 +88,20 @@
       } else {
         final String url = cla.getAgreementUrl();
         if (url != null && url.length() > 0) {
-          final Anchor a = new Anchor(cla.getShortName(), url);
+          final Anchor a = new Anchor(cla.getName(), url);
           a.setTarget("_blank");
           table.setWidget(row, 2, a);
         } else {
-          table.setText(row, 2, cla.getShortName());
+          table.setText(row, 2, cla.getName());
         }
-        table.setText(row, 3, cla.getShortDescription());
+        table.setText(row, 3, cla.getDescription());
       }
-
-      final SafeHtmlBuilder b = new SafeHtmlBuilder();
-      b.append(FormatUtil.mediumFormat(k.getAcceptedOn()));
-      b.br();
-      b.append(FormatUtil.mediumFormat(k.getReviewedOn()));
-      SafeHtml.set(table, row, 4, b);
-
       final FlexCellFormatter fmt = table.getFlexCellFormatter();
-      for (int c = 1; c <= 4; c++) {
+      for (int c = 1; c < 4; c++) {
         fmt.addStyleName(row, c, Gerrit.RESOURCES.css().dataCell());
       }
-      fmt.addStyleName(row, 4, Gerrit.RESOURCES.css().cLastUpdate());
 
-      setRowItem(row, k);
+      setRowItem(row, cla);
     }
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGroupsScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGroupsScreen.java
index 719c395..6cb749d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGroupsScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGroupsScreen.java
@@ -16,7 +16,7 @@
 
 import com.google.gerrit.client.admin.GroupTable;
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
-import com.google.gerrit.common.data.GroupDetail;
+import com.google.gerrit.reviewdb.client.AccountGroup;
 
 import java.util.List;
 
@@ -33,8 +33,9 @@
   @Override
   protected void onLoad() {
     super.onLoad();
-    Util.ACCOUNT_SEC.myGroups(new ScreenLoadCallback<List<GroupDetail>>(this) {
-      public void preDisplay(final List<GroupDetail> result) {
+    Util.ACCOUNT_SEC.myGroups(new ScreenLoadCallback<List<AccountGroup>>(this) {
+      @Override
+      public void preDisplay(final List<AccountGroup> result) {
         groups.display(result);
       }
     });
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java
index 84e87aa..de6e666 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java
@@ -41,8 +41,8 @@
   private CheckBox showSiteHeader;
   private CheckBox useFlashClipboard;
   private CheckBox copySelfOnEmails;
-  private CheckBox displayPatchSetsInReverseOrder;
-  private CheckBox displayPersonNameInReviewCategory;
+  private CheckBox reversePatchSetOrder;
+  private CheckBox showUsernameInReviewCategory;
   private ListBox maximumPageSize;
   private ListBox dateFormat;
   private ListBox timeFormat;
@@ -74,11 +74,11 @@
     copySelfOnEmails = new CheckBox(Util.C.copySelfOnEmails());
     copySelfOnEmails.addClickHandler(onClickSave);
 
-    displayPatchSetsInReverseOrder = new CheckBox(Util.C.displayPatchSetsInReverseOrder());
-    displayPatchSetsInReverseOrder.addClickHandler(onClickSave);
+    reversePatchSetOrder = new CheckBox(Util.C.reversePatchSetOrder());
+    reversePatchSetOrder.addClickHandler(onClickSave);
 
-    displayPersonNameInReviewCategory = new CheckBox(Util.C.displayPersonNameInReviewCategory());
-    displayPersonNameInReviewCategory.addClickHandler(onClickSave);
+    showUsernameInReviewCategory = new CheckBox(Util.C.showUsernameInReviewCategory());
+    showUsernameInReviewCategory.addClickHandler(onClickSave);
 
     maximumPageSize = new ListBox();
     for (final short v : PAGESIZE_CHOICES) {
@@ -137,11 +137,11 @@
     row++;
 
     formGrid.setText(row, labelIdx, "");
-    formGrid.setWidget(row, fieldIdx, displayPatchSetsInReverseOrder);
+    formGrid.setWidget(row, fieldIdx, reversePatchSetOrder);
     row++;
 
     formGrid.setText(row, labelIdx, "");
-    formGrid.setWidget(row, fieldIdx, displayPersonNameInReviewCategory);
+    formGrid.setWidget(row, fieldIdx, showUsernameInReviewCategory);
     row++;
 
     formGrid.setText(row, labelIdx, Util.C.maximumPageSizeFieldLabel());
@@ -179,8 +179,8 @@
     showSiteHeader.setEnabled(on);
     useFlashClipboard.setEnabled(on);
     copySelfOnEmails.setEnabled(on);
-    displayPatchSetsInReverseOrder.setEnabled(on);
-    displayPersonNameInReviewCategory.setEnabled(on);
+    reversePatchSetOrder.setEnabled(on);
+    showUsernameInReviewCategory.setEnabled(on);
     maximumPageSize.setEnabled(on);
     dateFormat.setEnabled(on);
     timeFormat.setEnabled(on);
@@ -190,8 +190,8 @@
     showSiteHeader.setValue(p.isShowSiteHeader());
     useFlashClipboard.setValue(p.isUseFlashClipboard());
     copySelfOnEmails.setValue(p.isCopySelfOnEmails());
-    displayPatchSetsInReverseOrder.setValue(p.isDisplayPatchSetsInReverseOrder());
-    displayPersonNameInReviewCategory.setValue(p.isDisplayPersonNameInReviewCategory());
+    reversePatchSetOrder.setValue(p.isReversePatchSetOrder());
+    showUsernameInReviewCategory.setValue(p.isShowUsernameInReviewCategory());
     setListBox(maximumPageSize, DEFAULT_PAGESIZE, p.getMaximumPageSize());
     setListBox(dateFormat, AccountGeneralPreferences.DateFormat.STD, //
         p.getDateFormat());
@@ -251,8 +251,8 @@
     p.setShowSiteHeader(showSiteHeader.getValue());
     p.setUseFlashClipboard(useFlashClipboard.getValue());
     p.setCopySelfOnEmails(copySelfOnEmails.getValue());
-    p.setDisplayPatchSetsInReverseOrder(displayPatchSetsInReverseOrder.getValue());
-    p.setDisplayPersonNameInReviewCategory(displayPersonNameInReviewCategory.getValue());
+    p.setReversePatchSetOrder(reversePatchSetOrder.getValue());
+    p.setShowUsernameInReviewCategory(showUsernameInReviewCategory.getValue());
     p.setMaximumPageSize(getListBox(maximumPageSize, DEFAULT_PAGESIZE));
     p.setDateFormat(getListBox(dateFormat,
         AccountGeneralPreferences.DateFormat.STD,
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyProfileScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyProfileScreen.java
index 165168d..ae04e0a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyProfileScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyProfileScreen.java
@@ -38,21 +38,24 @@
       fieldIdx = 1;
     }
 
-    info = new Grid(5, 2);
+    info = new Grid((Gerrit.getConfig().siteHasUsernames() ? 1 : 0) + 4, 2);
     info.setStyleName(Gerrit.RESOURCES.css().infoBlock());
     info.addStyleName(Gerrit.RESOURCES.css().accountInfoBlock());
     add(info);
 
-    infoRow(0, Util.C.userName());
-    infoRow(1, Util.C.fullName());
-    infoRow(2, Util.C.preferredEmail());
-    infoRow(3, Util.C.registeredOn());
-    infoRow(4, Util.C.accountId());
+    int row = 0;
+    if (Gerrit.getConfig().siteHasUsernames()) {
+      infoRow(row++, Util.C.userName());
+    }
+    infoRow(row++, Util.C.fullName());
+    infoRow(row++, Util.C.preferredEmail());
+    infoRow(row++, Util.C.registeredOn());
+    infoRow(row++, Util.C.accountId());
 
     final CellFormatter fmt = info.getCellFormatter();
     fmt.addStyleName(0, 0, Gerrit.RESOURCES.css().topmost());
     fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().topmost());
-    fmt.addStyleName(4, 0, Gerrit.RESOURCES.css().bottomheader());
+    fmt.addStyleName(row - 1, 0, Gerrit.RESOURCES.css().bottomheader());
   }
 
   @Override
@@ -69,10 +72,13 @@
   }
 
   void display(final Account account) {
-    info.setWidget(0, fieldIdx, new UsernameField());
-    info.setText(1, fieldIdx, account.getFullName());
-    info.setText(2, fieldIdx, account.getPreferredEmail());
-    info.setText(3, fieldIdx, mediumFormat(account.getRegisteredOn()));
-    info.setText(4, fieldIdx, account.getId().toString());
+    int row = 0;
+    if (Gerrit.getConfig().siteHasUsernames()) {
+      info.setWidget(row++, fieldIdx, new UsernameField());
+    }
+    info.setText(row++, fieldIdx, account.getFullName());
+    info.setText(row++, fieldIdx, account.getPreferredEmail());
+    info.setText(row++, fieldIdx, mediumFormat(account.getRegisteredOn()));
+    info.setText(row++, fieldIdx, account.getId().toString());
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchedProjectsScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchedProjectsScreen.java
index 5a62eac..d38015c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchedProjectsScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchedProjectsScreen.java
@@ -18,70 +18,46 @@
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
 import com.google.gerrit.client.ui.HintTextBox;
+import com.google.gerrit.client.ui.ProjectListPopup;
 import com.google.gerrit.client.ui.ProjectNameSuggestOracle;
-import com.google.gerrit.client.ui.ProjectsTable;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.data.AccountProjectWatchInfo;
-import com.google.gerrit.common.data.ProjectList;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.event.dom.client.KeyCodes;
 import com.google.gwt.event.dom.client.KeyPressEvent;
 import com.google.gwt.event.dom.client.KeyPressHandler;
-import com.google.gwt.event.logical.shared.ResizeEvent;
-import com.google.gwt.event.logical.shared.ResizeHandler;
 import com.google.gwt.event.logical.shared.SelectionEvent;
 import com.google.gwt.event.logical.shared.SelectionHandler;
-import com.google.gwt.event.shared.HandlerRegistration;
-import com.google.gwt.user.client.Window;
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.Grid;
 import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
-import com.google.gwt.user.client.ui.PopupPanel;
-import com.google.gwt.user.client.ui.ScrollPanel;
 import com.google.gwt.user.client.ui.SuggestBox;
 import com.google.gwt.user.client.ui.SuggestBox.DefaultSuggestionDisplay;
 import com.google.gwt.user.client.ui.SuggestOracle.Suggestion;
-import com.google.gwtexpui.globalkey.client.GlobalKey;
-import com.google.gwtexpui.globalkey.client.HidePopupPanelCommand;
-import com.google.gwtexpui.user.client.PluginSafeDialogBox;
 
 import java.util.List;
 
-public class MyWatchedProjectsScreen extends SettingsScreen implements
-    ResizeHandler {
+public class MyWatchedProjectsScreen extends SettingsScreen {
   private Button addNew;
   private HintTextBox nameBox;
   private SuggestBox nameTxt;
   private HintTextBox filterTxt;
   private MyWatchesTable watchesTab;
   private Button browse;
-  private PluginSafeDialogBox popup;
-  private Button close;
-  private ProjectsTable projectsTab;
   private Button delSel;
-
-  private PopupPanel.PositionCallback popupPosition;
-  private HandlerRegistration regWindowResize;
-
-  private int preferredPopupWidth = -1;
-
   private boolean submitOnSelection;
-  private boolean firstPopupLoad = true;
-  private boolean popingUp;
-
-  private ScrollPanel sp;
+  private Grid grid;
+  private ProjectListPopup projectsPopup;
 
   @Override
   protected void onInitUI() {
     super.onInitUI();
     createWidgets();
 
-
     /* top table */
-
-    final Grid grid = new Grid(2, 2);
+    grid = new Grid(2, 2);
     grid.setStyleName(Gerrit.RESOURCES.css().infoBlock());
     grid.setText(0, 0, Util.C.watchedProjectName());
     grid.setWidget(0, 1, nameTxt);
@@ -105,62 +81,27 @@
 
 
     /* bottom table */
-
     add(watchesTab);
     add(delSel);
 
 
     /* popup */
-
-    final FlowPanel pfp = new FlowPanel();
-    sp = new ScrollPanel(projectsTab);
-    pfp.add(sp);
-    pfp.add(close);
-    popup.setWidget(pfp);
-
-    popupPosition = new PopupPanel.PositionCallback() {
-      public void setPosition(int offsetWidth, int offsetHeight) {
-        if (preferredPopupWidth == -1) {
-          preferredPopupWidth = offsetWidth;
+    projectsPopup = new ProjectListPopup() {
+      @Override
+      protected void onMovePointerTo(String projectName) {
+        // prevent user input from being overwritten by simply poping up
+        if (!projectsPopup.isPopingUp() || "".equals(nameBox.getText())) {
+          nameBox.setText(projectName);
         }
+      }
 
-        int top = grid.getAbsoluteTop() - 50; // under page header
-
-        // Try to place it to the right of everything else, but not
-        // right justified
-        int left = 5 + Math.max(
-                         grid.getAbsoluteLeft() + grid.getOffsetWidth(),
-                   watchesTab.getAbsoluteLeft() + watchesTab.getOffsetWidth() );
-
-        if (top + offsetHeight > Window.getClientHeight()) {
-          top = Window.getClientHeight() - offsetHeight;
-        }
-        if (left + offsetWidth > Window.getClientWidth()) {
-          left = Window.getClientWidth() - offsetWidth;
-        }
-
-        if (top < 0) {
-          sp.setHeight((sp.getOffsetHeight() + top) + "px");
-          top = 0;
-        }
-        if (left < 0) {
-          sp.setWidth((sp.getOffsetWidth() + left) + "px");
-          left = 0;
-        }
-
-        popup.setPopupPosition(left, top);
+      @Override
+      protected void openRow(String projectName) {
+        nameBox.setText(projectName);
+        doAddNew();
       }
     };
-  }
-
-  @Override
-  public void onResize(final ResizeEvent event) {
-    sp.setSize("100%","100%");
-
-    // For some reason keeping track of preferredWidth keeps the width better,
-    // but using 100% for height works better.
-    popup.setHeight("100%");
-    popupPosition.setPosition(preferredPopupWidth, popup.getOffsetHeight());
+    projectsPopup.initPopup(Util.C.projects(), PageLinks.SETTINGS_PROJECTS);
   }
 
   protected void createWidgets() {
@@ -213,49 +154,18 @@
       }
     });
 
-    projectsTab = new ProjectsTable() {
-      {
-        keysNavigation.add(new OpenKeyCommand(0, 'o', Util.C.projectListOpen()));
-        keysNavigation.add(new OpenKeyCommand(0, KeyCodes.KEY_ENTER,
-                                                      Util.C.projectListOpen()));
-      }
-
-      @Override
-      protected void movePointerTo(final int row, final boolean scroll) {
-        super.movePointerTo(row, scroll);
-
-        // prevent user input from being overwritten by simply poping up
-        if (! popingUp || "".equals(nameBox.getText()) ) {
-          nameBox.setText(getRowItem(row).getName());
-        }
-      }
-
-      @Override
-      protected void onOpenRow(final int row) {
-        super.onOpenRow(row);
-        nameBox.setText(getRowItem(row).getName());
-        doAddNew();
-      }
-    };
-    projectsTab.setSavePointerId(PageLinks.SETTINGS_PROJECTS);
-
-    close = new Button(Util.C.projectsClose());
-    close.addClickHandler(new ClickHandler() {
-      @Override
-      public void onClick(final ClickEvent event) {
-        closePopup();
-      }
-    });
-
-    popup = new PluginSafeDialogBox();
-    popup.setModal(false);
-    popup.setText(Util.C.projects());
-
     browse = new Button(Util.C.buttonBrowseProjects());
     browse.addClickHandler(new ClickHandler() {
       @Override
       public void onClick(final ClickEvent event) {
-        displayPopup();
+        int top = grid.getAbsoluteTop() - 50; // under page header
+        // Try to place it to the right of everything else, but not
+        // right justified
+        int left =
+            5 + Math.max(grid.getAbsoluteLeft() + grid.getOffsetWidth(),
+                watchesTab.getAbsoluteLeft() + watchesTab.getOffsetWidth());
+        projectsPopup.setPreferredCoordinates(top, left);
+        projectsPopup.displayPopup();
       }
     });
 
@@ -279,37 +189,7 @@
   @Override
   protected void onUnload() {
     super.onUnload();
-    closePopup();
-  }
-
-  protected void displayPopup() {
-    popingUp = true;
-    if (firstPopupLoad) { // For sizing/positioning, delay display until loaded
-      populateProjects();
-    } else {
-      popup.setPopupPositionAndShow(popupPosition);
-
-      GlobalKey.dialog(popup);
-      GlobalKey.addApplication(popup, new HidePopupPanelCommand(0,
-          KeyCodes.KEY_ESCAPE, popup));
-      projectsTab.setRegisterKeys(true);
-
-      projectsTab.finishDisplay();
-
-      if (regWindowResize == null) {
-        regWindowResize = Window.addResizeHandler(this);
-      }
-
-      popingUp = false;
-    }
-  }
-
-  protected void closePopup() {
-    popup.hide();
-    if (regWindowResize != null) {
-      regWindowResize.removeHandler();
-      regWindowResize = null;
-    }
+    projectsPopup.closePopup();
   }
 
   protected void doAddNew() {
@@ -359,18 +239,4 @@
       }
     });
   }
-
-  protected void populateProjects() {
-    Util.PROJECT_SVC.visibleProjects(
-        new GerritCallback<ProjectList>() {
-      @Override
-      public void onSuccess(final ProjectList result) {
-        projectsTab.display(result.getProjects());
-        if (firstPopupLoad) { // Display was delayed until table was loaded
-          firstPopupLoad = false;
-          displayPopup();
-        }
-      }
-    });
-  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/NewAgreementScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/NewAgreementScreen.java
index 154e2ce..ac44dbe 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/NewAgreementScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/NewAgreementScreen.java
@@ -22,10 +22,8 @@
 import com.google.gerrit.client.ui.SmallHeading;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.data.AgreementInfo;
+import com.google.gerrit.common.data.ContributorAgreement;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountAgreement;
-import com.google.gerrit.reviewdb.client.AccountGroupAgreement;
-import com.google.gerrit.reviewdb.client.ContributorAgreement;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
@@ -53,7 +51,7 @@
 
 public class NewAgreementScreen extends AccountScreen {
   private final String nextToken;
-  private Set<ContributorAgreement.Id> mySigned;
+  private Set<String> mySigned;
   private List<ContributorAgreement> available;
   private ContributorAgreement current;
 
@@ -83,13 +81,7 @@
     Util.ACCOUNT_SVC.myAgreements(new GerritCallback<AgreementInfo>() {
       public void onSuccess(AgreementInfo result) {
         if (isAttached()) {
-          mySigned = new HashSet<ContributorAgreement.Id>();
-          for (AccountAgreement a : result.userAccepted) {
-            mySigned.add(a.getAgreementId());
-          }
-          for (AccountGroupAgreement a : result.groupAccepted) {
-            mySigned.add(a.getAgreementId());
-          }
+          mySigned = new HashSet<String>(result.accepted);
           postRPC();
         }
       }
@@ -176,11 +168,11 @@
     radios.add(hdr);
 
     for (final ContributorAgreement cla : available) {
-      final RadioButton r = new RadioButton("cla_id", cla.getShortName());
+      final RadioButton r = new RadioButton("cla_id", cla.getName());
       r.addStyleName(Gerrit.RESOURCES.css().contributorAgreementButton());
       radios.add(r);
 
-      if (mySigned.contains(cla.getId())) {
+      if (mySigned.contains(cla.getName())) {
         r.setEnabled(false);
         final Label l = new Label(Util.C.newAgreementAlreadySubmitted());
         l.setStyleName(Gerrit.RESOURCES.css().contributorAgreementAlreadySubmitted());
@@ -194,9 +186,8 @@
         });
       }
 
-      if (cla.getShortDescription() != null
-          && !cla.getShortDescription().equals("")) {
-        final Label l = new Label(cla.getShortDescription());
+      if (cla.getDescription() != null && !cla.getDescription().equals("")) {
+        final Label l = new Label(cla.getDescription());
         l.setStyleName(Gerrit.RESOURCES.css().contributorAgreementShortDescription());
         radios.add(l);
       }
@@ -231,7 +222,7 @@
   }
 
   private void doEnterAgreement() {
-    Util.ACCOUNT_SEC.enterAgreement(current.getId(),
+    Util.ACCOUNT_SEC.enterAgreement(current.getName(),
         new GerritCallback<VoidResult>() {
           public void onSuccess(final VoidResult result) {
             Gerrit.display(nextToken);
@@ -284,8 +275,9 @@
       contactGroup.add(contactPanel);
       contactPanel.hideSaveButton();
     }
-    contactGroup.setVisible(cla.isRequireContactInformation());
-    finalGroup.setVisible(true);
+    contactGroup.setVisible(
+        cla.isRequireContactInformation() && cla.getAutoVerify() != null);
+    finalGroup.setVisible(cla.getAutoVerify() != null);
     yesIAgreeBox.setText("");
     submit.setEnabled(false);
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/RegisterScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/RegisterScreen.java
index 084ea6f..2810931 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/RegisterScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/RegisterScreen.java
@@ -125,7 +125,7 @@
       agreementGroup.add(whyAgreement);
 
       choices.add(new InlineHyperlink(Util.C.newAgreement(),
-          PageLinks.SETTINGS_NEW_AGREEMENT + "," + nextToken));
+          PageLinks.SETTINGS_NEW_AGREEMENT));
       choices
           .add(new InlineHyperlink(Util.C.welcomeAgreementLater(), nextToken));
       formBody.add(agreementGroup);
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..a9aa418 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
@@ -182,7 +182,7 @@
     Collections.sort(value.getPermissions());
 
     this.value = value;
-    this.readOnly = !editing || !projectAccess.isOwnerOf(value);
+    this.readOnly = !editing || !(projectAccess.isOwnerOf(value) || projectAccess.canUpload());
 
     name.setEnabled(!readOnly);
     deleteSection.setVisible(!readOnly);
@@ -223,22 +223,16 @@
 
     if (AccessSection.GLOBAL_CAPABILITIES.equals(value.getName())) {
       for (String varName : Util.C.capabilityNames().keySet()) {
-        if (value.getPermission(varName) == null) {
-          perms.add(varName);
-        }
+        addPermission(varName, perms);
       }
     } else if (RefConfigSection.isValid(value.getName())) {
       for (ApprovalType t : Gerrit.getConfig().getApprovalTypes()
           .getApprovalTypes()) {
         String varName = Permission.LABEL + t.getCategory().getLabelName();
-        if (value.getPermission(varName) == null) {
-          perms.add(varName);
-        }
+        addPermission(varName, perms);
       }
       for (String varName : Util.C.permissionNames().keySet()) {
-        if (value.getPermission(varName) == null) {
-          perms.add(varName);
-        }
+        addPermission(varName, perms);
       }
     }
     if (perms.isEmpty()) {
@@ -251,6 +245,19 @@
     }
   }
 
+  private void addPermission(final String permissionName,
+      final List<String> permissionList) {
+    if (value.getPermission(permissionName) != null) {
+      return;
+    }
+    if (Gerrit.getConfig().getWildProject()
+        .equals(projectAccess.getProjectName())
+        && !Permission.canBeOnAllProjects(value.getName(), permissionName)) {
+      return;
+    }
+    permissionList.add(permissionName);
+  }
+
   @Override
   public void flush() {
     List<Permission> src = permissions.getList();
@@ -276,7 +283,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 936bfe5..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,75 +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) {
-            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());
-            }
-            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;
@@ -444,19 +313,12 @@
     if (groupDetail.ownerGroup != null) {
       ownerTxt.setText(groupDetail.ownerGroup.getName());
     } else {
-      ownerTxt.setText(Util.M.deletedGroup(group.getOwnerGroupId().get()));
+      ownerTxt.setText(Util.M.deletedReference(group.getOwnerGroupUUID().get()));
     }
     descTxt.setText(group.getDescription());
 
     visibleToAllCheckBox.setValue(group.isVisibleToAll());
 
-    switch (group.getType()) {
-      case LDAP:
-        externalName.setText(group.getExternalNameKey() != null ? group
-            .getExternalNameKey().get() : Util.C.noGroupSelected());
-        break;
-    }
-
     setType(group.getType());
 
     enableForm(groupDetail.canModify);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java
index b5cca86..4c0b1ba 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java
@@ -17,8 +17,8 @@
 import com.google.gerrit.client.Dispatcher;
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.ui.AccountDashboardLink;
 import com.google.gerrit.client.ui.AccountGroupSuggestOracle;
+import com.google.gerrit.client.ui.AccountLink;
 import com.google.gerrit.client.ui.AddMemberBox;
 import com.google.gerrit.client.ui.FancyFlexTable;
 import com.google.gerrit.client.ui.Hyperlink;
@@ -286,7 +286,7 @@
       CheckBox checkBox = new CheckBox();
       table.setWidget(row, 1, checkBox);
       checkBox.setEnabled(enabled);
-      table.setWidget(row, 2, AccountDashboardLink.link(accounts, accountId));
+      table.setWidget(row, 2, AccountLink.link(accounts, accountId));
       table.setText(row, 3, accounts.get(accountId).getPreferredEmail());
 
       final FlexCellFormatter fmt = table.getFlexCellFormatter();
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 9250ca3..0c18169 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
@@ -48,6 +48,9 @@
   String suggestedGroupLabel();
   String parentSuggestions();
 
+  String buttonBrowseProjects();
+  String projects();
+  String projectRepoBrowser();
   String headingGroupUUID();
   String headingOwner();
   String headingDescription();
@@ -58,7 +61,6 @@
   String noMembersInfo();
   String headingExternalGroup();
   String headingCreateGroup();
-  String headingCreateProject();
   String headingParentProjectName();
   String columnProjectName();
   String headingAgreements();
@@ -89,12 +91,14 @@
   String initialRevision();
   String buttonAddBranch();
   String buttonDeleteBranch();
+  String branchDeletionOpenChanges();
 
   String groupListPrev();
   String groupListNext();
   String groupListOpen();
 
   String groupListTitle();
+  String createGroupTitle();
   String groupTabGeneral();
   String groupTabMembers();
   String projectListTitle();
@@ -103,6 +107,13 @@
   String projectAdminTabBranches();
   String projectAdminTabAccess();
 
+  String plugins();
+  String pluginDisabled();
+
+  String columnPluginName();
+  String columnPluginVersion();
+  String columnPluginStatus();
+
   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 ef7b73d..3ff3f43 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
@@ -17,6 +17,9 @@
 buttonSaveChanges = Save Changes
 checkBoxEmptyCommit = Create initial empty commit
 checkBoxPermissionsOnly = Only serve as parent for other projects
+buttonBrowseProjects = Browse
+projects = All projects
+projectRepoBrowser = Repository Browser
 useContentMerge = Automatically resolve conflicts
 useContributorAgreements = Require a valid contributor agreement to upload
 useSignedOffBy = Require <a href="http://gerrit.googlecode.com/svn/documentation/2.0/user-signedoffby.html#Signed-off-by" target="_blank"><code>Signed-off-by</code></a> in commit message
@@ -39,7 +42,6 @@
 noMembersInfo = Group Members can only be viewed for Gerrit internal groups. For external groups and Gerrit system groups the members cannot be displayed.
 headingExternalGroup = Selected External Group
 headingCreateGroup = Create New Group
-headingCreateProject = Create New Project
 headingAgreements = Contributor Agreements
 
 projectSubmitType_FAST_FORWARD_ONLY = Fast Forward Only
@@ -68,12 +70,15 @@
 initialRevision = Initial Revision
 buttonAddBranch = Create Branch
 buttonDeleteBranch = Delete
+branchDeletionOpenChanges = The following branches were not deleted \
+because they have open changes:
 
 groupListPrev = Previous group
 groupListNext = Next group
 groupListOpen = Open group
 
 groupListTitle = Groups
+createGroupTitle = Create Group
 groupTabGeneral = General
 groupTabMembers = Members
 projectListTitle = Projects
@@ -82,6 +87,12 @@
 projectAdminTabBranches = Branches
 projectAdminTabAccess = Access
 
+plugins = Plugins
+pluginDisabled = Disabled
+columnPluginName = Plugin Name
+columnPluginVersion = Version
+columnPluginStatus = Status
+
 noGroupSelected = (No group selected)
 errorNoMatchingGroups = No Matching Groups
 errorNoGitRepository = No Git Repository
@@ -91,6 +102,7 @@
 
 # Permission Names
 permissionNames = \
+	abandon, \
 	create, \
 	forgeAuthor, \
 	forgeCommitter, \
@@ -102,6 +114,7 @@
 	read, \
 	rebase, \
 	submit
+abandon = Abandon
 create = Create Reference
 forgeAuthor = Forge Author Identity
 forgeCommitter = Forge Committer Identity
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateGroupScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateGroupScreen.java
new file mode 100644
index 0000000..4574c6d
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateGroupScreen.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.client.admin;
+
+import static com.google.gerrit.common.data.GlobalCapability.CREATE_GROUP;
+
+import com.google.gerrit.client.Dispatcher;
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.NotFoundScreen;
+import com.google.gerrit.client.account.AccountCapabilities;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.ui.OnEditEnabler;
+import com.google.gerrit.client.ui.Screen;
+import com.google.gerrit.client.ui.SmallHeading;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.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.History;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.VerticalPanel;
+import com.google.gwtexpui.globalkey.client.NpTextBox;
+
+public class CreateGroupScreen extends Screen {
+
+  private NpTextBox addTxt;
+  private Button addNew;
+
+  public CreateGroupScreen() {
+    super();
+    setRequiresSignIn(true);
+  }
+
+  @Override
+  protected void onLoad() {
+    super.onLoad();
+    AccountCapabilities.all(new GerritCallback<AccountCapabilities>() {
+      @Override
+      public void onSuccess(AccountCapabilities ac) {
+        if (ac.canPerform(CREATE_GROUP)) {
+          display();
+        } else {
+          Gerrit.display(PageLinks.ADMIN_CREATE_GROUP, new NotFoundScreen());
+        }
+      }
+    }, CREATE_GROUP);
+  }
+
+  @Override
+  protected void onInitUI() {
+    super.onInitUI();
+    setPageTitle(Util.C.createGroupTitle());
+    addCreateGroupPanel();
+  }
+
+  private void addCreateGroupPanel() {
+    VerticalPanel addPanel = new VerticalPanel();
+    addPanel.setStyleName(Gerrit.RESOURCES.css().addSshKeyPanel());
+    addPanel.add(new SmallHeading(Util.C.headingCreateGroup()));
+
+    addTxt = new NpTextBox();
+    addTxt.setVisibleLength(60);
+    addTxt.addKeyPressHandler(new KeyPressHandler() {
+      @Override
+      public void onKeyPress(KeyPressEvent event) {
+        if (event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER) {
+          doCreateGroup();
+        }
+      }
+    });
+    addPanel.add(addTxt);
+
+    addNew = new Button(Util.C.buttonCreateGroup());
+    addNew.setEnabled(false);
+    addNew.addClickHandler(new ClickHandler() {
+      @Override
+      public void onClick(final ClickEvent event) {
+        doCreateGroup();
+      }
+    });
+    addPanel.add(addNew);
+    add(addPanel);
+
+    new OnEditEnabler(addNew, addTxt);
+  }
+
+  private void doCreateGroup() {
+    final String newName = addTxt.getText();
+    if (newName == null || newName.length() == 0) {
+      return;
+    }
+
+    addNew.setEnabled(false);
+    Util.GROUP_SVC.createGroup(newName, new GerritCallback<AccountGroup.Id>() {
+      public void onSuccess(final AccountGroup.Id result) {
+        History.newItem(Dispatcher.toGroup(result, AccountGroupScreen.MEMBERS));
+      }
+
+      @Override
+      public void onFailure(Throwable caught) {
+        super.onFailure(caught);
+        addNew.setEnabled(true);
+      }
+    });
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateProjectScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateProjectScreen.java
index 0ce2d77..b4950d8 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateProjectScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateProjectScreen.java
@@ -14,14 +14,22 @@
 
 package com.google.gerrit.client.admin;
 
+import static com.google.gerrit.common.data.GlobalCapability.CREATE_PROJECT;
+
 import com.google.gerrit.client.Dispatcher;
 import com.google.gerrit.client.ErrorDialog;
 import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.NotFoundScreen;
+import com.google.gerrit.client.account.AccountCapabilities;
+import com.google.gerrit.client.projects.ProjectInfo;
+import com.google.gerrit.client.projects.ProjectMap;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.ui.HintTextBox;
+import com.google.gerrit.client.ui.ProjectListPopup;
 import com.google.gerrit.client.ui.ProjectNameSuggestOracle;
 import com.google.gerrit.client.ui.ProjectsTable;
 import com.google.gerrit.client.ui.Screen;
+import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
@@ -38,16 +46,17 @@
 import com.google.gwtexpui.globalkey.client.NpTextBox;
 import com.google.gwtjsonrpc.common.VoidResult;
 
-import java.util.List;
-
 public class CreateProjectScreen extends Screen {
+  private Grid grid;
   private NpTextBox project;
   private Button create;
+  private Button browse;
   private HintTextBox parent;
   private SuggestBox sugestParent;
   private CheckBox emptyCommit;
   private CheckBox permissionsOnly;
   private ProjectsTable suggestedParentsTab;
+  private ProjectListPopup projectsPopup;
 
   public CreateProjectScreen() {
     super();
@@ -57,7 +66,22 @@
   @Override
   protected void onLoad() {
     super.onLoad();
-    display();
+    AccountCapabilities.all(new GerritCallback<AccountCapabilities>() {
+      @Override
+      public void onSuccess(AccountCapabilities ac) {
+        if (ac.canPerform(CREATE_PROJECT)) {
+          display();
+        } else {
+          Gerrit.display(PageLinks.ADMIN_CREATE_PROJECT, new NotFoundScreen());
+        }
+      }
+    }, CREATE_PROJECT);
+  }
+
+  @Override
+  protected void onUnload() {
+    super.onUnload();
+    projectsPopup.closePopup();
   }
 
   @Override
@@ -65,6 +89,18 @@
     super.onInitUI();
     setPageTitle(Util.C.createProjectTitle());
     addCreateProjectPanel();
+
+    /* popup */
+    projectsPopup = new ProjectListPopup() {
+      @Override
+      protected void onMovePointerTo(String projectName) {
+        // prevent user input from being overwritten by simply poping up
+        if (!projectsPopup.isPopingUp() || "".equals(sugestParent.getText())) {
+          sugestParent.setText(projectName);
+        }
+      }
+    };
+    projectsPopup.initPopup(Util.C.projects(), PageLinks.ADMIN_PROJECTS);
   }
 
   private void addCreateProjectPanel() {
@@ -82,12 +118,11 @@
     fp.add(emptyCommit);
     fp.add(permissionsOnly);
     fp.add(create);
-    VerticalPanel vp=new VerticalPanel();
+    VerticalPanel vp = new VerticalPanel();
     vp.add(fp);
     initSuggestedParents();
     vp.add(suggestedParentsTab);
     add(vp);
-
   }
 
   private void initCreateTxt() {
@@ -111,6 +146,23 @@
         doCreateProject();
       }
     });
+
+    browse = new Button(Util.C.buttonBrowseProjects());
+    browse.addClickHandler(new ClickHandler() {
+      @Override
+      public void onClick(final ClickEvent event) {
+        int top = grid.getAbsoluteTop() - 50; // under page header
+        // Try to place it to the right of everything else, but not
+        // right justified
+        int left =
+            5 + Math.max(
+                grid.getAbsoluteLeft() + grid.getOffsetWidth(),
+                suggestedParentsTab.getAbsoluteLeft()
+                    + suggestedParentsTab.getOffsetWidth());
+        projectsPopup.setPreferredCoordinates(top, left);
+        projectsPopup.displayPopup();
+      }
+    });
   }
 
   private void initParentBox() {
@@ -127,45 +179,44 @@
       }
 
       @Override
-      protected void populate(final int row, final Project k) {
-        final Anchor projectLink = new Anchor(k.getName());
+      protected void populate(final int row, final ProjectInfo k) {
+        final Anchor projectLink = new Anchor(k.name());
         projectLink.addClickHandler(new ClickHandler() {
 
           @Override
           public void onClick(ClickEvent event) {
-            sugestParent.setText(getRowItem(row).getName());
+            sugestParent.setText(getRowItem(row).name());
           }
         });
 
         table.setWidget(row, 1, projectLink);
-        table.setText(row, 2, k.getDescription());
+        table.setText(row, 2, k.description());
 
         setRowItem(row, k);
       }
     };
     suggestedParentsTab.setVisible(false);
 
-    Util.PROJECT_SVC
-        .suggestParentCandidates(new GerritCallback<List<Project>>() {
-          @Override
-          public void onSuccess(List<Project> result) {
-            if (result != null && !result.isEmpty()) {
-              suggestedParentsTab.setVisible(true);
-              suggestedParentsTab.display(result);
-              suggestedParentsTab.finishDisplay();
-            }
-          }
-        });
+    ProjectMap.parentCandidates(new GerritCallback<ProjectMap>() {
+      @Override
+      public void onSuccess(ProjectMap list) {
+        if (!list.isEmpty()) {
+          suggestedParentsTab.setVisible(true);
+          suggestedParentsTab.display(list);
+          suggestedParentsTab.finishDisplay();
+        }
+      }
+    });
   }
 
   private void addGrid(final VerticalPanel fp) {
-    final Grid grid = new Grid(2, 2);
+    grid = new Grid(2, 3);
     grid.setStyleName(Gerrit.RESOURCES.css().infoBlock());
     grid.setText(0, 0, Util.C.columnProjectName() + ":");
     grid.setWidget(0, 1, project);
     grid.setText(1, 0, Util.C.headingParentProjectName() + ":");
     grid.setWidget(1, 1, sugestParent);
-
+    grid.setWidget(1, 2, browse);
     fp.add(grid);
   }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java
index 9d62d34..3157d97 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java
@@ -14,37 +14,14 @@
 
 package com.google.gerrit.client.admin;
 
-import com.google.gerrit.client.Dispatcher;
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
 import com.google.gerrit.client.ui.AccountScreen;
-import com.google.gerrit.client.ui.OnEditEnabler;
-import com.google.gerrit.client.ui.SmallHeading;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.data.GroupList;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gwt.event.dom.client.BlurEvent;
-import com.google.gwt.event.dom.client.BlurHandler;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.dom.client.FocusEvent;
-import com.google.gwt.event.dom.client.FocusHandler;
-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.History;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.VerticalPanel;
-import com.google.gwtexpui.globalkey.client.NpTextBox;
 
 public class GroupListScreen extends AccountScreen {
   private GroupTable groups;
 
-  private VerticalPanel addPanel;
-  private NpTextBox addTxt;
-  private Button addNew;
-
   @Override
   protected void onLoad() {
     super.onLoad();
@@ -52,7 +29,6 @@
         .visibleGroups(new ScreenLoadCallback<GroupList>(this) {
           @Override
           protected void preDisplay(GroupList result) {
-            addPanel.setVisible(result.isCanCreateGroup());
             groups.display(result.getGroups());
             groups.finishDisplay();
           }
@@ -66,54 +42,6 @@
 
     groups = new GroupTable(true /* hyperlink to admin */, PageLinks.ADMIN_GROUPS);
     add(groups);
-
-    addPanel = new VerticalPanel();
-    addPanel.setStyleName(Gerrit.RESOURCES.css().addSshKeyPanel());
-    addPanel.add(new SmallHeading(Util.C.headingCreateGroup()));
-
-    addTxt = new NpTextBox();
-    addTxt.setVisibleLength(60);
-    addTxt.addKeyPressHandler(new KeyPressHandler() {
-      @Override
-      public void onKeyPress(KeyPressEvent event) {
-        if (event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER) {
-          doCreateGroup();
-        }
-      }
-    });
-    addPanel.add(addTxt);
-
-    addNew = new Button(Util.C.buttonCreateGroup());
-    addNew.setEnabled(false);
-    addNew.addClickHandler(new ClickHandler() {
-      @Override
-      public void onClick(final ClickEvent event) {
-        doCreateGroup();
-      }
-    });
-    addNew.addFocusHandler(new FocusHandler() {
-      @Override
-      public void onFocus(FocusEvent event) {
-        // unregister the keys for the 'groups' table so that pressing ENTER
-        // when the 'addNew' button has the focus triggers the button (if the
-        // keys for the 'groups' table would not be unregistered the 'addNew'
-        // button would not be triggered on ENTER but the group which is
-        // selected in the 'groups' table would be opened)
-        groups.setRegisterKeys(false);
-      }
-    });
-    addNew.addBlurHandler(new BlurHandler() {
-      @Override
-      public void onBlur(BlurEvent event) {
-        // re-register the keys for the 'groups' table when the 'addNew' button
-        // gets blurred
-        groups.setRegisterKeys(true);
-      }
-    });
-    addPanel.add(addNew);
-    add(addPanel);
-
-    new OnEditEnabler(addNew, addTxt);
   }
 
   @Override
@@ -121,24 +49,4 @@
     super.registerKeys();
     groups.setRegisterKeys(true);
   }
-
-  private void doCreateGroup() {
-    final String newName = addTxt.getText();
-    if (newName == null || newName.length() == 0) {
-      return;
-    }
-
-    addNew.setEnabled(false);
-    Util.GROUP_SVC.createGroup(newName, new GerritCallback<AccountGroup.Id>() {
-      public void onSuccess(final AccountGroup.Id result) {
-        History.newItem(Dispatcher.toGroup(result));
-      }
-
-      @Override
-      public void onFailure(Throwable caught) {
-        super.onFailure(caught);
-        addNew.setEnabled(true);
-      }
-    });
-  }
 }
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/GroupTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupTable.java
index 7f73226..6818841 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupTable.java
@@ -18,7 +18,6 @@
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.ui.Hyperlink;
 import com.google.gerrit.client.ui.NavigationTable;
-import com.google.gerrit.common.data.GroupDetail;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
@@ -32,7 +31,7 @@
 
 
 public class GroupTable extends NavigationTable<AccountGroup> {
-  private static final int NUM_COLS = 5;
+  private static final int NUM_COLS = 3;
 
   private final boolean enableLink;
 
@@ -52,9 +51,7 @@
 
     table.setText(0, 1, Util.C.columnGroupName());
     table.setText(0, 2, Util.C.columnGroupDescription());
-    table.setText(0, 3, Util.C.headingOwner());
-    table.setText(0, 4, Util.C.columnGroupType());
-    table.setText(0, 5, Util.C.columnGroupVisibleToAll());
+    table.setText(0, 3, Util.C.columnGroupVisibleToAll());
     table.addClickHandler(new ClickHandler() {
       @Override
       public void onClick(ClickEvent event) {
@@ -82,20 +79,19 @@
     History.newItem(Dispatcher.toGroup(getRowItem(row).getId()));
   }
 
-  public void display(final List<GroupDetail> result) {
+  public void display(final List<AccountGroup> result) {
     while (1 < table.getRowCount())
       table.removeRow(table.getRowCount() - 1);
 
-    for(GroupDetail detail : result) {
+    for(AccountGroup group : result) {
       final int row = table.getRowCount();
       table.insertRow(row);
       applyDataRowStyle(row);
-      populate(row, detail);
+      populate(row, group);
     }
   }
 
-  void populate(final int row, final GroupDetail detail) {
-    AccountGroup k = detail.group;
+  void populate(final int row, final AccountGroup k) {
     if (enableLink) {
       table.setWidget(row, 1, new Hyperlink(k.getName(),
           Dispatcher.toGroup(k.getId())));
@@ -103,10 +99,8 @@
       table.setText(row, 1, k.getName());
     }
     table.setText(row, 2, k.getDescription());
-    table.setText(row, 3, detail.ownerGroup.getName());
-    table.setText(row, 4, k.getType().toString());
     if (k.isVisibleToAll()) {
-      table.setWidget(row, 5, new Image(Gerrit.RESOURCES.greenCheck()));
+      table.setWidget(row, 3, new Image(Gerrit.RESOURCES.greenCheck()));
     }
 
     final FlexCellFormatter fmt = table.getFlexCellFormatter();
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..3948b35
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PluginListScreen.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.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.Anchor;
+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());
+      table.setText(0, 3, Util.C.columnPluginStatus());
+
+      final FlexCellFormatter fmt = table.getFlexCellFormatter();
+      fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().dataHeader());
+      fmt.addStyleName(0, 2, Gerrit.RESOURCES.css().dataHeader());
+      fmt.addStyleName(0, 3, 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) {
+      if (plugin.isDisabled()) {
+        table.setText(row, 1, plugin.name());
+      } else {
+        table.setWidget(
+            row,
+            1,
+            new Anchor(plugin.name(), Gerrit.selfRedirect("/plugins/"
+                + plugin.name() + "/")));
+      }
+      table.setText(row, 2, plugin.version());
+      if (plugin.isDisabled()) {
+        table.setText(row, 3, Util.C.pluginDisabled());
+      }
+
+      final FlexCellFormatter fmt = table.getFlexCellFormatter();
+      fmt.addStyleName(row, 1, Gerrit.RESOURCES.css().dataCell());
+      fmt.addStyleName(row, 2, Gerrit.RESOURCES.css().dataCell());
+      fmt.addStyleName(row, 3, Gerrit.RESOURCES.css().dataCell());
+
+      setRowItem(row, plugin);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/UnnamedCacheBinding.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PluginScreen.java
similarity index 60%
copy from gerrit-server/src/main/java/com/google/gerrit/server/cache/UnnamedCacheBinding.java
copy to gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PluginScreen.java
index 43039e1..dabcb45 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/UnnamedCacheBinding.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PluginScreen.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.
@@ -12,11 +12,20 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.cache;
+package com.google.gerrit.client.admin;
 
+import com.google.gerrit.client.ui.Screen;
 
-/** 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);
+public abstract class PluginScreen extends Screen {
+
+  public PluginScreen() {
+    setRequiresSignIn(true);
+  }
+
+  @Override
+  protected void onLoad() {
+    super.onLoad();
+    setPageTitle(Util.C.plugins());
+    display();
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.java
index e3bf555..32bc469 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.java
@@ -120,7 +120,7 @@
       history.getStyle().setDisplay(Display.NONE);
     }
 
-    addSection.setVisible(value != null && editing && !value.getOwnerOf().isEmpty());
+    addSection.setVisible(value != null && editing && (!value.getOwnerOf().isEmpty() || value.canUpload()));
   }
 
   @Override
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..4403ce6 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,7 +18,9 @@
 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.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.dom.client.DivElement;
@@ -30,9 +32,15 @@
 import com.google.gwt.user.client.Window;
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwt.user.client.ui.Label;
 import com.google.gwt.user.client.ui.UIObject;
+import com.google.gwt.user.client.ui.VerticalPanel;
 import com.google.gwtexpui.globalkey.client.NpTextArea;
 
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
 public class ProjectAccessScreen extends ProjectScreen {
   interface Binder extends UiBinder<HTMLPanel, ProjectAccessScreen> {
   }
@@ -57,6 +65,9 @@
   Button cancel2;
 
   @UiField
+  VerticalPanel error;
+
+  @UiField
   ProjectAccessEditor accessEditor;
 
   @UiField
@@ -68,6 +79,9 @@
   @UiField
   Button commit;
 
+  @UiField
+  Button review;
+
   private Driver driver;
 
   private ProjectAccess access;
@@ -101,8 +115,8 @@
   private void displayReadOnly(ProjectAccess access) {
     this.access = access;
     accessEditor.setEditing(false);
-    UIObject.setVisible(editTools, !access.getOwnerOf().isEmpty());
-    edit.setEnabled(!access.getOwnerOf().isEmpty());
+    UIObject.setVisible(editTools, !access.getOwnerOf().isEmpty() || access.canUpload());
+    edit.setEnabled(!access.getOwnerOf().isEmpty() || access.canUpload());
     cancel1.setVisible(false);
     UIObject.setVisible(commitTools, false);
     driver.edit(access);
@@ -110,13 +124,29 @@
 
   @UiHandler("edit")
   void onEdit(ClickEvent event) {
+    resetEditors();
+
     edit.setEnabled(false);
     cancel1.setVisible(true);
     UIObject.setVisible(commitTools, true);
+    commit.setVisible(!access.getOwnerOf().isEmpty());
+    review.setVisible(access.canUpload());
     accessEditor.setEditing(true);
     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()));
@@ -124,7 +154,7 @@
 
   @UiHandler("commit")
   void onCommit(ClickEvent event) {
-    ProjectAccess access = driver.flush();
+    final ProjectAccess access = driver.flush();
 
     if (driver.hasErrors()) {
       Window.alert(Util.C.errorsMustBeFixed());
@@ -144,14 +174,88 @@
         access.getLocal(), //
         new GerritCallback<ProjectAccess>() {
           @Override
-          public void onSuccess(ProjectAccess access) {
+          public void onSuccess(ProjectAccess newAccess) {
             enable(true);
             commitMessage.setText("");
-            displayReadOnly(access);
+            error.clear();
+            final Set<String> diffs = getDiffs(access, newAccess);
+            if (diffs.isEmpty()) {
+              displayReadOnly(newAccess);
+            } else {
+              error.add(new Label(Gerrit.C.projectAccessError()));
+              for (final String diff : diffs) {
+                error.add(new Label(diff));
+              }
+              if (access.canUpload()) {
+                error.add(new Label(Gerrit.C.projectAccessProposeForReviewHint()));
+              }
+            }
+          }
+
+          private Set<String> getDiffs(ProjectAccess wantedAccess,
+              ProjectAccess newAccess) {
+            final HashSet<AccessSection> same =
+                new HashSet<AccessSection>(wantedAccess.getLocal());
+            final HashSet<AccessSection> different =
+                new HashSet<AccessSection>(wantedAccess.getLocal().size()
+                    + newAccess.getLocal().size());
+            different.addAll(wantedAccess.getLocal());
+            different.addAll(newAccess.getLocal());
+            same.retainAll(newAccess.getLocal());
+            different.removeAll(same);
+
+            final Set<String> differentNames = new HashSet<String>();
+            for (final AccessSection s : different) {
+              differentNames.add(s.getName());
+            }
+            return differentNames;
           }
 
           @Override
           public void onFailure(Throwable caught) {
+            error.clear();
+            enable(true);
+            super.onFailure(caught);
+          }
+        });
+  }
+
+  @UiHandler("review")
+  void onReview(ClickEvent event) {
+    final ProjectAccess access = driver.flush();
+
+    if (driver.hasErrors()) {
+      Window.alert(Util.C.errorsMustBeFixed());
+      return;
+    }
+
+    String message = commitMessage.getText().trim();
+    if ("".equals(message)) {
+      message = null;
+    }
+
+    enable(false);
+    Util.PROJECT_SVC.reviewProjectAccess( //
+        getProjectKey(), //
+        access.getRevision(), //
+        message, //
+        access.getLocal(), //
+        new GerritCallback<Change.Id>() {
+          @Override
+          public void onSuccess(Change.Id changeId) {
+            enable(true);
+            commitMessage.setText("");
+            error.clear();
+            if (changeId != null) {
+              Gerrit.display(PageLinks.toChange(changeId));
+            } else {
+              displayReadOnly(access);
+            }
+          }
+
+          @Override
+          public void onFailure(Throwable caught) {
+            error.clear();
             enable(true);
             super.onFailure(caught);
           }
@@ -160,7 +264,8 @@
 
   private void enable(boolean enabled) {
     commitMessage.setEnabled(enabled);
-    commit.setEnabled(enabled);
+    commit.setEnabled(enabled ? !access.getOwnerOf().isEmpty() : false);
+    review.setEnabled(enabled ? access.canUpload() : false);
     cancel1.setEnabled(enabled);
     cancel2.setEnabled(enabled);
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.ui.xml
index 2536159..a664191 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.ui.xml
@@ -32,6 +32,11 @@
   .commitMessage .gwt-TextArea {
     margin: 5px 5px 5px 5px;
   }
+  .errorMessage {
+    margin-top: 5px;
+    margin-bottom: 5px;
+    color: red;
+  }
 </ui:style>
 
 <g:HTMLPanel>
@@ -58,12 +63,21 @@
           spellCheck='true'
           />
     </div>
+    <g:VerticalPanel
+      styleName='{style.errorMessage}'
+      ui:field='error'>
+    </g:VerticalPanel>
     <g:Button
         ui:field='commit'
         text='Save Changes'>
       <ui:attribute name='text'/>
     </g:Button>
     <g:Button
+        ui:field='review'
+        text='Save for Review'>
+      <ui:attribute name='text'/>
+    </g:Button>
+    <g:Button
         ui:field='cancel2'
         text='Cancel'>
       <ui:attribute name='text'/>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java
index f3ecbb3..56d9417 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectBranchesScreen.java
@@ -16,16 +16,19 @@
 
 import com.google.gerrit.client.ConfirmationCallback;
 import com.google.gerrit.client.ConfirmationDialog;
+import com.google.gerrit.client.ErrorDialog;
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.GitwebLink;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
+import com.google.gerrit.client.ui.BranchLink;
 import com.google.gerrit.client.ui.FancyFlexTable;
 import com.google.gerrit.client.ui.HintTextBox;
 import com.google.gerrit.common.data.ListBranchesResult;
 import com.google.gerrit.common.errors.InvalidNameException;
 import com.google.gerrit.common.errors.InvalidRevisionException;
 import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gwt.core.client.Scheduler;
 import com.google.gwt.core.client.Scheduler.ScheduledCommand;
@@ -41,6 +44,7 @@
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.Grid;
 import com.google.gwt.user.client.ui.Label;
+import com.google.gwt.user.client.ui.VerticalPanel;
 import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
 import com.google.gwtjsonrpc.client.RemoteJsonException;
 
@@ -260,24 +264,52 @@
               b.toSafeHtml(), new ConfirmationCallback() {
         @Override
         public void onOk() {
-          Util.PROJECT_SVC.deleteBranch(getProjectKey(), ids,
-              new GerritCallback<Set<Branch.NameKey>>() {
-                public void onSuccess(final Set<Branch.NameKey> deleted) {
-                  for (int row = 1; row < table.getRowCount();) {
-                    final Branch k = getRowItem(row);
-                    if (k != null && deleted.contains(k.getNameKey())) {
-                      table.removeRow(row);
-                    } else {
-                      row++;
-                    }
-                  }
-                }
-              });
+          deleteBranches(ids);
         }
       });
       confirmationDialog.center();
     }
 
+    private void deleteBranches(final Set<Branch.NameKey> branchIds) {
+      Util.PROJECT_SVC.deleteBranch(getProjectKey(), branchIds,
+          new GerritCallback<Set<Branch.NameKey>>() {
+            public void onSuccess(final Set<Branch.NameKey> deleted) {
+              if (!deleted.isEmpty()) {
+                for (int row = 1; row < table.getRowCount();) {
+                  final Branch k = getRowItem(row);
+                  if (k != null && deleted.contains(k.getNameKey())) {
+                    table.removeRow(row);
+                  } else {
+                    row++;
+                  }
+                }
+              }
+
+              branchIds.removeAll(deleted);
+              if (!branchIds.isEmpty()) {
+                final VerticalPanel p = new VerticalPanel();
+                final ErrorDialog errorDialog = new ErrorDialog(p);
+                final Label l = new Label(Util.C.branchDeletionOpenChanges());
+                l.setStyleName(Gerrit.RESOURCES.css().errorDialogText());
+                p.add(l);
+                for (final Branch.NameKey branch : branchIds) {
+                  final BranchLink link =
+                      new BranchLink(branch.getParentKey(), Change.Status.NEW,
+                          branch.get(), null) {
+                    @Override
+                    public void go() {
+                      errorDialog.hide();
+                      super.go();
+                    };
+                  };
+                  p.add(link);
+                }
+                errorDialog.center();
+              }
+            }
+          });
+    }
+
     void display(final List<Branch> result) {
       canDelete = false;
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java
index 3599c3c..9125622 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectListScreen.java
@@ -16,28 +16,27 @@
 
 import com.google.gerrit.client.Dispatcher;
 import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.GitwebLink;
+import com.google.gerrit.client.projects.ProjectInfo;
+import com.google.gerrit.client.projects.ProjectMap;
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
 import com.google.gerrit.client.ui.Hyperlink;
 import com.google.gerrit.client.ui.ProjectsTable;
 import com.google.gerrit.client.ui.Screen;
 import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.common.data.ProjectList;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gwt.user.client.History;
-import com.google.gwt.user.client.ui.VerticalPanel;
+import com.google.gwt.user.client.ui.Anchor;
 
 public class ProjectListScreen extends Screen {
-  private VerticalPanel createProjectLinkPanel;
   private ProjectsTable projects;
 
   @Override
   protected void onLoad() {
     super.onLoad();
-    Util.PROJECT_SVC.visibleProjects(new ScreenLoadCallback<ProjectList>(this) {
+    ProjectMap.all(new ScreenLoadCallback<ProjectMap>(this) {
       @Override
-      protected void preDisplay(final ProjectList result) {
-        createProjectLinkPanel.setVisible(result.canCreateProject());
-        projects.display(result.getProjects());
+      protected void preDisplay(final ProjectMap result) {
+        projects.display(result);
         projects.finishDisplay();
       }
     });
@@ -48,27 +47,44 @@
     super.onInitUI();
     setPageTitle(Util.C.projectListTitle());
 
-    createProjectLinkPanel = new VerticalPanel();
-    createProjectLinkPanel.setStyleName(Gerrit.RESOURCES.css()
-        .createProjectLink());
-    createProjectLinkPanel.add(new Hyperlink(Util.C.headingCreateProject(),
-        PageLinks.ADMIN_CREATE_PROJECT));
-    add(createProjectLinkPanel);
-
     projects = new ProjectsTable() {
       @Override
+      protected void initColumnHeaders() {
+        super.initColumnHeaders();
+        if (Gerrit.getGitwebLink() != null) {
+          table.setText(0, 3, Util.C.projectRepoBrowser());
+          table.getFlexCellFormatter().
+            addStyleName(0, 3, Gerrit.RESOURCES.css().dataHeader());
+        }
+      }
+
+      @Override
       protected void onOpenRow(final int row) {
         History.newItem(link(getRowItem(row)));
       }
 
-      private String link(final Project item) {
-        return Dispatcher.toProjectAdmin(item.getNameKey(), ProjectScreen.INFO);
+      private String link(final ProjectInfo item) {
+        return Dispatcher.toProjectAdmin(item.name_key(), ProjectScreen.INFO);
       }
 
       @Override
-      protected void populate(final int row, final Project k) {
-        table.setWidget(row, 1, new Hyperlink(k.getName(), link(k)));
-        table.setText(row, 2, k.getDescription());
+      protected void insert(int row, ProjectInfo k) {
+        super.insert(row, k);
+        if (Gerrit.getGitwebLink() != null) {
+          table.getFlexCellFormatter().
+            addStyleName(row, 3, Gerrit.RESOURCES.css().dataCell());
+        }
+      }
+
+      @Override
+      protected void populate(final int row, final ProjectInfo k) {
+        table.setWidget(row, 1, new Hyperlink(k.name(), link(k)));
+        table.setText(row, 2, k.description());
+        GitwebLink l = Gerrit.getGitwebLink();
+        if (l != null) {
+          table.setWidget(row, 3, new Anchor(l.getLinkName(), false, l.toProject(k
+              .name_key())));
+        }
 
         setRowItem(row, k);
       }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectScreen.java
index ccfe2e6..dd5b070 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectScreen.java
@@ -16,7 +16,6 @@
 
 import static com.google.gerrit.client.Dispatcher.toProjectAdmin;
 
-import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.ui.MenuScreen;
 import com.google.gerrit.reviewdb.client.Project;
 
@@ -30,12 +29,8 @@
   public ProjectScreen(final Project.NameKey toShow) {
     name = toShow;
 
-    final boolean isWild = toShow.equals(Gerrit.getConfig().getWildProject());
-
     link(Util.C.projectAdminTabGeneral(), toProjectAdmin(name, INFO));
-    if (!isWild) {
-      link(Util.C.projectAdminTabBranches(), toProjectAdmin(name, BRANCH));
-    }
+    link(Util.C.projectAdminTabBranches(), toProjectAdmin(name, BRANCH));
     link(Util.C.projectAdminTabAccess(), toProjectAdmin(name, ACCESS));
   }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdSsoPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdSsoPanel.java
new file mode 100644
index 0000000..85dd794
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdSsoPanel.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.client.auth.openid;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.common.auth.SignInMode;
+import com.google.gerrit.common.auth.openid.DiscoveryResult;
+import com.google.gwt.dom.client.FormElement;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.FormPanel;
+import com.google.gwt.user.client.ui.Hidden;
+
+import java.util.Map;
+
+public class OpenIdSsoPanel extends FlowPanel {
+  private final FormPanel redirectForm;
+  private final FlowPanel redirectBody;
+  private final String ssoUrl;
+
+  public OpenIdSsoPanel() {
+    super();
+    redirectBody = new FlowPanel();
+    redirectBody.setVisible(false);
+    redirectForm = new FormPanel();
+    redirectForm.add(redirectBody);
+
+    add(redirectForm);
+
+    ssoUrl = Gerrit.getConfig().getOpenIdSsoUrl();
+  }
+
+  public void authenticate(SignInMode requestedMode, final String token) {
+    OpenIdUtil.SVC.discover(ssoUrl, requestedMode, /* remember */ false, token,
+        new GerritCallback<DiscoveryResult>() {
+          public void onSuccess(final DiscoveryResult result) {
+            onDiscovery(result);
+          }
+        });
+  }
+
+  private void onDiscovery(final DiscoveryResult result) {
+    switch (result.status) {
+      case VALID:
+        redirectForm.setMethod(FormPanel.METHOD_POST);
+        redirectForm.setAction(result.providerUrl);
+        redirectBody.clear();
+        for (final Map.Entry<String, String> e : result.providerArgs.entrySet()) {
+          redirectBody.add(new Hidden(e.getKey(), e.getValue()));
+        }
+        FormElement.as(redirectForm.getElement()).setTarget("_top");
+        redirectForm.submit();
+        break;
+    }
+  }
+}
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 1d881b6..05dd5d9 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java
@@ -14,54 +14,65 @@
 
 package com.google.gerrit.client.changes;
 
-import com.google.gerrit.client.FormatUtil;
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.changes.ChangeTable.ApprovalViewType;
+import com.google.gerrit.client.NotFoundScreen;
+import com.google.gerrit.client.rpc.NativeList;
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
 import com.google.gerrit.client.ui.Screen;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.common.data.AccountDashboardInfo;
-import com.google.gerrit.common.data.AccountInfo;
 import com.google.gerrit.reviewdb.client.Account;
 
+import java.util.Collections;
+import java.util.Comparator;
 
 public class AccountDashboardScreen extends Screen implements ChangeListScreen {
   private final Account.Id ownerId;
-  private ChangeTable table;
-  private ChangeTable.Section byOwner;
-  private ChangeTable.Section forReview;
-  private ChangeTable.Section closed;
+  private final boolean mine;
+  private ChangeTable2 table;
+  private ChangeTable2.Section outgoing;
+  private ChangeTable2.Section incoming;
+  private ChangeTable2.Section closed;
 
   public AccountDashboardScreen(final Account.Id id) {
     ownerId = id;
+    mine = Gerrit.isSignedIn() && ownerId.equals(Gerrit.getUserAccount().getId());
   }
 
   @Override
   protected void onInitUI() {
     super.onInitUI();
-    table = new ChangeTable(true);
+    table = new ChangeTable2();
     table.addStyleName(Gerrit.RESOURCES.css().accountDashboard());
-    byOwner = new ChangeTable.Section("", ApprovalViewType.STRONGEST, null);
-    forReview = new ChangeTable.Section("", ApprovalViewType.USER, ownerId);
-    closed = new ChangeTable.Section("", ApprovalViewType.STRONGEST, null);
 
-    table.addSection(byOwner);
-    table.addSection(forReview);
+    outgoing = new ChangeTable2.Section();
+    incoming = new ChangeTable2.Section();
+    closed = new ChangeTable2.Section();
+
+    outgoing.setTitleText(Util.C.outgoingReviews());
+    incoming.setTitleText(Util.C.incomingReviews());
+    incoming.setHighlightUnreviewed(true);
+    closed.setTitleText(Util.C.recentlyClosed());
+
+    table.addSection(outgoing);
+    table.addSection(incoming);
     table.addSection(closed);
     add(table);
-    table.setSavePointerId(PageLinks.toAccountDashboard(ownerId));
+    table.setSavePointerId("owner:" + ownerId);
   }
 
   @Override
   protected void onLoad() {
     super.onLoad();
-    Util.LIST_SVC.forAccount(ownerId,
-        new ScreenLoadCallback<AccountDashboardInfo>(this) {
+    String who = mine ? "self" : ownerId.toString();
+    ChangeList.query(
+        new ScreenLoadCallback<NativeList<ChangeList>>(this) {
           @Override
-          protected void preDisplay(final AccountDashboardInfo r) {
-            display(r);
+          protected void preDisplay(NativeList<ChangeList> result) {
+            display(result);
           }
-        });
+        },
+        "is:open owner:" + who,
+        "is:open reviewer:" + who + " -owner:" + who,
+        "is:closed owner:" + who + " -age:1w limit:10");
   }
 
   @Override
@@ -70,20 +81,72 @@
     table.setRegisterKeys(true);
   }
 
-  private void display(final AccountDashboardInfo r) {
-    table.setAccountInfoCache(r.getAccounts());
+  private void display(NativeList<ChangeList> result) {
+    if (!mine && !hasChanges(result)) {
+      // When no results are returned and the data is not for the
+      // current user, the target user is presumed to not exist.
+      Gerrit.display(getToken(), new NotFoundScreen());
+      return;
+    }
 
-    final AccountInfo o = r.getAccounts().get(r.getOwner());
-    final String name = FormatUtil.name(o);
-    setWindowTitle(name);
-    setPageTitle(Util.M.accountDashboardTitle(name));
-    byOwner.setTitleText(Util.M.changesStartedBy(name));
-    forReview.setTitleText(Util.M.changesReviewableBy(name));
-    closed.setTitleText(Util.C.changesRecentlyClosed());
+    ChangeList out = result.get(0);
+    ChangeList in = result.get(1);
+    ChangeList done = result.get(2);
 
-    byOwner.display(r.getByOwner());
-    forReview.display(r.getForReview());
-    closed.display(r.getClosed());
+    if (mine) {
+      setWindowTitle(Util.C.myDashboardTitle());
+      setPageTitle(Util.C.myDashboardTitle());
+    } else {
+      // The server doesn't tell us who the dashboard is for. Try to guess
+      // by looking at a change started by the owner and extract the name.
+      String name = guessName(out);
+      if (name == null) {
+        name = guessName(done);
+      }
+      if (name != null) {
+        setWindowTitle(name);
+        setPageTitle(Util.M.accountDashboardTitle(name));
+      } else {
+        setWindowTitle(Util.C.unknownDashboardTitle());
+        setWindowTitle(Util.C.unknownDashboardTitle());
+      }
+    }
+
+    Collections.sort(out.asList(), outComparator());
+
+    table.updateColumnsForLabels(out, in, done);
+    outgoing.display(out);
+    incoming.display(in);
+    closed.display(done);
     table.finishDisplay();
   }
+
+  private Comparator<ChangeInfo> outComparator() {
+    return new Comparator<ChangeInfo>() {
+      @Override
+      public int compare(ChangeInfo a, ChangeInfo b) {
+        int cmp = a.created().compareTo(b.created());
+        if (cmp != 0) return cmp;
+        return a._number() - b._number();
+      }
+    };
+  }
+
+  private boolean hasChanges(NativeList<ChangeList> result) {
+    for (ChangeList list : result.asList()) {
+      if (!list.isEmpty()) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  private static String guessName(ChangeList list) {
+    for (ChangeInfo change : list.asList()) {
+      if (change.owner() != null && change.owner().name() != null) {
+        return change.owner().name();
+      }
+    }
+    return null;
+  }
 }
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 09716cc..73036ff 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ApprovalTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ApprovalTable.java
@@ -21,7 +21,7 @@
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.patches.PatchUtil;
 import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.ui.AccountDashboardLink;
+import com.google.gerrit.client.ui.AccountLink;
 import com.google.gerrit.client.ui.AddMemberBox;
 import com.google.gerrit.client.ui.ReviewerSuggestOracle;
 import com.google.gerrit.common.data.AccountInfoCache;
@@ -29,6 +29,7 @@
 import com.google.gerrit.common.data.ApprovalType;
 import com.google.gerrit.common.data.ApprovalTypes;
 import com.google.gerrit.common.data.ChangeDetail;
+import com.google.gerrit.common.data.PatchSetPublishDetail;
 import com.google.gerrit.common.data.ReviewerResult;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.reviewdb.client.Account;
@@ -129,16 +130,26 @@
     accountCache = aic;
   }
 
-  private AccountDashboardLink link(final Account.Id id) {
-    return AccountDashboardLink.link(accountCache, id);
+  private AccountLink link(final Account.Id id) {
+    return AccountLink.link(accountCache, id);
+  }
+
+  void display(PatchSetPublishDetail detail) {
+    doDisplay(detail.getChange(), detail.getApprovals(),
+        detail.getSubmitRecords());
   }
 
   void display(ChangeDetail detail) {
-    changeId = detail.getChange().getId();
+    doDisplay(detail.getChange(), detail.getApprovals(),
+        detail.getSubmitRecords());
+  }
+
+  private void doDisplay(Change change, List<ApprovalDetail> approvals,
+      List<SubmitRecord> submitRecords) {
+    changeId = change.getId();
     reviewerSuggestOracle.setChange(changeId);
 
     List<String> columns = new ArrayList<String>();
-    List<ApprovalDetail> rows = detail.getApprovals();
 
     final Element missingList = missing.getElement();
     while (DOM.getChildCount(missingList) > 0) {
@@ -146,16 +157,16 @@
     }
     missing.setVisible(false);
 
-    if (detail.getSubmitRecords() != null) {
+    if (submitRecords != null) {
       HashSet<String> reportedMissing = new HashSet<String>();
 
       HashMap<Account.Id, ApprovalDetail> byUser =
           new HashMap<Account.Id, ApprovalDetail>();
-      for (ApprovalDetail ad : detail.getApprovals()) {
+      for (ApprovalDetail ad : approvals) {
         byUser.put(ad.getAccount(), ad);
       }
 
-      for (SubmitRecord rec : detail.getSubmitRecords()) {
+      for (SubmitRecord rec : submitRecords) {
         if (rec.labels == null) {
           continue;
         }
@@ -182,6 +193,9 @@
               break;
             }
 
+            case MAY:
+              break;
+
             case NEED:
             case IMPOSSIBLE:
               if (reportedMissing.add(lbl.label)) {
@@ -197,17 +211,19 @@
       missing.setVisible(!reportedMissing.isEmpty());
 
     } else {
-      for (ApprovalDetail ad : rows) {
+      for (ApprovalDetail ad : approvals) {
         for (PatchSetApproval psa : ad.getPatchSetApprovals()) {
           ApprovalType legacyType = types.byId(psa.getCategoryId());
           if (legacyType == null) {
             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);
@@ -231,13 +247,13 @@
       }
     }
 
-    if (rows.isEmpty()) {
+    if (approvals.isEmpty()) {
       table.setVisible(false);
     } else {
       displayHeader(columns);
-      table.resizeRows(1 + rows.size());
-      for (int i = 0; i < rows.size(); i++) {
-        displayRow(i + 1, rows.get(i), detail.getChange(), columns);
+      table.resizeRows(1 + approvals.size());
+      for (int i = 0; i < approvals.size(); i++) {
+        displayRow(i + 1, approvals.get(i), change, columns);
       }
       table.setVisible(true);
     }
@@ -245,7 +261,7 @@
     addReviewer.setVisible(Gerrit.isSignedIn());
 
     if (Gerrit.getConfig().testChangeMerge()
-        && !detail.getChange().isMergeable()) {
+        && !change.isMergeable()) {
       Element li = DOM.createElement("li");
       li.setClassName(Gerrit.RESOURCES.css().missingApproval());
       DOM.setInnerText(li, Util.C.messageNeedsRebaseOrHasDependency());
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeCache.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeCache.java
index 27f76f6..5a9da1a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeCache.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeCache.java
@@ -38,7 +38,6 @@
   private Change.Id changeId;
   private ChangeDetailCache detail;
   private ListenableValue<ChangeInfo> info;
-  private StarCache starred;
 
   protected ChangeCache(Change.Id chg) {
     changeId = chg;
@@ -61,11 +60,4 @@
     }
     return info;
   }
-
-  public StarCache getStarCache() {
-    if (starred == null) {
-      starred = new StarCache(changeId);
-    }
-    return starred;
-  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
index 3372096..d42992f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
@@ -23,7 +23,11 @@
   String statusLongAbandoned();
   String statusLongDraft();
 
-  String changesRecentlyClosed();
+  String myDashboardTitle();
+  String unknownDashboardTitle();
+  String incomingReviews();
+  String outgoingReviews();
+  String recentlyClosed();
 
   String starredHeading();
   String watchedHeading();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
index ad70674..8ceb74c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.properties
@@ -7,7 +7,11 @@
 starredHeading = Starred Changes
 watchedHeading = Open Changes of Watched Projects
 draftsHeading = Changes with unpublished drafts
-changesRecentlyClosed = Recently closed
+myDashboardTitle = My Reviews
+unknownDashboardTitle = Code Review Dashboard
+incomingReviews = Incoming reviews
+outgoingReviews = Outgoing reviews
+recentlyClosed = Recently closed
 allOpenChanges = All open changes
 allAbandonedChanges = All abandoned changes
 allMergedChanges = All merged changes
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDescriptionBlock.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDescriptionBlock.java
index 9282709..c8b2a66 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDescriptionBlock.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDescriptionBlock.java
@@ -19,14 +19,15 @@
 import com.google.gerrit.reviewdb.client.PatchSetInfo;
 import com.google.gwt.user.client.ui.Composite;
 import com.google.gwt.user.client.ui.HorizontalPanel;
+import com.google.gwtexpui.globalkey.client.KeyCommandSet;
 
 public class ChangeDescriptionBlock extends Composite {
   private final ChangeInfoBlock infoBlock;
   private final CommitMessageBlock messageBlock;
 
-  public ChangeDescriptionBlock() {
+  public ChangeDescriptionBlock(KeyCommandSet keysAction) {
     infoBlock = new ChangeInfoBlock();
-    messageBlock = new CommitMessageBlock();
+    messageBlock = new CommitMessageBlock(keysAction);
 
     final HorizontalPanel hp = new HorizontalPanel();
     hp.add(infoBlock);
@@ -34,9 +35,9 @@
     initWidget(hp);
   }
 
-  public void display(final Change chg, final PatchSetInfo info,
+  public void display(Change chg, Boolean starred, PatchSetInfo info,
       final AccountInfoCache acc) {
     infoBlock.display(chg, acc);
-    messageBlock.display(info.getMessage());
+    messageBlock.display(chg.getId(), starred, info.getMessage());
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDetailCache.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDetailCache.java
index bb28e11..9c19d50 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDetailCache.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDetailCache.java
@@ -65,6 +65,7 @@
   public static void setChangeDetail(ChangeDetail detail) {
     Change.Id chgId = detail.getChange().getId();
     ChangeCache.get(chgId).getChangeDetailCache().set(detail);
+    StarredChanges.fireChangeStarEvent(chgId, detail.isStarred());
   }
 
   private final Change.Id changeId;
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
new file mode 100644
index 0000000..0c8e03e
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfo.java
@@ -0,0 +1,123 @@
+// 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.changes;
+
+import com.google.gerrit.client.rpc.Natives;
+import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwtjsonrpc.client.impl.ser.JavaSqlTimestamp_JsonSerializer;
+
+import java.sql.Timestamp;
+import java.util.Set;
+
+public class ChangeInfo extends JavaScriptObject {
+  public final Project.NameKey project_name_key() {
+    return new Project.NameKey(project());
+  }
+
+  public final Change.Id legacy_id() {
+    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());
+  }
+
+  public final String id_abbreviated() {
+    return new Change.Key(id()).abbreviate();
+  }
+
+  public final Change.Status status() {
+    return Change.Status.valueOf(statusRaw());
+  }
+
+  public final Set<String> labels() {
+    return Natives.keys(labels0());
+  }
+
+  public final native String project() /*-{ return this.project; }-*/;
+  public final native String branch() /*-{ return this.branch; }-*/;
+  public final native String topic() /*-{ return this.topic; }-*/;
+  public final native String id() /*-{ return this.id; }-*/;
+  private final native String statusRaw() /*-{ return this.status; }-*/;
+  public final native String subject() /*-{ return this.subject; }-*/;
+  public final native AccountInfo owner() /*-{ return this.owner; }-*/;
+  private final native String createdRaw() /*-{ return this.created; }-*/;
+  private final native String updatedRaw() /*-{ return this.updated; }-*/;
+  public final native boolean starred() /*-{ return this.starred ? true : false; }-*/;
+  public final native boolean reviewed() /*-{ return this.reviewed ? true : false; }-*/;
+  public final native String _sortkey() /*-{ return this._sortkey; }-*/;
+  private final native JavaScriptObject labels0() /*-{ return this.labels; }-*/;
+  public final native LabelInfo label(String n) /*-{ return this.labels[n]; }-*/;
+  final native int _number() /*-{ return this._number; }-*/;
+  final native boolean _more_changes()
+  /*-{ return this._more_changes ? true : false; }-*/;
+
+  protected ChangeInfo() {
+  }
+
+  public static class AccountInfo extends JavaScriptObject {
+    public final native String name() /*-{ return this.name; }-*/;
+
+    protected AccountInfo() {
+    }
+  }
+
+  public static class LabelInfo extends JavaScriptObject {
+    public final SubmitRecord.Label.Status status() {
+      if (approved() != null) {
+        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;
+      }
+    }
+
+    public final native String name() /*-{ return this._name; }-*/;
+    public final native AccountInfo approved() /*-{ return this.approved; }-*/;
+    public final native AccountInfo rejected() /*-{ return this.rejected; }-*/;
+
+    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;
+      if (this.disliked) return -1;
+      if (this.recommended) return 1;
+      return 0;
+    }-*/;
+
+    protected LabelInfo() {
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfoBlock.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfoBlock.java
index f8373cc..3ffacc3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfoBlock.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfoBlock.java
@@ -17,15 +17,13 @@
 import static com.google.gerrit.client.FormatUtil.mediumFormat;
 
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.ui.AccountDashboardLink;
+import com.google.gerrit.client.ui.AccountLink;
 import com.google.gerrit.client.ui.BranchLink;
-import com.google.gerrit.client.ui.ChangeLink;
 import com.google.gerrit.client.ui.ProjectLink;
 import com.google.gerrit.common.data.AccountInfoCache;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.Grid;
 import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
 import com.google.gwtexpui.clippy.client.CopyableLabel;
@@ -40,18 +38,15 @@
   private static final int R_UPDATED = 6;
   private static final int R_STATUS = 7;
   private static final int R_MERGE_TEST = 8;
-  private final int R_PERMALINK;
-  private static final int R_CNT = 10;
+  private static final int R_CNT = 9;
 
   private final Grid table;
 
   public ChangeInfoBlock() {
     if (Gerrit.getConfig().testChangeMerge()) {
       table = new Grid(R_CNT, 2);
-      R_PERMALINK = 9;
     } else {
       table = new Grid(R_CNT - 1, 2);
-      R_PERMALINK = 8;
     }
     table.setStyleName(Gerrit.RESOURCES.css().infoBlock());
     table.addStyleName(Gerrit.RESOURCES.css().changeInfoBlock());
@@ -73,8 +68,6 @@
     fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().topmost());
     fmt.addStyleName(R_CHANGE_ID, 1, Gerrit.RESOURCES.css().changeid());
     fmt.addStyleName(R_CNT - 2, 0, Gerrit.RESOURCES.css().bottomheader());
-    fmt.addStyleName(R_PERMALINK, 0, Gerrit.RESOURCES.css().permalink());
-    fmt.addStyleName(R_PERMALINK, 1, Gerrit.RESOURCES.css().permalink());
 
     initWidget(table);
   }
@@ -92,7 +85,7 @@
     changeIdLabel.setPreviewText(chg.getKey().get());
     table.setWidget(R_CHANGE_ID, 1, changeIdLabel);
 
-    table.setWidget(R_OWNER, 1, AccountDashboardLink.link(acc, chg.getOwner()));
+    table.setWidget(R_OWNER, 1, AccountLink.link(acc, chg.getOwner()));
     table.setWidget(R_PROJECT, 1, new ProjectLink(chg.getProject(), chg.getStatus()));
     table.setWidget(R_BRANCH, 1, new BranchLink(dst.getShortName(), chg
         .getProject(), chg.getStatus(), dst.get(), null));
@@ -101,20 +94,21 @@
     table.setText(R_UPLOADED, 1, mediumFormat(chg.getCreatedOn()));
     table.setText(R_UPDATED, 1, mediumFormat(chg.getLastUpdatedOn()));
     table.setText(R_STATUS, 1, Util.toLongString(chg.getStatus()));
+    final Change.Status status = chg.getStatus();
     if (Gerrit.getConfig().testChangeMerge()) {
-      table.setText(R_MERGE_TEST, 1, chg.isMergeable() ? Util.C
-          .changeInfoBlockCanMergeYes() : Util.C.changeInfoBlockCanMergeNo());
+      if (status.equals(Change.Status.NEW) || status.equals(Change.Status.DRAFT)) {
+        table.getRowFormatter().setVisible(R_MERGE_TEST, true);
+        table.setText(R_MERGE_TEST, 1, chg.isMergeable() ? Util.C
+            .changeInfoBlockCanMergeYes() : Util.C.changeInfoBlockCanMergeNo());
+      } else {
+        table.getRowFormatter().setVisible(R_MERGE_TEST, false);
+      }
     }
 
-    if (chg.getStatus().isClosed()) {
+    if (status.isClosed()) {
       table.getCellFormatter().addStyleName(R_STATUS, 1, Gerrit.RESOURCES.css().closedstate());
     } else {
       table.getCellFormatter().removeStyleName(R_STATUS, 1, Gerrit.RESOURCES.css().closedstate());
     }
-
-    final FlowPanel fp = new FlowPanel();
-    fp.add(new ChangeLink(Util.C.changePermalink(), chg.getId()));
-    fp.add(new CopyableLabel(ChangeLink.permalink(chg.getId()), false));
-    table.setWidget(R_PERMALINK, 1, fp);
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeList.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeList.java
new file mode 100644
index 0000000..560e6b3
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeList.java
@@ -0,0 +1,86 @@
+// 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.changes;
+
+import com.google.gerrit.client.rpc.NativeList;
+import com.google.gerrit.client.rpc.RestApi;
+import com.google.gerrit.common.changes.ListChangesOption;
+import com.google.gwtjsonrpc.common.AsyncCallback;
+import com.google.gwtorm.client.KeyUtil;
+
+import java.util.EnumSet;
+
+/** List of changes available from {@code /changes/}. */
+public class ChangeList extends NativeList<ChangeInfo> {
+  private static final String URI = "/changes/";
+
+  /** Run 2 or more queries in a single remote invocation. */
+  public static void query(
+      AsyncCallback<NativeList<ChangeList>> callback, String... queries) {
+    assert queries.length >= 2; // At least 2 is required for correct result.
+    RestApi call = new RestApi(URI);
+    for (String q : queries) {
+      call.addParameterRaw("q", KeyUtil.encode(q));
+    }
+    addOptions(call, ListChangesOption.LABELS);
+    call.send(callback);
+  }
+
+  public static void prev(String query,
+      int limit, String sortkey,
+      AsyncCallback<ChangeList> callback) {
+    RestApi call = newQuery(query);
+    if (limit > 0) {
+      call.addParameter("n", limit);
+    }
+    addOptions(call, ListChangesOption.LABELS);
+    if (!PagedSingleListScreen.MIN_SORTKEY.equals(sortkey)) {
+      call.addParameter("P", sortkey);
+    }
+    call.send(callback);
+  }
+
+  public static void next(String query,
+      int limit, String sortkey,
+      AsyncCallback<ChangeList> callback) {
+    RestApi call = newQuery(query);
+    if (limit > 0) {
+      call.addParameter("n", limit);
+    }
+    addOptions(call, ListChangesOption.LABELS);
+    if (!PagedSingleListScreen.MAX_SORTKEY.equals(sortkey)) {
+      call.addParameter("N", sortkey);
+    }
+    call.send(callback);
+  }
+
+  private static void addOptions(
+      RestApi call, ListChangesOption option1, ListChangesOption... options) {
+    EnumSet<ListChangesOption> s = EnumSet.of(option1, options);
+    call.addParameterRaw("O", Integer.toHexString(ListChangesOption.toBits(s)));
+  }
+
+  private static RestApi newQuery(String query) {
+    RestApi call = new RestApi(URI);
+    // The server default is ?q=status:open so don't repeat it.
+    if (!"status:open".equals(query) && !"is:open".equals(query)) {
+      call.addParameterRaw("q", KeyUtil.encode(query));
+    }
+    return call;
+  }
+
+  protected ChangeList() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java
index 41dc6c1..4bd0828 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java
@@ -18,8 +18,6 @@
 
 public interface ChangeMessages extends Messages {
   String accountDashboardTitle(String fullName);
-  String changesStartedBy(String fullName);
-  String changesReviewableBy(String fullName);
   String changesOpenInProject(String string);
   String changesMergedInProject(String string);
   String changesAbandonedInProject(String string);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
index 40088a1..2449613 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
@@ -1,6 +1,4 @@
 accountDashboardTitle = Code Review Dashboard for {0}
-changesStartedBy = Started by {0}
-changesReviewableBy = Review Requests for {0}
 changesOpenInProject = Open Changes In {0}
 changesMergedInProject = Merged Changes In {0}
 changesAbandonedInProject = Abandoned Changes In {0}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java
index 8d5e105..4dd6b03 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java
@@ -28,9 +28,9 @@
 import com.google.gerrit.common.data.ChangeInfo;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Change.Status;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Change.Status;
 import com.google.gwt.event.dom.client.ChangeEvent;
 import com.google.gwt.event.dom.client.ChangeHandler;
 import com.google.gwt.event.dom.client.KeyPressEvent;
@@ -42,7 +42,6 @@
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.Grid;
 import com.google.gwt.user.client.ui.HorizontalPanel;
-import com.google.gwt.user.client.ui.Image;
 import com.google.gwt.user.client.ui.InlineLabel;
 import com.google.gwt.user.client.ui.Label;
 import com.google.gwt.user.client.ui.ListBox;
@@ -60,9 +59,7 @@
   private final Change.Id changeId;
   private final PatchSet.Id openPatchSetId;
   private ChangeDetailCache detailCache;
-  private StarCache starred;
 
-  private Image starChange;
   private ChangeDescriptionBlock descriptionBlock;
   private ApprovalTable approvals;
 
@@ -118,14 +115,6 @@
   }
 
   @Override
-  public void onSignOut() {
-    super.onSignOut();
-    if (starChange != null) {
-      starChange.setVisible(false);
-    }
-  }
-
-  @Override
   protected void onLoad() {
     super.onLoad();
     detailCache.refresh();
@@ -163,9 +152,8 @@
     detailCache = cache.getChangeDetailCache();
     detailCache.addValueChangeHandler(this);
 
-    starred = cache.getStarCache();
-
     addStyleName(Gerrit.RESOURCES.css().changeScreen());
+    addStyleName(Gerrit.RESOURCES.css().screenNoHeader());
 
     keysNavigation = new KeyCommandSet(Gerrit.C.sectionNavigation());
     keysAction = new KeyCommandSet(Gerrit.C.sectionActions());
@@ -173,16 +161,11 @@
     keysNavigation.add(new ExpandCollapseDependencySectionKeyCommand(0, 'd', Util.C.expandCollapseDependencies()));
 
     if (Gerrit.isSignedIn()) {
-      keysAction.add(starred.new KeyCommand(0, 's', Util.C.changeTableStar()));
       keysAction.add(new PublishCommentsKeyCommand(0, 'r', Util.C
           .keyPublishComments()));
-
-      starChange = starred.createStar();
-      starChange.setStyleName(Gerrit.RESOURCES.css().changeScreenStarIcon());
-      setTitleWest(starChange);
     }
 
-    descriptionBlock = new ChangeDescriptionBlock();
+    descriptionBlock = new ChangeDescriptionBlock(keysAction);
     add(descriptionBlock);
 
     approvals = new ApprovalTable();
@@ -200,6 +183,23 @@
       }
     };
     dependsOn = new ChangeTable.Section(Util.C.changeScreenDependsOn());
+    dependsOn.setChangeRowFormatter(new ChangeTable.ChangeRowFormatter() {
+      @Override
+      public String getRowStyle(ChangeInfo c) {
+        if (! c.isLatest() || Change.Status.ABANDONED.equals(c.getStatus())) {
+          return Gerrit.RESOURCES.css().outdated();
+        }
+        return null;
+      }
+
+      @Override
+      public String getDisplayText(final ChangeInfo c, final String displayText) {
+        if (! c.isLatest()) {
+          return displayText + " [OUTDATED]";
+        }
+        return displayText;
+      }
+    });
     neededBy = new ChangeTable.Section(Util.C.changeScreenNeededBy());
     dependencies.addSection(dependsOn);
     dependencies.addSection(neededBy);
@@ -256,6 +256,7 @@
       }
     }
     setPageTitle(titleBuf.toString());
+    setHeaderVisible(false);
   }
 
   @Override
@@ -278,8 +279,10 @@
     dependencies.setAccountInfoCache(detail.getAccounts());
     approvals.setAccountInfoCache(detail.getAccounts());
 
-    descriptionBlock.display(detail.getChange(), detail
-        .getCurrentPatchSetDetail().getInfo(), detail.getAccounts());
+    descriptionBlock.display(detail.getChange(),
+        detail.isStarred(),
+        detail.getCurrentPatchSetDetail().getInfo(),
+        detail.getAccounts());
     dependsOn.display(detail.getDependsOn());
     neededBy.display(detail.getNeededBy());
     approvals.display(detail);
@@ -304,17 +307,28 @@
     addComments(detail);
 
     // If any dependency change is still open, or is outdated,
+    // or the change is needed by a change that is new or submitted,
     // show our dependency list.
     //
     boolean depsOpen = false;
     int outdated = 0;
-    if (!detail.getChange().getStatus().isClosed()
-        && detail.getDependsOn() != null) {
-      for (final ChangeInfo ci : detail.getDependsOn()) {
-        if (! ci.isLatest()) {
-          depsOpen = true;
-          outdated++;
-        } else if (ci.getStatus() != Change.Status.MERGED) {
+    if (!detail.getChange().getStatus().isClosed()) {
+      if (detail.getDependsOn() != null) {
+        for (final ChangeInfo ci : detail.getDependsOn()) {
+          if (!ci.isLatest()) {
+            depsOpen = true;
+            outdated++;
+          } else if (ci.getStatus() != Change.Status.MERGED) {
+            depsOpen = true;
+          }
+        }
+      }
+    }
+    if (detail.getNeededBy() != null) {
+      for (final ChangeInfo ci : detail.getNeededBy()) {
+        if ((ci.getStatus() == Change.Status.NEW) ||
+            (ci.getStatus() == Change.Status.SUBMITTED) ||
+            (ci.getStatus() == Change.Status.DRAFT)) {
           depsOpen = true;
         }
       }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java
index 5a568f1..ef4ef52 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java
@@ -20,7 +20,7 @@
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.patches.PatchUtil;
 import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.ui.AccountDashboardLink;
+import com.google.gerrit.client.ui.AccountLink;
 import com.google.gerrit.client.ui.BranchLink;
 import com.google.gerrit.client.ui.ChangeLink;
 import com.google.gerrit.client.ui.NavigationTable;
@@ -145,7 +145,7 @@
   protected void onStarClick(final int row) {
     final ChangeInfo c = getRowItem(row);
     if (c != null && Gerrit.isSignedIn()) {
-       ChangeCache.get(c.getId()).getStarCache().toggleStar();
+      ((StarredChanges.Icon) table.getWidget(row, C_STAR)).toggleStar();
     }
   }
 
@@ -191,29 +191,29 @@
     }
   }
 
-  private void populateChangeRow(final int row, final ChangeInfo c) {
+  private void populateChangeRow(final int row, final ChangeInfo c,
+      final ChangeRowFormatter changeRowFormatter) {
     ChangeCache cache = ChangeCache.get(c.getId());
     cache.getChangeInfoCache().set(c);
 
     final String idstr = c.getKey().abbreviate();
     table.setWidget(row, C_ARROW, null);
     if (Gerrit.isSignedIn()) {
-      table.setWidget(row, C_STAR, cache.getStarCache().createStar());
+      table.setWidget(row, C_STAR, StarredChanges.createIcon(c.getId(), c.isStarred()));
     }
     table.setWidget(row, C_ID, new TableChangeLink(idstr, c));
 
-    String s = c.getSubject();
-    if (s.length() > 80) {
-      s = s.substring(0, 80);
-    }
+    String s = Util.cropSubject(c.getSubject());
     if (c.getStatus() != null && c.getStatus() != Change.Status.NEW) {
       s += " (" + c.getStatus().name() + ")";
     }
-    if (! c.isLatest()) {
-      s += " [OUTDATED]";
-      table.getRowFormatter().addStyleName(row, Gerrit.RESOURCES.css().outdated());
-    } else {
-      table.getRowFormatter().removeStyleName(row, Gerrit.RESOURCES.css().outdated());
+    if (changeRowFormatter != null) {
+      removeChangeStyle(row, changeRowFormatter);
+      final String rowStyle = changeRowFormatter.getRowStyle(c);
+      if (rowStyle != null) {
+        table.getRowFormatter().addStyleName(row, rowStyle);
+      }
+      s = changeRowFormatter.getDisplayText(c, s);
     }
 
     table.setWidget(row, C_SUBJECT, new TableChangeLink(s, c));
@@ -223,11 +223,25 @@
     table.setWidget(row, C_BRANCH, new BranchLink(c.getProject().getKey(), c
         .getStatus(), c.getBranch(), c.getTopic()));
     table.setText(row, C_LAST_UPDATE, shortFormat(c.getLastUpdatedOn()));
+
     setRowItem(row, c);
   }
 
-  private AccountDashboardLink link(final Account.Id id) {
-    return AccountDashboardLink.link(accountCache, id);
+  private void removeChangeStyle(int row,
+      final ChangeRowFormatter changeRowFormatter) {
+    final ChangeInfo oldChange = getRowItem(row);
+    if (oldChange == null) {
+      return;
+    }
+
+    final String oldRowStyle = changeRowFormatter.getRowStyle(oldChange);
+    if (oldRowStyle != null) {
+      table.getRowFormatter().removeStyleName(row, oldRowStyle);
+    }
+  }
+
+  private AccountLink link(final Account.Id id) {
+    return AccountLink.link(accountCache, id);
   }
 
   public void addSection(final Section s) {
@@ -286,13 +300,13 @@
     int col = BASE_COLUMNS;
     boolean haveReview = false;
 
-    boolean displayPersonNameInReviewCategory = false;
+    boolean showUsernameInReviewCategory = false;
 
     if (Gerrit.isSignedIn()) {
       AccountGeneralPreferences prefs = Gerrit.getUserAccount().getGeneralPreferences();
 
-      if (prefs.isDisplayPersonNameInReviewCategory()) {
-        displayPersonNameInReviewCategory = true;
+      if (prefs.isShowUsernameInReviewCategory()) {
+        showUsernameInReviewCategory = true;
       }
     }
 
@@ -314,7 +328,7 @@
 
         if (type.isMaxNegative(ca)) {
 
-          if (displayPersonNameInReviewCategory) {
+          if (showUsernameInReviewCategory) {
             FlowPanel fp = new FlowPanel();
             fp.add(new Image(Gerrit.RESOURCES.redNot()));
             fp.add(new InlineLabel(FormatUtil.name(ai)));
@@ -325,7 +339,7 @@
 
         } else if (type.isMaxPositive(ca)) {
 
-          if (displayPersonNameInReviewCategory) {
+          if (showUsernameInReviewCategory) {
             FlowPanel fp = new FlowPanel();
             fp.add(new Image(Gerrit.RESOURCES.greenCheck()));
             fp.add(new InlineLabel(FormatUtil.name(ai)));
@@ -337,7 +351,7 @@
         } else {
           String vstr = String.valueOf(ca.getValue());
 
-          if (displayPersonNameInReviewCategory) {
+          if (showUsernameInReviewCategory) {
             vstr = vstr + " " + FormatUtil.name(ai);
           }
 
@@ -419,6 +433,8 @@
     int dataBegin;
     int rows;
 
+    private ChangeRowFormatter changeRowFormatter;
+
     public Section() {
       this(null, ApprovalViewType.NONE, null);
     }
@@ -441,6 +457,10 @@
       }
     }
 
+    public void setChangeRowFormatter(final ChangeRowFormatter changeRowFormatter) {
+      this.changeRowFormatter = changeRowFormatter;
+    }
+
     public void display(final List<ChangeInfo> changeList) {
       final int sz = changeList != null ? changeList.size() : 0;
       final boolean hadData = rows > 0;
@@ -470,7 +490,7 @@
 
         for (int i = 0; i < sz; i++) {
           ChangeInfo c = changeList.get(i);
-          parent.populateChangeRow(dataBegin + i, c);
+          parent.populateChangeRow(dataBegin + i, c, changeRowFormatter);
           cids.add(c.getId());
         }
 
@@ -489,4 +509,25 @@
       }
     }
   }
+
+  public static interface ChangeRowFormatter {
+    /**
+     * Returns the name of the CSS style that should be applied to the change
+     * row.
+     *
+     * @param c the change for which the styling should be returned
+     * @return the name of the CSS style that should be applied to the change
+     *         row
+     */
+    String getRowStyle(ChangeInfo c);
+
+    /**
+     * Returns the text that should be displayed for the change.
+     *
+     * @param c the change for which the display text should be returned
+     * @param displayText the current display text
+     * @return the new display text
+     */
+    String getDisplayText(ChangeInfo c, String displayText);
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable2.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable2.java
new file mode 100644
index 0000000..20dd80f
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable2.java
@@ -0,0 +1,416 @@
+// Copyright (C) 2008 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.changes;
+
+import static com.google.gerrit.client.FormatUtil.shortFormat;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.changes.ChangeInfo.LabelInfo;
+import com.google.gerrit.client.ui.BranchLink;
+import com.google.gerrit.client.ui.ChangeLink;
+import com.google.gerrit.client.ui.InlineHyperlink;
+import com.google.gerrit.client.ui.NavigationTable;
+import com.google.gerrit.client.ui.NeedsSignInKeyCommand;
+import com.google.gerrit.client.ui.ProjectLink;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gwt.dom.client.Element;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.HTMLTable.Cell;
+import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
+import com.google.gwt.user.client.ui.Image;
+import com.google.gwt.user.client.ui.InlineLabel;
+import com.google.gwt.user.client.ui.UIObject;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class ChangeTable2 extends NavigationTable<ChangeInfo> {
+  private static final int C_STAR = 1;
+  private static final int C_ID = 2;
+  private static final int C_SUBJECT = 3;
+  private static final int C_OWNER = 4;
+  private static final int C_PROJECT = 5;
+  private static final int C_BRANCH = 6;
+  private static final int C_LAST_UPDATE = 7;
+  private static final int BASE_COLUMNS = 8;
+
+  private final List<Section> sections;
+  private int columns;
+  private List<String> labelNames;
+
+  public ChangeTable2() {
+    columns = BASE_COLUMNS;
+    labelNames = Collections.emptyList();
+
+    keysNavigation.add(new PrevKeyCommand(0, 'k', Util.C.changeTablePrev()));
+    keysNavigation.add(new NextKeyCommand(0, 'j', Util.C.changeTableNext()));
+    keysNavigation.add(new OpenKeyCommand(0, 'o', Util.C.changeTableOpen()));
+    keysNavigation.add(
+        new OpenKeyCommand(0, KeyCodes.KEY_ENTER, Util.C.changeTableOpen()));
+
+    if (Gerrit.isSignedIn()) {
+      keysAction.add(new StarKeyCommand(0, 's', Util.C.changeTableStar()));
+    }
+
+    sections = new ArrayList<Section>();
+    table.setText(0, C_STAR, "");
+    table.setText(0, C_ID, Util.C.changeTableColumnID());
+    table.setText(0, C_SUBJECT, Util.C.changeTableColumnSubject());
+    table.setText(0, C_OWNER, Util.C.changeTableColumnOwner());
+    table.setText(0, C_PROJECT, Util.C.changeTableColumnProject());
+    table.setText(0, C_BRANCH, Util.C.changeTableColumnBranch());
+    table.setText(0, C_LAST_UPDATE, Util.C.changeTableColumnLastUpdate());
+
+    final FlexCellFormatter fmt = table.getFlexCellFormatter();
+    fmt.addStyleName(0, C_STAR, Gerrit.RESOURCES.css().iconHeader());
+    fmt.addStyleName(0, C_ID, Gerrit.RESOURCES.css().cID());
+    for (int i = C_ID; i < columns; i++) {
+      fmt.addStyleName(0, i, Gerrit.RESOURCES.css().dataHeader());
+    }
+
+    table.addClickHandler(new ClickHandler() {
+      @Override
+      public void onClick(final ClickEvent event) {
+        final Cell cell = table.getCellForEvent(event);
+        if (cell == null) {
+          return;
+        }
+        if (cell.getCellIndex() == C_STAR) {
+          // Don't do anything (handled by star itself).
+        } else if (cell.getCellIndex() == C_OWNER) {
+          // Don't do anything.
+        } else if (getRowItem(cell.getRowIndex()) != null) {
+          movePointerTo(cell.getRowIndex());
+        }
+      }
+    });
+  }
+
+  @Override
+  protected Object getRowItemKey(final ChangeInfo item) {
+    return item.legacy_id();
+  }
+
+  @Override
+  protected void onOpenRow(final int row) {
+    final ChangeInfo c = getRowItem(row);
+    final Change.Id id = c.legacy_id();
+    Gerrit.display(PageLinks.toChange(id), new ChangeScreen(id));
+  }
+
+  private void insertNoneRow(final int row) {
+    insertRow(row);
+    table.setText(row, 0, Util.C.changeTableNone());
+    final FlexCellFormatter fmt = table.getFlexCellFormatter();
+    fmt.setColSpan(row, 0, columns);
+    fmt.setStyleName(row, 0, Gerrit.RESOURCES.css().emptySection());
+  }
+
+  private void insertChangeRow(final int row) {
+    insertRow(row);
+    applyDataRowStyle(row);
+  }
+
+  @Override
+  protected void applyDataRowStyle(final int row) {
+    super.applyDataRowStyle(row);
+    final CellFormatter fmt = table.getCellFormatter();
+    fmt.addStyleName(row, C_STAR, Gerrit.RESOURCES.css().iconCell());
+    for (int i = C_ID; i < columns; i++) {
+      fmt.addStyleName(row, i, Gerrit.RESOURCES.css().dataCell());
+    }
+    fmt.addStyleName(row, C_ID, Gerrit.RESOURCES.css().cID());
+    fmt.addStyleName(row, C_SUBJECT, Gerrit.RESOURCES.css().cSUBJECT());
+    fmt.addStyleName(row, C_PROJECT, Gerrit.RESOURCES.css().cPROJECT());
+    fmt.addStyleName(row, C_BRANCH, Gerrit.RESOURCES.css().cPROJECT());
+    fmt.addStyleName(row, C_LAST_UPDATE, Gerrit.RESOURCES.css().cLastUpdate());
+    for (int i = BASE_COLUMNS; i < columns; i++) {
+      fmt.addStyleName(row, i, Gerrit.RESOURCES.css().cAPPROVAL());
+    }
+  }
+
+  public void updateColumnsForLabels(ChangeList... lists) {
+    labelNames = new ArrayList<String>();
+    for (ChangeList list : lists) {
+      for (int i = 0; i < list.size(); i++) {
+        for (String name : list.get(i).labels()) {
+          if (!labelNames.contains(name)) {
+            labelNames.add(name);
+          }
+        }
+      }
+    }
+    Collections.sort(labelNames);
+
+    if (BASE_COLUMNS + labelNames.size() < columns) {
+      int n = columns - (BASE_COLUMNS + labelNames.size());
+      for (int row = 0; row < table.getRowCount(); row++) {
+        table.removeCells(row, columns, n);
+      }
+    }
+    columns = BASE_COLUMNS + labelNames.size();
+
+    FlexCellFormatter fmt = table.getFlexCellFormatter();
+    for (int i = 0; i < labelNames.size(); i++) {
+      String name = labelNames.get(i);
+      int col = BASE_COLUMNS + i;
+
+      StringBuilder abbrev = new StringBuilder();
+      for (String t : name.split("-")) {
+        abbrev.append(t.substring(0, 1).toUpperCase());
+      }
+      table.setText(0, col, abbrev.toString());
+      table.getCellFormatter().getElement(0, col).setTitle(name);
+      fmt.addStyleName(0, col, Gerrit.RESOURCES.css().dataHeader());
+    }
+
+    for (Section s : sections) {
+      if (s.titleRow >= 0) {
+        fmt.setColSpan(s.titleRow, 0, columns);
+      }
+    }
+  }
+
+  private void populateChangeRow(final int row, final ChangeInfo c,
+      boolean highlightUnreviewed) {
+    if (Gerrit.isSignedIn()) {
+      table.setWidget(row, C_STAR, StarredChanges.createIcon(
+          c.legacy_id(),
+          c.starred()));
+    }
+    table.setWidget(row, C_ID, new TableChangeLink(c.id_abbreviated(), c));
+
+    String subject = Util.cropSubject(c.subject());
+    Change.Status status = c.status();
+    if (status != Change.Status.NEW) {
+      subject += " (" + Util.toLongString(status) + ")";
+    }
+    table.setWidget(row, C_SUBJECT, new TableChangeLink(subject, c));
+
+    String owner = "";
+    if (c.owner() != null && c.owner().name() != null) {
+      owner = c.owner().name();
+    }
+
+    table.setWidget(row, C_OWNER, new InlineHyperlink(owner,
+        PageLinks.toAccountQuery(owner, c.status())));
+
+    table.setWidget(
+        row, C_PROJECT, new ProjectLink(c.project_name_key(), c.status()));
+    table.setWidget(row, C_BRANCH, new BranchLink(c.project_name_key(), c
+        .status(), c.branch(), c.topic()));
+    table.setText(row, C_LAST_UPDATE, shortFormat(c.updated()));
+
+    boolean displayName = Gerrit.isSignedIn() && Gerrit.getUserAccount()
+        .getGeneralPreferences().isShowUsernameInReviewCategory();
+
+    CellFormatter fmt = table.getCellFormatter();
+    for (int idx = 0; idx < labelNames.size(); idx++) {
+      String name = labelNames.get(idx);
+      int col = BASE_COLUMNS + idx;
+
+      LabelInfo label = c.label(name);
+      if (label == null) {
+        table.clearCell(row, col);
+        continue;
+      }
+
+      String user;
+      if (label.rejected() != null) {
+        user = label.rejected().name();
+        if (displayName && user != null) {
+          FlowPanel panel = new FlowPanel();
+          panel.add(new Image(Gerrit.RESOURCES.redNot()));
+          panel.add(new InlineLabel(user));
+          table.setWidget(row, col, panel);
+        } else {
+          table.setWidget(row, col, new Image(Gerrit.RESOURCES.redNot()));
+        }
+      } else if (label.approved() != null) {
+        user = label.approved().name();
+        if (displayName && user != null) {
+          FlowPanel panel = new FlowPanel();
+          panel.add(new Image(Gerrit.RESOURCES.greenCheck()));
+          panel.add(new InlineLabel(user));
+          table.setWidget(row, col, panel);
+        } else {
+          table.setWidget(row, col, new Image(Gerrit.RESOURCES.greenCheck()));
+        }
+      } else if (label.disliked() != null) {
+        user = label.disliked().name();
+        String vstr = String.valueOf(label._value());
+        if (displayName && user != null) {
+          vstr = vstr + " " + user;
+        }
+        fmt.addStyleName(row, col, Gerrit.RESOURCES.css().negscore());
+        table.setText(row, col, vstr);
+      } else if (label.recommended() != null) {
+        user = label.recommended().name();
+        String vstr = "+" + label._value();
+        if (displayName && user != null) {
+          vstr = vstr + " " + user;
+        }
+        fmt.addStyleName(row, col, Gerrit.RESOURCES.css().posscore());
+        table.setText(row, col, vstr);
+      } else {
+        table.clearCell(row, col);
+        continue;
+      }
+      fmt.addStyleName(row, col, Gerrit.RESOURCES.css().singleLine());
+
+      if (!displayName && user != null) {
+        // Some web browsers ignore the embedded newline; some like it;
+        // so we include a space before the newline to accommodate both.
+        fmt.getElement(row, col).setTitle(name + " \nby " + user);
+      }
+    }
+
+    boolean needHighlight = false;
+    if (highlightUnreviewed && !c.reviewed()) {
+      needHighlight = true;
+    }
+    final Element tr = DOM.getParent(fmt.getElement(row, 0));
+    UIObject.setStyleName(tr, Gerrit.RESOURCES.css().needsReview(),
+        needHighlight);
+
+    setRowItem(row, c);
+  }
+
+  public void addSection(final Section s) {
+    assert s.parent == null;
+
+    if (s.titleText != null) {
+      s.titleRow = table.getRowCount();
+      table.setText(s.titleRow, 0, s.titleText);
+      final FlexCellFormatter fmt = table.getFlexCellFormatter();
+      fmt.setColSpan(s.titleRow, 0, columns);
+      fmt.addStyleName(s.titleRow, 0, Gerrit.RESOURCES.css().sectionHeader());
+    } else {
+      s.titleRow = -1;
+    }
+
+    s.parent = this;
+    s.dataBegin = table.getRowCount();
+    insertNoneRow(s.dataBegin);
+    sections.add(s);
+  }
+
+  private int insertRow(final int beforeRow) {
+    for (final Section s : sections) {
+      if (beforeRow <= s.titleRow) {
+        s.titleRow++;
+      }
+      if (beforeRow < s.dataBegin) {
+        s.dataBegin++;
+      }
+    }
+    return table.insertRow(beforeRow);
+  }
+
+  private void removeRow(final int row) {
+    for (final Section s : sections) {
+      if (row < s.titleRow) {
+        s.titleRow--;
+      }
+      if (row < s.dataBegin) {
+        s.dataBegin--;
+      }
+    }
+    table.removeRow(row);
+  }
+
+  public class StarKeyCommand extends NeedsSignInKeyCommand {
+    public StarKeyCommand(int mask, char key, String help) {
+      super(mask, key, help);
+    }
+
+    @Override
+    public void onKeyPress(final KeyPressEvent event) {
+      int row = getCurrentRow();
+      ChangeInfo c = getRowItem(row);
+      if (c != null && Gerrit.isSignedIn()) {
+        ((StarredChanges.Icon) table.getWidget(row, C_STAR)).toggleStar();
+      }
+    }
+  }
+
+  private final class TableChangeLink extends ChangeLink {
+    private TableChangeLink(final String text, final ChangeInfo c) {
+      super(text, c.legacy_id());
+    }
+
+    @Override
+    public void go() {
+      movePointerTo(cid);
+      super.go();
+    }
+  }
+
+  public static class Section {
+    ChangeTable2 parent;
+    String titleText;
+    int titleRow = -1;
+    int dataBegin;
+    int rows;
+    private boolean highlightUnreviewed;
+
+    public void setHighlightUnreviewed(boolean value) {
+      this.highlightUnreviewed = value;
+    }
+
+    public void setTitleText(final String text) {
+      titleText = text;
+      if (titleRow >= 0) {
+        parent.table.setText(titleRow, 0, titleText);
+      }
+    }
+
+    public void display(ChangeList changeList) {
+      final int sz = changeList != null ? changeList.size() : 0;
+      final boolean hadData = rows > 0;
+
+      if (hadData) {
+        while (sz < rows) {
+          parent.removeRow(dataBegin);
+          rows--;
+        }
+      } else {
+        parent.removeRow(dataBegin);
+      }
+
+      if (sz == 0) {
+        parent.insertNoneRow(dataBegin);
+        return;
+      }
+
+      while (rows < sz) {
+        parent.insertChangeRow(dataBegin + rows);
+        rows++;
+      }
+      for (int i = 0; i < sz; i++) {
+        parent.populateChangeRow(dataBegin + i, changeList.get(i),
+            highlightUnreviewed);
+      }
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommitMessageBlock.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommitMessageBlock.java
index 1a6ea3e..eff3cd5 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommitMessageBlock.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommitMessageBlock.java
@@ -15,28 +15,98 @@
 package com.google.gerrit.client.changes;
 
 import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.ui.ChangeLink;
 import com.google.gerrit.client.ui.CommentLinkProcessor;
+import com.google.gerrit.reviewdb.client.Change;
 import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.HTML;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.dom.client.PreElement;
+import com.google.gwt.dom.client.Style.Display;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwt.user.client.ui.SimplePanel;
+import com.google.gwtexpui.clippy.client.CopyableLabel;
+import com.google.gwtexpui.globalkey.client.KeyCommandSet;
 import com.google.gwtexpui.safehtml.client.SafeHtml;
 import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
 
 public class CommitMessageBlock extends Composite {
-  private final HTML description;
+  interface Binder extends UiBinder<HTMLPanel, CommitMessageBlock> {
+  }
+
+  private static Binder uiBinder = GWT.create(Binder.class);
+
+  private KeyCommandSet keysAction;
+
+  @UiField
+  SimplePanel starPanel;
+  @UiField
+  FlowPanel permalinkPanel;
+  @UiField
+  PreElement commitSummaryPre;
+  @UiField
+  PreElement commitBodyPre;
 
   public CommitMessageBlock() {
-    description = new HTML();
-    description.setStyleName(Gerrit.RESOURCES.css().changeScreenDescription());
-    initWidget(description);
+    initWidget(uiBinder.createAndBindUi(this));
+  }
+
+  public CommitMessageBlock(KeyCommandSet keysAction) {
+    this.keysAction = keysAction;
+    initWidget(uiBinder.createAndBindUi(this));
   }
 
   public void display(final String commitMessage) {
-    SafeHtml msg = new SafeHtmlBuilder().append(commitMessage);
-    msg = msg.linkify();
-    msg = CommentLinkProcessor.apply(msg);
-    msg = new SafeHtmlBuilder().openElement("p").append(msg).closeElement("p");
-    msg = msg.replaceAll("\n\n", "</p><p>");
-    msg = msg.replaceAll("\n", "<br />");
-    SafeHtml.set(description, msg);
+    display(null, null, commitMessage);
+  }
+
+  public void display(Change.Id changeId, Boolean starred, String commitMessage) {
+    starPanel.clear();
+
+    if (changeId != null && starred != null && Gerrit.isSignedIn()) {
+      StarredChanges.Icon star = StarredChanges.createIcon(changeId, starred);
+      star.setStyleName(Gerrit.RESOURCES.css().changeScreenStarIcon());
+      starPanel.add(star);
+
+      if (keysAction != null) {
+        keysAction.add(StarredChanges.newKeyCommand(star));
+      }
+    }
+
+    permalinkPanel.clear();
+    if (changeId != null) {
+      permalinkPanel.add(new ChangeLink(Util.C.changePermalink(), changeId));
+      permalinkPanel.add(new CopyableLabel(ChangeLink.permalink(changeId), false));
+    }
+
+    String[] splitCommitMessage = commitMessage.split("\n", 2);
+
+    String commitSummary = splitCommitMessage[0];
+    String commitBody = "";
+    if (splitCommitMessage.length > 1) {
+      commitBody = splitCommitMessage[1];
+    }
+
+    // Linkify commit summary
+    SafeHtml commitSummaryLinkified = new SafeHtmlBuilder().append(commitSummary);
+    commitSummaryLinkified = commitSummaryLinkified.linkify();
+    commitSummaryLinkified = CommentLinkProcessor.apply(commitSummaryLinkified);
+    commitSummaryPre.setInnerHTML(commitSummaryLinkified.asString());
+
+    // Hide commit body if there is no body
+    if (commitBody.trim().isEmpty()) {
+      commitBodyPre.getStyle().setDisplay(Display.NONE);
+    } else {
+      // Linkify commit body
+      SafeHtml commitBodyLinkified = new SafeHtmlBuilder().append(commitBody);
+      commitBodyLinkified = commitBodyLinkified.linkify();
+      commitBodyLinkified = CommentLinkProcessor.apply(commitBodyLinkified);
+      commitBodyLinkified = commitBodyLinkified.replaceAll("\n\n", "<p></p>");
+      commitBodyLinkified = commitBodyLinkified.replaceAll("\n", "<br />");
+      commitBodyPre.setInnerHTML(commitBodyLinkified.asString());
+    }
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommitMessageBlock.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommitMessageBlock.ui.xml
new file mode 100644
index 0000000..ca81537
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommitMessageBlock.ui.xml
@@ -0,0 +1,102 @@
+<?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.
+-->
+
+<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
+    xmlns:g='urn:import:com.google.gwt.user.client.ui'>
+
+
+  <ui:with field='res' type='com.google.gerrit.client.GerritResources'/>
+  <ui:style>
+    @eval selectionColor com.google.gerrit.client.Gerrit.getTheme().selectionColor;
+    @eval trimColor com.google.gerrit.client.Gerrit.getTheme().trimColor;
+    @eval backgroundColor com.google.gerrit.client.Gerrit.getTheme().backgroundColor;
+
+    .commitMessageTable {
+      border-collapse: separate;
+      border-spacing: 0;
+      margin-bottom: 10px;
+    }
+
+    .header {
+      background-color: trimColor;
+      white-space: nowrap;
+      color: textColor;
+      font-size: 10pt;
+      font-style: italic;
+      padding: 2px 6px 1px;
+    }
+
+    .contents {
+      border-bottom: 1px solid trimColor;
+      border-left: 1px solid trimColor;
+      border-right: 1px solid trimColor;
+      padding: 5px;
+    }
+
+    .contents span {
+      font-weight: bold;
+    }
+
+    .contents pre {
+      margin: 0;
+    }
+
+    .commitSummary {
+      font-weight: bold;
+    }
+
+    .commitBody p {
+      padding-top: 0px;
+    }
+
+    .starPanel {
+      float: left;
+    }
+
+    .boxTitle {
+      float: left;
+      margin-right: 10px;
+    }
+
+    .permalinkPanel {
+      float: right;
+    }
+
+    .permalinkPanel a {
+      float: left;
+    }
+
+    .permalinkPanel div {
+      display: inline;
+    }
+  </ui:style>
+
+  <g:HTMLPanel>
+    <table class='{style.commitMessageTable}'>
+      <tr><td class='{style.header}'>
+        <g:SimplePanel styleName='{style.starPanel}' ui:field='starPanel'></g:SimplePanel>
+        <div class='{style.boxTitle}'>Commit Message</div>
+        <g:FlowPanel styleName='{style.permalinkPanel}' ui:field='permalinkPanel'></g:FlowPanel>
+      </td></tr>
+      <tr><td class='{style.contents}'>
+        <pre class='{style.commitSummary} {res.css.changeScreenDescription}' ui:field='commitSummaryPre'/>
+        <pre class='{style.commitBody} {res.css.changeScreenDescription}' ui:field='commitBodyPre'/>
+      </td></tr>
+    </table>
+  </g:HTMLPanel>
+</ui:UiBinder>
+
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CustomDashboardScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CustomDashboardScreen.java
new file mode 100644
index 0000000..c9f1dc6
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CustomDashboardScreen.java
@@ -0,0 +1,112 @@
+// 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.changes;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.rpc.NativeList;
+import com.google.gerrit.client.rpc.ScreenLoadCallback;
+import com.google.gerrit.client.ui.Screen;
+import com.google.gwt.http.client.URL;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class CustomDashboardScreen extends Screen implements ChangeListScreen {
+  private String title;
+  private List<String> titles;
+  private List<String> queries;
+  private ChangeTable2 table;
+  private List<ChangeTable2.Section> sections;
+
+  public CustomDashboardScreen(String params) {
+    titles = new ArrayList<String>();
+    queries = new ArrayList<String>();
+    for (String kvPair : params.split("[,;&]")) {
+      String[] kv = kvPair.split("=", 2);
+      if (kv.length != 2 || kv[0].isEmpty()) {
+        continue;
+      }
+
+      if ("title".equals(kv[0])) {
+        title = URL.decodeQueryString(kv[1]);
+      } else {
+        titles.add(URL.decodeQueryString(kv[0]));
+        queries.add(URL.decodeQueryString(kv[1]));
+      }
+    }
+  }
+
+  @Override
+  protected void onInitUI() {
+    super.onInitUI();
+
+    if (title != null) {
+      setWindowTitle(title);
+      setPageTitle(title);
+    }
+
+    table = new ChangeTable2();
+    table.addStyleName(Gerrit.RESOURCES.css().accountDashboard());
+
+    sections = new ArrayList<ChangeTable2.Section>();
+    for (String title : titles) {
+      ChangeTable2.Section s = new ChangeTable2.Section();
+      s.setTitleText(title);
+      table.addSection(s);
+      sections.add(s);
+    }
+    add(table);
+  }
+
+  @Override
+  protected void onLoad() {
+    super.onLoad();
+
+    if (queries.isEmpty()) {
+      display();
+    } else if (queries.size() == 1) {
+      ChangeList.next(queries.get(0),
+          0, PagedSingleListScreen.MAX_SORTKEY,
+          new ScreenLoadCallback<ChangeList>(this) {
+            @Override
+            protected void preDisplay(ChangeList result) {
+              table.updateColumnsForLabels(result);
+              sections.get(0).display(result);
+              table.finishDisplay();
+            }
+        });
+    } else {
+      ChangeList.query(
+          new ScreenLoadCallback<NativeList<ChangeList>>(this) {
+            @Override
+            protected void preDisplay(NativeList<ChangeList> result) {
+              table.updateColumnsForLabels(
+                  result.asList().toArray(new ChangeList[result.size()]));
+              for (int i = 0; i < result.size(); i++) {
+                sections.get(i).display(result.get(i));
+              }
+              table.finishDisplay();
+            }
+          },
+          queries.toArray(new String[queries.size()]));
+    }
+  }
+
+  @Override
+  public void registerKeys() {
+    super.registerKeys();
+    table.setRegisterKeys(true);
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PagedSingleListScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PagedSingleListScreen.java
index 72300b2..23ce178 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PagedSingleListScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PagedSingleListScreen.java
@@ -15,12 +15,9 @@
 package com.google.gerrit.client.changes;
 
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.changes.ChangeTable.ApprovalViewType;
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
 import com.google.gerrit.client.ui.Hyperlink;
 import com.google.gerrit.client.ui.Screen;
-import com.google.gerrit.common.data.ChangeInfo;
-import com.google.gerrit.common.data.SingleListChangeInfo;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
 import com.google.gwt.event.dom.client.KeyPressEvent;
 import com.google.gwt.user.client.History;
@@ -28,19 +25,16 @@
 import com.google.gwt.user.client.ui.HorizontalPanel;
 import com.google.gwtexpui.globalkey.client.KeyCommand;
 
-import java.util.List;
-
-
 public abstract class PagedSingleListScreen extends Screen {
   protected static final String MIN_SORTKEY = "";
   protected static final String MAX_SORTKEY = "z";
 
   protected final int pageSize;
-  private ChangeTable table;
-  private ChangeTable.Section section;
+  private ChangeTable2 table;
+  private ChangeTable2.Section section;
   protected Hyperlink prev;
   protected Hyperlink next;
-  protected List<ChangeInfo> changes;
+  protected ChangeList changes;
 
   protected final String anchorPrefix;
   protected boolean useLoadPrev;
@@ -71,7 +65,7 @@
     next = new Hyperlink(Util.C.pagedChangeListNext(), true, "");
     next.setVisible(false);
 
-    table = new ChangeTable(true) {
+    table = new ChangeTable2() {
       {
         keysNavigation.add(new DoLinkCommand(0, 'p', Util.C
             .changeTablePagePrev(), prev));
@@ -79,8 +73,7 @@
             .changeTablePageNext(), next));
       }
     };
-    section = new ChangeTable.Section(null, ApprovalViewType.STRONGEST, null);
-
+    section = new ChangeTable2.Section();
     table.addSection(section);
     table.setSavePointerId(anchorPrefix);
     add(table);
@@ -112,36 +105,34 @@
 
   protected abstract void loadNext();
 
-  protected AsyncCallback<SingleListChangeInfo> loadCallback() {
-    return new ScreenLoadCallback<SingleListChangeInfo>(this) {
+  protected AsyncCallback<ChangeList> loadCallback() {
+    return new ScreenLoadCallback<ChangeList>(this) {
       @Override
-      protected void preDisplay(final SingleListChangeInfo result) {
+      protected void preDisplay(ChangeList result) {
         display(result);
       }
     };
   }
 
-  protected void display(final SingleListChangeInfo result) {
-    changes = result.getChanges();
-
+  protected void display(final ChangeList result) {
+    changes = result;
     if (!changes.isEmpty()) {
       final ChangeInfo f = changes.get(0);
       final ChangeInfo l = changes.get(changes.size() - 1);
 
-      prev.setTargetHistoryToken(anchorPrefix + ",p," + f.getSortKey());
-      next.setTargetHistoryToken(anchorPrefix + ",n," + l.getSortKey());
+      prev.setTargetHistoryToken(anchorPrefix + ",p," + f._sortkey());
+      next.setTargetHistoryToken(anchorPrefix + ",n," + l._sortkey());
 
       if (useLoadPrev) {
-        prev.setVisible(!result.isAtEnd());
+        prev.setVisible(f._more_changes());
         next.setVisible(!MIN_SORTKEY.equals(pos));
       } else {
         prev.setVisible(!MAX_SORTKEY.equals(pos));
-        next.setVisible(!result.isAtEnd());
+        next.setVisible(l._more_changes());
       }
     }
-
-    table.setAccountInfoCache(result.getAccounts());
-    section.display(result.getChanges());
+    table.updateColumnsForLabels(result);
+    section.display(result);
     table.finishDisplay();
   }
 
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 81b4f14..7e314cc 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
@@ -20,16 +20,16 @@
 import com.google.gerrit.client.GitwebLink;
 import com.google.gerrit.client.patches.PatchUtil;
 import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.ui.AccountDashboardLink;
 import com.google.gerrit.client.ui.CommentedActionDialog;
 import com.google.gerrit.client.ui.ComplexDisclosurePanel;
+import com.google.gerrit.client.ui.InlineHyperlink;
 import com.google.gerrit.client.ui.ListenableAccountDiffPreference;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.data.ChangeDetail;
 import com.google.gerrit.common.data.PatchSetDetail;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountDiffPreference;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
+import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.client.Patch;
@@ -128,6 +128,12 @@
    * followed by the action buttons.
    */
   public void ensureLoaded(final PatchSetDetail detail) {
+    loadInfoTable(detail);
+    loadActionPanel(detail);
+    loadPatchTable(detail);
+  }
+
+  public void loadInfoTable(final PatchSetDetail detail) {
     infoTable = new Grid(R_CNT, 2);
     infoTable.setStyleName(Gerrit.RESOURCES.css().infoBlock());
     infoTable.addStyleName(Gerrit.RESOURCES.css().patchSetInfoBlock());
@@ -153,15 +159,13 @@
     displayDownload();
 
     body.add(infoTable);
+  }
 
+  public void loadActionPanel(final PatchSetDetail detail) {
     if (!patchSet.getId().equals(diffBaseId)) {
-      patchTable = new PatchTable();
-      patchTable.setSavePointerId("PatchTable " + patchSet.getId());
-      patchTable.display(diffBaseId, detail);
-
       actionsPanel = new FlowPanel();
       actionsPanel.setStyleName(Gerrit.RESOURCES.css().patchSetActions());
-      body.add(actionsPanel);
+      actionsPanel.setVisible(true);
       if (Gerrit.isSignedIn()) {
         if (changeDetail.canEdit()) {
           populateReviewAction();
@@ -173,18 +177,28 @@
           if (changeDetail.canPublish()) {
             populatePublishAction();
           }
-          if (changeDetail.canDeleteDraft() &&
-              changeDetail.getPatchSets().size() > 1) {
+          if (changeDetail.canDeleteDraft()
+              && changeDetail.getPatchSets().size() > 1) {
             populateDeleteDraftPatchSetAction();
           }
         }
       }
       populateDiffAllActions(detail);
-      body.add(patchTable);
+      body.add(actionsPanel);
+    }
+  }
 
-      for(ClickHandler clickHandler : registeredClickHandler) {
+  public void loadPatchTable(final PatchSetDetail detail) {
+    if (!patchSet.getId().equals(diffBaseId)) {
+      patchTable = new PatchTable();
+      patchTable.setSavePointerId("PatchTable " + patchSet.getId());
+      patchTable.display(diffBaseId, detail);
+      for (ClickHandler clickHandler : registeredClickHandler) {
         patchTable.addClickHandler(clickHandler);
       }
+      patchTable.setRegisterKeys(true);
+      setActive(true);
+      body.add(patchTable);
     }
   }
 
@@ -195,6 +209,7 @@
     final DownloadCommandPanel commands = new DownloadCommandPanel();
     final DownloadUrlPanel urls = new DownloadUrlPanel(commands);
     final Set<DownloadScheme> allowedSchemes = Gerrit.getConfig().getDownloadSchemes();
+    final Set<DownloadCommand> allowedCommands = Gerrit.getConfig().getDownloadCommands();
 
     copyLabel.setStyleName(Gerrit.RESOURCES.css().downloadLinkCopyLabel());
 
@@ -232,9 +247,8 @@
           .anonymousDownload("HTTP"), r.toString()));
     }
 
-    if (Gerrit.getConfig().getSshdAddress() != null && Gerrit.isSignedIn()
-        && Gerrit.getUserAccount().getUserName() != null
-        && Gerrit.getUserAccount().getUserName().length() > 0
+    if (Gerrit.getConfig().getSshdAddress() != null
+        && hasUserName()
         && (allowedSchemes.contains(DownloadScheme.SSH) ||
             allowedSchemes.contains(DownloadScheme.DEFAULT_DOWNLOADS))) {
       String sshAddr = Gerrit.getConfig().getSshdAddress();
@@ -256,13 +270,12 @@
       urls.add(new DownloadUrlLink(DownloadScheme.SSH, "SSH", r.toString()));
     }
 
-    if (Gerrit.isSignedIn() && Gerrit.getUserAccount().getUserName() != null
-        && Gerrit.getUserAccount().getUserName().length() > 0
-        && (allowedSchemes.contains(DownloadScheme.HTTP) ||
-            allowedSchemes.contains(DownloadScheme.DEFAULT_DOWNLOADS))) {
+    if ((hasUserName() || siteReliesOnHttp())
+        && (allowedSchemes.contains(DownloadScheme.HTTP)
+            || allowedSchemes.contains(DownloadScheme.DEFAULT_DOWNLOADS))) {
       final StringBuilder r = new StringBuilder();
       if (Gerrit.getConfig().getGitHttpUrl() != null
-          && changeDetail.isAllowsAnonymous()) {
+          && (changeDetail.isAllowsAnonymous() || siteReliesOnHttp())) {
         r.append(Gerrit.getConfig().getGitHttpUrl());
       } else {
         String base = hostPageUrl;
@@ -311,39 +324,52 @@
     }
 
     if (!urls.isEmpty()) {
-      commands.add(new DownloadCommandLink(DownloadCommand.CHECKOUT, "checkout") {
-        @Override
-        void setCurrentUrl(DownloadUrlLink link) {
-          urls.setVisible(true);
-          copyLabel.setText("git fetch " + link.urlData
-              + " && git checkout FETCH_HEAD");
-        }
-      });
-      commands.add(new DownloadCommandLink(DownloadCommand.PULL, "pull") {
-        @Override
-        void setCurrentUrl(DownloadUrlLink link) {
-          urls.setVisible(true);
-          copyLabel.setText("git pull " + link.urlData);
-        }
-      });
-      commands.add(new DownloadCommandLink(DownloadCommand.CHERRY_PICK,
-          "cherry-pick") {
-        @Override
-        void setCurrentUrl(DownloadUrlLink link) {
-          urls.setVisible(true);
-          copyLabel.setText("git fetch " + link.urlData
-              + " && git cherry-pick FETCH_HEAD");
-        }
-      });
-      commands.add(new DownloadCommandLink(DownloadCommand.FORMAT_PATCH,
-          "patch") {
-        @Override
-        void setCurrentUrl(DownloadUrlLink link) {
-          urls.setVisible(true);
-          copyLabel.setText("git fetch " + link.urlData
-              + " && git format-patch -1 --stdout FETCH_HEAD");
-        }
-      });
+      if (allowedCommands.contains(DownloadCommand.CHECKOUT)
+          || allowedCommands.contains(DownloadCommand.DEFAULT_DOWNLOADS)) {
+        commands.add(new DownloadCommandLink(DownloadCommand.CHECKOUT,
+            "checkout") {
+          @Override
+          void setCurrentUrl(DownloadUrlLink link) {
+            urls.setVisible(true);
+            copyLabel.setText("git fetch " + link.urlData
+                + " && git checkout FETCH_HEAD");
+          }
+        });
+      }
+      if (allowedCommands.contains(DownloadCommand.PULL)
+          || allowedCommands.contains(DownloadCommand.DEFAULT_DOWNLOADS)) {
+        commands.add(new DownloadCommandLink(DownloadCommand.PULL, "pull") {
+          @Override
+          void setCurrentUrl(DownloadUrlLink link) {
+            urls.setVisible(true);
+            copyLabel.setText("git pull " + link.urlData);
+          }
+        });
+      }
+      if (allowedCommands.contains(DownloadCommand.CHERRY_PICK)
+          || allowedCommands.contains(DownloadCommand.DEFAULT_DOWNLOADS)) {
+        commands.add(new DownloadCommandLink(DownloadCommand.CHERRY_PICK,
+            "cherry-pick") {
+          @Override
+          void setCurrentUrl(DownloadUrlLink link) {
+            urls.setVisible(true);
+            copyLabel.setText("git fetch " + link.urlData
+                + " && git cherry-pick FETCH_HEAD");
+          }
+        });
+      }
+      if (allowedCommands.contains(DownloadCommand.FORMAT_PATCH)
+          || allowedCommands.contains(DownloadCommand.DEFAULT_DOWNLOADS)) {
+        commands.add(new DownloadCommandLink(DownloadCommand.FORMAT_PATCH,
+            "patch") {
+          @Override
+          void setCurrentUrl(DownloadUrlLink link) {
+            urls.setVisible(true);
+            copyLabel.setText("git fetch " + link.urlData
+                + " && git format-patch -1 --stdout FETCH_HEAD");
+          }
+        });
+      }
     }
 
     final FlowPanel fp = new FlowPanel();
@@ -372,6 +398,18 @@
     infoTable.setWidget(R_DOWNLOAD, 1, fp);
   }
 
+  private static boolean siteReliesOnHttp() {
+    return Gerrit.getConfig().getGitHttpUrl() != null
+        && Gerrit.getConfig().getAuthType() == AuthType.CUSTOM_EXTENSION
+        && !Gerrit.getConfig().siteHasUsernames();
+  }
+
+  private static boolean hasUserName() {
+    return Gerrit.isSignedIn()
+        && Gerrit.getUserAccount().getUserName() != null
+        && Gerrit.getUserAccount().getUserName().length() > 0;
+  }
+
   private void displayUserIdentity(final int row, final UserIdentity who) {
     if (who == null) {
       infoTable.clearCell(row, 1);
@@ -381,9 +419,9 @@
     final FlowPanel fp = new FlowPanel();
     fp.setStyleName(Gerrit.RESOURCES.css().patchSetUserIdentity());
     if (who.getName() != null) {
-      final Account.Id aId = who.getAccount();
-      if (aId != null) {
-        fp.add(new AccountDashboardLink(who.getName(), aId));
+      if (who.getAccount() != null) {
+        fp.add(new InlineHyperlink(who.getName(),
+            PageLinks.toAccountQuery(who.getName())));
       } else {
         final InlineLabel lbl = new InlineLabel(who.getName());
         lbl.setStyleName(Gerrit.RESOURCES.css().accountName());
@@ -414,7 +452,8 @@
       parentsTable.setWidget(row, 0, new InlineLabel(parent.id.get()));
       ptfmt.addStyleName(row, 0, Gerrit.RESOURCES.css().noborder());
       ptfmt.addStyleName(row, 0, Gerrit.RESOURCES.css().monospace());
-      parentsTable.setWidget(row, 1, new InlineLabel(parent.shortMessage));
+      parentsTable.setWidget(row, 1,
+          new InlineLabel(Util.cropSubject(parent.shortMessage)));
       ptfmt.addStyleName(row, 1, Gerrit.RESOURCES.css().noborder());
       row++;
     }
@@ -437,16 +476,10 @@
         public void onClick(final ClickEvent event) {
           b.setEnabled(false);
           Util.MANAGE_SVC.submit(patchSet.getId(),
-              new GerritCallback<ChangeDetail>() {
+              new ChangeDetailCache.GerritWidgetCallback(b) {
                 public void onSuccess(ChangeDetail result) {
                   onSubmitResult(result);
                 }
-
-                @Override
-                public void onFailure(Throwable caught) {
-                  b.setEnabled(true);
-                  super.onFailure(caught);
-                }
               });
         }
       });
@@ -611,17 +644,7 @@
       public void onClick(final ClickEvent event) {
         b.setEnabled(false);
         Util.MANAGE_SVC.publish(patchSet.getId(),
-            new GerritCallback<ChangeDetail>() {
-              public void onSuccess(ChangeDetail result) {
-                detailCache.set(result);
-              }
-
-              @Override
-              public void onFailure(Throwable caught) {
-                b.setEnabled(true);
-                super.onFailure(caught);
-              }
-            });
+            new ChangeDetailCache.GerritWidgetCallback(b));
       }
     });
     actionsPanel.add(b);
@@ -634,7 +657,7 @@
       public void onClick(final ClickEvent event) {
         b.setEnabled(false);
         PatchUtil.DETAIL_SVC.deleteDraftPatchSet(patchSet.getId(),
-            new GerritCallback<ChangeDetail>() {
+            new ChangeDetailCache.GerritWidgetCallback(b) {
               public void onSuccess(final ChangeDetail result) {
                 if (result != null) {
                   detailCache.set(result);
@@ -642,12 +665,6 @@
                   Gerrit.display(PageLinks.MINE);
                 }
               }
-
-              @Override
-              public void onFailure(Throwable caught) {
-                b.setEnabled(true);
-                super.onFailure(caught);
-              }
             });
       }
     });
@@ -655,34 +672,45 @@
   }
 
   public void refresh() {
-    AccountDiffPreference diffPrefs;
-    if (patchTable == null) {
-      diffPrefs = new ListenableAccountDiffPreference().get();
+    if (patchSet.getId().equals(diffBaseId)) {
+      if (patchTable != null) {
+        patchTable.setVisible(false);
+      }
+      if (actionsPanel != null) {
+        actionsPanel.setVisible(false);
+      }
     } else {
-      diffPrefs = patchTable.getPreferences().get();
-    }
+      if (patchTable != null) {
+        if (patchTable.getBase() == null && diffBaseId == null
+            || patchTable.getBase() != null
+            && patchTable.getBase().equals(diffBaseId)) {
+          actionsPanel.setVisible(true);
+          patchTable.setVisible(true);
+          return;
+        }
+      }
 
-    Util.DETAIL_SVC.patchSetDetail2(diffBaseId, patchSet.getId(), diffPrefs,
-        new GerritCallback<PatchSetDetail>() {
-          @Override
-          public void onSuccess(PatchSetDetail result) {
-            if (patchSet.getId().equals(diffBaseId)) {
-              patchTable.setVisible(false);
-              actionsPanel.setVisible(false);
-            } else {
-              if (patchTable != null) {
-                patchTable.removeFromParent();
-              }
-              patchTable = new PatchTable();
-              patchTable.display(diffBaseId, result);
-              body.add(patchTable);
+      AccountDiffPreference diffPrefs;
+      if (patchTable == null) {
+        diffPrefs = new ListenableAccountDiffPreference().get();
+      } else {
+        diffPrefs = patchTable.getPreferences().get();
+        patchTable.setVisible(false);
+      }
 
-              for (ClickHandler clickHandler : registeredClickHandler) {
-                patchTable.addClickHandler(clickHandler);
+      Util.DETAIL_SVC.patchSetDetail2(diffBaseId, patchSet.getId(), diffPrefs,
+          new GerritCallback<PatchSetDetail>() {
+            @Override
+            public void onSuccess(PatchSetDetail result) {
+              if (actionsPanel != null) {
+                actionsPanel.setVisible(true);
+              } else {
+                loadActionPanel(result);
               }
+              loadPatchTable(result);
             }
-          }
-        });
+          });
+    }
   }
 
   @Override
@@ -698,8 +726,8 @@
       Util.DETAIL_SVC.patchSetDetail2(diffBaseId, patchSet.getId(), diffPrefs,
           new GerritCallback<PatchSetDetail>() {
             public void onSuccess(final PatchSetDetail result) {
-              ensureLoaded(result);
-              patchTable.setRegisterKeys(true);
+              loadInfoTable(result);
+              loadActionPanel(result);
             }
           });
     }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetsBlock.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetsBlock.java
index 7e659a1..b9ed4e7 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetsBlock.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetsBlock.java
@@ -79,7 +79,7 @@
     if (Gerrit.isSignedIn()) {
       final AccountGeneralPreferences p =
           Gerrit.getUserAccount().getGeneralPreferences();
-      if (p.isDisplayPatchSetsInReverseOrder()) {
+      if (p.isReversePatchSetOrder()) {
         Collections.reverse(patchSets);
       }
     }
@@ -175,7 +175,6 @@
         deactivate();
         PatchSetComplexDisclosurePanel patchSetPanel =
             patchSetPanels.get(patchSetId);
-        patchSetPanel.setOpen(true);
         patchSetPanel.setActive(true);
         activePatchSetId = patchSetId;
       }
@@ -226,6 +225,9 @@
     public void onOpen(OpenEvent<DisclosurePanel> event) {
       // when a patch set panel is opened by the user
       // it should automatically become active
+      PatchSetComplexDisclosurePanel patchSetPanel =
+          patchSetPanels.get(patchSetId);
+      patchSetPanel.refresh();
       activate(patchSetId);
     }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchTable.java
index 7177525..b8acd56 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchTable.java
@@ -50,6 +50,28 @@
 import java.util.List;
 
 public class PatchTable extends Composite {
+  public interface PatchValidator {
+    /**
+     * Returns true if patch is valid
+     *
+     * @param patch
+     * @return
+     */
+    boolean isValid(Patch patch);
+  }
+
+  public final PatchValidator PREFERENCE_VALIDATOR =
+      new PatchValidator() {
+        @Override
+        public boolean isValid(Patch patch) {
+          return !((listenablePrefs.get().isSkipDeleted()
+              && patch.getChangeType().equals(ChangeType.DELETED))
+              || (listenablePrefs.get().isSkipUncommented()
+              && patch.getCommentCount() == 0));
+        }
+
+      };
+
   private final FlowPanel myBody;
   private PatchSetDetail detail;
   private Command onLoadCommand;
@@ -97,6 +119,10 @@
     }
   }
 
+  public PatchSet.Id getBase() {
+    return base;
+  }
+
   public void setSavePointerId(final String id) {
     savePointerId = id;
   }
@@ -176,29 +202,32 @@
   /**
    * @return a link to the previous file in this patch set, or null.
    */
-  public InlineHyperlink getPreviousPatchLink(int index, PatchScreen.Type patchType) {
-    for(index--; index > -1; index--) {
-      InlineHyperlink link = createLink(index, patchType, SafeHtml.asis(Util.C
-          .prevPatchLinkIcon()), null);
-      if (link != null) {
-        return link;
-      }
+  public InlineHyperlink getPreviousPatchLink(int index,
+      PatchScreen.Type patchType) {
+    int previousPatchIndex = getPreviousPatch(index, PREFERENCE_VALIDATOR);
+    if (previousPatchIndex < 0) {
+      return null;
     }
-    return null;
+    InlineHyperlink link =
+        createLink(previousPatchIndex, patchType,
+            SafeHtml.asis(Util.C.prevPatchLinkIcon()), null);
+
+    return link;
   }
 
   /**
    * @return a link to the next file in this patch set, or null.
    */
   public InlineHyperlink getNextPatchLink(int index, PatchScreen.Type patchType) {
-    for(index++; index < patchList.size(); index++) {
-      InlineHyperlink link = createLink(index, patchType, null, SafeHtml.asis(Util.C
-          .nextPatchLinkIcon()));
-      if (link != null) {
-        return link;
-      }
+    int nextPatchIndex = getNextPatch(index, false, PREFERENCE_VALIDATOR);
+    if (nextPatchIndex < 0) {
+      return null;
     }
-    return null;
+    InlineHyperlink link =
+        createLink(nextPatchIndex, patchType, null,
+            SafeHtml.asis(Util.C.nextPatchLinkIcon()));
+
+    return link;
   }
 
   /**
@@ -208,16 +237,9 @@
    * @param before A string to display at the beginning of the href text
    * @param after A string to display at the end of the href text
    */
-  private PatchLink createLink(int index, PatchScreen.Type patchType,
+  public PatchLink createLink(int index, PatchScreen.Type patchType,
       SafeHtml before, SafeHtml after) {
     Patch patch = patchList.get(index);
-    if (( listenablePrefs.get().isSkipDeleted() &&
-          patch.getChangeType().equals(ChangeType.DELETED) )
-        ||
-        ( listenablePrefs.get().isSkipUncommented() &&
-          patch.getCommentCount() == 0 ) ) {
-      return null;
-    }
 
     Key thisKey = patch.getKey();
     PatchLink link;
@@ -814,4 +836,75 @@
       return System.currentTimeMillis() - start > 200;
     }
   }
+
+
+  /**
+   * Gets the next patch
+   *
+   * @param currentIndex
+   * @param validators
+   * @param loopAround loops back around to the front and traverses if this is
+   *        true
+   * @return
+   */
+  public int getNextPatch(int currentIndex, boolean loopAround,
+      PatchValidator... validators) {
+    return getNextPatchHelper(currentIndex, loopAround, detail.getPatches()
+        .size(), validators);
+  }
+
+  /**
+   * Helper function for getNextPatch
+   *
+   * @param currentIndex
+   * @param validators
+   * @param loopAround
+   * @param maxIndex will only traverse up to this index
+   * @return
+   */
+  private int getNextPatchHelper(int currentIndex, boolean loopAround,
+      int maxIndex, PatchValidator... validators) {
+    for (int i = currentIndex + 1; i < maxIndex; i++) {
+      Patch patch = detail.getPatches().get(i);
+      if (patch != null && patchIsValid(patch, validators)) {
+        return i;
+      }
+    }
+
+    if (loopAround) {
+      return getNextPatchHelper(-1, false, currentIndex, validators);
+    }
+
+    return -1;
+  }
+
+  /**
+   * @return the index to the previous patch
+   */
+  public int getPreviousPatch(int currentIndex, PatchValidator... validators) {
+    for (int i = currentIndex - 1; i >= 0; i--) {
+      Patch patch = detail.getPatches().get(i);
+      if (patch != null && patchIsValid(patch, validators)) {
+        return i;
+      }
+    }
+
+    return -1;
+  }
+
+  /**
+   * Helper function that returns whether a patch is valid or not
+   *
+   * @param patch
+   * @param validators
+   * @return whether the patch is valid based on the validators
+   */
+  private boolean patchIsValid(Patch patch, PatchValidator... validators) {
+    for (PatchValidator v : validators) {
+      if (!v.isValid(patch)) {
+        return false;
+      }
+    }
+    return true;
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PublishCommentScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PublishCommentScreen.java
index 0c08491..4705aad 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PublishCommentScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PublishCommentScreen.java
@@ -64,6 +64,7 @@
   private final PatchSet.Id patchSetId;
   private Collection<ValueRadioButton> approvalButtons;
   private ChangeDescriptionBlock descBlock;
+  private ApprovalTable approvals;
   private Panel approvalPanel;
   private NpTextArea message;
   private FlowPanel draftsPanel;
@@ -83,9 +84,12 @@
     addStyleName(Gerrit.RESOURCES.css().publishCommentsScreen());
 
     approvalButtons = new ArrayList<ValueRadioButton>();
-    descBlock = new ChangeDescriptionBlock();
+    descBlock = new ChangeDescriptionBlock(null);
     add(descBlock);
 
+    approvals = new ApprovalTable();
+    add(approvals);
+
     final FormPanel form = new FormPanel();
     final FlowPanel body = new FlowPanel();
     form.setWidget(body);
@@ -270,10 +274,15 @@
   private void display(final PatchSetPublishDetail r) {
     setPageTitle(Util.M.publishComments(r.getChange().getKey().abbreviate(),
         patchSetId.get()));
-    descBlock.display(r.getChange(), r.getPatchSetInfo(), r.getAccounts());
+    descBlock.display(r.getChange(), null, r.getPatchSetInfo(), r.getAccounts());
 
     if (r.getChange().getStatus().isOpen()) {
       initApprovals(r, approvalPanel);
+
+      approvals.setAccountInfoCache(r.getAccounts());
+      approvals.display(r);
+    } else {
+      approvals.setVisible(false);
     }
 
     if (lastState != null && patchSetId.equals(lastState.patchSetId)) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/QueryScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/QueryScreen.java
index cf9c526..b94fcae 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/QueryScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/QueryScreen.java
@@ -17,13 +17,11 @@
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.common.data.ChangeInfo;
-import com.google.gerrit.common.data.SingleListChangeInfo;
+import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.gwtorm.client.KeyUtil;
 
-
 public class QueryScreen extends PagedSingleListScreen implements
     ChangeListScreen {
   public static QueryScreen forQuery(String query) {
@@ -49,13 +47,15 @@
   }
 
   @Override
-  protected AsyncCallback<SingleListChangeInfo> loadCallback() {
-    return new GerritCallback<SingleListChangeInfo>() {
-      public final void onSuccess(final SingleListChangeInfo result) {
+  protected AsyncCallback<ChangeList> loadCallback() {
+    return new GerritCallback<ChangeList>() {
+      @Override
+      public final void onSuccess(ChangeList result) {
         if (isAttached()) {
-          if (result.getChanges().size() == 1 && isSingleQuery(query)) {
-            final ChangeInfo c = result.getChanges().get(0);
-            Gerrit.display(PageLinks.toChange(c), new ChangeScreen(c));
+          if (result.size() == 1 && isSingleQuery(query)) {
+            ChangeInfo c = result.get(0);
+            Change.Id id = c.legacy_id();
+            Gerrit.display(PageLinks.toChange(id), new ChangeScreen(id));
           } else {
             Gerrit.setQueryString(query);
             display(result);
@@ -68,12 +68,12 @@
 
   @Override
   protected void loadPrev() {
-    Util.LIST_SVC.allQueryPrev(query, pos, pageSize, loadCallback());
+    ChangeList.prev(query, pageSize, pos, loadCallback());
   }
 
   @Override
   protected void loadNext() {
-    Util.LIST_SVC.allQueryNext(query, pos, pageSize, loadCallback());
+    ChangeList.next(query, pageSize, pos, loadCallback());
   }
 
   private static boolean isSingleQuery(String query) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarCache.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarCache.java
deleted file mode 100644
index d7624a6..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarCache.java
+++ /dev/null
@@ -1,139 +0,0 @@
-// 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.changes;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.ui.NeedsSignInKeyCommand;
-import com.google.gerrit.common.data.ChangeDetail;
-import com.google.gerrit.common.data.ChangeInfo;
-import com.google.gerrit.common.data.ToggleStarRequest;
-import com.google.gerrit.reviewdb.client.Change;
-
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.event.logical.shared.HasValueChangeHandlers;
-import com.google.gwt.event.logical.shared.ValueChangeEvent;
-import com.google.gwt.event.logical.shared.ValueChangeHandler;
-import com.google.gwt.event.shared.GwtEvent;
-import com.google.gwt.event.shared.HandlerManager;
-import com.google.gwt.event.shared.HandlerRegistration;
-import com.google.gwt.resources.client.ImageResource;
-import com.google.gwt.user.client.ui.Image;
-import com.google.gwtjsonrpc.common.VoidResult;
-
-public class StarCache implements HasValueChangeHandlers<Boolean> {
-  public class KeyCommand extends NeedsSignInKeyCommand {
-    public KeyCommand(int mask, char key, String help) {
-      super(mask, key, help);
-    }
-
-    @Override
-    public void onKeyPress(final KeyPressEvent event) {
-      StarCache.this.toggleStar();
-    }
-  }
-
-  ChangeCache cache;
-
-  private HandlerManager manager = new HandlerManager(this);
-
-  public StarCache(final Change.Id chg) {
-    cache = ChangeCache.get(chg);
-  }
-
-  public boolean get() {
-    ChangeDetail detail = cache.getChangeDetailCache().get();
-    if (detail != null) {
-      return detail.isStarred();
-    }
-    ChangeInfo info = cache.getChangeInfoCache().get();
-    if (info != null) {
-      return info.isStarred();
-    }
-    return false;
-  }
-
-  public void set(final boolean s) {
-    if (Gerrit.isSignedIn() && s != get()) {
-      final ToggleStarRequest req = new ToggleStarRequest();
-      req.toggle(cache.getChangeId(), s);
-
-      Util.LIST_SVC.toggleStars(req, new GerritCallback<VoidResult>() {
-        public void onSuccess(final VoidResult result) {
-          setStarred(s);
-          fireEvent(new ValueChangeEvent<Boolean>(s){});
-        }
-      });
-    }
-  }
-
-  private void setStarred(final boolean s) {
-    ChangeDetail detail = cache.getChangeDetailCache().get();
-    if (detail != null) {
-      detail.setStarred(s);
-    }
-    ChangeInfo info = cache.getChangeInfoCache().get();
-    if (info != null) {
-      info.setStarred(s);
-    }
-  }
-
-  public void toggleStar() {
-    set(!get());
-  }
-
-  @SuppressWarnings("unchecked")
-  public Image createStar() {
-    final Image star = new Image(getResource());
-    star.setVisible(Gerrit.isSignedIn());
-
-    star.addClickHandler(new ClickHandler() {
-      @Override
-      public void onClick(final ClickEvent event) {
-        StarCache.this.toggleStar();
-      }
-    });
-
-    @SuppressWarnings("rawtypes")
-    ValueChangeHandler starUpdater = new ValueChangeHandler() {
-        @Override
-        public void onValueChange(ValueChangeEvent event) {
-          star.setResource(StarCache.this.getResource());
-        }
-      };
-
-    cache.getChangeDetailCache().addValueChangeHandler(starUpdater);
-    cache.getChangeInfoCache().addValueChangeHandler(starUpdater);
-
-    this.addValueChangeHandler(starUpdater);
-
-    return star;
-  }
-
-  private ImageResource getResource() {
-    return get() ? Gerrit.RESOURCES.starFilled() : Gerrit.RESOURCES.starOpen();
-  }
-
-  public void fireEvent(GwtEvent<?> event) {
-    manager.fireEvent(event);
-  }
-
-  public HandlerRegistration addValueChangeHandler(
-      ValueChangeHandler<Boolean> handler) {
-    return manager.addHandler(ValueChangeEvent.getType(), handler);
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarredChanges.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarredChanges.java
new file mode 100644
index 0000000..8b5aa1c
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarredChanges.java
@@ -0,0 +1,216 @@
+// 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.changes;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.common.data.ToggleStarRequest;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.shared.EventBus;
+import com.google.gwt.event.shared.SimpleEventBus;
+import com.google.gwt.resources.client.ImageResource;
+import com.google.gwt.user.client.ui.Image;
+import com.google.gwtexpui.globalkey.client.KeyCommand;
+import com.google.gwtjsonrpc.common.VoidResult;
+import com.google.web.bindery.event.shared.Event;
+import com.google.web.bindery.event.shared.HandlerRegistration;
+
+/** Supports the star icon displayed on changes and tracking the status. */
+public class StarredChanges {
+  private static final EventBus eventBus = new SimpleEventBus();
+  private static final Event.Type<ChangeStarHandler> TYPE =
+      new Event.Type<ChangeStarHandler>();
+
+  /** Handler that can receive notifications of a change's starred status. */
+  public static interface ChangeStarHandler {
+    public void onChangeStar(ChangeStarEvent event);
+  }
+
+  /** Event fired when a star changes status. The new status is reported. */
+  public static class ChangeStarEvent extends Event<ChangeStarHandler> {
+    private boolean starred;
+
+    public ChangeStarEvent(Change.Id source, boolean starred) {
+      setSource(source);
+      this.starred = starred;
+    }
+
+    public boolean isStarred() {
+      return starred;
+    }
+
+    @Override
+    public Type<ChangeStarHandler> getAssociatedType() {
+      return TYPE;
+    }
+
+    @Override
+    protected void dispatch(ChangeStarHandler handler) {
+      handler.onChangeStar(this);
+    }
+  }
+
+  /**
+   * Create a star icon for the given change, and current status. Returns null
+   * if the user is not signed in and cannot support starred changes.
+   */
+  public static Icon createIcon(Change.Id source, boolean starred) {
+    return Gerrit.isSignedIn() ? new Icon(source, starred) : null;
+  }
+
+  /** Make a key command that toggles the star for a change. */
+  public static KeyCommand newKeyCommand(final Icon icon) {
+    return new KeyCommand(0, 's', Util.C.changeTableStar()) {
+      @Override
+      public void onKeyPress(KeyPressEvent event) {
+        icon.toggleStar();
+      }
+    };
+  }
+
+  /** Add a handler to listen for starred status to change. */
+  public static HandlerRegistration addHandler(
+      Change.Id source,
+      ChangeStarHandler handler) {
+    return eventBus.addHandlerToSource(TYPE, source, handler);
+  }
+
+  /**
+   * Broadcast the current starred value of a change to UI widgets. This does
+   * not RPC to the server and does not alter the starred status of a change.
+   */
+  public static void fireChangeStarEvent(Change.Id id, boolean starred) {
+    eventBus.fireEventFromSource(
+        new ChangeStarEvent(id, starred),
+        id);
+  }
+
+  /**
+   * Set the starred status of a change. This method broadcasts to all
+   * interested UI widgets and sends an RPC to the server to record the
+   * updated status.
+   */
+  public static void toggleStar(
+      final Change.Id changeId,
+      final boolean newValue) {
+    if (next == null) {
+      next = new ToggleStarRequest();
+    }
+    next.toggle(changeId, newValue);
+    fireChangeStarEvent(changeId, newValue);
+    if (!busy) {
+      start();
+    }
+  }
+
+  private static ToggleStarRequest next;
+  private static boolean busy;
+
+  private static void start() {
+    final ToggleStarRequest req = next;
+    next = null;
+    busy = true;
+
+    Util.LIST_SVC.toggleStars(req, new GerritCallback<VoidResult>() {
+      @Override
+      public void onSuccess(VoidResult result) {
+        if (next != null) {
+          start();
+        } else {
+          busy = false;
+        }
+      }
+
+      @Override
+      public void onFailure(Throwable caught) {
+        rollback(req);
+        if (next != null) {
+          rollback(next);
+          next = null;
+        }
+        busy = false;
+        super.onFailure(caught);
+      }
+    });
+  }
+
+  private static void rollback(ToggleStarRequest req) {
+    if (req.getAddSet() != null) {
+      for (Change.Id id : req.getAddSet()) {
+        fireChangeStarEvent(id, false);
+      }
+    }
+    if (req.getRemoveSet() != null) {
+      for (Change.Id id : req.getRemoveSet()) {
+        fireChangeStarEvent(id, true);
+      }
+    }
+  }
+
+  public static class Icon extends Image
+      implements ChangeStarHandler, ClickHandler {
+    private final Change.Id changeId;
+    private boolean starred;
+    private HandlerRegistration handler;
+
+    Icon(Change.Id changeId, boolean starred) {
+      super(resource(starred));
+      this.changeId = changeId;
+      this.starred = starred;
+      addClickHandler(this);
+    }
+
+    /**
+     * Toggles the state of the star, as if the user clicked on the image. This
+     * will broadcast the new star status to all interested UI widgets, and RPC
+     * to the server to store the changed value.
+     */
+    public void toggleStar() {
+      StarredChanges.toggleStar(changeId, !starred);
+    }
+
+    @Override
+    protected void onLoad() {
+      handler = StarredChanges.addHandler(changeId, this);
+    }
+
+    @Override
+    protected void onUnload() {
+      handler.removeHandler();
+      handler = null;
+    }
+
+    @Override
+    public void onChangeStar(ChangeStarEvent event) {
+      setResource(resource(event.isStarred()));
+      starred = event.isStarred();
+    }
+
+    @Override
+    public void onClick(ClickEvent event) {
+      toggleStar();
+    }
+
+    private static ImageResource resource(boolean starred) {
+      return starred ? Gerrit.RESOURCES.starFilled() : Gerrit.RESOURCES.starOpen();
+    }
+  }
+
+  private StarredChanges() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/Util.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/Util.java
index e84cac8..590ad87 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/Util.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/Util.java
@@ -30,6 +30,10 @@
   public static final ChangeListService LIST_SVC;
   public static final ChangeManageService MANAGE_SVC;
 
+  private static final int SUBJECT_MAX_LENGTH = 80;
+  private static final String SUBJECT_CROP_APPENDIX = "...";
+  private static final int SUBJECT_CROP_RANGE = 10;
+
   static {
     DETAIL_SVC = GWT.create(ChangeDetailService.class);
     JsonUtil.bind(DETAIL_SVC, "rpc/ChangeDetailService");
@@ -60,4 +64,40 @@
         return status.name();
     }
   }
+
+  /**
+   * Crops the given change subject if needed so that it has at most
+   * {@link #SUBJECT_MAX_LENGTH} characters.
+   *
+   * If the given subject is not longer than {@link #SUBJECT_MAX_LENGTH}
+   * characters it is returned unchanged.
+   *
+   * If the length of the given subject exceeds {@link #SUBJECT_MAX_LENGTH}
+   * characters it is cropped. In this case {@link #SUBJECT_CROP_APPENDIX} is
+   * appended to the cropped subject, the cropped subject including the appendix
+   * has at most {@link #SUBJECT_MAX_LENGTH} characters.
+   *
+   * If cropping is needed, the subject will be cropped after the last space
+   * character that is found within the last {@link #SUBJECT_CROP_RANGE}
+   * characters of the potentially visible characters. If no such space is
+   * found, the subject will be cropped so that the cropped subject including
+   * the appendix has exactly {@link #SUBJECT_MAX_LENGTH} characters.
+   *
+   * @return the subject, cropped if needed
+   */
+  @SuppressWarnings("deprecation")
+  public static String cropSubject(final String subject) {
+    if (subject.length() > SUBJECT_MAX_LENGTH) {
+      final int maxLength = SUBJECT_MAX_LENGTH - SUBJECT_CROP_APPENDIX.length();
+      for (int cropPosition = maxLength; cropPosition > maxLength - SUBJECT_CROP_RANGE; cropPosition--) {
+        // Character.isWhitespace(char) can't be used because this method is not supported by GWT,
+        // see https://developers.google.com/web-toolkit/doc/1.6/RefJreEmulation#Package_java_lang
+        if (Character.isSpace(subject.charAt(cropPosition - 1))) {
+          return subject.substring(0, cropPosition) + SUBJECT_CROP_APPENDIX;
+        }
+      }
+      return subject.substring(0, maxLength) + SUBJECT_CROP_APPENDIX;
+    }
+    return subject;
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/downloadIcon.png b/gerrit-gwtui/src/main/java/com/google/gerrit/client/downloadIcon.png
new file mode 100644
index 0000000..22ff495
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/downloadIcon.png
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css
index 1081e47..7512d8c 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
@@ -28,6 +28,7 @@
 @external .gwt-InlineHyperlink;
 @external .gwt-RadioButton;
 
+@external .searchPanel;
 @external .smallHeading;
 @external .wdi;
 @external .wdd;
@@ -43,7 +44,9 @@
 @eval textColor com.google.gerrit.client.Gerrit.getTheme().textColor;
 @eval trimColor com.google.gerrit.client.Gerrit.getTheme().trimColor;
 @eval selectionColor com.google.gerrit.client.Gerrit.getTheme().selectionColor;
-
+@eval changeTableOutdatedColor com.google.gerrit.client.Gerrit.getTheme().changeTableOutdatedColor;
+@eval tableOddRowColor com.google.gerrit.client.Gerrit.getTheme().tableOddRowColor;
+@eval tableEvenRowColor com.google.gerrit.client.Gerrit.getTheme().tableEvenRowColor;
 
 @sprite .greenCheckClass {
   gwt-image: "greenCheck";
@@ -375,6 +378,18 @@
   width: 100%;
   margin-top: 15px;
 }
+.errorDialogText {
+  font-size: 15px;
+  font-family: verdana;
+}
+.errorDialog a,
+.errorDialog a:visited,
+.errorDialog a:hover {
+  color: white;
+  font-weight: bold;
+  font-size: 15px;
+  font-family: verdana;
+}
 
 
 /** Screen **/
@@ -389,6 +404,9 @@
   overflow: hidden;
 }
 
+.screenNoHeader {
+  margin-top: 5px;
+}
 
 /** ChangeTable **/
 .changeTable {
@@ -396,8 +414,16 @@
   border-spacing: 0;
 }
 
+.changeTable tr:nth-child\(even\) {
+  background: tableEvenRowColor;
+}
+
+.changeTable tr:nth-child\(odd\) {
+  background: tableOddRowColor;
+}
+
 .changeTable .outdated {
-  background: red;
+  background: changeTableOutdatedColor !important;
 }
 
 .changeTable .iconCell {
@@ -467,7 +493,6 @@
 
 .accountDashboard.changeTable tr {
   color: #444444;
-  background: #F6F6F6;
 }
 .accountDashboard.changeTable tr a {
   color: #444444;
@@ -477,13 +502,12 @@
 .accountDashboard.changeTable .needsReview a {
   font-weight: bold;
   color: textColor;
-  background: backgroundColor;
 }
 
 .changeTable .activeRow,
 .accountDashboard.changeTable .activeRow,
 .accountDashboard.changeTable .activeRow a {
-  background: selectionColor;
+  background: selectionColor !important;
 }
 
 .changeTable .cID {
@@ -662,11 +686,17 @@
   white-space: pre;
   width: 3.5em;
   text-align: right;
-  border-right: thin solid #b0bdcc;
   padding-right: 0.2em;
   background: white;
   border-bottom: 1px solid white;
 }
+.lineNumber.rightmost {
+  border-left: thin solid #b0bdcc;
+}
+.lineNumber a {
+  color: #888;
+  text-decoration: none;
+}
 .patchContentTable td.fileColumnHeader {
   background: trimColor;
   font-family: norm-font;
@@ -689,6 +719,7 @@
   padding-left: 0;
   padding-right: 0;
   white-space: pre;
+  border-left: thin solid #b0bdcc;
 }
 .fileLineNone {
   background: #eeeeee;
@@ -723,11 +754,21 @@
   font-family: norm-font;
   text-align: center;
   font-style: italic;
-  background: lightblue;
+  background: #def;
+  color: grey;
 }
 .patchContentTable .skipLine div {
   display: inline;
 }
+.patchContentTable a.skipLine {
+  color: grey;
+  text-decoration: none;
+}
+.patchContentTable a:hover.skipLine {
+  background: white;
+  color: #00A;
+  text-decoration: underline;
+}
 
 .patchContentTable .activeRow .iconCell,
 .patchContentTable .activeRow .lineNumber {
@@ -882,15 +923,6 @@
   font-weight: bold;
 }
 
-.infoBlock td.permalink {
-  border-right: 1px none;
-  border-bottom: 1px none;
-  text-align: right;
-}
-.infoBlock td.permalink div div {
-  display: inline;
-}
-
 .infoBlock td.useridentity {
   white-space: nowrap;
 }
@@ -923,7 +955,7 @@
   margin-top: 5px;
 }
 
-.changeScreen .approvalTable {
+.approvalTable {
   margin-top: 1em;
   margin-bottom: 1em;
 }
@@ -1019,6 +1051,10 @@
   display: table;
 }
 
+.sideBySideScreenSideBySideTable .fileLine {
+  width: 50%;
+}
+
 .sideBySideScreenLinkTable {
   width: 100%;
 }
@@ -1082,6 +1118,9 @@
 .accountInfoBlock {
   margin-bottom: 10px;
 }
+.accountInfoBlock .gwt-Button {
+  margin-left: 10px;
+}
 .accountContactPrivacyDetails {
   margin-left: 10px;
   margin-top: 5px;
@@ -1110,7 +1149,7 @@
   padding: 5px 5px 5px 5px;
 }
 
-.createProjectLink {
+.createGroupLink {
   margin-bottom: 10px;
 }
 
@@ -1354,3 +1393,7 @@
   font-style: italic;
   padding: 2px 6px 1px;
 }
+
+/** PluginListScreen **/
+.pluginsTable {
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/AbstractPatchContentTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/AbstractPatchContentTable.java
index 8282caa..24a2ae5 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/AbstractPatchContentTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/AbstractPatchContentTable.java
@@ -516,22 +516,6 @@
         Gerrit.RESOURCES.css().iconCell());
   }
 
-  protected void addStyle(final int row, final int col, final String style) {
-    table.getCellFormatter().addStyleName(row, col, style);
-  }
-
-  protected void removeRow(final int row) {
-    table.removeRow(row);
-  }
-
-  protected void setHtml(final int row, final int col, final String html) {
-    table.setHTML(row, col, html);
-  }
-
-  protected void setWidget(final int row, final int col, final Widget widget) {
-    table.setWidget(row, col, widget);
-  }
-
   @Override
   protected void onOpenRow(final int row) {
     final Object item = getRowItem(row);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.java
index fd34729..4801e65 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.java
@@ -31,6 +31,7 @@
   String patchHeaderPatchSet();
   String patchHeaderOld();
   String patchHeaderNew();
+  String patchSet();
 
   String patchHistoryTitle();
   String disabledOnLargeFiles();
@@ -47,6 +48,9 @@
   String fileList();
   String expandComment();
 
+  String toggleReviewed();
+  String markAsReviewedAndGoToNext();
+
   String commentEditorSet();
   String commentInsert();
   String commentSaveDraft();
@@ -60,7 +64,8 @@
   String previousFileHelp();
   String nextFileHelp();
 
-  String reviewed();
+  String reviewedAnd();
+  String next();
   String download();
 
   String buttonReplyDone();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.properties
index 6a1dbc1..11823ac 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchConstants.properties
@@ -15,6 +15,7 @@
 patchHeaderOld = Old Version
 patchHeaderNew = New Version
 patchHistoryTitle = Patch History
+patchSet = Patch Set
 disabledOnLargeFiles = Disabled on very large source files.
 intralineFailure = Intraline difference not available due to server error.
 illegalNumberOfColumns = The number of columns cannot be zero or negative
@@ -29,6 +30,9 @@
 fileList = Browse files in patch set
 expandComment = Expand or collapse comment
 
+toggleReviewed = Toggle the reviewed flag
+markAsReviewedAndGoToNext = Mark patch as reviewed and go to next unreviewed patch
+
 commentEditorSet = Comment Editing
 commentInsert = Create a new inline comment
 commentSaveDraft = Save draft comment
@@ -42,11 +46,12 @@
 previousFileHelp = Previous file
 nextFileHelp = Next file
 
-reviewed = Reviewed
-download = (Download)
+reviewedAnd = Reviewed &
+next = next
+download = Download
 
 fileTypeSymlink = Type: Symbolic Link
 fileTypeGitlink = Type: Git Commit in Subproject
 
-patchSkipRegionStart = (... skipping
-patchSkipRegionEnd = common lines ...)
+patchSkipRegionStart = ... skipped
+patchSkipRegionEnd = common lines ...
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchMessages.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchMessages.properties
index 52dcba2..2d01e24 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchMessages.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchMessages.properties
@@ -1,4 +1,3 @@
-expandBefore = Expand {0} before
-expandAfter = Expand {0} after
-
+expandBefore = +{0}&#x21e7;
+expandAfter = +{0}&#x21e9;
 draftSaved = Draft saved at {0,time,short}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchMessages_en.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchMessages_en.properties
index 58c4f6c..2f3c20a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchMessages_en.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchMessages_en.properties
@@ -1,2 +1,2 @@
-expandBefore = Expand {0} before
-expandAfter = Expand {0} after
+expandBefore = +{0}&#x21e7;
+expandAfter = +{0}&#x21e9;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScreen.java
index ffc8960..4f8731c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScreen.java
@@ -20,9 +20,12 @@
 import com.google.gerrit.client.RpcStatus;
 import com.google.gerrit.client.changes.CommitMessageBlock;
 import com.google.gerrit.client.changes.PatchTable;
+import com.google.gerrit.client.changes.PatchTable.PatchValidator;
 import com.google.gerrit.client.changes.Util;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
+import com.google.gerrit.client.ui.ChangeLink;
+import com.google.gerrit.client.ui.InlineHyperlink;
 import com.google.gerrit.client.ui.ListenableAccountDiffPreference;
 import com.google.gerrit.client.ui.Screen;
 import com.google.gerrit.common.data.PatchScript;
@@ -34,17 +37,22 @@
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gwt.core.client.Scheduler;
 import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.event.dom.client.KeyPressEvent;
 import com.google.gwt.event.logical.shared.ValueChangeEvent;
 import com.google.gwt.event.logical.shared.ValueChangeHandler;
 import com.google.gwt.event.shared.HandlerRegistration;
 import com.google.gwtjsonrpc.common.AsyncCallback;
+import com.google.gwt.user.client.ui.Anchor;
 import com.google.gwt.user.client.ui.CheckBox;
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.Label;
 import com.google.gwtexpui.globalkey.client.GlobalKey;
 import com.google.gwtexpui.globalkey.client.KeyCommand;
 import com.google.gwtexpui.globalkey.client.KeyCommandSet;
+import com.google.gwtexpui.safehtml.client.SafeHtml;
+import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
 import com.google.gwtjsonrpc.common.VoidResult;
 
 public abstract class PatchScreen extends Screen implements
@@ -102,10 +110,13 @@
   protected PatchScriptSettingsPanel settingsPanel;
   protected TopView topView;
 
-  private CheckBox reviewed;
+  private CheckBox reviewedCheckBox;
+  private FlowPanel reviewedPanel;
+  private InlineHyperlink reviewedLink;
   private HistoryTable historyTable;
   private FlowPanel topPanel;
   private FlowPanel contentPanel;
+  private PatchTableHeader header;
   private Label noDifference;
   private AbstractPatchContentTable contentTable;
   private CommitMessageBlock commitMessageBlock;
@@ -121,7 +132,9 @@
 
   /** Keys that cause an action on this screen */
   private KeyCommandSet keysNavigation;
+  private KeyCommandSet keysAction;
   private HandlerRegistration regNavigation;
+  private HandlerRegistration regAction;
   private boolean intralineFailure;
 
   /**
@@ -143,15 +156,6 @@
     idSideB = id.getParentKey();
     this.patchIndex = patchIndex;
 
-    reviewed = new CheckBox(Util.C.reviewed());
-    reviewed.addValueChangeHandler(
-        new ValueChangeHandler<Boolean>() {
-          @Override
-          public void onValueChange(ValueChangeEvent<Boolean> event) {
-            setReviewedByCurrentUser(event.getValue());
-          }
-        });
-
     prefs = fileList != null ? fileList.getPreferences() :
                                new ListenableAccountDiffPreference();
     if (Gerrit.isSignedIn()) {
@@ -165,9 +169,63 @@
           }
         });
 
+    reviewedPanel = new FlowPanel();
     settingsPanel = new PatchScriptSettingsPanel(prefs);
   }
 
+  private void populateReviewedPanel(){
+    reviewedPanel.clear();
+
+    reviewedCheckBox = new CheckBox(PatchUtil.C.reviewedAnd() + " ");
+    reviewedCheckBox.addValueChangeHandler(new ValueChangeHandler<Boolean>() {
+      @Override
+      public void onValueChange(ValueChangeEvent<Boolean> event) {
+        setReviewedByCurrentUser(event.getValue());
+      }
+    });
+
+    reviewedPanel.add(reviewedCheckBox);
+    reviewedPanel.add(getReviewedAnchor());
+  }
+
+  private Anchor getReviewedAnchor() {
+    SafeHtmlBuilder text = new SafeHtmlBuilder();
+    text.append(PatchUtil.C.next());
+    text.append(SafeHtml.asis(Util.C.nextPatchLinkIcon()));
+
+    Anchor reviewedAnchor = new Anchor("");
+    SafeHtml.set(reviewedAnchor, text);
+
+    final PatchValidator unreviewedValidator = new PatchValidator() {
+      public boolean isValid(Patch patch) {
+        return !patch.isReviewedByCurrentUser();
+      }
+    };
+
+    int nextUnreviewedPatchIndex =
+        fileList.getNextPatch(patchIndex, true, unreviewedValidator,
+            fileList.PREFERENCE_VALIDATOR);
+
+    if (nextUnreviewedPatchIndex > -1) {
+      // Create invisible patch link to change page
+      reviewedLink =
+          fileList.createLink(nextUnreviewedPatchIndex, getPatchScreenType(),
+              null, null);
+      reviewedLink.setText("");
+    } else {
+      reviewedLink = new ChangeLink("", patchKey.getParentKey());
+    }
+    reviewedAnchor.addClickHandler(new ClickHandler() {
+      @Override
+      public void onClick(ClickEvent event) {
+        setReviewedByCurrentUser(true);
+        reviewedLink.go();
+      }
+    });
+
+    return reviewedAnchor;
+  }
+
   @Override
   public void notifyDraftDelta(int delta) {
     lastScript = null;
@@ -180,9 +238,9 @@
 
   private void update(AccountDiffPreference dp) {
     // Did the user just turn on auto-review?
-    if (!reviewed.getValue() && prefs.getOld().isManualReview()
+    if (!reviewedCheckBox.getValue() && prefs.getOld().isManualReview()
         && !dp.isManualReview()) {
-      reviewed.setValue(true);
+      reviewedCheckBox.setValue(true);
       setReviewedByCurrentUser(true);
     }
 
@@ -236,13 +294,21 @@
     super.onInitUI();
 
     if (Gerrit.isSignedIn()) {
-      setTitleFarEast(reviewed);
+      setTitleFarEast(reviewedPanel);
     }
 
     keysNavigation = new KeyCommandSet(Gerrit.C.sectionNavigation());
     keysNavigation.add(new UpToChangeCommand(patchKey.getParentKey(), 0, 'u'));
     keysNavigation.add(new FileListCmd(0, 'f', PatchUtil.C.fileList()));
 
+    if (Gerrit.isSignedIn()) {
+      keysAction = new KeyCommandSet(Gerrit.C.sectionActions());
+      keysAction
+          .add(new ToggleReviewedCmd(0, 'm', PatchUtil.C.toggleReviewed()));
+      keysAction.add(new MarkAsReviewedAndGoToNextCmd(0, 'M', PatchUtil.C
+          .markAsReviewedAndGoToNext()));
+    }
+
     historyTable = new HistoryTable(this);
 
     commitMessageBlock = new CommitMessageBlock();
@@ -250,6 +316,8 @@
     topPanel = new FlowPanel();
     add(topPanel);
 
+    header = new PatchTableHeader(getPatchScreenType());
+
     noDifference = new Label(PatchUtil.C.noDifference());
     noDifference.setStyleName(Gerrit.RESOURCES.css().patchNoDifference());
     noDifference.setVisible(false);
@@ -264,6 +332,7 @@
     contentPanel = new FlowPanel();
     contentPanel.setStyleName(Gerrit.RESOURCES.css()
         .sideBySideScreenSideBySideTable());
+    contentPanel.add(header);
     contentPanel.add(noDifference);
     contentPanel.add(contentTable);
     add(contentPanel);
@@ -297,6 +366,7 @@
   @Override
   protected void onLoad() {
     super.onLoad();
+
     if (patchSetDetail == null) {
       Util.DETAIL_SVC.patchSetDetail(idSideB,
           new GerritCallback<PatchSetDetail>() {
@@ -322,6 +392,10 @@
       regNavigation.removeHandler();
       regNavigation = null;
     }
+    if (regAction != null) {
+      regAction.removeHandler();
+      regAction = null;
+    }
     super.onUnload();
   }
 
@@ -334,6 +408,13 @@
       regNavigation = null;
     }
     regNavigation = GlobalKey.add(this, keysNavigation);
+    if (regAction != null) {
+      regAction.removeHandler();
+      regAction = null;
+    }
+    if (keysAction != null) {
+      regAction = GlobalKey.add(this, keysAction);
+    }
   }
 
   protected abstract AbstractPatchContentTable createContentTable();
@@ -368,6 +449,10 @@
     final int rpcseq = ++rpcSequence;
     lastScript = null;
     settingsPanel.setEnabled(false);
+    populateReviewedPanel();
+    if (isFirst && fileList != null) {
+      fileList.movePointerTo(patchKey);
+    }
     PatchUtil.DETAIL_SVC.patchScript(patchKey, idSideA, idSideB, //
         settingsPanel.getValue(), new ScreenLoadCallback<PatchScript>(this) {
           @Override
@@ -435,6 +520,8 @@
       setToken(Dispatcher.toPatchUnified(idSideA, patchKey));
     }
 
+    header.display(patchSetDetail, script, patchKey, idSideA, idSideB);
+
     if (hasDifferences) {
       contentTable.display(patchKey, idSideA, idSideB, script);
       contentTable.display(script.getCommentDetail(), script.isExpandAllComments());
@@ -464,7 +551,7 @@
           }
         }
       }
-      reviewed.setValue(isReviewed);
+      reviewedCheckBox.setValue(isReviewed);
     }
 
     intralineFailure = isFirst && script.hasIntralineFailure();
@@ -526,4 +613,31 @@
       p.open();
     }
   }
+
+  public class ToggleReviewedCmd extends KeyCommand {
+    public ToggleReviewedCmd(int mask, int key, String help) {
+      super(mask, key, help);
+    }
+
+    @Override
+    public void onKeyPress(final KeyPressEvent event) {
+      final boolean isReviewed = !reviewedCheckBox.getValue();
+      reviewedCheckBox.setValue(isReviewed);
+      setReviewedByCurrentUser(isReviewed);
+    }
+  }
+
+  public class MarkAsReviewedAndGoToNextCmd extends KeyCommand {
+    public MarkAsReviewedAndGoToNextCmd(int mask, int key, String help) {
+      super(mask, key, help);
+    }
+
+    @Override
+    public void onKeyPress(final KeyPressEvent event) {
+      if (reviewedLink != null) {
+        setReviewedByCurrentUser(true);
+        reviewedLink.go();
+      }
+    }
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.java
index 871eb2b..a689259 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.java
@@ -76,6 +76,9 @@
   CheckBox whitespaceErrors;
 
   @UiField
+  CheckBox showLineEndings;
+
+  @UiField
   CheckBox showTabs;
 
   @UiField
@@ -210,6 +213,7 @@
     colWidth.setIntValue(dp.getLineLength());
     intralineDifference.setValue(dp.isIntralineDifference());
     whitespaceErrors.setValue(dp.isShowWhitespaceErrors());
+    showLineEndings.setValue(dp.isShowLineEndings());
     showTabs.setValue(dp.isShowTabs());
     skipDeleted.setValue(dp.isSkipDeleted());
     skipUncommented.setValue(dp.isSkipUncommented());
@@ -242,6 +246,7 @@
     dp.setSyntaxHighlighting(syntaxHighlighting.getValue());
     dp.setIntralineDifference(intralineDifference.getValue());
     dp.setShowWhitespaceErrors(whitespaceErrors.getValue());
+    dp.setShowLineEndings(showLineEndings.getValue());
     dp.setShowTabs(showTabs.getValue());
     dp.setSkipDeleted(skipDeleted.getValue());
     dp.setSkipUncommented(skipUncommented.getValue());
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.ui.xml
index 2c7afff..586b767 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.ui.xml
@@ -108,8 +108,8 @@
       </g:CheckBox>
       <br/>
       <g:CheckBox
-          ui:field='showTabs'
-          text='Show Tabs'
+          ui:field='showLineEndings'
+          text='Show Line Endings'
           tabIndex='8'>
         <ui:attribute name='text'/>
       </g:CheckBox>
@@ -117,15 +117,15 @@
 
     <td rowspan='2'>
       <g:CheckBox
-          ui:field='expandAllComments'
-          text='Expand All Comments'
+          ui:field='showTabs'
+          text='Show Tabs'
           tabIndex='9'>
         <ui:attribute name='text'/>
       </g:CheckBox>
       <br/>
       <g:CheckBox
-          ui:field='retainHeader'
-          text='Retain Header On File Switch'
+          ui:field='expandAllComments'
+          text='Expand All Comments'
           tabIndex='10'>
         <ui:attribute name='text'/>
       </g:CheckBox>
@@ -133,15 +133,15 @@
 
     <td rowspan='2'>
       <g:CheckBox
-          ui:field='skipUncommented'
-          text='Skip Uncommented Files'
+          ui:field='retainHeader'
+          text='Retain Header On File Switch'
           tabIndex='11'>
         <ui:attribute name='text'/>
       </g:CheckBox>
       <br/>
       <g:CheckBox
-          ui:field='skipDeleted'
-          text='Skip Deleted Files'
+          ui:field='skipUncommented'
+          text='Skip Uncommented Files'
           tabIndex='12'>
         <ui:attribute name='text'/>
       </g:CheckBox>
@@ -149,9 +149,16 @@
 
     <td valign='bottom' rowspan='2'>
       <g:CheckBox
+          ui:field='skipDeleted'
+          text='Skip Deleted Files'
+          tabIndex='13'>
+        <ui:attribute name='text'/>
+      </g:CheckBox>
+      <br/>
+      <g:CheckBox
           ui:field='manualReview'
           text='Manual Review'
-          tabIndex='13'>
+          tabIndex='14'>
         <ui:attribute name='text'/>
       </g:CheckBox>
     </td>
@@ -162,14 +169,14 @@
           ui:field='update'
           text='Update'
           styleName='{style.updateButton}'
-          tabIndex='14'>
+          tabIndex='15'>
         <ui:attribute name='text'/>
       </g:Button>
       <g:Button
           ui:field='save'
           text='Save'
           styleName='{style.updateButton}'
-          tabIndex='15'>
+          tabIndex='16'>
         <ui:attribute name='text'/>
       </g:Button>
     </td>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchSetSelectBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchSetSelectBox.java
new file mode 100644
index 0000000..afaf7fd
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchSetSelectBox.java
@@ -0,0 +1,189 @@
+// 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.patches;
+
+import com.google.gerrit.client.Dispatcher;
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.common.data.PatchScript;
+import com.google.gerrit.common.data.PatchSetDetail;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.dom.client.DivElement;
+import com.google.gwt.dom.client.Style.Display;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.resources.client.CssResource;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.ui.Anchor;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwt.user.client.ui.Image;
+import com.google.gwtorm.client.KeyUtil;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class PatchSetSelectBox extends Composite {
+  interface Binder extends UiBinder<HTMLPanel, PatchSetSelectBox> {
+  }
+
+  private static Binder uiBinder = GWT.create(Binder.class);
+
+  interface BoxStyle extends CssResource {
+    String selected();
+
+    String hidden();
+
+    String downloadLink();
+  }
+
+  public enum Side {
+    A, B
+  }
+
+  PatchScript script;
+  Patch.Key patchKey;
+  PatchSet.Id idSideA;
+  PatchSet.Id idSideB;
+  PatchSet.Id idActive;
+  Side side;
+  PatchScreen.Type screenType;
+  Map<Integer, Anchor> links;
+
+  @UiField
+  HTMLPanel linkPanel;
+
+  @UiField
+  BoxStyle style;
+
+  @UiField
+  DivElement sideMarker;
+
+  public PatchSetSelectBox(Side side, final PatchScreen.Type type) {
+    this.side = side;
+    this.screenType = type;
+
+    initWidget(uiBinder.createAndBindUi(this));
+  }
+
+  public void display(final PatchSetDetail detail, final PatchScript script, Patch.Key key,
+      PatchSet.Id idSideA, PatchSet.Id idSideB) {
+    this.script = script;
+    this.patchKey = key;
+    this.idSideA = idSideA;
+    this.idSideB = idSideB;
+    this.idActive = (side == Side.A) ? idSideA : idSideB;
+    this.links = new HashMap<Integer, Anchor>();
+
+    linkPanel.clear();
+
+    if (screenType == PatchScreen.Type.UNIFIED) {
+      sideMarker.setInnerText((side == Side.A) ? "(-)" : "(+)");
+    } else {
+      sideMarker.getStyle().setDisplay(Display.NONE);
+    }
+
+    Anchor baseLink = null;
+    if (detail.getInfo().getParents().size() > 1) {
+      baseLink = createLink(PatchUtil.C.patchBaseAutoMerge(), null);
+    } else {
+      baseLink = createLink(PatchUtil.C.patchBase(), null);
+    }
+
+    links.put(0, baseLink);
+    if (screenType == PatchScreen.Type.UNIFIED || side == Side.A) {
+      linkPanel.add(baseLink);
+    }
+
+    if (side == Side.B) {
+      links.get(0).setStyleName(style.hidden());
+    }
+
+    for (Patch patch : script.getHistory()) {
+      PatchSet.Id psId = patch.getKey().getParentKey();
+      Anchor anchor = createLink(Integer.toString(psId.get()), psId);
+      links.put(psId.get(), anchor);
+      linkPanel.add(anchor);
+    }
+
+    if (idActive == null && side == Side.A) {
+      links.get(0).setStyleName(style.selected());
+    } else {
+      links.get(idActive.get()).setStyleName(style.selected());
+    }
+
+    Anchor downloadLink = createDownloadLink();
+    if (downloadLink != null) {
+      linkPanel.add(downloadLink);
+    }
+  }
+
+  private Anchor createLink(String label, final PatchSet.Id id) {
+    final Anchor anchor = new Anchor(label);
+    anchor.addClickHandler(new ClickHandler() {
+      @Override
+      public void onClick(ClickEvent event) {
+        if (side == Side.A) {
+          idSideA = id;
+        } else {
+          idSideB = id;
+        }
+
+        Patch.Key keySideB = new Patch.Key(idSideB, patchKey.get());
+
+        switch (screenType) {
+          case SIDE_BY_SIDE:
+            Gerrit.display(Dispatcher.toPatchSideBySide(idSideA, keySideB));
+            break;
+          case UNIFIED:
+            Gerrit.display(Dispatcher.toPatchUnified(idSideA, keySideB));
+            break;
+        }
+      }
+
+    });
+
+    return anchor;
+  }
+
+  private Anchor createDownloadLink() {
+    boolean isCommitMessage = Patch.COMMIT_MSG.equals(script.getNewName());
+
+    if (isCommitMessage || (side == Side.A && 0 >= script.getA().size())
+        || (side == Side.B && 0 >= script.getB().size())) {
+      return null;
+    }
+
+    Patch.Key key =
+        (idSideA == null) ? patchKey : (new Patch.Key(idSideA, patchKey.get()));
+
+    String sideURL = (side == Side.A) ? "1" : "0";
+    final String base = GWT.getHostPageBaseURL() + "cat/";
+
+    Image image = new Image(Gerrit.RESOURCES.downloadIcon());
+
+    final Anchor anchor = new Anchor();
+    anchor.setHref(base + KeyUtil.encode(key.toString()) + "^" + sideURL);
+    anchor.setTitle(PatchUtil.C.download());
+    anchor.setStyleName(style.downloadLink());
+    DOM.insertBefore(anchor.getElement(), image.getElement(),
+        DOM.getFirstChild(anchor.getElement()));
+
+    return anchor;
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchSetSelectBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchSetSelectBox.ui.xml
new file mode 100644
index 0000000..2fd183c
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchSetSelectBox.ui.xml
@@ -0,0 +1,98 @@
+<?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.
+-->
+
+<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
+    xmlns:g='urn:import:com.google.gwt.user.client.ui'>
+
+
+  <ui:with field='res' type='com.google.gerrit.client.GerritResources'/>
+  <ui:with field='cons' type='com.google.gerrit.client.patches.PatchConstants'/>
+  <ui:style type='com.google.gerrit.client.patches.PatchSetSelectBox.BoxStyle'>
+    @eval selectionColor com.google.gerrit.client.Gerrit.getTheme().selectionColor;
+    @eval trimColor com.google.gerrit.client.Gerrit.getTheme().trimColor;
+    @eval backgroundColor com.google.gerrit.client.Gerrit.getTheme().backgroundColor;
+
+    .wrapper {
+      width: 100%;
+      text-align: center;
+      font-size: 0; /* inline-block spacing fix */
+    }
+
+    .linkPanel {
+      display: inline-block;
+    }
+
+    .linkPanel > div {
+      display: inline-block;
+      float: left;
+    }
+
+    .linkPanel {
+      overflow: hidden; /* div clear fix */
+      font-size: 12px;
+    }
+
+    .linkPanel > a {
+      padding: 3px;
+      display: inline-block;
+      text-decoration: none;
+      float: left;
+    }
+
+    .patchSetLabel {
+      font-weight: bold;
+      float: left;
+      padding: 3px;
+    }
+
+    .sideMarker {
+      padding: 3px;
+    }
+
+    .downloadLink {
+      float: left;
+      padding: 1px !important;
+      margin-left: 3px;
+    }
+
+    .downloadLink > a {
+      text-size: 0;
+    }
+
+    .selected {
+      font-weight: bold;
+      background-color: selectionColor;
+    }
+
+    .sideMarker {
+      font-family: monospace;
+      float: left;
+    }
+
+    .hidden {
+      visibility: hidden;
+    }
+  </ui:style>
+
+  <g:HTMLPanel styleName='wrapper'>
+    <g:HTMLPanel styleName='{style.linkPanel}' ui:field='linkPanel'>
+      <div class='{style.patchSetLabel}'><ui:text from="{cons.patchSet}" /></div>
+      <div class='{style.sideMarker}' ui:field='sideMarker'></div>
+    </g:HTMLPanel>
+  </g:HTMLPanel>
+</ui:UiBinder>
+
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchTableHeader.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchTableHeader.java
new file mode 100644
index 0000000..3dd8908
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchTableHeader.java
@@ -0,0 +1,71 @@
+// 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.patches;
+
+import com.google.gerrit.common.data.PatchScript;
+import com.google.gerrit.common.data.PatchSetDetail;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.uibinder.client.UiTemplate;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwt.user.client.ui.SimplePanel;
+
+public class PatchTableHeader extends Composite {
+
+  @UiTemplate("PatchTableHeaderSideBySide.ui.xml")
+  interface SideBySideBinder extends UiBinder<HTMLPanel, PatchTableHeader> {
+  }
+
+  @UiTemplate("PatchTableHeaderUnified.ui.xml")
+  interface UnifiedBinder extends UiBinder<HTMLPanel, PatchTableHeader> {
+  }
+
+  private static SideBySideBinder uiBinderS = GWT.create(SideBySideBinder.class);
+  private static UnifiedBinder uiBinderU = GWT.create(UnifiedBinder.class);
+
+  @UiField
+  SimplePanel sideAPanel;
+
+  @UiField
+  SimplePanel sideBPanel;
+
+  PatchSetSelectBox listA;
+  PatchSetSelectBox listB;
+
+  public PatchTableHeader(PatchScreen.Type type) {
+    listA = new PatchSetSelectBox(PatchSetSelectBox.Side.A, type);
+    listB = new PatchSetSelectBox(PatchSetSelectBox.Side.B, type);
+
+    if (type == PatchScreen.Type.SIDE_BY_SIDE) {
+      initWidget(uiBinderS.createAndBindUi(this));
+    } else {
+      initWidget(uiBinderU.createAndBindUi(this));
+    }
+
+    sideAPanel.add(listA);
+    sideBPanel.add(listB);
+  }
+
+
+  public void display(final PatchSetDetail detail, PatchScript script, final Patch.Key patchKey,
+      final PatchSet.Id idSideA, final PatchSet.Id idSideB) {
+    listA.display(detail, script, patchKey, idSideA, idSideB);
+    listB.display(detail, script, patchKey, idSideA, idSideB);
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchTableHeaderSideBySide.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchTableHeaderSideBySide.ui.xml
new file mode 100644
index 0000000..d6fd717
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchTableHeaderSideBySide.ui.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.
+-->
+
+<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
+    xmlns:g='urn:import:com.google.gwt.user.client.ui'>
+
+
+  <ui:style>
+    @eval trimColor com.google.gerrit.client.Gerrit.getTheme().trimColor;
+
+    .wrapper {
+      width: 100%;
+      background-color: trimColor;
+      overflow: hidden;
+      font-size: 0; /* inline-block spacing fix */
+    }
+
+    .wrapper .box {
+      width: 100%;
+      text-align: center;
+    }
+
+    .leftWrapper {
+      width: 50%;
+      float: left;
+    }
+
+    .rightWrapper {
+      width: 50%;
+      overflow: hidden;
+    }
+
+    .leftBox {
+      float:left;
+    }
+
+    .rightBox {
+      float: right;
+    }
+  </ui:style>
+
+  <g:HTMLPanel styleName="{style.wrapper}">
+    <div class='{style.leftWrapper}'>
+      <g:SimplePanel addStyleNames='{style.box} {style.leftBox}' ui:field='sideAPanel'/>
+    </div>
+    <div class='{style.rightWrapper}'>
+      <g:SimplePanel addStyleNames='{style.box} {style.rightBox}' ui:field='sideBPanel'/>
+    </div>
+  </g:HTMLPanel>
+</ui:UiBinder>
+
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchTableHeaderUnified.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchTableHeaderUnified.ui.xml
new file mode 100644
index 0000000..24acfa3
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchTableHeaderUnified.ui.xml
@@ -0,0 +1,42 @@
+<?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.
+-->
+
+<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
+    xmlns:g='urn:import:com.google.gwt.user.client.ui'>
+
+  <ui:style>
+    @eval trimColor com.google.gerrit.client.Gerrit.getTheme().trimColor;
+
+    .wrapper {
+      width: 100%;
+      background-color: trimColor;
+      font-size: 0; /* inline-block spacing fix */
+    }
+
+    .wrapper .box {
+      width: 100%;
+      text-align: left;
+      margin-left: 3px;
+    }
+  </ui:style>
+
+  <g:HTMLPanel styleName="{style.wrapper}">
+    <g:SimplePanel addStyleNames='{style.box}' ui:field='sideAPanel'/>
+    <g:SimplePanel addStyleNames='{style.box}' ui:field='sideBPanel'/>
+  </g:HTMLPanel>
+</ui:UiBinder>
+
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/SideBySideTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/SideBySideTable.java
index 6379e23..ec63a83 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/SideBySideTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/SideBySideTable.java
@@ -25,21 +25,16 @@
 import com.google.gerrit.common.data.PatchScript.FileMode;
 import com.google.gerrit.prettify.common.EditList;
 import com.google.gerrit.prettify.common.SparseHtmlFile;
-import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
-import com.google.gerrit.reviewdb.client.Patch.ChangeType;
-import com.google.gwt.core.client.GWT;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.user.client.ui.Anchor;
 import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.HTMLTable.Cell;
 import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
-import com.google.gwt.user.client.ui.Label;
+import com.google.gwt.user.client.ui.HasVerticalAlignment;
+import com.google.gwt.user.client.ui.InlineLabel;
 import com.google.gwtexpui.safehtml.client.SafeHtml;
 import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
-import com.google.gwtorm.client.KeyUtil;
-
 import org.eclipse.jgit.diff.Edit;
 
 import java.util.ArrayList;
@@ -47,9 +42,8 @@
 import java.util.List;
 
 public class SideBySideTable extends AbstractPatchContentTable {
-  private static final int COL_A = 2;
-  private static final int COL_B = 4;
-
+  private static final int A = 2;
+  private static final int B = 3;
   private static final int NUM_ROWS_TO_EXPAND = 10;
 
   private SparseHtmlFile a;
@@ -59,24 +53,17 @@
   protected void onCellDoubleClick(final int row, int column) {
     if (column > 0 && getRowItem(row) instanceof PatchLine) {
       final PatchLine line = (PatchLine) getRowItem(row);
-      final short file = (short) ((column - 1) / 2);
-      if (column < (1 + file * 2 + 1)) {
-        column++;
-      }
-      switch (file) {
-        case 0:
-          createCommentEditor(row + 1, column, line.getLineA(), file);
-          break;
-        case 1:
-          createCommentEditor(row + 1, column, line.getLineB(), file);
-          break;
+      if (column == 1 || column == A) {
+        createCommentEditor(row + 1, A, line.getLineA(), (short) 0);
+      } else if (column == B || column == 4) {
+        createCommentEditor(row + 1, B, line.getLineB(), (short) 1);
       }
     }
   }
 
   @Override
   protected void onCellSingleClick(int row, int column) {
-    if (column == 1 || column == 3) {
+    if (column == 1 || column == 4) {
       onCellDoubleClick(row, column);
     }
   }
@@ -84,7 +71,7 @@
   @Override
   protected void onInsertComment(final PatchLine line) {
     final int row = getCurrentRow();
-    createCommentEditor(row + 1, 4, line.getLineB(), (short) 1);
+    createCommentEditor(row + 1, B, line.getLineB(), (short) 1);
   }
 
   @Override
@@ -97,10 +84,8 @@
         script.getDiffPrefs().isIntralineDifference()
             && script.hasIntralineDifference();
 
-    appendHeader(script, nc);
-    lines.add(null);
-
-    if(script.getFileModeA()!=FileMode.FILE||script.getFileModeB()!=FileMode.FILE){
+    if (script.getFileModeA() != FileMode.FILE
+        || script.getFileModeB() != FileMode.FILE) {
       openLine(nc);
       appendModeLine(nc, script.getFileModeA());
       appendModeLine(nc, script.getFileModeB());
@@ -121,13 +106,14 @@
         if (hunk.isContextLine()) {
           openLine(nc);
           final SafeHtml ctx = a.getSafeHtmlLine(hunk.getCurA());
-          appendLineText(nc, hunk.getCurA(), CONTEXT, ctx, false, false);
+          appendLineNumber(nc, hunk.getCurA(), false);
+          appendLineText(nc, CONTEXT, ctx, false, false);
           if (ignoreWS && b.contains(hunk.getCurB())) {
-            appendLineText(nc, hunk.getCurB(), CONTEXT, b, hunk.getCurB(),
-                false);
+            appendLineText(nc, CONTEXT, b, hunk.getCurB(), false);
           } else {
-            appendLineText(nc, hunk.getCurB(), CONTEXT, ctx, false, false);
+            appendLineText(nc, CONTEXT, ctx, false, false);
           }
+          appendLineNumber(nc, hunk.getCurB(), true);
           closeLine(nc);
           hunk.incBoth();
           lines.add(new PatchLine(CONTEXT, hunk.getCurA(), hunk.getCurB()));
@@ -140,21 +126,27 @@
           openLine(nc);
 
           if (del) {
-            appendLineText(nc, hunk.getCurA(), DELETE, a, hunk.getCurA(), full);
+            appendLineNumber(nc, hunk.getCurA(), false);
+            appendLineText(nc, DELETE, a, hunk.getCurA(), full);
             hunk.incA();
           } else if (hunk.getCurEdit().getType() == Edit.Type.REPLACE) {
+            appendLineNumber(nc, false);
             appendLineNone(nc, DELETE);
           } else {
+            appendLineNumber(nc, false);
             appendLineNone(nc, CONTEXT);
           }
 
           if (ins) {
-            appendLineText(nc, hunk.getCurB(), INSERT, b, hunk.getCurB(), full);
+            appendLineText(nc, INSERT, b, hunk.getCurB(), full);
+            appendLineNumber(nc, hunk.getCurB(), true);
             hunk.incB();
           } else if (hunk.getCurEdit().getType() == Edit.Type.REPLACE) {
             appendLineNone(nc, INSERT);
+            appendLineNumber(nc, true);
           } else {
             appendLineNone(nc, CONTEXT);
+            appendLineNumber(nc, true);
           }
 
           closeLine(nc);
@@ -229,13 +221,13 @@
           final PatchLineComment ac = ai.next();
           final PatchLineComment bc = bi.next();
           insertRow(row);
-          bindComment(row, COL_A, ac, !ai.hasNext(), expandComments);
-          bindComment(row, COL_B, bc, !bi.hasNext(), expandComments);
+          bindComment(row, A, ac, !ai.hasNext(), expandComments);
+          bindComment(row, B, bc, !bi.hasNext(), expandComments);
           row++;
         }
 
-        row = finish(ai, row, COL_A, expandComments);
-        row = finish(bi, row, COL_B, expandComments);
+        row = finish(ai, row, A, expandComments);
+        row = finish(bi, row, B, expandComments);
       } else {
         row++;
       }
@@ -246,10 +238,10 @@
   protected void insertRow(final int row) {
     super.insertRow(row);
     final CellFormatter fmt = table.getCellFormatter();
-    fmt.addStyleName(row, COL_A - 1, Gerrit.RESOURCES.css().lineNumber());
-    fmt.addStyleName(row, COL_A, Gerrit.RESOURCES.css().diffText());
-    fmt.addStyleName(row, COL_B - 1, Gerrit.RESOURCES.css().lineNumber());
-    fmt.addStyleName(row, COL_B, Gerrit.RESOURCES.css().diffText());
+    fmt.addStyleName(row, A - 1, Gerrit.RESOURCES.css().lineNumber());
+    fmt.addStyleName(row, A, Gerrit.RESOURCES.css().diffText());
+    fmt.addStyleName(row, B, Gerrit.RESOURCES.css().diffText());
+    fmt.addStyleName(row, B + 1, Gerrit.RESOURCES.css().lineNumber());
   }
 
   private int finish(final Iterator<PatchLineComment> i, int row, final int col, boolean expandComment) {
@@ -262,65 +254,6 @@
     return row;
   }
 
-  private void appendHeader(PatchScript script, final SafeHtmlBuilder m) {
-    m.openTr();
-
-    m.openTd();
-    m.addStyleName(Gerrit.RESOURCES.css().iconCell());
-    m.addStyleName(Gerrit.RESOURCES.css().fileColumnHeader());
-    m.closeTd();
-
-    m.openTd();
-    m.addStyleName(Gerrit.RESOURCES.css().fileColumnHeader());
-    m.addStyleName(Gerrit.RESOURCES.css().lineNumber());
-    m.closeTd();
-
-    m.openTd();
-    m.setStyleName(Gerrit.RESOURCES.css().fileColumnHeader());
-    m.setAttribute("width", "50%");
-    if (script.getChangeType() == ChangeType.RENAMED
-        || script.getChangeType() == ChangeType.COPIED) {
-      m.append(script.getOldName());
-    } else {
-      m.append(PatchUtil.C.patchHeaderOld());
-    }
-    m.br();
-    if (0 < script.getA().size()) {
-      if (idSideA == null) {
-        downloadLink(m, patchKey, "1");
-      } else {
-        downloadLink(m, new Patch.Key(idSideA, patchKey.get()), "0");
-      }
-    }
-    m.closeTd();
-
-    m.openTd();
-    m.addStyleName(Gerrit.RESOURCES.css().fileColumnHeader());
-    m.addStyleName(Gerrit.RESOURCES.css().lineNumber());
-    m.closeTd();
-
-    m.openTd();
-    m.setStyleName(Gerrit.RESOURCES.css().fileColumnHeader());
-    m.setAttribute("width", "50%");
-    m.append(PatchUtil.C.patchHeaderNew());
-    m.br();
-    if (0 < script.getB().size()) {
-      downloadLink(m, new Patch.Key(idSideB, patchKey.get()), "0");
-    }
-    m.closeTd();
-
-    m.closeTr();
-  }
-
-  private void downloadLink(final SafeHtmlBuilder m, final Patch.Key key,
-      final String side) {
-    final String base = GWT.getHostPageBaseURL() + "cat/";
-    m.openAnchor();
-    m.setAttribute("href", base + KeyUtil.encode(key.toString()) + "^" + side);
-    m.append(PatchUtil.C.download());
-    m.closeAnchor();
-  }
-
   private void appendSkipLine(final SafeHtmlBuilder m, final int skipCnt) {
     m.openTr();
 
@@ -336,21 +269,21 @@
     m.closeTr();
   }
 
-  ClickHandler expandAllListener = new ClickHandler() {
+  private ClickHandler expandAllListener = new ClickHandler() {
     @Override
     public void onClick(ClickEvent event) {
       expand(event, 0);
     }
   };
 
-  ClickHandler expandBeforeListener = new ClickHandler() {
+  private ClickHandler expandBeforeListener = new ClickHandler() {
     @Override
     public void onClick(ClickEvent event) {
       expand(event, NUM_ROWS_TO_EXPAND);
     }
   };
 
-  ClickHandler expandAfterListener = new ClickHandler() {
+  private ClickHandler expandAfterListener = new ClickHandler() {
     @Override
     public void onClick(ClickEvent event) {
       expand(event, -NUM_ROWS_TO_EXPAND);
@@ -358,11 +291,11 @@
   };
 
   private void expand(ClickEvent event, final int numRows) {
-    Cell cell = table.getCellForEvent(event);
-    int row = cell.getRowIndex();
+    int row = table.getCellForEvent(event).getRowIndex();
     if (!(getRowItem(row) instanceof SkippedLine)) {
       return;
     }
+
     SkippedLine line = (SkippedLine) getRowItem(row);
     int loopTo = numRows;
     if (numRows == 0) {
@@ -374,29 +307,34 @@
     if (numRows < 0) {
       offset = 1;
     }
+
+    CellFormatter fmt = table.getCellFormatter();
     for (int i = 0 + offset; i < loopTo + offset; i++) {
-      insertRow(row + i);
+      // The overridden version of insertRow adds some css classes we don't
+      // want.
+      super.insertRow(row + i);
+      table.getRowFormatter().setVerticalAlign(row + i,
+          HasVerticalAlignment.ALIGN_TOP);
       int lineA = line.getStartA() + i;
       int lineB = line.getStartB() + i;
       if (numRows < 0) {
         lineA = line.getStartA() + line.getSize() + numRows + i - offset;
         lineB = line.getStartB() + line.getSize() + numRows + i - offset;
       }
-      setHtml(row + i, 1, "<a href=\"javascript:void(0)\">" + (lineA + 1)
-          + "</a>");
-      addStyle(row + i, 1, Gerrit.RESOURCES.css().lineNumber());
 
-      setHtml(row + i, 2, a.getSafeHtmlLine(lineA).asString());
-      addStyle(row + i, 2, Gerrit.RESOURCES.css().fileLine());
-      addStyle(row + i, 2, Gerrit.RESOURCES.css().fileLineCONTEXT());
+      table.setHTML(row + i, A - 1, "<a href=\"javascript:;\">" + (lineA + 1) + "</a>");
+      fmt.addStyleName(row + i, A - 1, Gerrit.RESOURCES.css().lineNumber());
 
-      setHtml(row + i, 3, "<a href=\"javascript:void(0)\">" + (lineB + 1)
-          + "</a>");
-      addStyle(row + i, 3, Gerrit.RESOURCES.css().lineNumber());
+      table.setHTML(row + i, A, a.getSafeHtmlLine(lineA).asString());
+      fmt.addStyleName(row + i, A, Gerrit.RESOURCES.css().fileLine());
+      fmt.addStyleName(row + i, A, Gerrit.RESOURCES.css().fileLineCONTEXT());
 
-      setHtml(row + i, 4, b.getSafeHtmlLine(lineB).asString());
-      addStyle(row + i, 4, Gerrit.RESOURCES.css().fileLine());
-      addStyle(row + i, 4, Gerrit.RESOURCES.css().fileLineCONTEXT());
+      table.setHTML(row + i, B, b.getSafeHtmlLine(lineB).asString());
+      fmt.addStyleName(row + i, B, Gerrit.RESOURCES.css().fileLine());
+      fmt.addStyleName(row + i, B, Gerrit.RESOURCES.css().fileLineCONTEXT());
+
+      table.setHTML(row + i, B + 1, "<a href=\"javascript:;\">" + (lineB + 1) + "</a>");
+      fmt.addStyleName(row + i, B + 1, Gerrit.RESOURCES.css().lineNumber());
 
       setRowItem(row + i, new PatchLine(CONTEXT, lineA, lineB));
     }
@@ -408,34 +346,41 @@
       line.reduceSize(-numRows);
       createSkipLine(row, line);
     } else {
-      removeRow(row + loopTo);
+      table.removeRow(row + loopTo);
     }
   }
 
   private void createSkipLine(int row, SkippedLine line) {
     FlowPanel p = new FlowPanel();
-    Label l1 = new Label(" " + PatchUtil.C.patchSkipRegionStart() + " ");
+    InlineLabel l1 = new InlineLabel(" " + PatchUtil.C.patchSkipRegionStart() + " ");
+    InlineLabel l2 = new InlineLabel(" " + PatchUtil.C.patchSkipRegionEnd() + " ");
+
     Anchor all = new Anchor(String.valueOf(line.getSize()));
-    Label l2 = new Label(" " + PatchUtil.C.patchSkipRegionEnd() + " ");
     all.addClickHandler(expandAllListener);
+    all.setStyleName(Gerrit.RESOURCES.css().skipLine());
+
     if (line.getSize() > 30) {
-      // We only show the expand before & after links if we skip more than
-      // 30 lines.
-      Anchor before = new Anchor(PatchUtil.M.expandBefore(NUM_ROWS_TO_EXPAND));
-      before.addClickHandler(expandBeforeListener);
-      Anchor after = new Anchor(PatchUtil.M.expandAfter(NUM_ROWS_TO_EXPAND));
-      after.addClickHandler(expandAfterListener);
-      p.add(before);
+      // Only show the expand before/after if skipped more than 30 lines.
+      Anchor b = new Anchor(PatchUtil.M.expandBefore(NUM_ROWS_TO_EXPAND), true);
+      Anchor a = new Anchor(PatchUtil.M.expandAfter(NUM_ROWS_TO_EXPAND), true);
+
+      b.addClickHandler(expandBeforeListener);
+      a.addClickHandler(expandAfterListener);
+
+      b.setStyleName(Gerrit.RESOURCES.css().skipLine());
+      a.setStyleName(Gerrit.RESOURCES.css().skipLine());
+
+      p.add(b);
       p.add(l1);
       p.add(all);
       p.add(l2);
-      p.add(after);
+      p.add(a);
     } else {
       p.add(l1);
       p.add(all);
       p.add(l2);
     }
-    setWidget(row, 1, p);
+    table.setWidget(row, 1, p);
   }
 
   private void openLine(final SafeHtmlBuilder m) {
@@ -447,22 +392,34 @@
     m.closeTd();
   }
 
-  private void appendLineText(final SafeHtmlBuilder m,
-      final int lineNumberMinusOne, final PatchLine.Type type,
-      final SparseHtmlFile src, final int i, final boolean fullBlock) {
-    appendLineText(m, lineNumberMinusOne, type, //
-        src.getSafeHtmlLine(i), src.hasTrailingEdit(i), fullBlock);
+  private void appendLineNumber(SafeHtmlBuilder m, boolean right) {
+    m.openTd();
+    m.setStyleName(Gerrit.RESOURCES.css().lineNumber());
+    if (right) {
+      m.addStyleName(Gerrit.RESOURCES.css().rightmost());
+    }
+    m.closeTd();
+  }
+
+  private void appendLineNumber(SafeHtmlBuilder m, int lineNumberMinusOne, boolean right) {
+    m.openTd();
+    m.setStyleName(Gerrit.RESOURCES.css().lineNumber());
+    if (right) {
+      m.addStyleName(Gerrit.RESOURCES.css().rightmost());
+    }
+    m.append(SafeHtml.asis("<a href=\"javascript:;\">"+ (lineNumberMinusOne + 1) + "</a>"));
+    m.closeTd();
   }
 
   private void appendLineText(final SafeHtmlBuilder m,
-      final int lineNumberMinusOne, final PatchLine.Type type,
-      final SafeHtml lineHtml, final boolean trailingEdit,
+      final PatchLine.Type type, final SparseHtmlFile src, final int i,
       final boolean fullBlock) {
-    m.openTd();
-    m.setStyleName(Gerrit.RESOURCES.css().lineNumber());
-    m.append(SafeHtml.asis("<a href=\"javascript:void(0)\">"+ (lineNumberMinusOne + 1) + "</a>"));
-    m.closeTd();
+    appendLineText(m, type, src.getSafeHtmlLine(i), src.hasTrailingEdit(i), fullBlock);
+  }
 
+  private void appendLineText(final SafeHtmlBuilder m,
+      final PatchLine.Type type, final SafeHtml lineHtml,
+      final boolean trailingEdit, final boolean fullBlock) {
     m.openTd();
     m.addStyleName(Gerrit.RESOURCES.css().fileLine());
     switch (type) {
@@ -488,10 +445,6 @@
 
   private void appendLineNone(final SafeHtmlBuilder m, final PatchLine.Type type) {
     m.openTd();
-    m.setStyleName(Gerrit.RESOURCES.css().lineNumber());
-    m.closeTd();
-
-    m.openTd();
     m.addStyleName(Gerrit.RESOURCES.css().fileLine());
     switch (type != null ? type : PatchLine.Type.CONTEXT) {
       case DELETE:
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/plugins/PluginInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/plugins/PluginInfo.java
new file mode 100644
index 0000000..ace4a49
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/plugins/PluginInfo.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.client.plugins;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+public class PluginInfo extends JavaScriptObject {
+
+  public final native String name() /*-{ return this.name; }-*/;
+  public final native String version() /*-{ return this.version; }-*/;
+  public final native boolean isDisabled()
+      /*-{ return this.disabled ? true : false; }-*/;
+
+  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..6eca206
--- /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/").addParameterTrue("all")
+        .send(NativeMap.copyKeysIntoChildren(callback));
+  }
+
+  protected PluginMap() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectInfo.java
new file mode 100644
index 0000000..80c1feb
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectInfo.java
@@ -0,0 +1,46 @@
+// 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.projects;
+
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.user.client.ui.SuggestOracle;
+
+public class ProjectInfo
+    extends JavaScriptObject
+    implements SuggestOracle.Suggestion {
+  public final Project.NameKey name_key() {
+    return new Project.NameKey(name());
+  }
+
+  public final native String name() /*-{ return this.name; }-*/;
+  public final native String description() /*-{ return this.description; }-*/;
+
+  @Override
+  public final String getDisplayString() {
+    if (description() != null) {
+      return name() + " (" + description() + ")";
+    }
+    return name();
+  }
+
+  @Override
+  public final String getReplacementString() {
+    return name();
+  }
+
+  protected ProjectInfo() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectMap.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectMap.java
new file mode 100644
index 0000000..408919e
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectMap.java
@@ -0,0 +1,58 @@
+// 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.projects;
+
+import com.google.gerrit.client.rpc.NativeMap;
+import com.google.gerrit.client.rpc.RestApi;
+import com.google.gwtjsonrpc.common.AsyncCallback;
+import com.google.gwt.http.client.URL;
+
+/** Projects available from {@code /projects/}. */
+public class ProjectMap extends NativeMap<ProjectInfo> {
+  public static void all(AsyncCallback<ProjectMap> callback) {
+    new RestApi("/projects/")
+        .addParameterRaw("type", "ALL")
+        .addParameterTrue("all")
+        .addParameterTrue("d") // description
+        .send(NativeMap.copyKeysIntoChildren(callback));
+  }
+
+  public static void permissions(AsyncCallback<ProjectMap> callback) {
+    new RestApi("/projects/")
+        .addParameterRaw("type", "PERMISSIONS")
+        .addParameterTrue("all")
+        .addParameterTrue("d") // description
+        .send(NativeMap.copyKeysIntoChildren(callback));
+  }
+
+  public static void parentCandidates(AsyncCallback<ProjectMap> callback) {
+    new RestApi("/projects/")
+        .addParameterRaw("type", "PARENT_CANDIDATES")
+        .addParameterTrue("all")
+        .addParameterTrue("d") // description
+        .send(NativeMap.copyKeysIntoChildren(callback));
+  }
+
+  public static void suggest(String prefix, int limit, AsyncCallback<ProjectMap> cb) {
+    new RestApi("/projects/" + URL.encode(prefix).replaceAll("[?]", "%3F"))
+        .addParameterRaw("type", "ALL")
+        .addParameter("n", limit)
+        .addParameterTrue("d") // description
+        .send(NativeMap.copyKeysIntoChildren(cb));
+  }
+
+  protected ProjectMap() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/GerritCallback.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/GerritCallback.java
index 98ae46f..dce5bb6 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/GerritCallback.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/GerritCallback.java
@@ -37,8 +37,11 @@
       new NotSignedInDialog().center();
 
     } else if (isNoSuchEntity(caught)) {
-      new ErrorDialog(Gerrit.C.notFoundBody()).center();
-
+      if (Gerrit.isSignedIn()) {
+        new ErrorDialog(Gerrit.C.notFoundBody()).center();
+      } else {
+        new NotSignedInDialog().center();
+      }
     } else if (isInactiveAccount(caught)) {
       new ErrorDialog(Gerrit.C.inactiveAccountBody()).center();
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/NativeList.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/NativeList.java
new file mode 100644
index 0000000..e820fe0
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/NativeList.java
@@ -0,0 +1,55 @@
+// 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.rpc;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+import java.util.AbstractList;
+import java.util.List;
+
+/** A read-only list of native JavaScript objects stored in a JSON array. */
+public class NativeList<T extends JavaScriptObject> extends JavaScriptObject {
+  protected NativeList() {
+  }
+
+  public final List<T> asList() {
+    return new AbstractList<T>() {
+      @Override
+      public T set(int index, T element) {
+        T old = NativeList.this.get(index);
+        NativeList.this.set0(index, element);
+        return old;
+      }
+
+      @Override
+      public T get(int index) {
+        return NativeList.this.get(index);
+      }
+
+      @Override
+      public int size() {
+        return NativeList.this.size();
+      }
+    };
+  }
+
+  public final boolean isEmpty() {
+    return size() == 0;
+  }
+
+  public final native int size() /*-{ return this.length; }-*/;
+  public final native T get(int i) /*-{ return this[i]; }-*/;
+  private final native void set0(int i, T v) /*-{ this[i] = v; }-*/;
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/NativeMap.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/NativeMap.java
new file mode 100644
index 0000000..cde9041
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/NativeMap.java
@@ -0,0 +1,93 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.rpc;
+
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwtjsonrpc.common.AsyncCallback;
+
+import java.util.Set;
+
+/** A map of native JSON objects, keyed by a string. */
+public class NativeMap<T extends JavaScriptObject> extends JavaScriptObject {
+  /**
+   * Loop through the result map's entries and copy the key strings into the
+   * "name" property of the corresponding child object. This only runs on the
+   * top level map of the result, and requires the children to be JSON objects
+   * and not a JSON primitive (e.g. boolean or string).
+   */
+  public static <T extends JavaScriptObject,
+      M extends NativeMap<T>> AsyncCallback<M> copyKeysIntoChildren(
+      AsyncCallback<M> callback) {
+    return copyKeysIntoChildren("name", callback);
+  }
+
+  /** Loop through the result map and set asProperty on the children. */
+  public static <T extends JavaScriptObject,
+      M extends NativeMap<T>> AsyncCallback<M> copyKeysIntoChildren(
+      final String asProperty, AsyncCallback<M> callback) {
+    return new TransformCallback<M, M>(callback) {
+      @Override
+      protected M transform(M result) {
+        result.copyKeysIntoChildren(asProperty);
+        return result;
+      }
+    };
+  }
+
+  protected NativeMap() {
+  }
+
+  public final Set<String> keySet() {
+    return Natives.keys(this);
+  }
+
+  public final native NativeList<T> values()
+  /*-{
+    var s = this;
+    var v = [];
+    var i = 0;
+    for (var k in s) {
+      if (s.hasOwnProperty(k)) {
+        v[i++] = s[k];
+      }
+    }
+    return v;
+  }-*/;
+
+  public final int size() {
+    return keySet().size();
+  }
+
+  public final boolean isEmpty() {
+    return size() == 0;
+  }
+
+  public final boolean containsKey(String n) {
+    return get(n) != null;
+  }
+
+  public final native T get(String n) /*-{ return this[n]; }-*/;
+
+  public final native void copyKeysIntoChildren(String p)
+  /*-{
+    var s = this;
+    for (var k in s) {
+      if (s.hasOwnProperty(k)) {
+        var c = s[k];
+        c[p] = k;
+      }
+    }
+  }-*/;
+}
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
new file mode 100644
index 0000000..a6c609c
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/Natives.java
@@ -0,0 +1,57 @@
+// 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.rpc;
+
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.json.client.JSONObject;
+
+import java.util.Collections;
+import java.util.Set;
+
+public class Natives {
+  /**
+   * Get the names of defined properties on the object. The returned set
+   * iterates in the native iteration order, which may match the source order.
+   */
+  public static Set<String> keys(JavaScriptObject obj) {
+    if (obj != null) {
+      return new JSONObject(obj).keySet();
+    }
+    return Collections.emptySet();
+  }
+
+  public static <T extends JavaScriptObject> T parseJSON(String json) {
+    if (parser == null) {
+      parser = bestJsonParser();
+    }
+    // javac generics bug
+    return Natives.<T>parse0(parser, json);
+  }
+
+  private static native <T extends JavaScriptObject>
+  T parse0(JavaScriptObject p, String s)
+  /*-{ return p(s); }-*/;
+
+  private static JavaScriptObject parser;
+  private static native JavaScriptObject bestJsonParser()
+  /*-{
+    if ($wnd.JSON && typeof $wnd.JSON.parse === 'function')
+      return $wnd.JSON.parse;
+    return function(s) { return eval('(' + s + ')'); };
+  }-*/;
+
+  private Natives() {
+  }
+}
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
new file mode 100644
index 0000000..650cacd
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RestApi.java
@@ -0,0 +1,215 @@
+// 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.rpc;
+
+import com.google.gerrit.client.RpcStatus;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.http.client.Request;
+import com.google.gwt.http.client.RequestBuilder;
+import com.google.gwt.http.client.RequestCallback;
+import com.google.gwt.http.client.RequestException;
+import com.google.gwt.http.client.Response;
+import com.google.gwt.http.client.URL;
+import com.google.gwt.user.client.rpc.StatusCodeException;
+import com.google.gwtjsonrpc.client.RemoteJsonException;
+import com.google.gwtjsonrpc.client.ServerUnavailableException;
+import com.google.gwtjsonrpc.common.AsyncCallback;
+import com.google.gwtjsonrpc.common.JsonConstants;
+
+/** Makes a REST API call to the server. */
+public class RestApi {
+  /**
+   * Expected JSON content body prefix that prevents XSSI.
+   * <p>
+   * The server always includes this line as the first line of the response
+   * content body when the response body is formatted as JSON. It gets inserted
+   * by the server to prevent the resource from being imported into another
+   * domain's page using a &lt;script&gt; tag. This line must be removed before
+   * the JSON can be parsed.
+   */
+  private static final String JSON_MAGIC = ")]}'\n";
+
+  private class MyRequestCallback<T extends JavaScriptObject> implements
+      RequestCallback {
+    private final boolean wasGet;
+    private final AsyncCallback<T> cb;
+
+    public MyRequestCallback(boolean wasGet, AsyncCallback<T> cb) {
+      this.wasGet = wasGet;
+      this.cb = cb;
+    }
+
+    @Override
+    public void onResponseReceived(Request req, Response res) {
+      int status = res.getStatusCode();
+      if (status != 200) {
+        RpcStatus.INSTANCE.onRpcComplete();
+        if ((400 <= status && status < 600) && isTextBody(res)) {
+          cb.onFailure(new RemoteJsonException(res.getText(), status, null));
+        } else {
+          cb.onFailure(new StatusCodeException(status, res.getStatusText()));
+        }
+        return;
+      }
+
+      if (!isJsonBody(res)) {
+        RpcStatus.INSTANCE.onRpcComplete();
+        cb.onFailure(new RemoteJsonException("Invalid JSON"));
+        return;
+      }
+
+      String json = res.getText();
+      if (!json.startsWith(JSON_MAGIC)) {
+        RpcStatus.INSTANCE.onRpcComplete();
+        cb.onFailure(new RemoteJsonException("Invalid JSON"));
+        return;
+      }
+      json = json.substring(JSON_MAGIC.length());
+
+      if (wasGet && json.startsWith("{\"_authkey\":")) {
+        RestApi.this.resendPost(cb, json);
+        return;
+      }
+
+      T data;
+      try {
+        // javac generics bug
+        data = Natives.<T> parseJSON(json);
+      } catch (RuntimeException e) {
+        RpcStatus.INSTANCE.onRpcComplete();
+        cb.onFailure(new RemoteJsonException("Invalid JSON"));
+        return;
+      }
+
+      cb.onSuccess(data);
+      RpcStatus.INSTANCE.onRpcComplete();
+    }
+
+    @Override
+    public void onError(Request req, Throwable err) {
+      RpcStatus.INSTANCE.onRpcComplete();
+      if (err.getMessage().contains("XmlHttpRequest.status")) {
+        cb.onFailure(new ServerUnavailableException());
+      } else {
+        cb.onFailure(err);
+      }
+    }
+  }
+
+  private StringBuilder url;
+  private boolean hasQueryParams;
+
+  /**
+   * Initialize a new API call.
+   * <p>
+   * By default the JSON format will be selected by including an HTTP Accept
+   * header in the request.
+   *
+   * @param name URL of the REST resource to access, e.g. {@code "/projects/"}
+   *        to list accessible projects from the server.
+   */
+  public RestApi(String name) {
+    if (name.startsWith("/")) {
+      name = name.substring(1);
+    }
+
+    url = new StringBuilder();
+    url.append(GWT.getHostPageBaseURL());
+    url.append(name);
+  }
+
+  public RestApi addParameter(String name, String value) {
+    return addParameterRaw(name, URL.encodeQueryString(value));
+  }
+
+  public RestApi addParameterTrue(String name) {
+    return addParameterRaw(name, null);
+  }
+
+  public RestApi addParameter(String name, boolean value) {
+    return addParameterRaw(name, value ? "t" : "f");
+  }
+
+  public RestApi addParameter(String name, int value) {
+    return addParameterRaw(name, String.valueOf(value));
+  }
+
+  public RestApi addParameter(String name, Enum<?> value) {
+    return addParameterRaw(name, value.name());
+  }
+
+  public RestApi addParameterRaw(String name, String value) {
+    if (hasQueryParams) {
+      url.append("&");
+    } else {
+      url.append("?");
+      hasQueryParams = true;
+    }
+    url.append(name);
+    if (value != null) {
+      url.append("=").append(value);
+    }
+    return this;
+  }
+
+  public <T extends JavaScriptObject> void send(final AsyncCallback<T> cb) {
+    RequestBuilder req = new RequestBuilder(RequestBuilder.GET, url.toString());
+    req.setHeader("Accept", JsonConstants.JSON_TYPE);
+    req.setCallback(new MyRequestCallback<T>(true, cb));
+    try {
+      RpcStatus.INSTANCE.onRpcStart();
+      req.send();
+    } catch (RequestException e) {
+      RpcStatus.INSTANCE.onRpcComplete();
+      cb.onFailure(e);
+    }
+  }
+
+  private <T extends JavaScriptObject> void resendPost(
+      final AsyncCallback<T> cb, String token) {
+    RequestBuilder req = new RequestBuilder(RequestBuilder.POST, url.toString());
+    req.setHeader("Accept", JsonConstants.JSON_TYPE);
+    req.setHeader("Content-Type", JsonConstants.JSON_TYPE);
+    req.setRequestData(token);
+    req.setCallback(new MyRequestCallback<T>(false, cb));
+    try {
+      req.send();
+    } catch (RequestException e) {
+      RpcStatus.INSTANCE.onRpcComplete();
+      cb.onFailure(e);
+    }
+  }
+
+  private static boolean isJsonBody(Response res) {
+    return isContentType(res, JsonConstants.JSON_TYPE);
+  }
+
+  private static boolean isTextBody(Response res) {
+    return isContentType(res, "text/plain");
+  }
+
+  private static boolean isContentType(Response res, String want) {
+    String type = res.getHeader("Content-Type");
+    if (type == null) {
+      return false;
+    }
+    int semi = type.indexOf(';');
+    if (semi >= 0) {
+      type = type.substring(0, semi).trim();
+    }
+    return want.equals(type);
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/TransformCallback.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/TransformCallback.java
new file mode 100644
index 0000000..2cd22cb
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/TransformCallback.java
@@ -0,0 +1,38 @@
+// 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.rpc;
+
+import com.google.gwtjsonrpc.common.AsyncCallback;
+
+/** Transforms a value and passes it on to another callback. */
+public abstract class TransformCallback<I, O> implements AsyncCallback<I>{
+  private final AsyncCallback<O> callback;
+
+  protected TransformCallback(AsyncCallback<O> callback) {
+    this.callback = callback;
+  }
+
+  @Override
+  public void onSuccess(I result) {
+    callback.onSuccess(transform(result));
+  }
+
+  @Override
+  public void onFailure(Throwable caught) {
+    callback.onFailure(caught);
+  }
+
+  protected abstract O transform(I result);
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountDashboardLink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountDashboardLink.java
deleted file mode 100644
index 5233a6b..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountDashboardLink.java
+++ /dev/null
@@ -1,56 +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.client.ui;
-
-import com.google.gerrit.client.FormatUtil;
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.changes.AccountDashboardScreen;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.common.data.AccountInfo;
-import com.google.gerrit.common.data.AccountInfoCache;
-import com.google.gerrit.reviewdb.client.Account;
-
-/** Link to any user's account dashboard. */
-public class AccountDashboardLink extends InlineHyperlink {
-  /** Create a link after locating account details from an active cache. */
-  public static AccountDashboardLink link(final AccountInfoCache cache,
-      final Account.Id id) {
-    final AccountInfo ai = cache.get(id);
-    return ai != null ? new AccountDashboardLink(ai) : null;
-  }
-
-  private Account.Id accountId;
-
-  public AccountDashboardLink(final AccountInfo ai) {
-    this(FormatUtil.name(ai), ai);
-  }
-
-  public AccountDashboardLink(final String text, final AccountInfo ai) {
-    this(text, ai.getId());
-    setTitle(FormatUtil.nameEmail(ai));
-  }
-
-  public AccountDashboardLink(final String text, final Account.Id ai) {
-    super(text, PageLinks.toAccountDashboard(ai));
-    addStyleName(Gerrit.RESOURCES.css().accountName());
-    accountId = ai;
-  }
-
-  @Override
-  public void go() {
-    Gerrit.display(getTargetHistoryToken(), //
-        new AccountDashboardScreen(accountId));
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountGroupSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountGroupSuggestOracle.java
index 885f53b..5da00cd 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountGroupSuggestOracle.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountGroupSuggestOracle.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gwt.user.client.ui.SuggestOracle;
 import com.google.gwtexpui.safehtml.client.HighlightSuggestOracle;
 
@@ -31,11 +32,14 @@
   private Map<String, AccountGroup.UUID> priorResults =
       new HashMap<String, AccountGroup.UUID>();
 
+  private Project.NameKey projectName;
+
   @Override
   public void onRequestSuggestions(final Request req, final Callback callback) {
     RpcStatus.hide(new Runnable() {
       public void run() {
-        SuggestUtil.SVC.suggestAccountGroup(req.getQuery(), req.getLimit(),
+        SuggestUtil.SVC.suggestAccountGroupForProject(
+            projectName, req.getQuery(), req.getLimit(),
             new GerritCallback<List<GroupReference>>() {
               public void onSuccess(final List<GroupReference> result) {
                 priorResults.clear();
@@ -52,6 +56,10 @@
     });
   }
 
+  public void setProject(Project.NameKey projectName) {
+    this.projectName = projectName;
+  }
+
   private static class AccountGroupSuggestion implements
       SuggestOracle.Suggestion {
     private final GroupReference info;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLink.java
new file mode 100644
index 0000000..790102c
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLink.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.client.ui;
+
+import com.google.gerrit.client.FormatUtil;
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.common.data.AccountInfo;
+import com.google.gerrit.common.data.AccountInfoCache;
+import com.google.gerrit.reviewdb.client.Account;
+
+/** Link to any user's account dashboard. */
+public class AccountLink extends InlineHyperlink {
+  /** Create a link after locating account details from an active cache. */
+  public static AccountLink link(final AccountInfoCache cache,
+      final Account.Id id) {
+    final AccountInfo ai = cache.get(id);
+    return ai != null ? new AccountLink(ai) : null;
+  }
+
+  public AccountLink(final AccountInfo ai) {
+    super(FormatUtil.name(ai), PageLinks.toAccountQuery(owner(ai)));
+    setTitle(FormatUtil.nameEmail(ai));
+  }
+
+  private static String owner(AccountInfo ai) {
+    if (ai.getPreferredEmail() != null) {
+      return ai.getPreferredEmail();
+    } else if (ai.getFullName() != null) {
+      return ai.getFullName();
+    }
+    return "" + ai.getId().get();
+  }
+
+  @Override
+  public void go() {
+    Gerrit.display(getTargetHistoryToken());
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentPanel.java
index 0cea2c7..ddd2b27 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentPanel.java
@@ -61,7 +61,7 @@
 
     setMessageText(message);
     setAuthorNameText(FormatUtil.name(author));
-    setDateText(FormatUtil.shortFormat(when));
+    setDateText(FormatUtil.shortFormatDayTime(when));
 
     final CellFormatter fmt = header.getCellFormatter();
     fmt.getElement(0, 0).setTitle(FormatUtil.nameEmail(author));
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectListPopup.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectListPopup.java
new file mode 100644
index 0000000..217ca5a
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectListPopup.java
@@ -0,0 +1,160 @@
+// Copyright (C) 2011 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.ui;
+
+import com.google.gerrit.client.account.Util;
+import com.google.gerrit.client.projects.ProjectMap;
+import com.google.gerrit.client.rpc.GerritCallback;
+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.user.client.Window;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwt.user.client.ui.ScrollPanel;
+import com.google.gwtexpui.globalkey.client.GlobalKey;
+import com.google.gwtexpui.globalkey.client.HidePopupPanelCommand;
+import com.google.gwtexpui.user.client.PluginSafeDialogBox;
+
+/** It creates a popup containing all the projects. */
+public class ProjectListPopup {
+  private ProjectsTable projectsTab;
+  private PluginSafeDialogBox popup;
+  private Button close;
+  private ScrollPanel sp;
+  private PopupPanel.PositionCallback popupPosition;
+  private int preferredTop;
+  private int preferredLeft;
+  private boolean popingUp;
+  private boolean firstPopupLoad = true;
+
+  public void initPopup(final String popupText, final String currentPageLink) {
+    createWidgets(popupText, currentPageLink);
+    final FlowPanel pfp = new FlowPanel();
+    sp = new ScrollPanel(projectsTab);
+    sp.setSize("100%", "100%");
+    pfp.add(sp);
+    pfp.add(close);
+    popup.setWidget(pfp);
+    popup.setHeight("100%");
+    popupPosition = getPositionCallback();
+  }
+
+  protected PopupPanel.PositionCallback getPositionCallback() {
+    return new PopupPanel.PositionCallback() {
+      @Override
+      public void setPosition(int offsetWidth, int offsetHeight) {
+        if (preferredTop + offsetHeight > Window.getClientWidth()) {
+          preferredTop = Window.getClientWidth() - offsetHeight;
+        }
+        if (preferredLeft + offsetWidth > Window.getClientWidth()) {
+          preferredLeft = Window.getClientWidth() - offsetWidth;
+        }
+
+        if (preferredTop < 0) {
+          sp.setHeight((sp.getOffsetHeight() + preferredTop) + "px");
+          preferredTop = 0;
+        }
+        if (preferredLeft < 0) {
+          sp.setWidth((sp.getOffsetWidth() + preferredLeft) + "px");
+          preferredLeft = 0;
+        }
+
+        popup.setPopupPosition(preferredLeft, preferredTop);
+      }
+    };
+  }
+
+  protected void onMovePointerTo(String projectName) {
+  }
+
+  protected void openRow(String projectName) {
+  }
+
+  public boolean isPopingUp() {
+    return popingUp;
+  }
+
+  private void createWidgets(final String popupText,
+      final String currentPageLink) {
+    projectsTab = new ProjectsTable() {
+      @Override
+      protected void movePointerTo(final int row, final boolean scroll) {
+        super.movePointerTo(row, scroll);
+        onMovePointerTo(getRowItem(row).name());
+      }
+
+      @Override
+      protected void onOpenRow(final int row) {
+        super.onOpenRow(row);
+        openRow(getRowItem(row).name());
+      }
+    };
+    projectsTab.setSavePointerId(currentPageLink);
+
+    close = new Button(Util.C.projectsClose());
+    close.addClickHandler(new ClickHandler() {
+      @Override
+      public void onClick(final ClickEvent event) {
+        closePopup();
+      }
+    });
+
+    popup = new PluginSafeDialogBox();
+    popup.setModal(false);
+    popup.setText(popupText);
+  }
+
+  public void displayPopup() {
+    popingUp = true;
+    if (firstPopupLoad) { // For sizing/positioning, delay display until loaded
+      populateProjects();
+    } else {
+      popup.setPopupPositionAndShow(popupPosition);
+      GlobalKey.dialog(popup);
+      try {
+        GlobalKey.addApplication(popup, new HidePopupPanelCommand(0,
+            KeyCodes.KEY_ESCAPE, popup));
+      } catch (Throwable e) {
+      }
+      projectsTab.setRegisterKeys(true);
+      projectsTab.finishDisplay();
+      popingUp = false;
+    }
+  }
+
+  public void closePopup() {
+    popup.hide();
+  }
+
+  public void setPreferredCoordinates(final int top, final int left) {
+    this.preferredTop = top;
+    this.preferredLeft = left;
+  }
+
+  protected void populateProjects() {
+    ProjectMap.all(new GerritCallback<ProjectMap>() {
+      @Override
+      public void onSuccess(final ProjectMap result) {
+        projectsTab.display(result);
+        if (firstPopupLoad) { // Display was delayed until table was loaded
+          firstPopupLoad = false;
+          displayPopup();
+        }
+      }
+    });
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectNameSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectNameSuggestOracle.java
index be82eff..25ed258 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectNameSuggestOracle.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectNameSuggestOracle.java
@@ -15,49 +15,25 @@
 package com.google.gerrit.client.ui;
 
 import com.google.gerrit.client.RpcStatus;
+import com.google.gerrit.client.projects.ProjectMap;
 import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gwt.user.client.ui.SuggestOracle;
 import com.google.gwtexpui.safehtml.client.HighlightSuggestOracle;
 
-import java.util.ArrayList;
-import java.util.List;
-
 /** Suggestion Oracle for Project.NameKey entities. */
 public class ProjectNameSuggestOracle extends HighlightSuggestOracle {
   @Override
   public void onRequestSuggestions(final Request req, final Callback callback) {
     RpcStatus.hide(new Runnable() {
+      @Override
       public void run() {
-        SuggestUtil.SVC.suggestProjectNameKey(req.getQuery(), req.getLimit(),
-            new GerritCallback<List<Project.NameKey>>() {
-              public void onSuccess(final List<Project.NameKey> result) {
-                final ArrayList<ProjectNameSuggestion> r =
-                    new ArrayList<ProjectNameSuggestion>(result.size());
-                for (final Project.NameKey p : result) {
-                  r.add(new ProjectNameSuggestion(p));
-                }
-                callback.onSuggestionsReady(req, new Response(r));
+        ProjectMap.suggest(req.getQuery(), req.getLimit(),
+            new GerritCallback<ProjectMap>() {
+              @Override
+              public void onSuccess(ProjectMap map) {
+                callback.onSuggestionsReady(req, new Response(map.values().asList()));
               }
             });
       }
     });
   }
-
-  private static class ProjectNameSuggestion implements
-      SuggestOracle.Suggestion {
-    private final Project.NameKey key;
-
-    ProjectNameSuggestion(final Project.NameKey k) {
-      key = k;
-    }
-
-    public String getDisplayString() {
-      return key.get();
-    }
-
-    public String getReplacementString() {
-      return key.get();
-    }
-  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectsTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectsTable.java
index b768643..0cbe194 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectsTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectsTable.java
@@ -15,16 +15,19 @@
 package com.google.gerrit.client.ui;
 
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.client.projects.ProjectInfo;
+import com.google.gerrit.client.projects.ProjectMap;
 import com.google.gwt.event.dom.client.KeyCodes;
 import com.google.gwt.user.client.DOM;
 import com.google.gwt.user.client.Element;
 import com.google.gwt.user.client.Event;
 import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
 
+import java.util.Collections;
+import java.util.Comparator;
 import java.util.List;
 
-public class ProjectsTable extends NavigationTable<Project> {
+public class ProjectsTable extends NavigationTable<ProjectInfo> {
 
   public ProjectsTable() {
     keysNavigation.add(new PrevKeyCommand(0, 'k', Util.C.projectListPrev()));
@@ -32,7 +35,10 @@
     keysNavigation.add(new OpenKeyCommand(0, 'o', Util.C.projectListOpen()));
     keysNavigation.add(new OpenKeyCommand(0, KeyCodes.KEY_ENTER,
                                                   Util.C.projectListOpen()));
+    initColumnHeaders();
+  }
 
+  protected void initColumnHeaders() {
     table.setText(0, 1, Util.C.projectName());
     table.setText(0, 2, Util.C.projectDescription());
 
@@ -41,6 +47,7 @@
     fmt.addStyleName(0, 2, Gerrit.RESOURCES.css().dataHeader());
   }
 
+  @Override
   protected MyFlexTable createFlexTable() {
     MyFlexTable table = new MyFlexTable() {
       @Override
@@ -78,8 +85,8 @@
   }
 
   @Override
-  protected Object getRowItemKey(final Project item) {
-    return item.getNameKey();
+  protected Object getRowItemKey(final ProjectInfo item) {
+    return item.name();
   }
 
   @Override
@@ -89,17 +96,24 @@
     }
   }
 
-  public void display(final List<Project> projects) {
+  public void display(ProjectMap projects) {
     while (1 < table.getRowCount())
       table.removeRow(table.getRowCount() - 1);
 
-    for (final Project k : projects)
-      insert(table.getRowCount(), k);
+    List<ProjectInfo> list = projects.values().asList();
+    Collections.sort(list, new Comparator<ProjectInfo>() {
+      @Override
+      public int compare(ProjectInfo a, ProjectInfo b) {
+        return a.name().compareTo(b.name());
+      }
+    });
+    for(ProjectInfo p : list)
+      insert(table.getRowCount(), p);
 
     finishDisplay();
   }
 
-  protected void insert(final int row, final Project k) {
+  protected void insert(final int row, final ProjectInfo k) {
     table.insertRow(row);
 
     applyDataRowStyle(row);
@@ -112,9 +126,9 @@
     populate(row, k);
   }
 
-  protected void populate(final int row, final Project k) {
-    table.setText(row, 1, k.getName());
-    table.setText(row, 2, k.getDescription());
+  protected void populate(final int row, final ProjectInfo k) {
+    table.setText(row, 1, k.name());
+    table.setText(row, 2, k.description());
 
     setRowItem(row, k);
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Screen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Screen.java
index 5cb4727..845a046 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Screen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Screen.java
@@ -15,8 +15,6 @@
 package com.google.gerrit.client.ui;
 
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.common.PageLinks;
-import com.google.gwt.user.client.History;
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.Grid;
 import com.google.gwt.user.client.ui.HasHorizontalAlignment;
@@ -97,6 +95,10 @@
     }
   }
 
+  protected void setHeaderVisible(boolean value) {
+    header.setVisible(value);
+  }
+
   protected void setTitleEast(final Widget w) {
     header.setWidget(0, Cols.East.ordinal(), w);
   }
@@ -148,13 +150,6 @@
     return requiresSignIn;
   }
 
-  /** Invoked if this screen is the current screen and the user signs out. */
-  public void onSignOut() {
-    if (isRequiresSignIn()) {
-      History.newItem(PageLinks.toChangeQuery("status:open"));
-    }
-  }
-
   public void onShowView() {
     if (windowTitle != null) {
       Gerrit.setWindowTitle(this, windowTitle);
diff --git a/gerrit-httpd/.gitignore b/gerrit-httpd/.gitignore
index 194bedc..5bbeafd 100644
--- a/gerrit-httpd/.gitignore
+++ b/gerrit-httpd/.gitignore
@@ -2,4 +2,5 @@
 /.classpath
 /.project
 /.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-httpd.iml
\ No newline at end of file
diff --git a/gerrit-httpd/.settings/org.eclipse.core.resources.prefs b/gerrit-httpd/.settings/org.eclipse.core.resources.prefs
index 9df523e..839d647 100644
--- a/gerrit-httpd/.settings/org.eclipse.core.resources.prefs
+++ b/gerrit-httpd/.settings/org.eclipse.core.resources.prefs
@@ -1,4 +1,3 @@
-#Thu Jul 28 11:02:36 PDT 2011
 eclipse.preferences.version=1
 encoding//src/main/java=UTF-8
 encoding//src/main/resources=UTF-8
diff --git a/gerrit-httpd/pom.xml b/gerrit-httpd/pom.xml
index a6374da..ceacb66 100644
--- a/gerrit-httpd/pom.xml
+++ b/gerrit-httpd/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.4-SNAPSHOT</version>
+    <version>2.5-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-httpd</artifactId>
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/AllRequestFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/AllRequestFilter.java
new file mode 100644
index 0000000..c8d237c
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/AllRequestFilter.java
@@ -0,0 +1,86 @@
+// 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;
+
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+import com.google.inject.servlet.ServletModule;
+
+import java.io.IOException;
+import java.util.Iterator;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+
+/** Filters all HTTP requests passing through the server. */
+public abstract class AllRequestFilter implements Filter {
+  public static ServletModule module() {
+    return new ServletModule() {
+      @Override
+      protected void configureServlets() {
+        DynamicSet.setOf(binder(), AllRequestFilter.class);
+        filter("/*").through(FilterProxy.class);
+      }
+    };
+  }
+
+  @Singleton
+  static class FilterProxy implements Filter {
+    private final DynamicSet<AllRequestFilter> filters;
+
+    @Inject
+    FilterProxy(DynamicSet<AllRequestFilter> filters) {
+      this.filters = filters;
+    }
+
+    @Override
+    public void doFilter(ServletRequest req, ServletResponse res,
+        final FilterChain last) throws IOException, ServletException {
+      final Iterator<AllRequestFilter> itr = filters.iterator();
+      new FilterChain() {
+        @Override
+        public void doFilter(ServletRequest req, ServletResponse res)
+            throws IOException, ServletException {
+          if (itr.hasNext()) {
+            itr.next().doFilter(req, res, this);
+          } else {
+            last.doFilter(req, res);
+          }
+        }
+      }.doFilter(req, res);
+    }
+
+    @Override
+    public void init(FilterConfig config) throws ServletException {
+    }
+
+    @Override
+    public void destroy() {
+    }
+  }
+
+  @Override
+  public void init(FilterConfig config) throws ServletException {
+  }
+
+  @Override
+  public void destroy() {
+  }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CacheBasedWebSession.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CacheBasedWebSession.java
index fb30a4d..ca3d287 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
@@ -25,17 +25,17 @@
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AuthMethod;
 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 org.eclipse.jgit.http.server.GitSmartHttpTools;
+
 import javax.servlet.http.Cookie;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
@@ -49,13 +49,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)
@@ -71,8 +67,9 @@
   private final AuthConfig authConfig;
   private final Provider<AnonymousUser> anonymousProvider;
   private final IdentifiedUser.RequestFactory identified;
-  private AccessPath accessPath = AccessPath.WEB_UI;
+  private AccessPath accessPath;
   private Cookie outCookie;
+  private AuthMethod authMethod;
 
   private Key key;
   private Val val;
@@ -90,6 +87,12 @@
     this.anonymousProvider = anonymousProvider;
     this.identified = identified;
 
+    if (GitSmartHttpTools.isGitClient(request)) {
+      accessPath = AccessPath.GIT;
+    } else {
+      accessPath = AccessPath.WEB_UI;
+    }
+
     final String cookie = readCookie();
     if (cookie != null) {
       key = new Key(cookie);
@@ -98,6 +101,7 @@
       key = null;
       val = null;
     }
+    authMethod = isSignedIn() ? AuthMethod.COOKIE : AuthMethod.NONE;
 
     if (isSignedIn() && val.needsCookieRefresh()) {
       // Cookie is more than half old. Send the cookie again to the
@@ -149,7 +153,8 @@
     return anonymousProvider.get();
   }
 
-  public void login(final AuthResult res, final boolean rememberMe) {
+  public void login(final AuthResult res, final AuthMethod meth,
+                    final boolean rememberMe) {
     final Account.Id id = res.getAccountId();
     final AccountExternalId.Key identity = res.getExternalId();
 
@@ -160,17 +165,15 @@
     key = manager.createKey(id);
     val = manager.createVal(key, id, rememberMe, identity, null);
     saveCookie();
-  }
 
-  /** Change the access path from the default of {@link AccessPath#WEB_UI}. */
-  public void setAccessPath(AccessPath path) {
-    accessPath = path;
+    authMethod = meth;
   }
 
   /** Set the user account for this current request only. */
-  public void setUserAccountId(Account.Id id) {
+  public void setUserAccountId(Account.Id id, AuthMethod method) {
     key = new Key("id:" + id);
-    val = new Val(id, 0, false, null, "");
+    val = new Val(id, 0, false, null, "", 0);
+    authMethod = method;
   }
 
   public void logout() {
@@ -217,4 +220,8 @@
   private static boolean isSecure(final HttpServletRequest req) {
     return req.isSecure() || "https".equals(req.getScheme());
   }
+
+  public AuthMethod getAuthMethod() {
+    return authMethod;
+  }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ContainerAuthFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ContainerAuthFilter.java
index c0e3f42..9ce2298 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ContainerAuthFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ContainerAuthFilter.java
@@ -19,13 +19,12 @@
 
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.account.AuthMethod;
 import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gwtjsonrpc.server.XsrfException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
-import org.eclipse.jgit.http.server.GitSmartHttpTools;
 import org.eclipse.jgit.lib.Config;
 
 import java.io.IOException;
@@ -39,7 +38,6 @@
 import javax.servlet.ServletResponse;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
-import javax.servlet.http.HttpServletResponseWrapper;
 
 /**
  * Trust the authentication which is done by the container.
@@ -62,7 +60,7 @@
 
   @Inject
   ContainerAuthFilter(Provider<WebSession> session, AccountCache accountCache,
-      @GerritServerConfig Config config) throws XsrfException {
+      @GerritServerConfig Config config) {
     this.session = session;
     this.accountCache = accountCache;
     this.config = config;
@@ -80,20 +78,14 @@
   public void doFilter(ServletRequest request, ServletResponse response,
       FilterChain chain) throws IOException, ServletException {
     HttpServletRequest req = (HttpServletRequest) request;
-    if (!GitSmartHttpTools.isGitClient(req)) {
-      chain.doFilter(request, response);
-      return;
-    }
-
-    HttpServletResponseWrapper rsp =
-        new HttpServletResponseWrapper((HttpServletResponse) response);
+    HttpServletResponse rsp = (HttpServletResponse) response;
 
     if (verify(req, rsp)) {
       chain.doFilter(req, response);
     }
   }
 
-  private boolean verify(HttpServletRequest req, HttpServletResponseWrapper rsp)
+  private boolean verify(HttpServletRequest req, HttpServletResponse rsp)
       throws IOException {
     String username = req.getRemoteUser();
     if (username == null) {
@@ -108,7 +100,9 @@
       rsp.sendError(SC_UNAUTHORIZED);
       return false;
     }
-    session.get().setUserAccountId(who.getAccount().getId());
+    session.get().setUserAccountId(
+        who.getAccount().getId(),
+        AuthMethod.PASSWORD);
     return true;
   }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritConfigProvider.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritConfigProvider.java
index 1953480..c1f3ae4 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritConfigProvider.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritConfigProvider.java
@@ -22,7 +22,7 @@
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.config.AuthConfig;
-import com.google.gerrit.server.config.DownloadSchemeConfig;
+import com.google.gerrit.server.config.DownloadConfig;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.contact.ContactStore;
 import com.google.gerrit.server.mail.EmailSender;
@@ -48,7 +48,7 @@
   private final Realm realm;
   private final Config cfg;
   private final AuthConfig authConfig;
-  private final DownloadSchemeConfig schemeConfig;
+  private final DownloadConfig downloadConfig;
   private final GitWebConfig gitWebConfig;
   private final AllProjectsName wildProject;
   private final SshInfo sshInfo;
@@ -63,12 +63,12 @@
   GerritConfigProvider(final Realm r, @GerritServerConfig final Config gsc,
       final AuthConfig ac, final GitWebConfig gwc, final AllProjectsName wp,
       final SshInfo si, final ApprovalTypes at, final ContactStore cs,
-      final ServletContext sc, final DownloadSchemeConfig dc,
+      final ServletContext sc, final DownloadConfig dc,
       final @AnonymousCowardName String acn) {
     realm = r;
     cfg = gsc;
     authConfig = ac;
-    schemeConfig = dc;
+    downloadConfig = dc;
     gitWebConfig = gwc;
     sshInfo = si;
     wildProject = wp;
@@ -90,13 +90,19 @@
         config.setAllowedOpenIDs(authConfig.getAllowedOpenIDs());
         break;
 
+      case OPENID_SSO:
+        config.setOpenIdSsoUrl(authConfig.getOpenIdSsoUrl());
+        break;
+
       case LDAP:
       case LDAP_BIND:
         config.setRegisterUrl(cfg.getString("auth", null, "registerurl"));
+        config.setEditFullNameUrl(cfg.getString("auth", null, "editFullNameUrl"));
         break;
 
       case CUSTOM_EXTENSION:
         config.setRegisterUrl(cfg.getString("auth", null, "registerurl"));
+        config.setEditFullNameUrl(cfg.getString("auth", null, "editFullNameUrl"));
         config.setHttpPasswordUrl(cfg.getString("auth", null, "httpPasswordUrl"));
         break;
     }
@@ -105,7 +111,8 @@
     config.setGitDaemonUrl(cfg.getString("gerrit", null, "canonicalgiturl"));
     config.setGitHttpUrl(cfg.getString("gerrit", null, "gitHttpUrl"));
     config.setUseContactInfo(contactStore != null && contactStore.isEnabled());
-    config.setDownloadSchemes(schemeConfig.getDownloadScheme());
+    config.setDownloadSchemes(downloadConfig.getDownloadSchemes());
+    config.setDownloadCommands(downloadConfig.getDownloadCommands());
     config.setAuthType(authConfig.getAuthType());
     config.setWildProject(wildProject);
     config.setApprovalTypes(approvalTypes);
@@ -115,6 +122,13 @@
         "test", false));
     config.setAnonymousCowardName(anonymousCowardName);
 
+    config.setReportBugUrl(cfg.getString("gerrit", null, "reportBugUrl"));
+    if (config.getReportBugUrl() == null) {
+      config.setReportBugUrl("http://code.google.com/p/gerrit/issues/list");
+    } else if (config.getReportBugUrl().isEmpty()) {
+      config.setReportBugUrl(null);
+    }
+
     final Set<Account.FieldName> fields = new HashSet<Account.FieldName>();
     for (final Account.FieldName n : Account.FieldName.values()) {
       if (realm.allowsEdit(n)) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpModule.java
index 6fd94c9..6ca9949 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpModule.java
@@ -34,6 +34,8 @@
     Class<? extends Filter> authFilter;
     if (authConfig.isTrustContainerAuth()) {
       authFilter = ContainerAuthFilter.class;
+    } else if (authConfig.isGitBasichAuth()) {
+      authFilter = ProjectBasicAuthFilter.class;
     } else {
       authFilter = ProjectDigestFilter.class;
     }
@@ -41,5 +43,7 @@
     String git = GitOverHttpServlet.URL_REGEX;
     filterRegex(git).through(authFilter);
     serveRegex(git).with(GitOverHttpServlet.class);
+
+    filter("/a/*").through(authFilter);
   }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java
index c36df04a..6bd35dd 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,12 @@
 
 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;
@@ -44,7 +43,6 @@
 import org.eclipse.jgit.http.server.resolver.AsIsFileService;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.storage.pack.PackConfig;
 import org.eclipse.jgit.transport.ReceivePack;
 import org.eclipse.jgit.transport.UploadPack;
 import org.eclipse.jgit.transport.resolver.ReceivePackFactory;
@@ -99,11 +97,11 @@
       install(new CacheModule() {
         @Override
         protected void configure() {
-          TypeLiteral<Cache<AdvertisedObjectsCacheKey, Set<ObjectId>>> cache =
-              new TypeLiteral<Cache<AdvertisedObjectsCacheKey, Set<ObjectId>>>() {};
-          core(cache, ID_CACHE)
-            .memoryLimit(4096)
-            .maxAge(10, TimeUnit.MINUTES);
+          cache(ID_CACHE,
+              AdvertisedObjectsCacheKey.class,
+              new TypeLiteral<Set<ObjectId>>() {})
+            .maximumWeight(4096)
+            .expireAfterWrite(10, TimeUnit.MINUTES);
         }
       });
     }
@@ -167,25 +165,30 @@
       }
       req.setAttribute(ATT_CONTROL, pc);
 
-      return manager.openRepository(pc.getProject().getNameKey());
+      try {
+        return manager.openRepository(pc.getProject().getNameKey());
+      } catch (IOException e) {
+        throw new RepositoryNotFoundException(
+            pc.getProject().getNameKey().get(), e);
+      }
     }
   }
 
   static class UploadFactory implements UploadPackFactory<HttpServletRequest> {
-    private final PackConfig packConfig;
+    private final TransferConfig config;
     private final Provider<WebSession> session;
 
     @Inject
     UploadFactory(TransferConfig tc, Provider<WebSession> session) {
-      this.packConfig = tc.getPackConfig();
+      this.config = tc;
       this.session = session;
     }
 
     @Override
     public UploadPack create(HttpServletRequest req, Repository repo) {
       UploadPack up = new UploadPack(repo);
-      up.setPackConfig(packConfig);
-      session.get().setAccessPath(AccessPath.GIT);
+      up.setPackConfig(config.getPackConfig());
+      up.setTimeout(config.getTimeout());
       return up;
     }
   }
@@ -234,12 +237,14 @@
   static class ReceiveFactory implements ReceivePackFactory<HttpServletRequest> {
     private final AsyncReceiveCommits.Factory factory;
     private final Provider<WebSession> session;
+    private final TransferConfig config;
 
     @Inject
     ReceiveFactory(AsyncReceiveCommits.Factory factory,
-        Provider<WebSession> session) {
+        Provider<WebSession> session, TransferConfig config) {
       this.factory = factory;
       this.session = session;
+      this.config = config;
     }
 
     @Override
@@ -254,10 +259,12 @@
 
       final IdentifiedUser user = (IdentifiedUser) pc.getCurrentUser();
       final ReceiveCommits rc = factory.create(pc, db).getReceiveCommits();
-      rc.getReceivePack().setRefLogIdent(user.newRefLogIdent());
+      ReceivePack rp = rc.getReceivePack();
+      rp.setRefLogIdent(user.newRefLogIdent());
+      rp.setTimeout(config.getTimeout());
+      rp.setMaxObjectSizeLimit(config.getMaxObjectSizeLimit());
       req.setAttribute(ATT_RC, rc);
-      session.get().setAccessPath(AccessPath.GIT);
-      return rc.getReceivePack();
+      return rp;
     }
   }
 
@@ -315,12 +322,12 @@
 
       if (isGet) {
         rc.advertiseHistory();
-        cache.remove(cacheKey);
+        cache.invalidate(cacheKey);
       } else {
-        Set<ObjectId> ids = cache.get(cacheKey);
+        Set<ObjectId> ids = cache.getIfPresent(cacheKey);
         if (ids != null) {
           rp.getAdvertisedObjects().addAll(ids);
-          cache.remove(cacheKey);
+          cache.invalidate(cacheKey);
         }
       }
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpIdentifiedUserProvider.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpIdentifiedUserProvider.java
deleted file mode 100644
index 6c420a5..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpIdentifiedUserProvider.java
+++ /dev/null
@@ -1,41 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.httpd;
-
-import com.google.gerrit.common.errors.NotSignedInException;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.ProvisionException;
-
-class HttpIdentifiedUserProvider implements Provider<IdentifiedUser> {
-  private final Provider<CurrentUser> currUserProvider;
-
-  @Inject
-  HttpIdentifiedUserProvider(Provider<CurrentUser> currUserProvider) {
-    this.currUserProvider = currUserProvider;
-  }
-
-  @Override
-  public IdentifiedUser get() {
-    CurrentUser user = currUserProvider.get();
-    if (user instanceof IdentifiedUser) {
-      return (IdentifiedUser) user;
-    }
-    throw new ProvisionException(NotSignedInException.MESSAGE,
-        new NotSignedInException());
-  }
-}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpLogoutServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpLogoutServlet.java
index 13a6f43..e9b3500 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpLogoutServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpLogoutServlet.java
@@ -14,7 +14,10 @@
 
 package com.google.gerrit.httpd;
 
+import com.google.gerrit.audit.AuditEvent;
+import com.google.gerrit.audit.AuditService;
 import com.google.common.base.Strings;
+import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.AccountManager;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.CanonicalWebUrl;
@@ -36,19 +39,21 @@
   private final Provider<WebSession> webSession;
   private final Provider<String> urlProvider;
   private final String logoutUrl;
+  private final AuditService audit;
 
   @Inject
   HttpLogoutServlet(final AuthConfig authConfig,
       final Provider<WebSession> webSession,
       @CanonicalWebUrl @Nullable final Provider<String> urlProvider,
-      final AccountManager accountManager) {
+      final AccountManager accountManager,
+      final AuditService audit) {
     this.webSession = webSession;
     this.urlProvider = urlProvider;
     this.logoutUrl = authConfig.getLogoutURL();
+    this.audit = audit;
   }
 
-  @Override
-  protected void doGet(final HttpServletRequest req,
+  private void doLogout(final HttpServletRequest req,
       final HttpServletResponse rsp) throws IOException {
     webSession.get().logout();
     if (logoutUrl != null) {
@@ -67,4 +72,22 @@
       rsp.sendRedirect(url);
     }
   }
+
+  @Override
+  protected void doGet(final HttpServletRequest req,
+      final HttpServletResponse rsp) throws IOException {
+
+    final String sid = webSession.get().getToken();
+    final CurrentUser currentUser = webSession.get().getCurrentUser();
+    final String what = "sign out";
+    final long when = System.currentTimeMillis();
+
+    try {
+      doLogout(req, rsp);
+    } finally {
+      audit.dispatch(new AuditEvent(sid, currentUser,
+          what, when, null, null));
+    }
+  }
+
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpCurrentUserProvider.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpRequestContext.java
similarity index 81%
rename from gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpCurrentUserProvider.java
rename to gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpRequestContext.java
index c87a143..8ef826b 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpCurrentUserProvider.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpRequestContext.java
@@ -15,19 +15,19 @@
 package com.google.gerrit.httpd;
 
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.util.RequestContext;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 
-class HttpCurrentUserProvider implements Provider<CurrentUser> {
+class HttpRequestContext implements RequestContext {
   private final WebSession session;
 
   @Inject
-  HttpCurrentUserProvider(final WebSession session) {
+  HttpRequestContext(final WebSession session) {
     this.session = session;
   }
 
   @Override
-  public CurrentUser get() {
+  public CurrentUser getCurrentUser() {
     return session.getCurrentUser();
   }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java
new file mode 100644
index 0000000..5b39cb2
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java
@@ -0,0 +1,202 @@
+// 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;
+
+import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Strings;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.AccountException;
+import com.google.gerrit.server.account.AccountManager;
+import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.account.AuthMethod;
+import com.google.gerrit.server.account.AuthRequest;
+import com.google.gerrit.server.account.AuthResult;
+import com.google.gerrit.server.config.AuthConfig;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import org.apache.commons.codec.binary.Base64;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.Locale;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpServletResponseWrapper;
+
+/**
+ * Authenticates the current user by HTTP basic authentication.
+ * <p>
+ * The current HTTP request is authenticated by looking up the username and
+ * password from the Base64 encoded Authorization header and validating them
+ * against any username/password configured authentication system in Gerrit.
+ * This filter is intended only to protect the {@link ProjectServlet} and its
+ * handled URLs, which provide remote repository access over HTTP.
+ *
+ * @see <a href="http://www.ietf.org/rfc/rfc2617.txt">RFC 2617</a>
+ */
+@Singleton
+class ProjectBasicAuthFilter implements Filter {
+  private static final Logger log = LoggerFactory
+      .getLogger(ProjectBasicAuthFilter.class);
+
+  public static final String REALM_NAME = "Gerrit Code Review";
+  private static final String AUTHORIZATION = "Authorization";
+  private static final String LIT_BASIC = "Basic ";
+
+  private final Provider<WebSession> session;
+  private final AccountCache accountCache;
+  private final AccountManager accountManager;
+  private final AuthConfig authConfig;
+
+  @Inject
+  ProjectBasicAuthFilter(Provider<WebSession> session,
+      AccountCache accountCache, AccountManager accountManager,
+      AuthConfig authConfig) {
+    this.session = session;
+    this.accountCache = accountCache;
+    this.accountManager = accountManager;
+    this.authConfig = authConfig;
+  }
+
+  @Override
+  public void init(FilterConfig config) {
+  }
+
+  @Override
+  public void destroy() {
+  }
+
+  @Override
+  public void doFilter(ServletRequest request, ServletResponse response,
+      FilterChain chain) throws IOException, ServletException {
+    HttpServletRequest req = (HttpServletRequest) request;
+    Response rsp = new Response((HttpServletResponse) response);
+
+    if (verify(req, rsp)) {
+      chain.doFilter(req, rsp);
+    }
+  }
+
+  private boolean verify(HttpServletRequest req, Response rsp)
+      throws IOException {
+    final String hdr = req.getHeader(AUTHORIZATION);
+    if (hdr == null) {
+      // Allow an anonymous connection through, or it might be using a
+      // session cookie instead of basic authentication.
+      //
+      return true;
+    }
+
+    final byte[] decoded =
+        Base64.decodeBase64(hdr.substring(LIT_BASIC.length()));
+    String usernamePassword = new String(decoded, encoding(req));
+    int splitPos = usernamePassword.indexOf(':');
+    if (splitPos < 1) {
+      rsp.sendError(SC_UNAUTHORIZED);
+      return false;
+    }
+
+    String username = usernamePassword.substring(0, splitPos);
+    String password = usernamePassword.substring(splitPos + 1);
+    if (Strings.isNullOrEmpty(password)) {
+      rsp.sendError(SC_UNAUTHORIZED);
+      return false;
+    }
+    if (authConfig.isUserNameToLowerCase()) {
+      username = username.toLowerCase(Locale.US);
+    }
+
+    final AccountState who = accountCache.getByUsername(username);
+    if (who == null || !who.getAccount().isActive()) {
+      log.warn("Authentication failed for " + username
+          + ": account inactive or not provisioned in Gerrit");
+      rsp.sendError(SC_UNAUTHORIZED);
+      return false;
+    }
+
+    AuthRequest whoAuth = AuthRequest.forUser(username);
+    whoAuth.setPassword(password);
+
+    try {
+      AuthResult whoAuthResult = accountManager.authenticate(whoAuth);
+      session.get().setUserAccountId(whoAuthResult.getAccountId(),
+          AuthMethod.PASSWORD);
+      return true;
+    } catch (AccountException e) {
+      log.warn("Authentication failed for " + username, e);
+      rsp.sendError(SC_UNAUTHORIZED);
+      return false;
+    }
+  }
+
+  private String encoding(HttpServletRequest req) {
+    return Objects.firstNonNull(req.getCharacterEncoding(), "UTF-8");
+  }
+
+  class Response extends HttpServletResponseWrapper {
+    private static final String WWW_AUTHENTICATE = "WWW-Authenticate";
+
+    Response(HttpServletResponse rsp) {
+      super(rsp);
+    }
+
+    private void status(int sc) {
+      if (sc == SC_UNAUTHORIZED) {
+        StringBuilder v = new StringBuilder();
+        v.append(LIT_BASIC);
+        v.append("realm=\"" + REALM_NAME + "\"");
+        setHeader(WWW_AUTHENTICATE, v.toString());
+      } else if (containsHeader(WWW_AUTHENTICATE)) {
+        setHeader(WWW_AUTHENTICATE, null);
+      }
+    }
+
+    @Override
+    public void sendError(int sc, String msg) throws IOException {
+      status(sc);
+      super.sendError(sc, msg);
+    }
+
+    @Override
+    public void sendError(int sc) throws IOException {
+      status(sc);
+      super.sendError(sc);
+    }
+
+    @Override
+    public void setStatus(int sc, String sm) {
+      status(sc);
+      super.setStatus(sc, sm);
+    }
+
+    @Override
+    public void setStatus(int sc) {
+      status(sc);
+      super.setStatus(sc);
+    }
+  }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectDigestFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectDigestFilter.java
index c5b0e90..84aa532 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectDigestFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectDigestFilter.java
@@ -22,6 +22,7 @@
 
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.account.AuthMethod;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gwtjsonrpc.server.SignedToken;
@@ -30,7 +31,6 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
-import org.eclipse.jgit.http.server.GitSmartHttpTools;
 import org.eclipse.jgit.lib.Config;
 
 import java.io.IOException;
@@ -100,12 +100,7 @@
   public void doFilter(ServletRequest request, ServletResponse response,
       FilterChain chain) throws IOException, ServletException {
     HttpServletRequest req = (HttpServletRequest) request;
-    if (!GitSmartHttpTools.isGitClient(req)) {
-      chain.doFilter(request, response);
-      return;
-    }
-
-    Response rsp = new Response((HttpServletResponse) response);
+    Response rsp = new Response(req, (HttpServletResponse) response);
 
     if (verify(req, rsp)) {
       chain.doFilter(req, rsp);
@@ -170,7 +165,9 @@
     if (expect.equals(response)) {
       try {
         if (tokens.checkToken(nonce, "") != null) {
-          session.get().setUserAccountId(who.getAccount().getId());
+          session.get().setUserAccountId(
+              who.getAccount().getId(),
+              AuthMethod.PASSWORD);
           return true;
 
         } else {
@@ -281,10 +278,6 @@
     return p;
   }
 
-  private String getDomain() {
-    return urlProvider.get() + "p/";
-  }
-
   private String newNonce() {
     try {
       return tokens.newToken("");
@@ -295,11 +288,12 @@
 
   class Response extends HttpServletResponseWrapper {
     private static final String WWW_AUTHENTICATE = "WWW-Authenticate";
-
+    private final HttpServletRequest req;
     Boolean stale;
 
-    Response(HttpServletResponse rsp) {
+    Response(HttpServletRequest req, HttpServletResponse rsp) {
       super(rsp);
+      this.req = req;
     }
 
     private void status(int sc) {
@@ -307,7 +301,18 @@
         StringBuilder v = new StringBuilder();
         v.append("Digest");
         v.append(" realm=\"" + REALM_NAME + "\"");
-        v.append(", domain=\"" + getDomain() + "\"");
+
+        String url = urlProvider.get();
+        if (url == null) {
+          url = req.getContextPath();
+          if (url != null && !url.isEmpty() && !url.endsWith("/")) {
+            url += "/";
+          }
+        }
+        if (url != null && !url.isEmpty()) {
+          v.append(", domain=\"" + url + "\"");
+        }
+
         v.append(", qop=\"auth\"");
         if (stale != null) {
           v.append(", stale=" + stale);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequestCleanupFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequestContextFilter.java
similarity index 61%
copy from gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequestCleanupFilter.java
copy to gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequestContextFilter.java
index 0e6a567..b46505f 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequestCleanupFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequestContextFilter.java
@@ -15,9 +15,13 @@
 package com.google.gerrit.httpd;
 
 import com.google.gerrit.server.RequestCleanup;
+import com.google.gerrit.server.util.RequestContext;
+import com.google.gerrit.server.util.ThreadLocalRequestContext;
 import com.google.inject.Inject;
+import com.google.inject.Module;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
+import com.google.inject.servlet.ServletModule;
 
 import java.io.IOException;
 
@@ -30,12 +34,27 @@
 
 /** Executes any pending {@link RequestCleanup} at the end of a request. */
 @Singleton
-class RequestCleanupFilter implements Filter {
+public class RequestContextFilter implements Filter {
+  public static Module module() {
+    return new ServletModule() {
+      @Override
+      protected void configureServlets() {
+        filter("/*").through(RequestContextFilter.class);
+      }
+    };
+  }
+
   private final Provider<RequestCleanup> cleanup;
+  private final Provider<HttpRequestContext> requestContext;
+  private final ThreadLocalRequestContext local;
 
   @Inject
-  RequestCleanupFilter(final Provider<RequestCleanup> r) {
+  RequestContextFilter(final Provider<RequestCleanup> r,
+      final Provider<HttpRequestContext> c,
+      final ThreadLocalRequestContext l) {
     cleanup = r;
+    requestContext = c;
+    local = l;
   }
 
   @Override
@@ -50,10 +69,15 @@
   public void doFilter(final ServletRequest request,
       final ServletResponse response, final FilterChain chain)
       throws IOException, ServletException {
+    RequestContext old = local.setContext(requestContext.get());
     try {
-      chain.doFilter(request, response);
+      try {
+        chain.doFilter(request, response);
+      } finally {
+        cleanup.get().run();
+      }
     } finally {
-      cleanup.get().run();
+      local.setContext(old);
     }
   }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequestCleanupFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequireIdentifiedUserFilter.java
similarity index 62%
rename from gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequestCleanupFilter.java
rename to gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequireIdentifiedUserFilter.java
index 0e6a567..499c2a5 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequestCleanupFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RequireIdentifiedUserFilter.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,7 +14,8 @@
 
 package com.google.gerrit.httpd;
 
-import com.google.gerrit.server.RequestCleanup;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -27,15 +28,16 @@
 import javax.servlet.ServletException;
 import javax.servlet.ServletRequest;
 import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletResponse;
 
-/** Executes any pending {@link RequestCleanup} at the end of a request. */
+/** Requires the user to be authenticated over HTTP. */
 @Singleton
-class RequestCleanupFilter implements Filter {
-  private final Provider<RequestCleanup> cleanup;
+class RequireIdentifiedUserFilter implements Filter {
+  private final Provider<CurrentUser> user;
 
   @Inject
-  RequestCleanupFilter(final Provider<RequestCleanup> r) {
-    cleanup = r;
+  RequireIdentifiedUserFilter(Provider<CurrentUser> user) {
+    this.user = user;
   }
 
   @Override
@@ -47,13 +49,14 @@
   }
 
   @Override
-  public void doFilter(final ServletRequest request,
-      final ServletResponse response, final FilterChain chain)
+  public void doFilter(ServletRequest request,
+      ServletResponse response, FilterChain chain)
       throws IOException, ServletException {
-    try {
+    if (user.get() instanceof IdentifiedUser) {
       chain.doFilter(request, response);
-    } finally {
-      cleanup.get().run();
+    } else {
+      HttpServletResponse res = (HttpServletResponse) response;
+      res.sendError(HttpServletResponse.SC_UNAUTHORIZED);
     }
   }
 }
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
new file mode 100644
index 0000000..99db2f0
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RestApiServlet.java
@@ -0,0 +1,232 @@
+// 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;
+
+import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
+import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Strings;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.CapabilityControl;
+import com.google.gerrit.util.cli.CmdLineParser;
+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;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.StringWriter;
+import java.io.UnsupportedEncodingException;
+import java.util.Collections;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+public abstract class RestApiServlet extends HttpServlet {
+  private static final long serialVersionUID = 1L;
+  private static final Logger log =
+      LoggerFactory.getLogger(RestApiServlet.class);
+
+  /** MIME type used for a JSON response body. */
+  protected static final String JSON_TYPE = JsonConstants.JSON_TYPE;
+
+  /**
+   * Garbage prefix inserted before JSON output to prevent XSSI.
+   * <p>
+   * This prefix is ")]}'\n" and is designed to prevent a web browser from
+   * executing the response body if the resource URI were to be referenced using
+   * a &lt;script src="...&gt; HTML tag from another web site. Clients using the
+   * HTTP interface will need to always strip the first line of response data to
+   * remove this magic header.
+   */
+  protected static final byte[] JSON_MAGIC;
+
+  static {
+    try {
+      JSON_MAGIC = ")]}'\n".getBytes("UTF-8");
+    } catch (UnsupportedEncodingException e) {
+      throw new RuntimeException("UTF-8 not supported", e);
+    }
+  }
+
+  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 {
+    res.setHeader("Expires", "Fri, 01 Jan 1980 00:00:00 GMT");
+    res.setHeader("Pragma", "no-cache");
+    res.setHeader("Cache-Control", "no-cache, must-revalidate");
+    res.setHeader("Content-Disposition", "attachment");
+
+    try {
+      checkRequiresCapability();
+      super.service(req, res);
+    } catch (RequireCapabilityException err) {
+      sendError(res, SC_FORBIDDEN, err.getMessage());
+    } catch (Error err) {
+      handleException(err, req, res);
+    } catch (RuntimeException err) {
+      handleException(err, req, res);
+    }
+  }
+
+  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.",
+          Objects.firstNonNull(
+            user.getUserName(),
+            user instanceof IdentifiedUser
+              ? ((IdentifiedUser) user).getNameEmail()
+              : user.toString()),
+          rc.value());
+        throw new RequireCapabilityException(msg);
+      }
+    }
+  }
+
+  private static void handleException(Throwable err, HttpServletRequest req,
+      HttpServletResponse res) throws IOException {
+    String uri = req.getRequestURI();
+    if (!Strings.isNullOrEmpty(req.getQueryString())) {
+      uri += "?" + req.getQueryString();
+    }
+    log.error(String.format("Error in %s %s", req.getMethod(), uri), err);
+
+    if (!res.isCommitted()) {
+      res.reset();
+      sendError(res, SC_INTERNAL_SERVER_ERROR, "Internal Server Error");
+    }
+  }
+
+  protected static void sendError(HttpServletResponse res,
+      int statusCode, String msg) throws IOException {
+    res.setStatus(statusCode);
+    sendText(null, res, msg);
+  }
+
+  protected static boolean acceptsJson(HttpServletRequest req) {
+    String accept = req.getHeader("Accept");
+    if (accept == null) {
+      return false;
+    } else if (JSON_TYPE.equals(accept)) {
+      return true;
+    } else if (accept.startsWith(JSON_TYPE + ",")) {
+      return true;
+    }
+    for (String p : accept.split("[ ,;][ ,;]*")) {
+      if (JSON_TYPE.equals(p)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  protected static void sendText(@Nullable HttpServletRequest req,
+      HttpServletResponse res, String data) throws IOException {
+    res.setContentType("text/plain");
+    res.setCharacterEncoding("UTF-8");
+    send(req, res, data.getBytes("UTF-8"));
+  }
+
+  protected static void send(@Nullable HttpServletRequest req,
+      HttpServletResponse res, byte[] data) throws IOException {
+    if (data.length > 256 && req != null
+        && RPCServletUtils.acceptsGzipEncoding(req)) {
+      res.setHeader("Content-Encoding", "gzip");
+      data = HtmlDomUtil.compress(data);
+    }
+    res.setContentLength(data.length);
+    OutputStream out = res.getOutputStream();
+    try {
+      out.write(data);
+    } finally {
+      out.close();
+    }
+  }
+
+  public static class ParameterParser {
+    private final CmdLineParser.Factory parserFactory;
+
+    @Inject
+    ParameterParser(CmdLineParser.Factory pf) {
+      this.parserFactory = pf;
+    }
+
+    public <T> boolean parse(T param, HttpServletRequest req,
+        HttpServletResponse res) throws IOException {
+      return parse(param, req, res, Collections.<String>emptySet());
+    }
+
+    public <T> boolean parse(T param, HttpServletRequest req,
+        HttpServletResponse res, Set<String> argNames) throws IOException {
+      CmdLineParser clp = parserFactory.create(param);
+      try {
+        @SuppressWarnings("unchecked")
+        Map<String, String[]> parameterMap = req.getParameterMap();
+        clp.parseOptionMap(parameterMap, argNames);
+      } catch (CmdLineException e) {
+        if (!clp.wasHelpRequestedByOption()) {
+          res.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+          sendText(req, res, e.getMessage());
+          return false;
+        }
+      }
+
+      if (clp.wasHelpRequestedByOption()) {
+        StringWriter msg = new StringWriter();
+        clp.printQueryStringUsage(req.getRequestURI(), msg);
+        msg.write('\n');
+        msg.write('\n');
+        clp.printUsage(msg, null);
+        msg.write('\n');
+        sendText(req, res, msg.toString());
+        return false;
+      }
+
+      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/RestTokenVerifier.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RestTokenVerifier.java
new file mode 100644
index 0000000..783ebc7
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RestTokenVerifier.java
@@ -0,0 +1,58 @@
+// 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;
+
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.mail.RegisterNewEmailSender;
+
+/** Verifies the token sent by {@link RegisterNewEmailSender}. */
+public interface RestTokenVerifier {
+  /**
+   * Construct a token to verify a REST PUT request.
+   *
+   * @param user the caller that wants to make a PUT request
+   * @param url the URL being requested
+   * @return an unforgeable string to send to the user as the body of a GET
+   *         request. Presenting the string in a follow-up POST request provides
+   *         proof the user has the ability to read messages sent to thier
+   *         browser and they likely aren't making the request via XSRF.
+   */
+  public String sign(Account.Id user, String url);
+
+  /**
+   * Decode a token previously created.
+   *
+   * @param user the user making the verify request.
+   * @param url the url user is attempting to access.
+   * @param token the string created by sign.
+   * @throws InvalidTokenException the token is invalid, expired, malformed,
+   *         etc.
+   */
+  public void verify(Account.Id user, String url, String token)
+      throws InvalidTokenException;
+
+  /** Exception thrown when a token does not parse correctly. */
+  public static class InvalidTokenException extends Exception {
+    private static final long serialVersionUID = 1L;
+
+    public InvalidTokenException() {
+      super("Invalid token");
+    }
+
+    public InvalidTokenException(Throwable cause) {
+      super("Invalid token", cause);
+    }
+  }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/SignedTokenRestTokenVerifier.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/SignedTokenRestTokenVerifier.java
new file mode 100644
index 0000000..83d6caa
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/SignedTokenRestTokenVerifier.java
@@ -0,0 +1,97 @@
+// 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;
+
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.config.AuthConfig;
+import com.google.gwtjsonrpc.server.SignedToken;
+import com.google.gwtjsonrpc.server.ValidToken;
+import com.google.gwtjsonrpc.server.XsrfException;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.util.Base64;
+
+import java.io.UnsupportedEncodingException;
+
+/** Verifies the token sent by {@link RestApiServlet}. */
+public class SignedTokenRestTokenVerifier implements RestTokenVerifier {
+  private final SignedToken restToken;
+
+  public static class Module extends AbstractModule {
+    @Override
+    protected void configure() {
+      bind(RestTokenVerifier.class).to(SignedTokenRestTokenVerifier.class);
+    }
+  }
+
+  @Inject
+  SignedTokenRestTokenVerifier(AuthConfig config) {
+    restToken = config.getRestToken();
+  }
+
+  @Override
+  public String sign(Account.Id user, String url) {
+    try {
+      String payload = String.format("%s:%s", user, url);
+      byte[] utf8 = payload.getBytes("UTF-8");
+      String base64 = Base64.encodeBytes(utf8);
+      return restToken.newToken(base64);
+    } catch (XsrfException e) {
+      throw new IllegalArgumentException(e);
+    } catch (UnsupportedEncodingException e) {
+      throw new IllegalArgumentException(e);
+    }
+  }
+
+  @Override
+  public void verify(Account.Id user, String url, String tokenString)
+      throws InvalidTokenException {
+    ValidToken token;
+    try {
+      token = restToken.checkToken(tokenString, null);
+    } catch (XsrfException err) {
+      throw new InvalidTokenException(err);
+    }
+    if (token == null || token.getData() == null || token.getData().isEmpty()) {
+      throw new InvalidTokenException();
+    }
+
+    String payload;
+    try {
+      payload = new String(Base64.decode(token.getData()), "UTF-8");
+    } catch (UnsupportedEncodingException err) {
+      throw new InvalidTokenException(err);
+    }
+
+    int colonPos = payload.indexOf(':');
+    if (colonPos == -1) {
+      throw new InvalidTokenException();
+    }
+
+    Account.Id tokenUser;
+    try {
+      tokenUser = Account.Id.parse(payload.substring(0, colonPos));
+    } catch (IllegalArgumentException err) {
+      throw new InvalidTokenException(err);
+    }
+
+    String tokenUrl = payload.substring(colonPos+1);
+
+    if (!tokenUser.equals(user) || !tokenUrl.equals(url)) {
+      throw new InvalidTokenException();
+    }
+  }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/TokenVerifiedRestApiServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/TokenVerifiedRestApiServlet.java
new file mode 100644
index 0000000..98a1b57
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/TokenVerifiedRestApiServlet.java
@@ -0,0 +1,263 @@
+// 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;
+
+import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
+import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Iterators;
+import com.google.common.collect.Maps;
+import com.google.gerrit.httpd.RestTokenVerifier.InvalidTokenException;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.OutputFormat;
+import com.google.gson.Gson;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonParser;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.net.URLDecoder;
+import java.net.URLEncoder;
+import java.util.Enumeration;
+import java.util.Map;
+
+import javax.annotation.Nullable;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
+import javax.servlet.http.HttpServletResponse;
+
+public abstract class TokenVerifiedRestApiServlet extends RestApiServlet {
+  private static final long serialVersionUID = 1L;
+  private static final String FORM_ENCODED = "application/x-www-form-urlencoded";
+  private static final String UTF_8 = "UTF-8";
+  private static final String AUTHKEY_NAME = "_authkey";
+  private static final String AUTHKEY_HEADER = "X-authkey";
+
+  private final Gson gson;
+  private final Provider<CurrentUser> userProvider;
+  private final RestTokenVerifier verifier;
+
+  @Inject
+  protected TokenVerifiedRestApiServlet(Provider<CurrentUser> userProvider,
+      RestTokenVerifier verifier) {
+    super(userProvider);
+    this.gson = OutputFormat.JSON_COMPACT.newGson();
+    this.userProvider = userProvider;
+    this.verifier = verifier;
+  }
+
+  /**
+   * Process the (possibly state changing) request.
+   *
+   * @param req incoming HTTP request.
+   * @param res outgoing response.
+   * @param requestData JSON object representing the HTTP request parameters.
+   *        Null if the request body was not supplied in JSON format.
+   * @throws IOException
+   * @throws ServletException
+   */
+  protected abstract void doRequest(HttpServletRequest req,
+      HttpServletResponse res,
+      @Nullable JsonObject requestData) throws IOException, ServletException;
+
+  @Override
+  protected final void doGet(HttpServletRequest req, HttpServletResponse res)
+      throws ServletException, IOException {
+    CurrentUser user = userProvider.get();
+    if (!(user instanceof IdentifiedUser)) {
+      sendError(res, SC_UNAUTHORIZED, "API requires authentication");
+      return;
+    }
+
+    TokenInfo info = new TokenInfo();
+    info._authkey = verifier.sign(
+        ((IdentifiedUser) user).getAccountId(),
+        computeUrl(req));
+
+    ByteArrayOutputStream buf = new ByteArrayOutputStream();
+    String type;
+    buf.write(JSON_MAGIC);
+    if (acceptsJson(req)) {
+      type = JSON_TYPE;
+      buf.write(gson.toJson(info).getBytes(UTF_8));
+    } else {
+      type = FORM_ENCODED;
+      buf.write(String.format("%s=%s",
+          AUTHKEY_NAME,
+          URLEncoder.encode(info._authkey, UTF_8)).getBytes(UTF_8));
+    }
+
+    res.setContentType(type);
+    res.setCharacterEncoding(UTF_8);
+    res.setHeader("Content-Disposition", "attachment");
+    send(req, res, buf.toByteArray());
+  }
+
+  @Override
+  protected final void doPost(HttpServletRequest req, HttpServletResponse res)
+      throws IOException, ServletException {
+    CurrentUser user = userProvider.get();
+    if (!(user instanceof IdentifiedUser)) {
+      sendError(res, SC_UNAUTHORIZED, "API requires authentication");
+      return;
+    }
+
+    ParsedBody body;
+    if (JSON_TYPE.equals(req.getContentType())) {
+      body = parseJson(req, res);
+    } else if (FORM_ENCODED.equals(req.getContentType())) {
+      body = parseForm(req, res);
+    } else {
+      sendError(res, SC_BAD_REQUEST, String.format(
+          "Expected Content-Type: %s or %s",
+          JSON_TYPE, FORM_ENCODED));
+      return;
+    }
+
+    if (body == null) {
+      return;
+    }
+
+    if (Strings.isNullOrEmpty(body._authkey)) {
+      String h = req.getHeader(AUTHKEY_HEADER);
+      if (Strings.isNullOrEmpty(h)) {
+        sendError(res, SC_BAD_REQUEST, String.format(
+            "Expected %s in request body or %s in HTTP headers",
+            AUTHKEY_NAME, AUTHKEY_HEADER));
+        return;
+      }
+      body._authkey = URLDecoder.decode(h, UTF_8);
+    }
+
+    try {
+      verifier.verify(
+          ((IdentifiedUser) user).getAccountId(),
+          computeUrl(req),
+          body._authkey);
+    } catch (InvalidTokenException err) {
+      sendError(res, SC_BAD_REQUEST,
+          String.format("Invalid or expired %s", AUTHKEY_NAME));
+      return;
+    }
+
+    doRequest(body.req, res, body.json);
+  }
+
+  private static ParsedBody parseJson(HttpServletRequest req,
+      HttpServletResponse res) throws IOException {
+    try {
+      JsonElement element = new JsonParser().parse(req.getReader());
+      if (!element.isJsonObject()) {
+        sendError(res, SC_BAD_REQUEST, "Expected JSON object in request body");
+        return null;
+      }
+
+      ParsedBody body = new ParsedBody();
+      body.req = req;
+      body.json = (JsonObject) element;
+      JsonElement authKey = body.json.remove(AUTHKEY_NAME);
+      if (authKey != null
+          && authKey.isJsonPrimitive()
+          && authKey.getAsJsonPrimitive().isString()) {
+        body._authkey = authKey.getAsString();
+      }
+      return body;
+    } catch (JsonParseException e) {
+      sendError(res, SC_BAD_REQUEST, "Invalid JSON object in request body");
+      return null;
+    }
+  }
+
+  private static ParsedBody parseForm(HttpServletRequest req,
+      HttpServletResponse res) throws IOException {
+    ParsedBody body = new ParsedBody();
+    body.req = new WrappedRequest(req);
+    body._authkey = req.getParameter(AUTHKEY_NAME);
+    return body;
+  }
+
+  private static String computeUrl(HttpServletRequest req) {
+    StringBuffer url = req.getRequestURL();
+    String qs = req.getQueryString();
+    if (!Strings.isNullOrEmpty(qs)) {
+      url.append('?').append(qs);
+    }
+    return url.toString();
+  }
+
+  private static class TokenInfo {
+    String _authkey;
+  }
+
+  private static class ParsedBody {
+    HttpServletRequest req;
+    String _authkey;
+    JsonObject json;
+  }
+
+  private static class WrappedRequest extends HttpServletRequestWrapper {
+    @SuppressWarnings("rawtypes")
+    private Map parameters;
+
+    WrappedRequest(HttpServletRequest req) {
+      super(req);
+    }
+
+    @Override
+    public String getParameter(String name) {
+      if (AUTHKEY_NAME.equals(name)) {
+        return null;
+      }
+      return super.getParameter(name);
+    }
+
+    @Override
+    public String[] getParameterValues(String name) {
+      if (AUTHKEY_NAME.equals(name)) {
+        return null;
+      }
+      return super.getParameterValues(name);
+    }
+
+    @SuppressWarnings({"rawtypes", "unchecked"})
+    @Override
+    public Map getParameterMap() {
+      Map m = parameters;
+      if (m == null) {
+        m = super.getParameterMap();
+        if (m.containsKey(AUTHKEY_NAME)) {
+          m = Maps.newHashMap(m);
+          m.remove(AUTHKEY_NAME);
+        }
+        parameters = m;
+      }
+      return m;
+    }
+
+    @SuppressWarnings({"rawtypes", "unchecked"})
+    @Override
+    public Enumeration getParameterNames() {
+      return Iterators.asEnumeration(getParameterMap().keySet().iterator());
+    }
+  }
+}
+
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 f90c20d..a7fde9b 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
@@ -16,6 +16,7 @@
 
 import static com.google.inject.Scopes.SINGLETON;
 
+import com.google.common.base.Strings;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.httpd.raw.CatServlet;
 import com.google.gerrit.httpd.raw.HostPageServlet;
@@ -23,14 +24,22 @@
 import com.google.gerrit.httpd.raw.SshInfoServlet;
 import com.google.gerrit.httpd.raw.StaticServlet;
 import com.google.gerrit.httpd.raw.ToolServlet;
+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.project.ListProjectsServlet;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gwtexpui.server.CacheControlFilter;
+import com.google.inject.Inject;
 import com.google.inject.Key;
 import com.google.inject.Provider;
 import com.google.inject.internal.UniqueAnnotations;
 import com.google.inject.servlet.ServletModule;
 
+import org.eclipse.jgit.lib.Config;
+
 import java.io.IOException;
 
 import javax.servlet.http.HttpServlet;
@@ -38,6 +47,21 @@
 import javax.servlet.http.HttpServletResponse;
 
 class UrlModule extends ServletModule {
+  static class UrlConfig {
+    private final boolean deprecatedQuery;
+
+    @Inject
+    UrlConfig(@GerritServerConfig Config cfg) {
+      deprecatedQuery = cfg.getBoolean("site", "enableDeprecatedQuery", true);
+    }
+  }
+
+  private final UrlConfig cfg;
+
+  UrlModule(UrlConfig cfg) {
+    this.cfg = cfg;
+  }
+
   @Override
   protected void configureServlets() {
     filter("/*").through(Key.get(CacheControlFilter.class));
@@ -48,7 +72,6 @@
     serve("/Gerrit/*").with(legacyGerritScreen());
     serve("/cat/*").with(CatServlet.class);
     serve("/logout").with(HttpLogoutServlet.class);
-    serve("/query").with(ChangeQueryServlet.class);
     serve("/signout").with(HttpLogoutServlet.class);
     serve("/ssh_info").with(SshInfoServlet.class);
     serve("/static/*").with(StaticServlet.class);
@@ -69,6 +92,15 @@
     serveRegex("^/([1-9][0-9]*)/?$").with(directChangeById());
     serveRegex("^/p/(.*)$").with(queryProjectNew());
     serveRegex("^/r/(.+)/?$").with(DirectChangeByCommit.class);
+
+    filter("/a/*").through(RequireIdentifiedUserFilter.class);
+    serveRegex("^/(?:a/)?accounts/self/capabilities$").with(AccountCapabilitiesServlet.class);
+    serveRegex("^/(?:a/)?changes/$").with(ListChangesServlet.class);
+    serveRegex("^/(?:a/)?projects/(.*)?$").with(ListProjectsServlet.class);
+
+    if (cfg.deprecatedQuery) {
+      serve("/query").with(DeprecatedChangeQueryServlet.class);
+    }
   }
 
   private Key<HttpServlet> notFound() {
@@ -133,6 +165,11 @@
       protected void doGet(HttpServletRequest req, HttpServletResponse rsp)
           throws IOException {
         String name = req.getPathInfo();
+        if (Strings.isNullOrEmpty(name)) {
+          toGerrit(PageLinks.ADMIN_PROJECTS, req, rsp);
+          return;
+        }
+
         while (name.endsWith("/")) {
           name = name.substring(0, name.length() - 1);
         }
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 8ee2c41..1a48bb5 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.httpd;
 
 import static com.google.inject.Scopes.SINGLETON;
+import static com.google.gerrit.extensions.registration.PrivateInternals_DynamicTypes.registerInParentInjectors;
 
 import com.google.gerrit.common.data.GerritConfig;
 import com.google.gerrit.httpd.auth.become.BecomeAnyAccountLoginServlet;
@@ -23,8 +24,8 @@
 import com.google.gerrit.httpd.auth.ldap.LdapAuthModule;
 import com.google.gerrit.httpd.gitweb.GitWebModule;
 import com.google.gerrit.httpd.rpc.UiRpcModule;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.server.CmdLineParserModule;
 import com.google.gerrit.server.RemotePeer;
 import com.google.gerrit.server.account.AccountManager;
 import com.google.gerrit.server.account.ChangeUserName;
@@ -51,14 +52,17 @@
 
 public class WebModule extends FactoryModule {
   private final AuthConfig authConfig;
+  private final UrlModule.UrlConfig urlConfig;
   private final boolean wantSSL;
   private final GitWebConfig gitWebConfig;
 
   @Inject
   WebModule(final AuthConfig authConfig,
+      final UrlModule.UrlConfig urlConfig,
       @CanonicalWebUrl @Nullable final String canonicalUrl,
       final Injector creatingInjector) {
     this.authConfig = authConfig;
+    this.urlConfig = urlConfig;
     this.wantSSL = canonicalUrl != null && canonicalUrl.startsWith("https:");
 
     this.gitWebConfig =
@@ -72,13 +76,8 @@
 
   @Override
   protected void configure() {
-    install(new ServletModule() {
-      @Override
-      protected void configureServlets() {
-        filter("/*").through(RequestCleanupFilter.class);
-      }
-    });
     bind(RequestScopePropagator.class).to(GuiceRequestScopePropagator.class);
+    bind(HttpRequestContext.class);
 
     if (wantSSL) {
       install(new RequireSslFilter.Module());
@@ -109,6 +108,7 @@
         break;
 
       case OPENID:
+      case OPENID_SSO:
         // OpenID support is bound in WebAppInitializer and Daemon.
       case CUSTOM_EXTENSION:
         break;
@@ -116,7 +116,7 @@
         throw new ProvisionException("Unsupported loginType: " + authConfig.getAuthType());
     }
 
-    install(new UrlModule());
+    install(new UrlModule(urlConfig));
     install(new UiRpcModule());
     install(new GerritRequestModule());
     install(new GitOverHttpServlet.Module());
@@ -135,12 +135,17 @@
     bind(ChangeUserName.CurrentUser.class);
     factory(ChangeUserName.Factory.class);
     factory(ClearPassword.Factory.class);
+    install(new CmdLineParserModule());
     factory(GeneratePassword.Factory.class);
 
     bind(SocketAddress.class).annotatedWith(RemotePeer.class).toProvider(
         HttpRemotePeerProvider.class).in(RequestScoped.class);
 
-    bind(CurrentUser.class).toProvider(HttpCurrentUserProvider.class);
-    bind(IdentifiedUser.class).toProvider(HttpIdentifiedUserProvider.class);
+    install(new LifecycleModule() {
+      @Override
+      protected void configure() {
+        listener().toInstance(registerInParentInjectors());
+      }
+    });
   }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSession.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSession.java
index 2925896..44920a48 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSession.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSession.java
@@ -16,11 +16,13 @@
 
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
-import com.google.gerrit.server.AccessPath;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.AuthMethod;
 import com.google.gerrit.server.account.AuthResult;
 
 public interface WebSession {
+  public AuthMethod getAuthMethod();
+
   public boolean isSignedIn();
 
   public String getToken();
@@ -31,13 +33,10 @@
 
   public CurrentUser getCurrentUser();
 
-  public void login(AuthResult res, boolean rememberMe);
-
-  /** Change the access path from the default of {@link AccessPath#WEB_UI}. */
-  public void setAccessPath(AccessPath path);
+  public void login(AuthResult res, AuthMethod meth, boolean rememberMe);
 
   /** Set the user account for this current request only. */
-  public void setUserAccountId(Account.Id id);
+  public void setUserAccountId(Account.Id id, AuthMethod method);
 
   public void logout();
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSessionManager.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSessionManager.java
index 55d0ca5..4b4edf4 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSessionManager.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSessionManager.java
@@ -26,9 +26,9 @@
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static java.util.concurrent.TimeUnit.MINUTES;
 
+import com.google.common.cache.Cache;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
-import com.google.gerrit.server.cache.Cache;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.inject.Inject;
@@ -43,6 +43,7 @@
 import java.io.ObjectOutputStream;
 import java.io.Serializable;
 import java.security.SecureRandom;
+import java.util.concurrent.TimeUnit;
 
 @Singleton
 class WebSessionManager {
@@ -54,11 +55,11 @@
 
   private final long sessionMaxAgeMillis;
   private final SecureRandom prng;
-  private final Cache<Key, Val> self;
+  private final Cache<String, Val> self;
 
   @Inject
   WebSessionManager(@GerritServerConfig Config cfg,
-      @Named(CACHE_NAME) final Cache<Key, Val> cache) {
+      @Named(CACHE_NAME) final Cache<String, Val> cache) {
     prng = new SecureRandom();
     self = cache;
 
@@ -75,7 +76,7 @@
       prng.nextBytes(rnd);
 
       buf = new ByteArrayOutputStream(3 + nonceLen);
-      writeVarInt32(buf, (int) Key.serialVersionUID);
+      writeVarInt32(buf, (int) Val.serialVersionUID);
       writeVarInt32(buf, who.get());
       writeBytes(buf, rnd);
 
@@ -104,7 +105,9 @@
     final long halfAgeRefresh = sessionMaxAgeMillis >>> 1;
     final long minRefresh = MILLISECONDS.convert(1, HOURS);
     final long refresh = Math.min(halfAgeRefresh, minRefresh);
-    final long refreshCookieAt = now() + refresh;
+    final long now = now();
+    final long refreshCookieAt = now + refresh;
+    final long expiresAt = now + sessionMaxAgeMillis;
 
     if (xsrfToken == null) {
       // If we don't yet have a token for this session, establish one.
@@ -115,8 +118,9 @@
       xsrfToken = CookieBase64.encode(rnd);
     }
 
-    Val val = new Val(who, refreshCookieAt, remember, lastLogin, xsrfToken);
-    self.put(key, val);
+    Val val = new Val(who, refreshCookieAt, remember,
+        lastLogin, xsrfToken, expiresAt);
+    self.put(key.token, val);
     return val;
   }
 
@@ -137,16 +141,19 @@
   }
 
   Val get(final Key key) {
-    return self.get(key);
+    Val val = self.getIfPresent(key.token);
+    if (val != null && val.expiresAt <= now()) {
+      self.invalidate(key.token);
+      return null;
+    }
+    return val;
   }
 
   void destroy(final Key key) {
-    self.remove(key);
+    self.invalidate(key.token);
   }
 
-  static final class Key implements Serializable {
-    static final long serialVersionUID = 2L;
-
+  static final class Key  {
     private transient String token;
 
     Key(final String t) {
@@ -166,33 +173,28 @@
     public boolean equals(Object obj) {
       return obj instanceof Key && token.equals(((Key) obj).token);
     }
-
-    private void writeObject(final ObjectOutputStream out) throws IOException {
-      writeString(out, token);
-    }
-
-    private void readObject(final ObjectInputStream in) throws IOException {
-      token = readString(in);
-    }
   }
 
   static final class Val implements Serializable {
-    static final long serialVersionUID = Key.serialVersionUID;
+    static final long serialVersionUID = 2L;
 
     private transient Account.Id accountId;
     private transient long refreshCookieAt;
     private transient boolean persistentCookie;
     private transient AccountExternalId.Key externalId;
     private transient String xsrfToken;
+    private transient long expiresAt;
 
     Val(final Account.Id accountId, final long refreshCookieAt,
         final boolean persistentCookie, final AccountExternalId.Key externalId,
-        final String xsrfToken) {
+        final String xsrfToken,
+        final long expiresAt) {
       this.accountId = accountId;
       this.refreshCookieAt = refreshCookieAt;
       this.persistentCookie = persistentCookie;
       this.externalId = externalId;
       this.xsrfToken = xsrfToken;
+      this.expiresAt = expiresAt;
     }
 
     Account.Id getAccountId() {
@@ -233,6 +235,9 @@
       writeVarInt32(out, 5);
       writeString(out, xsrfToken);
 
+      writeVarInt32(out, 6);
+      writeFixInt64(out, expiresAt);
+
       writeVarInt32(out, 0);
     }
 
@@ -257,10 +262,16 @@
           case 5:
             xsrfToken = readString(in);
             continue;
+          case 6:
+            expiresAt = readFixInt64(in);
+            continue;
           default:
             throw new IOException("Unknown tag found in object: " + tag);
         }
       }
+      if (expiresAt == 0) {
+        expiresAt = refreshCookieAt + TimeUnit.HOURS.toMillis(2);
+      }
     }
   }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
index 4710c39..0821496 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
@@ -24,6 +24,7 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.account.AccountException;
 import com.google.gerrit.server.account.AccountManager;
+import com.google.gerrit.server.account.AuthMethod;
 import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.account.AuthResult;
 import com.google.gwtorm.server.OrmException;
@@ -113,7 +114,7 @@
     }
 
     if (res != null) {
-      webSession.get().login(res, false);
+      webSession.get().login(res, AuthMethod.BACKDOOR, false);
       final StringBuilder rdr = new StringBuilder();
       rdr.append(req.getContextPath());
       if (IS_DEV && req.getParameter("gwt.codesvr") != null) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java
index 5df004e..9b7eaf5 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java
@@ -19,6 +19,7 @@
 import com.google.gerrit.httpd.WebSession;
 import com.google.gerrit.server.account.AccountException;
 import com.google.gerrit.server.account.AccountManager;
+import com.google.gerrit.server.account.AuthMethod;
 import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.account.AuthResult;
 import com.google.gerrit.server.config.AuthConfig;
@@ -135,7 +136,8 @@
     }
     rdr.append(token);
 
-    webSession.get().login(arsp, true /* persistent cookie */);
+    webSession.get().login(arsp, AuthMethod.COOKIE,
+                           true /* persistent cookie */);
     rsp.sendRedirect(rdr.toString());
   }
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertAuthFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertAuthFilter.java
index 381daa8..ff0eb29 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertAuthFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertAuthFilter.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.httpd.WebSession;
 import com.google.gerrit.server.account.AccountException;
 import com.google.gerrit.server.account.AccountManager;
+import com.google.gerrit.server.account.AuthMethod;
 import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.account.AuthResult;
 import com.google.inject.Inject;
@@ -84,7 +85,7 @@
       log.error(err, e);
       throw new ServletException(err, e);
     }
-    webSession.get().login(arsp, true);
+    webSession.get().login(arsp, AuthMethod.COOKIE, true);
     chain.doFilter(req, rsp);
   }
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/ldap/UserPassAuthServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/ldap/UserPassAuthServiceImpl.java
index 9d14872..348ecbb 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/ldap/UserPassAuthServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/ldap/UserPassAuthServiceImpl.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.server.account.AccountException;
 import com.google.gerrit.server.account.AccountManager;
 import com.google.gerrit.server.account.AccountUserNameException;
+import com.google.gerrit.server.account.AuthMethod;
 import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.account.AuthResult;
 import com.google.gerrit.server.auth.AuthenticationUnavailableException;
@@ -29,11 +30,17 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 class UserPassAuthServiceImpl implements UserPassAuthService {
   private final Provider<WebSession> webSession;
   private final AccountManager accountManager;
   private final AuthType authType;
 
+  private static final Logger log = LoggerFactory
+      .getLogger(UserPassAuthServiceImpl.class);
+
   @Inject
   UserPassAuthServiceImpl(final Provider<WebSession> webSession,
       final AccountManager accountManager, final AuthConfig authConfig) {
@@ -72,6 +79,7 @@
       callback.onSuccess(result);
       return;
     } catch (AccountException e) {
+      log.info(String.format("'%s' failed to sign in: %s", username, e.getMessage()));
       result.setError(LoginResult.Error.INVALID_LOGIN);
       callback.onSuccess(result);
       return;
@@ -79,7 +87,8 @@
 
     result.success = true;
     result.isNew = res.isNew();
-    webSession.get().login(res, true /* persistent cookie */);
+    webSession.get().login(res, AuthMethod.PASSWORD,
+                           true /* persistent cookie */);
     callback.onSuccess(result);
   }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitWebServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitWebServlet.java
index fb04226..6c37e43 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitWebServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitWebServlet.java
@@ -370,7 +370,7 @@
     final ProjectControl project;
     try {
       project = projectControl.validateFor(nameKey);
-      if (!project.allRefsAreVisible()) {
+      if (!project.allRefsAreVisible() && !project.isOwner()) {
          // Pretend the project doesn't exist
         throw new NoSuchProjectException(nameKey);
       }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpAutoRegisterModuleGenerator.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpAutoRegisterModuleGenerator.java
new file mode 100644
index 0000000..2d957f2
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpAutoRegisterModuleGenerator.java
@@ -0,0 +1,69 @@
+// 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.common.collect.Maps;
+import com.google.gerrit.extensions.annotations.Export;
+import com.google.gerrit.server.plugins.InvalidPluginException;
+import com.google.gerrit.server.plugins.ModuleGenerator;
+import com.google.inject.Module;
+import com.google.inject.Scopes;
+import com.google.inject.servlet.ServletModule;
+
+import java.util.Map;
+
+import javax.servlet.http.HttpServlet;
+
+class HttpAutoRegisterModuleGenerator extends ServletModule
+    implements ModuleGenerator {
+  private final Map<String, Class<HttpServlet>> serve = Maps.newHashMap();
+
+  @Override
+  protected void configureServlets() {
+    for (Map.Entry<String, Class<HttpServlet>> e : serve.entrySet()) {
+      bind(e.getValue()).in(Scopes.SINGLETON);
+      serve(e.getKey()).with(e.getValue());
+    }
+  }
+
+  @Override
+  public void setPluginName(String name) {
+  }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public void export(Export export, Class<?> type)
+      throws InvalidPluginException {
+    if (HttpServlet.class.isAssignableFrom(type)) {
+      Class<HttpServlet> old = serve.get(export.value());
+      if (old != null) {
+        throw new InvalidPluginException(String.format(
+            "@Export(\"%s\") has duplicate bindings:\n  %s\n  %s",
+            export.value(), old.getName(), type.getName()));
+      }
+      serve.put(export.value(), (Class<HttpServlet>) type);
+    } else {
+      throw new InvalidPluginException(String.format(
+          "Class %s with @Export(\"%s\") must extend %s",
+          type.getName(), export.value(),
+          HttpServlet.class.getName()));
+    }
+  }
+
+  @Override
+  public Module create() throws InvalidPluginException {
+    return this;
+  }
+}
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
new file mode 100644
index 0000000..2bcaa30
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginModule.java
@@ -0,0 +1,53 @@
+// 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.cache.CacheModule;
+import com.google.gerrit.server.plugins.ModuleGenerator;
+import com.google.gerrit.server.plugins.ReloadPluginListener;
+import com.google.gerrit.server.plugins.StartPluginListener;
+import com.google.inject.internal.UniqueAnnotations;
+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);
+    serve("/plugins/*").with(HttpPluginServlet.class);
+    serveRegex("^/(?:a/)?plugins/(.*)?$").with(HttpPluginServlet.class);
+
+    bind(StartPluginListener.class)
+      .annotatedWith(UniqueAnnotations.create())
+      .to(HttpPluginServlet.class);
+
+    bind(ReloadPluginListener.class)
+      .annotatedWith(UniqueAnnotations.create())
+      .to(HttpPluginServlet.class);
+
+    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
new file mode 100644
index 0000000..e737700
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
@@ -0,0 +1,596 @@
+// 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.common.base.Strings;
+import com.google.common.cache.Cache;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.gerrit.extensions.registration.RegistrationHandle;
+import com.google.gerrit.httpd.rpc.plugin.ListPluginsServlet;
+import com.google.gerrit.server.MimeUtilFileTypeRegistry;
+import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.documentation.MarkdownFormatter;
+import com.google.gerrit.server.plugins.Plugin;
+import com.google.gerrit.server.plugins.ReloadPluginListener;
+import com.google.gerrit.server.plugins.StartPluginListener;
+import com.google.gerrit.server.ssh.SshInfo;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import com.google.inject.name.Named;
+import com.google.inject.servlet.GuiceFilter;
+
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.util.IO;
+import org.eclipse.jgit.util.RawParseUtils;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+import java.nio.charset.Charset;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Enumeration;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentMap;
+import java.util.jar.Attributes;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+import javax.servlet.FilterChain;
+import javax.servlet.ServletConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletRequestWrapper;
+import javax.servlet.http.HttpServletResponse;
+
+@Singleton
+class HttpPluginServlet extends HttpServlet
+    implements StartPluginListener, ReloadPluginListener {
+  private static final int SMALL_RESOURCE = 128 * 1024;
+  private static final long serialVersionUID = 1L;
+  private static final Logger log
+      = LoggerFactory.getLogger(HttpPluginServlet.class);
+
+  private final MimeUtilFileTypeRegistry mimeUtil;
+  private final Provider<String> webUrl;
+  private final Cache<ResourceKey, Resource> resourceCache;
+  private final String sshHost;
+  private final int sshPort;
+  private final ListPluginsServlet listServlet;
+
+  private List<Plugin> pending = Lists.newArrayList();
+  private String base;
+  private final ConcurrentMap<String, PluginHolder> plugins
+      = Maps.newConcurrentMap();
+
+  @Inject
+  HttpPluginServlet(MimeUtilFileTypeRegistry mimeUtil,
+      @CanonicalWebUrl Provider<String> webUrl,
+      @Named(HttpPluginModule.PLUGIN_RESOURCES) Cache<ResourceKey, Resource> cache,
+      @GerritServerConfig Config cfg,
+      SshInfo sshInfo, ListPluginsServlet listServlet) {
+    this.mimeUtil = mimeUtil;
+    this.webUrl = webUrl;
+    this.resourceCache = cache;
+    this.listServlet = listServlet;
+
+    String sshHost = "review.example.com";
+    int sshPort = 29418;
+    if (!sshInfo.getHostKeys().isEmpty()) {
+      String host = sshInfo.getHostKeys().get(0).getHost();
+      int c = host.lastIndexOf(':');
+      if (0 <= c) {
+        sshHost = host.substring(0, c);
+        sshPort = Integer.parseInt(host.substring(c+1));
+      } else {
+        sshHost = host;
+        sshPort = 22;
+      }
+    }
+    this.sshHost = sshHost;
+    this.sshPort = sshPort;
+  }
+
+  @Override
+  public synchronized void init(ServletConfig config) throws ServletException {
+    super.init(config);
+
+    String path = config.getServletContext().getContextPath();
+    base = Strings.nullToEmpty(path) + "/plugins/";
+    for (Plugin plugin : pending) {
+      install(plugin);
+    }
+    pending = null;
+  }
+
+  @Override
+  public synchronized void onStartPlugin(Plugin plugin) {
+    if (pending != null) {
+      pending.add(plugin);
+    } else {
+      install(plugin);
+    }
+  }
+
+  @Override
+  public void onReloadPlugin(Plugin oldPlugin, Plugin newPlugin) {
+    install(newPlugin);
+  }
+
+  private void install(Plugin plugin) {
+    GuiceFilter filter = load(plugin);
+    final String name = plugin.getName();
+    final PluginHolder holder = new PluginHolder(plugin, filter);
+    plugin.add(new RegistrationHandle() {
+      @Override
+      public void remove() {
+        plugins.remove(name, holder);
+      }
+    });
+    plugins.put(name, holder);
+  }
+
+  private GuiceFilter load(Plugin plugin) {
+    if (plugin.getHttpInjector() != null) {
+      final String name = plugin.getName();
+      final GuiceFilter filter;
+      try {
+        filter = plugin.getHttpInjector().getInstance(GuiceFilter.class);
+      } catch (RuntimeException e) {
+        log.warn(String.format("Plugin %s cannot load GuiceFilter", name), e);
+        return null;
+      }
+
+      try {
+        WrappedContext ctx = new WrappedContext(plugin, base + name);
+        filter.init(new WrappedFilterConfig(ctx));
+      } catch (ServletException e) {
+        log.warn(String.format("Plugin %s failed to initialize HTTP", name), e);
+        return null;
+      }
+
+      plugin.add(new RegistrationHandle() {
+        @Override
+        public void remove() {
+          filter.destroy();
+        }
+      });
+      return filter;
+    }
+    return null;
+  }
+
+  @Override
+  public void service(HttpServletRequest req, HttpServletResponse res)
+      throws IOException, ServletException {
+    String name = extractName(req);
+    if (name.equals("")) {
+      listServlet.service(req, res);
+      return;
+    }
+    final PluginHolder holder = plugins.get(name);
+    if (holder == null) {
+      noCache(res);
+      res.sendError(HttpServletResponse.SC_NOT_FOUND);
+      return;
+    }
+
+    WrappedRequest wr = new WrappedRequest(req, base + name);
+    FilterChain chain = new FilterChain() {
+      @Override
+      public void doFilter(ServletRequest req, ServletResponse res)
+          throws IOException {
+        onDefault(holder, (HttpServletRequest) req, (HttpServletResponse) res);
+      }
+    };
+    if (holder.filter != null) {
+      holder.filter.doFilter(wr, res, chain);
+    } else {
+      chain.doFilter(wr, res);
+    }
+  }
+
+  private void onDefault(PluginHolder holder,
+      HttpServletRequest req,
+      HttpServletResponse res) throws IOException {
+    if (!"GET".equals(req.getMethod()) && !"HEAD".equals(req.getMethod())) {
+      noCache(res);
+      res.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
+      return;
+    }
+
+    String uri = req.getRequestURI();
+    String ctx = req.getContextPath();
+    if (uri.length() <= ctx.length()) {
+      Resource.NOT_FOUND.send(req, res);
+      return;
+    }
+
+    String file = uri.substring(ctx.length() + 1);
+    ResourceKey key = new ResourceKey(holder.plugin, file);
+    Resource rsc = resourceCache.getIfPresent(key);
+    if (rsc != null) {
+      rsc.send(req, res);
+      return;
+    }
+
+    if ("".equals(file)) {
+      res.sendRedirect(uri + "Documentation/index.html");
+    } else if (file.startsWith("static/")) {
+      JarFile jar = holder.plugin.getJarFile();
+      JarEntry entry = jar.getJarEntry(file);
+      if (exists(entry)) {
+        sendResource(jar, entry, key, res);
+      } else {
+        resourceCache.put(key, Resource.NOT_FOUND);
+        Resource.NOT_FOUND.send(req, res);
+      }
+    } else if (file.equals("Documentation")) {
+      res.sendRedirect(uri + "/index.html");
+    } else if (file.startsWith("Documentation/") && file.endsWith("/")) {
+      res.sendRedirect(uri + "index.html");
+    } else if (file.startsWith("Documentation/")) {
+      JarFile jar = holder.plugin.getJarFile();
+      JarEntry entry = jar.getJarEntry(file);
+      if (!exists(entry)) {
+        entry = findSource(jar, file);
+      }
+      if (!exists(entry) && file.endsWith("/index.html")) {
+        String pfx = file.substring(0, file.length() - "index.html".length());
+        sendAutoIndex(jar, pfx, holder.plugin.getName(), key, res);
+      } else if (exists(entry) && entry.getName().endsWith(".md")) {
+        sendMarkdownAsHtml(jar, entry, holder.plugin.getName(), key, res);
+      } else if (exists(entry)) {
+        sendResource(jar, entry, key, res);
+      } else {
+        resourceCache.put(key, Resource.NOT_FOUND);
+        Resource.NOT_FOUND.send(req, res);
+      }
+    } else {
+      resourceCache.put(key, Resource.NOT_FOUND);
+      Resource.NOT_FOUND.send(req, res);
+    }
+  }
+
+  private void sendAutoIndex(JarFile jar,
+      String prefix, String pluginName,
+      ResourceKey cacheKey, HttpServletResponse res) throws IOException {
+    List<JarEntry> cmds = Lists.newArrayList();
+    List<JarEntry> docs = Lists.newArrayList();
+    Enumeration<JarEntry> entries = jar.entries();
+    while (entries.hasMoreElements()) {
+      JarEntry entry = entries.nextElement();
+      String name = entry.getName();
+      long size = entry.getSize();
+      if (name.startsWith(prefix)
+          && (name.endsWith(".md")
+              || name.endsWith(".html"))
+          && 0 < size && size <= SMALL_RESOURCE) {
+        if (name.substring(prefix.length()).startsWith("cmd-")) {
+          cmds.add(entry);
+        } else {
+          docs.add(entry);
+        }
+      }
+    }
+    Collections.sort(cmds, new Comparator<JarEntry>() {
+      @Override
+      public int compare(JarEntry a, JarEntry b) {
+        return a.getName().compareTo(b.getName());
+      }
+    });
+    Collections.sort(docs, new Comparator<JarEntry>() {
+      @Override
+      public int compare(JarEntry a, JarEntry b) {
+        return a.getName().compareTo(b.getName());
+      }
+    });
+
+    StringBuilder md = new StringBuilder();
+    md.append(String.format("# Plugin %s #\n", pluginName));
+    md.append("\n");
+    appendPluginInfoTable(md, jar.getManifest().getMainAttributes());
+
+    if (!docs.isEmpty()) {
+      md.append("## Documentation ##\n");
+      for(JarEntry entry : docs) {
+        String rsrc = entry.getName().substring(prefix.length());
+        String title;
+        if (rsrc.endsWith(".html")) {
+          title = rsrc.substring(0, rsrc.length() - 5).replace('-', ' ');
+        } else if (rsrc.endsWith(".md")) {
+          title = extractTitleFromMarkdown(jar, entry);
+          if (Strings.isNullOrEmpty(title)) {
+            title = rsrc.substring(0, rsrc.length() - 3).replace('-', ' ');
+          }
+          rsrc = rsrc.substring(0, rsrc.length() - 3) + ".html";
+        } else {
+          title = rsrc.replace('-', ' ');
+        }
+        md.append(String.format("* [%s](%s)\n", title, rsrc));
+      }
+      md.append("\n");
+    }
+
+    if (!cmds.isEmpty()) {
+      md.append("## Commands ##\n");
+      for(JarEntry entry : cmds) {
+        String rsrc = entry.getName().substring(prefix.length());
+        String title;
+        if (rsrc.endsWith(".html")) {
+          title = rsrc.substring(4, rsrc.length() - 5).replace('-', ' ');
+        } else if (rsrc.endsWith(".md")) {
+          title = extractTitleFromMarkdown(jar, entry);
+          if (Strings.isNullOrEmpty(title)) {
+            title = rsrc.substring(4, rsrc.length() - 3).replace('-', ' ');
+          }
+          rsrc = rsrc.substring(0, rsrc.length() - 3) + ".html";
+        } else {
+          title = rsrc.substring(4).replace('-', ' ');
+        }
+        md.append(String.format("* [%s](%s)\n", title, rsrc));
+      }
+      md.append("\n");
+    }
+
+    sendMarkdownAsHtml(md.toString(), pluginName, cacheKey, res);
+  }
+
+  private void sendMarkdownAsHtml(String md, String pluginName,
+      ResourceKey cacheKey, HttpServletResponse res)
+      throws UnsupportedEncodingException, IOException {
+    Map<String, String> macros = Maps.newHashMap();
+    macros.put("PLUGIN", pluginName);
+    macros.put("SSH_HOST", sshHost);
+    macros.put("SSH_PORT", "" + sshPort);
+    String url = webUrl.get();
+    if (Strings.isNullOrEmpty(url)) {
+      url = "http://review.example.com/";
+    }
+    macros.put("URL", url);
+
+    Matcher m = Pattern.compile("(\\\\)?@([A-Z_]+)@").matcher(md);
+    StringBuffer sb = new StringBuffer();
+    while (m.find()) {
+      String key = m.group(2);
+      String val = macros.get(key);
+      if (m.group(1) != null) {
+        m.appendReplacement(sb, "@" + key + "@");
+      } else if (val != null) {
+        m.appendReplacement(sb, val);
+      } else {
+        m.appendReplacement(sb, "@" + key + "@");
+      }
+    }
+    m.appendTail(sb);
+
+    byte[] html = new MarkdownFormatter()
+      .markdownToDocHtml(sb.toString(), "UTF-8");
+    resourceCache.put(cacheKey, new SmallResource(html)
+        .setContentType("text/html")
+        .setCharacterEncoding("UTF-8"));
+    res.setContentType("text/html");
+    res.setCharacterEncoding("UTF-8");
+    res.setContentLength(html.length);
+    res.getOutputStream().write(html);
+  }
+
+  private static void appendPluginInfoTable(StringBuilder html, Attributes main) {
+    if (main != null) {
+      String t = main.getValue(Attributes.Name.IMPLEMENTATION_TITLE);
+      String n = main.getValue(Attributes.Name.IMPLEMENTATION_VENDOR);
+      String v = main.getValue(Attributes.Name.IMPLEMENTATION_VERSION);
+      String u = main.getValue(Attributes.Name.IMPLEMENTATION_URL);
+      String a = main.getValue("Gerrit-ApiVersion");
+
+      html.append("<table class=\"plugin_info\">");
+      if (!Strings.isNullOrEmpty(t)) {
+        html.append("<tr><th>Name</th><td>")
+            .append(t)
+            .append("</td></tr>\n");
+      }
+      if (!Strings.isNullOrEmpty(n)) {
+        html.append("<tr><th>Vendor</th><td>")
+            .append(n)
+            .append("</td></tr>\n");
+      }
+      if (!Strings.isNullOrEmpty(v)) {
+        html.append("<tr><th>Version</th><td>")
+            .append(v)
+            .append("</td></tr>\n");
+      }
+      if (!Strings.isNullOrEmpty(u)) {
+        html.append("<tr><th>URL</th><td>")
+            .append(String.format("<a href=\"%s\">%s</a>", u, u))
+            .append("</td></tr>\n");
+      }
+      if (!Strings.isNullOrEmpty(a)) {
+        html.append("<tr><th>API Version</th><td>")
+            .append(a)
+            .append("</td></tr>\n");
+      }
+      html.append("</table>\n");
+    }
+  }
+
+  private static String extractTitleFromMarkdown(JarFile jar, JarEntry entry)
+        throws IOException {
+    String charEnc = null;
+    Attributes atts = entry.getAttributes();
+    if (atts != null) {
+      charEnc = Strings.emptyToNull(atts.getValue("Character-Encoding"));
+    }
+    if (charEnc == null) {
+      charEnc = "UTF-8";
+    }
+    return new MarkdownFormatter().extractTitleFromMarkdown(
+          readWholeEntry(jar, entry),
+          charEnc);
+  }
+
+  private static JarEntry findSource(JarFile jar, String file) {
+    if (file.endsWith(".html")) {
+      int d = file.lastIndexOf('.');
+      return jar.getJarEntry(file.substring(0, d) + ".md");
+    }
+    return null;
+  }
+
+  private static boolean exists(JarEntry entry) {
+    return entry != null && entry.getSize() > 0;
+  }
+
+  private void sendMarkdownAsHtml(JarFile jar, JarEntry entry,
+      String pluginName, ResourceKey key, HttpServletResponse res)
+      throws IOException {
+    byte[] rawmd = readWholeEntry(jar, entry);
+    String encoding = null;
+    Attributes atts = entry.getAttributes();
+    if (atts != null) {
+      encoding = Strings.emptyToNull(atts.getValue("Character-Encoding"));
+    }
+
+    String txtmd = RawParseUtils.decode(
+        Charset.forName(encoding != null ? encoding : "UTF-8"),
+        rawmd);
+    long time = entry.getTime();
+    if (0 < time) {
+      res.setDateHeader("Last-Modified", time);
+    }
+    sendMarkdownAsHtml(txtmd, pluginName, key, res);
+  }
+
+  private void sendResource(JarFile jar, JarEntry entry,
+      ResourceKey key, HttpServletResponse res)
+      throws IOException {
+    byte[] data = null;
+    if (entry.getSize() <= SMALL_RESOURCE) {
+      data = readWholeEntry(jar, entry);
+    }
+
+    String contentType = null;
+    String charEnc = null;
+    Attributes atts = entry.getAttributes();
+    if (atts != null) {
+      contentType = Strings.emptyToNull(atts.getValue("Content-Type"));
+      charEnc = Strings.emptyToNull(atts.getValue("Character-Encoding"));
+    }
+    if (contentType == null) {
+      contentType = mimeUtil.getMimeType(entry.getName(), data).toString();
+    }
+
+    long time = entry.getTime();
+    if (0 < time) {
+      res.setDateHeader("Last-Modified", time);
+    }
+    res.setHeader("Content-Length", Long.toString(entry.getSize()));
+    res.setContentType(contentType);
+    if (charEnc != null) {
+      res.setCharacterEncoding(charEnc);
+    }
+    if (data != null) {
+      resourceCache.put(key, new SmallResource(data)
+          .setContentType(contentType)
+          .setCharacterEncoding(charEnc)
+          .setLastModified(time));
+      res.getOutputStream().write(data);
+    } else {
+      InputStream in = jar.getInputStream(entry);
+      try {
+        OutputStream out = res.getOutputStream();
+        try {
+          byte[] tmp = new byte[1024];
+          int n;
+          while ((n = in.read(tmp)) > 0) {
+            out.write(tmp, 0, n);
+          }
+        } finally {
+          out.close();
+        }
+      } finally {
+        in.close();
+      }
+    }
+  }
+
+  private static byte[] readWholeEntry(JarFile jar, JarEntry entry)
+      throws IOException {
+    byte[] data = new byte[(int) entry.getSize()];
+    InputStream in = jar.getInputStream(entry);
+    try {
+      IO.readFully(in, data, 0, data.length);
+    } finally {
+      in.close();
+    }
+    return data;
+  }
+
+  private static String extractName(HttpServletRequest req) {
+    String path = req.getPathInfo();
+    if (Strings.isNullOrEmpty(path) || "/".equals(path)) {
+      return "";
+    }
+    int s = path.indexOf('/', 1);
+    return 0 <= s ? path.substring(1, s) : path.substring(1);
+  }
+
+  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");
+    res.setHeader("Content-Disposition", "attachment");
+  }
+
+  private static class PluginHolder {
+    final Plugin plugin;
+    final GuiceFilter filter;
+
+    PluginHolder(Plugin plugin, GuiceFilter filter) {
+      this.plugin = plugin;
+      this.filter = filter;
+    }
+  }
+
+  private static class WrappedRequest extends HttpServletRequestWrapper {
+    private final String contextPath;
+
+    WrappedRequest(HttpServletRequest req, String contextPath) {
+      super(req);
+      this.contextPath = contextPath;
+    }
+
+    @Override
+    public String getContextPath() {
+      return contextPath;
+    }
+
+    @Override
+    public String getServletPath() {
+      return ((HttpServletRequest) getRequest()).getRequestURI();
+    }
+  }
+}
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/cache/UnnamedCacheBinding.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/ResourceWeigher.java
similarity index 63%
copy from gerrit-server/src/main/java/com/google/gerrit/server/cache/UnnamedCacheBinding.java
copy to gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/ResourceWeigher.java
index 43039e1..2514272 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/UnnamedCacheBinding.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/ResourceWeigher.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.
@@ -12,11 +12,13 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.cache;
+package com.google.gerrit.httpd.plugins;
 
+import com.google.common.cache.Weigher;
 
-/** 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);
+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/plugins/WrappedContext.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/WrappedContext.java
new file mode 100644
index 0000000..daeb6ff
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/WrappedContext.java
@@ -0,0 +1,178 @@
+// 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.common.collect.Maps;
+import com.google.gerrit.common.Version;
+import com.google.gerrit.server.plugins.Plugin;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Collections;
+import java.util.Enumeration;
+import java.util.Set;
+import java.util.concurrent.ConcurrentMap;
+
+import javax.servlet.RequestDispatcher;
+import javax.servlet.Servlet;
+import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
+
+class WrappedContext implements ServletContext {
+  private static final Logger log = LoggerFactory.getLogger("plugin");
+  private final Plugin plugin;
+  private final String contextPath;
+  private final ConcurrentMap<String, Object> attributes;
+
+  WrappedContext(Plugin plugin, String contextPath) {
+    this.plugin = plugin;
+    this.contextPath = contextPath;
+    this.attributes = Maps.newConcurrentMap();
+  }
+
+  @Override
+  public String getContextPath() {
+    return contextPath;
+  }
+
+  @Override
+  public String getInitParameter(String name) {
+    return null;
+  }
+
+  @SuppressWarnings("rawtypes")
+  @Override
+  public Enumeration getInitParameterNames() {
+    return Collections.enumeration(Collections.emptyList());
+  }
+
+  @Override
+  public ServletContext getContext(String name) {
+    return null;
+  }
+
+  @Override
+  public RequestDispatcher getNamedDispatcher(String name) {
+    return null;
+  }
+
+  @Override
+  public RequestDispatcher getRequestDispatcher(String name) {
+    return null;
+  }
+
+  @Override
+  public URL getResource(String name) throws MalformedURLException {
+    return null;
+  }
+
+  @Override
+  public InputStream getResourceAsStream(String name) {
+    return null;
+  }
+
+  @SuppressWarnings("rawtypes")
+  @Override
+  public Set getResourcePaths(String name) {
+    return null;
+  }
+
+  @Override
+  public Servlet getServlet(String name) throws ServletException {
+    return null;
+  }
+
+  @Override
+  public String getRealPath(String name) {
+    return null;
+  }
+
+  @Override
+  public String getServletContextName() {
+    return plugin.getName();
+  }
+
+  @SuppressWarnings("rawtypes")
+  @Override
+  public Enumeration getServletNames() {
+    return Collections.enumeration(Collections.emptyList());
+  }
+
+  @SuppressWarnings("rawtypes")
+  @Override
+  public Enumeration getServlets() {
+    return Collections.enumeration(Collections.emptyList());
+  }
+
+  @Override
+  public void log(Exception reason, String msg) {
+    log(msg, reason);
+  }
+
+  @Override
+  public void log(String msg) {
+    log(msg, null);
+  }
+
+  @Override
+  public void log(String msg, Throwable reason) {
+    log.warn(String.format("[plugin %s] %s", plugin.getName(), msg), reason);
+  }
+
+  @Override
+  public Object getAttribute(String name) {
+    return attributes.get(name);
+  }
+
+  @Override
+  public Enumeration<String> getAttributeNames() {
+    return Collections.enumeration(attributes.keySet());
+  }
+
+  @Override
+  public void setAttribute(String name, Object value) {
+    attributes.put(name, value);
+  }
+
+  @Override
+  public void removeAttribute(String name) {
+    attributes.remove(name);
+  }
+
+  @Override
+  public String getMimeType(String file) {
+    return null;
+  }
+
+  @Override
+  public int getMajorVersion() {
+    return 2;
+  }
+
+  @Override
+  public int getMinorVersion() {
+    return 5;
+  }
+
+  @Override
+  public String getServerInfo() {
+    String v = Version.getVersion();
+    return "Gerrit Code Review/" + (v != null ? v : "dev");
+  }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/WrappedFilterConfig.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/WrappedFilterConfig.java
new file mode 100644
index 0000000..c9107dc
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/WrappedFilterConfig.java
@@ -0,0 +1,52 @@
+// 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.inject.servlet.GuiceFilter;
+
+import java.util.Collections;
+import java.util.Enumeration;
+
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletContext;
+
+class WrappedFilterConfig implements FilterConfig {
+  private final WrappedContext context;
+
+  WrappedFilterConfig(WrappedContext context) {
+    this.context = context;
+  }
+
+  @Override
+  public String getFilterName() {
+    return GuiceFilter.class.getName();
+  }
+
+  @Override
+  public String getInitParameter(String name) {
+    return null;
+  }
+
+  @SuppressWarnings("rawtypes")
+  @Override
+  public Enumeration getInitParameterNames() {
+    return Collections.enumeration(Collections.emptyList());
+  }
+
+  @Override
+  public ServletContext getServletContext() {
+    return context;
+  }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ThemeFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ThemeFactory.java
index 68379d7..9723674 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ThemeFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ThemeFactory.java
@@ -43,6 +43,9 @@
     theme.trimColor = color(name, "trimColor", "#D4E9A9");
     theme.selectionColor = color(name, "selectionColor", "#FFFFCC");
     theme.topMenuColor = color(name, "topMenuColor", theme.trimColor);
+    theme.changeTableOutdatedColor = color(name, "changeTableOutdatedColor", "#F08080");
+    theme.tableOddRowColor = color(name, "tableOddRowColor", "transparent");
+    theme.tableEvenRowColor = color(name, "tableEvenRowColor", "transparent");
     return theme;
   }
 
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 9a101d3..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,259 +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.AccountControl;
-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,
@@ -298,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/GerritJsonServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/GerritJsonServlet.java
index 3513f89..1b24792 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/GerritJsonServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/GerritJsonServlet.java
@@ -14,43 +14,71 @@
 
 package com.google.gerrit.httpd.rpc;
 
+import com.google.common.collect.Lists;
+import com.google.gerrit.audit.AuditEvent;
+import com.google.gerrit.audit.AuditService;
+import com.google.gerrit.common.audit.Audit;
 import com.google.gerrit.common.auth.SignInRequired;
 import com.google.gerrit.common.errors.NotSignedInException;
 import com.google.gerrit.httpd.WebSession;
+import com.google.gerrit.server.CurrentUser;
 import com.google.gson.GsonBuilder;
 import com.google.gwtjsonrpc.common.RemoteJsonService;
 import com.google.gwtjsonrpc.server.ActiveCall;
 import com.google.gwtjsonrpc.server.JsonServlet;
+import com.google.gwtjsonrpc.server.MethodHandle;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.lang.reflect.Field;
+import java.util.Arrays;
+import java.util.List;
+
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
-
 /**
  * Base JSON servlet to ensure the current user is not forged.
  */
 @SuppressWarnings("serial")
 final class GerritJsonServlet extends JsonServlet<GerritJsonServlet.GerritCall> {
+  private static final Logger log = LoggerFactory.getLogger(GerritJsonServlet.class);
+  private static final ThreadLocal<GerritCall> currentCall =
+      new ThreadLocal<GerritCall>();
+  private static final ThreadLocal<MethodHandle> currentMethod =
+      new ThreadLocal<MethodHandle>();
   private final Provider<WebSession> session;
   private final RemoteJsonService service;
+  private final AuditService audit;
+
 
   @Inject
-  GerritJsonServlet(final Provider<WebSession> w, final RemoteJsonService s) {
+  GerritJsonServlet(final Provider<WebSession> w, final RemoteJsonService s,
+      final AuditService a) {
     session = w;
     service = s;
+    audit = a;
   }
 
   @Override
   protected GerritCall createActiveCall(final HttpServletRequest req,
       final HttpServletResponse rsp) {
-    return new GerritCall(session.get(), req, rsp);
+    final GerritCall call = new GerritCall(session.get(), req, rsp);
+    currentCall.set(call);
+    return call;
   }
 
   @Override
   protected GsonBuilder createGsonBuilder() {
-    final GsonBuilder g = super.createGsonBuilder();
+    return gerritDefaultGsonBuilder();
+  }
+
+  private static GsonBuilder gerritDefaultGsonBuilder() {
+    final GsonBuilder g = defaultGsonBuilder();
 
     g.registerTypeAdapter(org.eclipse.jgit.diff.Edit.class,
         new org.eclipse.jgit.diff.EditDeserializer());
@@ -83,13 +111,117 @@
     return service;
   }
 
+  @Override
+  protected void service(final HttpServletRequest req,
+      final HttpServletResponse resp) throws IOException {
+    try {
+      super.service(req, resp);
+    } finally {
+      audit();
+      currentCall.set(null);
+    }
+  }
+
+  private void audit() {
+    try {
+      GerritCall call = currentCall.get();
+      MethodHandle method = call.getMethod();
+      if (method == null) {
+        return;
+      }
+      Audit note = (Audit) method.getAnnotation(Audit.class);
+      if (note != null) {
+        final String sid = call.getWebSession().getToken();
+        final CurrentUser username = call.getWebSession().getCurrentUser();
+        final List<Object> args =
+            extractParams(note, call);
+        final String what = extractWhat(note, method.getName());
+        final Object result = call.getResult();
+
+        audit.dispatch(new AuditEvent(sid, username, what, call.getWhen(), args,
+            result));
+      }
+    } catch (Throwable all) {
+      log.error("Unable to log the call", all);
+    }
+  }
+
+  private List<Object> extractParams(final Audit note, final GerritCall call) {
+    List<Object> args = Lists.newArrayList(Arrays.asList(call.getParams()));
+    for (int idx : note.obfuscate()) {
+      args.set(idx, "*****");
+    }
+    return args;
+  }
+
+  private String extractWhat(final Audit note, final String methodName) {
+    String what = note.action();
+    if (what.length() == 0) {
+      boolean ccase = Character.isLowerCase(methodName.charAt(0));
+
+      StringBuilder sb = new StringBuilder();
+      for (int i = 0; i < methodName.length(); i++) {
+        char c = methodName.charAt(i);
+        if (ccase && !Character.isLowerCase(c)) {
+          sb.append(' ');
+        }
+        sb.append(Character.toLowerCase(c));
+      }
+      what = sb.toString();
+    }
+
+    return what;
+  }
+
   static class GerritCall extends ActiveCall {
     private final WebSession session;
+    private final long when;
+    private static final Field resultField;
+
+    // Needed to allow access to non-public result field in GWT/JSON-RPC
+    static {
+      Field declaredField = null;
+      try {
+        declaredField = ActiveCall.class.getDeclaredField("result");
+        declaredField.setAccessible(true);
+      } catch (Exception e) {
+        log.error("Unable to expose RPS/JSON result field");
+      }
+
+      resultField = declaredField;
+    }
+
+    // Surrogate of the missing getResult() in GWT/JSON-RPC
+    public Object getResult() {
+      if (resultField == null) {
+        return null;
+      }
+
+      try {
+        return resultField.get(this);
+      } catch (IllegalArgumentException e) {
+        log.error("Cannot access result field");
+      } catch (IllegalAccessException e) {
+        log.error("No permissions to access result field");
+      }
+
+      return null;
+    }
 
     GerritCall(final WebSession session, final HttpServletRequest i,
         final HttpServletResponse o) {
       super(i, o);
       this.session = session;
+      this.when = System.currentTimeMillis();
+    }
+
+    @Override
+    public MethodHandle getMethod() {
+      if (currentMethod.get() == null) {
+        return super.getMethod();
+      } else {
+        return currentMethod.get();
+      }
     }
 
     @Override
@@ -120,5 +252,18 @@
         return session.isSignedIn() && session.isTokenValid(keyIn);
       }
     }
+
+    public WebSession getWebSession() {
+      return session;
+    }
+
+    public long getWhen() {
+      return when;
+    }
+
+    public long getElapsed() {
+      return System.currentTimeMillis() - when;
+    }
   }
+
 }
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 f3e8e65e..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,15 +31,13 @@
 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.ProjectCache;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.gwtorm.server.OrmException;
@@ -55,46 +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 ProjectControl.Factory projectControlFactory;
-  private final ProjectCache projectCache;
   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 ProjectControl.Factory projectControlFactory,
-      final ProjectCache projectCache, final AccountCache accountCache,
-      final GroupControl.Factory groupControlFactory,
+      final AccountCache accountCache,
       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.projectControlFactory = projectControlFactory;
-    this.projectCache = projectCache;
     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;
@@ -111,28 +106,6 @@
     }
   }
 
-  public void suggestProjectNameKey(final String query, final int limit,
-      final AsyncCallback<List<Project.NameKey>> callback) {
-    final int max = 10;
-    final int n = limit <= 0 ? max : Math.min(limit, max);
-
-    final List<Project.NameKey> r = new ArrayList<Project.NameKey>(n);
-    for (final Project.NameKey nameKey : projectCache.byName(query)) {
-      final ProjectControl ctl;
-      try {
-        ctl = projectControlFactory.validateFor(nameKey);
-      } catch (NoSuchProjectException e) {
-        continue;
-      }
-
-      r.add(ctl.getProject().getNameKey());
-      if (r.size() == n) {
-        break;
-      }
-    }
-    callback.onSuccess(r);
-  }
-
   private interface VisibilityControl {
     boolean isVisible(Account account) throws OrmException;
   }
@@ -205,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
@@ -258,7 +229,9 @@
           public boolean isVisible(Account account) throws OrmException {
             IdentifiedUser who =
                 identifiedUserFactory.create(reviewDbProvider, account.getId());
-            return changeControl.forUser(who).isVisible(reviewDbProvider.get());
+            // we can't use changeControl directly as it won't suggest reviewers
+            // to drafts
+            return changeControl.forUser(who).isRefVisible();
           }
         };
 
@@ -270,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/SystemInfoServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SystemInfoServiceImpl.java
index 9de7d88..931605c 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SystemInfoServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SystemInfoServiceImpl.java
@@ -14,16 +14,15 @@
 
 package com.google.gerrit.httpd.rpc;
 
+import com.google.common.collect.Lists;
+import com.google.gerrit.common.data.ContributorAgreement;
 import com.google.gerrit.common.data.GerritConfig;
 import com.google.gerrit.common.data.SshHostKey;
 import com.google.gerrit.common.data.SystemInfoService;
-import com.google.gerrit.reviewdb.client.ContributorAgreement;
-import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.ssh.SshInfo;
 import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.gwtjsonrpc.common.VoidResult;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
@@ -34,6 +33,7 @@
 import org.slf4j.LoggerFactory;
 
 import java.util.ArrayList;
+import java.util.Collection;
 import java.util.List;
 
 import javax.servlet.http.HttpServletRequest;
@@ -44,32 +44,31 @@
 
   private static final JSch JSCH = new JSch();
 
-  private final SchemaFactory<ReviewDb> schema;
   private final List<HostKey> hostKeys;
   private final Provider<HttpServletRequest> httpRequest;
   private final Provider<GerritConfig> config;
+  private final ProjectCache projectCache;
 
   @Inject
-  SystemInfoServiceImpl(final SchemaFactory<ReviewDb> sf, final SshInfo daemon,
-      final Provider<HttpServletRequest> hsr, Provider<GerritConfig> cfg) {
-    schema = sf;
+  SystemInfoServiceImpl(final SshInfo daemon,
+      final Provider<HttpServletRequest> hsr, final Provider<GerritConfig> cfg,
+      final ProjectCache pc) {
     hostKeys = daemon.getHostKeys();
     httpRequest = hsr;
     config = cfg;
+    projectCache = pc;
   }
 
   public void contributorAgreements(
       final AsyncCallback<List<ContributorAgreement>> callback) {
-    try {
-      final ReviewDb db = schema.open();
-      try {
-        callback.onSuccess(db.contributorAgreements().active().toList());
-      } finally {
-        db.close();
-      }
-    } catch (OrmException e) {
-      callback.onFailure(e);
+    Collection<ContributorAgreement> agreements =
+        projectCache.getAllProjects().getConfig().getContributorAgreements();
+    List<ContributorAgreement> cas =
+        Lists.newArrayListWithCapacity(agreements.size());
+    for (ContributorAgreement ca : agreements) {
+      cas.add(ca.forUi());
     }
+    callback.onSuccess(cas);
   }
 
   public void daemonHostKeys(final AsyncCallback<List<SshHostKey>> callback) {
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
new file mode 100644
index 0000000..a33c209
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountCapabilitiesServlet.java
@@ -0,0 +1,189 @@
+// 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.account;
+
+import static com.google.gerrit.common.data.GlobalCapability.CREATE_ACCOUNT;
+import static com.google.gerrit.common.data.GlobalCapability.CREATE_GROUP;
+import static com.google.gerrit.common.data.GlobalCapability.CREATE_PROJECT;
+import static com.google.gerrit.common.data.GlobalCapability.FLUSH_CACHES;
+import static com.google.gerrit.common.data.GlobalCapability.KILL_TASK;
+import static com.google.gerrit.common.data.GlobalCapability.PRIORITY;
+import static com.google.gerrit.common.data.GlobalCapability.START_REPLICATION;
+import static com.google.gerrit.common.data.GlobalCapability.VIEW_CACHES;
+import static com.google.gerrit.common.data.GlobalCapability.VIEW_CONNECTIONS;
+import static com.google.gerrit.common.data.GlobalCapability.VIEW_QUEUE;
+
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.common.data.PermissionRange;
+import com.google.gerrit.httpd.RestApiServlet;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.OutputFormat;
+import com.google.gerrit.server.account.CapabilityControl;
+import com.google.gerrit.server.git.QueueProvider;
+import com.google.gson.reflect.TypeToken;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import org.kohsuke.args4j.Option;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+@Singleton
+public class AccountCapabilitiesServlet extends RestApiServlet {
+  private static final long serialVersionUID = 1L;
+  private final ParameterParser paramParser;
+  private final Provider<Impl> factory;
+
+  @Inject
+  AccountCapabilitiesServlet(final Provider<CurrentUser> currentUser,
+      ParameterParser paramParser, Provider<Impl> factory) {
+    super(currentUser);
+    this.paramParser = paramParser;
+    this.factory = factory;
+  }
+
+  @Override
+  protected void doGet(HttpServletRequest req, HttpServletResponse res)
+      throws IOException {
+    Impl impl = factory.get();
+    if (acceptsJson(req)) {
+      impl.format = OutputFormat.JSON_COMPACT;
+    }
+    if (paramParser.parse(impl, req, res)) {
+      impl.compute();
+
+      ByteArrayOutputStream buf = new ByteArrayOutputStream();
+      OutputStreamWriter out = new OutputStreamWriter(buf, "UTF-8");
+      if (impl.format.isJson()) {
+        res.setContentType(JSON_TYPE);
+        buf.write(JSON_MAGIC);
+        impl.format.newGson().toJson(
+            impl.have,
+            new TypeToken<Map<String, Object>>() {}.getType(),
+            out);
+        out.flush();
+        buf.write('\n');
+      } else {
+        res.setContentType("text/plain");
+        for (Map.Entry<String, Object> e : impl.have.entrySet()) {
+          out.write(e.getKey());
+          if (!(e.getValue() instanceof Boolean)) {
+            out.write(": ");
+            out.write(e.getValue().toString());
+          }
+          out.write('\n');
+        }
+        out.flush();
+      }
+      res.setCharacterEncoding("UTF-8");
+      send(req, res, buf.toByteArray());
+    }
+  }
+
+  static class Impl {
+    private final CapabilityControl cc;
+    private final Map<String, Object> have;
+
+    @Option(name = "--format", metaVar = "FMT", usage = "Output display format")
+    private OutputFormat format = OutputFormat.TEXT;
+
+    @Option(name = "-q", metaVar = "CAP", multiValued = true, usage = "Capability to inspect")
+    void addQuery(String name) {
+      if (query == null) {
+        query = Sets.newHashSet();
+      }
+      query.add(name.toLowerCase());
+    }
+    private Set<String> query;
+
+    @Inject
+    Impl(CurrentUser user) {
+      cc = user.getCapabilities();
+      have = Maps.newLinkedHashMap();
+    }
+
+    void compute() {
+      for (String name : GlobalCapability.getAllNames()) {
+        if (!name.equals(PRIORITY) && want(name) && cc.canPerform(name)) {
+          if (GlobalCapability.hasRange(name)) {
+            have.put(name, new Range(cc.getRange(name)));
+          } else {
+            have.put(name, true);
+          }
+        }
+      }
+
+      have.put(CREATE_ACCOUNT, cc.canCreateAccount());
+      have.put(CREATE_GROUP, cc.canCreateGroup());
+      have.put(CREATE_PROJECT, cc.canCreateProject());
+      have.put(KILL_TASK, cc.canKillTask());
+      have.put(VIEW_CACHES, cc.canViewCaches());
+      have.put(FLUSH_CACHES, cc.canFlushCaches());
+      have.put(VIEW_CONNECTIONS, cc.canViewConnections());
+      have.put(VIEW_QUEUE, cc.canViewQueue());
+      have.put(START_REPLICATION, cc.canStartReplication());
+
+      QueueProvider.QueueType queue = cc.getQueueType();
+      if (queue != QueueProvider.QueueType.INTERACTIVE
+          || (query != null && query.contains(PRIORITY))) {
+        have.put(PRIORITY, queue);
+      }
+
+      Iterator<Map.Entry<String, Object>> itr = have.entrySet().iterator();
+      while (itr.hasNext()) {
+        Map.Entry<String, Object> e = itr.next();
+        if (!want(e.getKey())) {
+          itr.remove();
+        } else if (e.getValue() instanceof Boolean && !((Boolean) e.getValue())) {
+          itr.remove();
+        }
+      }
+    }
+
+    private boolean want(String name) {
+      return query == null || query.contains(name.toLowerCase());
+    }
+  }
+
+  private static class Range {
+    private transient PermissionRange range;
+    @SuppressWarnings("unused")
+    private int min;
+    @SuppressWarnings("unused")
+    private int max;
+
+    Range(PermissionRange r) {
+      range = r;
+      min = r.getMin();
+      max = r.getMax();
+    }
+
+    @Override
+    public String toString() {
+      return range.toString();
+    }
+  }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java
index aa94759f..e3b7408 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountSecurityImpl.java
@@ -14,23 +14,26 @@
 
 package com.google.gerrit.httpd.rpc.account;
 
+import com.google.common.base.Strings;
 import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.common.data.AccountSecurity;
-import com.google.gerrit.common.data.GroupDetail;
+import com.google.gerrit.common.data.ContributorAgreement;
 import com.google.gerrit.common.errors.ContactInformationStoreException;
 import com.google.gerrit.common.errors.InvalidSshKeyException;
 import com.google.gerrit.common.errors.NameAlreadyUsedException;
 import com.google.gerrit.common.errors.NoSuchEntityException;
 import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.common.errors.PermissionDeniedException;
 import com.google.gerrit.httpd.rpc.BaseServiceImplementation;
 import com.google.gerrit.httpd.rpc.Handler;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountAgreement;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupMember;
+import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
 import com.google.gerrit.reviewdb.client.AccountSshKey;
 import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.reviewdb.client.ContactInformation;
-import com.google.gerrit.reviewdb.client.ContributorAgreement;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
@@ -42,12 +45,14 @@
 import com.google.gerrit.server.account.ChangeUserName;
 import com.google.gerrit.server.account.ClearPassword;
 import com.google.gerrit.server.account.GeneratePassword;
+import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.account.Realm;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.contact.ContactStore;
 import com.google.gerrit.server.mail.EmailException;
 import com.google.gerrit.server.mail.EmailTokenVerifier;
 import com.google.gerrit.server.mail.RegisterNewEmailSender;
+import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.ssh.SshKeyCache;
 import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.gwtjsonrpc.common.VoidResult;
@@ -68,6 +73,7 @@
   private final ContactStore contactStore;
   private final AuthConfig authConfig;
   private final Realm realm;
+  private final ProjectCache projectCache;
   private final Provider<IdentifiedUser> user;
   private final EmailTokenVerifier emailTokenVerifier;
   private final RegisterNewEmailSender.Factory registerNewEmailFactory;
@@ -85,12 +91,13 @@
   private final MyGroupsFactory.Factory myGroupsFactory;
 
   private final ChangeHooks hooks;
+  private final GroupCache groupCache;
 
   @Inject
   AccountSecurityImpl(final Provider<ReviewDb> schema,
       final Provider<CurrentUser> currentUser, final ContactStore cs,
       final AuthConfig ac, final Realm r, final Provider<IdentifiedUser> u,
-      final EmailTokenVerifier etv,
+      final EmailTokenVerifier etv, final ProjectCache pc,
       final RegisterNewEmailSender.Factory esf, final SshKeyCache skc,
       final AccountByEmailCache abec, final AccountCache uac,
       final AccountManager am,
@@ -100,13 +107,14 @@
       final DeleteExternalIds.Factory deleteExternalIdsFactory,
       final ExternalIdDetailFactory.Factory externalIdDetailFactory,
       final MyGroupsFactory.Factory myGroupsFactory,
-      final ChangeHooks hooks) {
+      final ChangeHooks hooks, final GroupCache groupCache) {
     super(schema, currentUser);
     contactStore = cs;
     authConfig = ac;
     realm = r;
     user = u;
     emailTokenVerifier = etv;
+    projectCache = pc;
     registerNewEmailFactory = esf;
     sshKeyCache = skc;
     byEmailCache = abec;
@@ -122,6 +130,7 @@
     this.externalIdDetailFactory = externalIdDetailFactory;
     this.myGroupsFactory = myGroupsFactory;
     this.hooks = hooks;
+    this.groupCache = groupCache;
   }
 
   public void mySshKeys(final AsyncCallback<List<AccountSshKey>> callback) {
@@ -205,9 +214,9 @@
   }
 
   @Override
-  public void myGroups(final AsyncCallback<List<GroupDetail>> callback) {
-    run(callback, new Action<List<GroupDetail>>() {
-      public List<GroupDetail> run(final ReviewDb db) throws OrmException,
+  public void myGroups(final AsyncCallback<List<AccountGroup>> callback) {
+    run(callback, new Action<List<AccountGroup>>() {
+      public List<AccountGroup> run(final ReviewDb db) throws OrmException,
           NoSuchGroupException, Failure {
         return myGroupsFactory.create().call();
       }
@@ -223,12 +232,17 @@
       final ContactInformation info, final AsyncCallback<Account> callback) {
     run(callback, new Action<Account>() {
       public Account run(ReviewDb db) throws OrmException, Failure {
-        final Account me = db.accounts().get(user.get().getAccountId());
+        IdentifiedUser self = user.get();
+        final Account me = db.accounts().get(self.getAccountId());
         final String oldEmail = me.getPreferredEmail();
         if (realm.allowsEdit(Account.FieldName.FULL_NAME)) {
-          me.setFullName(name != null && !name.isEmpty() ? name : null);
+          me.setFullName(Strings.emptyToNull(name));
         }
-        me.setPreferredEmail(emailAddr);
+        if (!Strings.isNullOrEmpty(emailAddr)
+            && !self.getEmailAddresses().contains(emailAddr)) {
+          throw new Failure(new PermissionDeniedException("Email address must be verified"));
+        }
+        me.setPreferredEmail(Strings.emptyToNull(emailAddr));
         if (useContactInfo) {
           if (ContactInformation.hasAddress(info)
               || (me.isContactFiled() && ContactInformation.hasData(info))) {
@@ -260,24 +274,43 @@
     return a != null && a.equals(b);
   }
 
-  public void enterAgreement(final ContributorAgreement.Id id,
+  public void enterAgreement(final String agreementName,
       final AsyncCallback<VoidResult> callback) {
     run(callback, new Action<VoidResult>() {
       public VoidResult run(final ReviewDb db) throws OrmException, Failure {
-        final ContributorAgreement cla = db.contributorAgreements().get(id);
-        if (cla == null || !cla.isActive()) {
+        ContributorAgreement ca = projectCache.getAllProjects().getConfig()
+            .getContributorAgreement(agreementName);
+        if (ca == null) {
           throw new Failure(new NoSuchEntityException());
         }
 
-        final AccountAgreement a =
-            new AccountAgreement(new AccountAgreement.Key(user.get()
-                .getAccountId(), id));
-        if (cla.isAutoVerify()) {
-          a.review(AccountAgreement.Status.VERIFIED, null);
-
-          hooks.doClaSignupHook(user.get().getAccount(), cla);
+        if (ca.getAutoVerify() == null) {
+          throw new Failure(new IllegalStateException(
+              "cannot enter a non-autoVerify agreement"));
+        } else if (ca.getAutoVerify().getUUID() == null) {
+          throw new Failure(new NoSuchEntityException());
         }
-        db.accountAgreements().insert(Collections.singleton(a));
+
+        AccountGroup group = groupCache.get(ca.getAutoVerify().getUUID());
+        if (group == null) {
+          throw new Failure(new NoSuchEntityException());
+        }
+
+        Account account = user.get().getAccount();
+        hooks.doClaSignupHook(account, ca);
+
+        final AccountGroupMember.Key key =
+            new AccountGroupMember.Key(account.getId(), group.getId());
+        AccountGroupMember m = db.accountGroupMembers().get(key);
+        if (m == null) {
+          m = new AccountGroupMember(key);
+          db.accountGroupMembersAudit().insert(
+              Collections.singleton(
+                  new AccountGroupMemberAudit(m, account.getId())));
+          db.accountGroupMembers().insert(Collections.singleton(m));
+          accountCache.evict(m.getAccountId());
+        }
+
         return VoidResult.INSTANCE;
       }
     });
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AgreementInfoFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AgreementInfoFactory.java
index 39712e4..712f5a2 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AgreementInfoFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AgreementInfoFactory.java
@@ -14,83 +14,71 @@
 
 package com.google.gerrit.httpd.rpc.account;
 
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
 import com.google.gerrit.common.data.AgreementInfo;
+import com.google.gerrit.common.data.ContributorAgreement;
+import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.common.data.PermissionRule.Action;
 import com.google.gerrit.httpd.rpc.Handler;
-import com.google.gerrit.reviewdb.client.AccountAgreement;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupAgreement;
-import com.google.gerrit.reviewdb.client.ContributorAgreement;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.project.ProjectCache;
 import com.google.inject.Inject;
 
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collection;
 import java.util.List;
 import java.util.Map;
 
 class AgreementInfoFactory extends Handler<AgreementInfo> {
+  private final Logger log = LoggerFactory.getLogger(getClass());
+
   interface Factory {
     AgreementInfoFactory create();
   }
 
-  private final ReviewDb db;
-  private final GroupCache groupCache;
   private final IdentifiedUser user;
+  private final ProjectCache projectCache;
 
   private AgreementInfo info;
 
   @Inject
-  AgreementInfoFactory(final ReviewDb db, final GroupCache groupCache,
-      final IdentifiedUser user) {
-    this.db = db;
-    this.groupCache = groupCache;
+  AgreementInfoFactory(final IdentifiedUser user,
+      final ProjectCache projectCache) {
     this.user = user;
+    this.projectCache = projectCache;
   }
 
   @Override
   public AgreementInfo call() throws Exception {
-    final List<AccountAgreement> userAccepted =
-        db.accountAgreements().byAccount(user.getAccountId()).toList();
+    List<String> accepted = Lists.newArrayList();
+    Map<String, ContributorAgreement> agreements = Maps.newHashMap();
+    Collection<ContributorAgreement> cas =
+        projectCache.getAllProjects().getConfig().getContributorAgreements();
+    for (ContributorAgreement ca : cas) {
+      agreements.put(ca.getName(), ca.forUi());
 
-    Collections.reverse(userAccepted);
-
-    final List<AccountGroupAgreement> groupAccepted =
-        new ArrayList<AccountGroupAgreement>();
-    for (final AccountGroup.UUID groupUUID : user.getEffectiveGroups().getKnownGroups()) {
-      AccountGroup group = groupCache.get(groupUUID);
-      if (group == null) {
-        continue;
+      List<AccountGroup.UUID> groupIds = Lists.newArrayList();
+      for (PermissionRule rule : ca.getAccepted()) {
+        if ((rule.getAction() == Action.ALLOW) && (rule.getGroup() != null)) {
+          if (rule.getGroup().getUUID() == null) {
+            log.warn("group \"" + rule.getGroup().getName() + "\" does not " +
+                " exist, referenced in CLA \"" + ca.getName() + "\"");
+          } else {
+            groupIds.add(new AccountGroup.UUID(rule.getGroup().getUUID().get()));
+          }
+        }
       }
-
-      final List<AccountGroupAgreement> temp =
-          db.accountGroupAgreements().byGroup(group.getId()).toList();
-
-      Collections.reverse(temp);
-
-      groupAccepted.addAll(temp);
-    }
-
-    final Map<ContributorAgreement.Id, ContributorAgreement> agreements =
-        new HashMap<ContributorAgreement.Id, ContributorAgreement>();
-    for (final AccountAgreement a : userAccepted) {
-      final ContributorAgreement.Id id = a.getAgreementId();
-      if (!agreements.containsKey(id)) {
-        agreements.put(id, db.contributorAgreements().get(id));
-      }
-    }
-    for (final AccountGroupAgreement a : groupAccepted) {
-      final ContributorAgreement.Id id = a.getAgreementId();
-      if (!agreements.containsKey(id)) {
-        agreements.put(id, db.contributorAgreements().get(id));
+      if (user.getEffectiveGroups().containsAnyOf(groupIds)) {
+        accepted.add(ca.getName());
       }
     }
 
     info = new AgreementInfo();
-    info.setUserAccepted(userAccepted);
-    info.setGroupAccepted(groupAccepted);
+    info.setAccepted(accepted);
     info.setAgreements(agreements);
     return info;
   }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/GroupAdminServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/GroupAdminServiceImpl.java
index e90c2bf..aca2e05 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;
@@ -30,33 +31,38 @@
 import com.google.gerrit.reviewdb.client.AccountGroupIncludeAudit;
 import com.google.gerrit.reviewdb.client.AccountGroupMember;
 import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
+import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.AccountException;
+import com.google.gerrit.server.account.AccountManager;
 import com.google.gerrit.server.account.AccountResolver;
+import com.google.gerrit.server.account.AuthRequest;
+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.gerrit.server.config.AuthConfig;
 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 AccountManager accountManager;
+  private final AuthType authType;
   private final GroupCache groupCache;
+  private final GroupBackend groupBackend;
   private final GroupIncludeCache groupIncludeCache;
   private final GroupControl.Factory groupControlFactory;
 
@@ -70,8 +76,11 @@
       final Provider<IdentifiedUser> currentUser,
       final AccountCache accountCache,
       final GroupIncludeCache groupIncludeCache,
-      final AccountResolver accountResolver, final Realm accountRealm,
+      final AccountResolver accountResolver,
+      final AccountManager accountManager,
+      final AuthConfig authConfig,
       final GroupCache groupCache,
+      final GroupBackend groupBackend,
       final GroupControl.Factory groupControlFactory,
       final CreateGroup.Factory createGroupFactory,
       final RenameGroup.Factory renameGroupFactory,
@@ -81,8 +90,10 @@
     this.accountCache = accountCache;
     this.groupIncludeCache = groupIncludeCache;
     this.accountResolver = accountResolver;
-    this.accountRealm = accountRealm;
+    this.accountManager = accountManager;
+    this.authType = authConfig.getAuthType();
     this.groupCache = groupCache;
+    this.groupBackend = groupBackend;
     this.groupControlFactory = groupControlFactory;
     this.createGroupFactory = createGroupFactory;
     this.renameGroupFactory = renameGroupFactory;
@@ -145,13 +156,13 @@
         final AccountGroup group = db.accountGroups().get(groupId);
         assertAmGroupOwner(db, group);
 
-        final AccountGroup owner =
-            groupCache.get(new AccountGroup.NameKey(newOwnerName));
+        GroupReference owner =
+            GroupBackends.findExactSuggestion(groupBackend, newOwnerName);
         if (owner == null) {
           throw new Failure(new NoSuchEntityException());
         }
 
-        group.setOwnerGroupId(owner.getId());
+        group.setOwnerGroupUUID(owner.getUUID());
         db.accountGroups().update(Collections.singleton(group));
         groupCache.evict(group);
         return VoidResult.INSTANCE;
@@ -178,43 +189,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 +230,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 +263,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 +317,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());
         }
 
@@ -396,13 +377,38 @@
 
   private Account findAccount(final String nameOrEmail) throws OrmException,
       Failure {
-    final Account r = accountResolver.find(nameOrEmail);
+    Account r = accountResolver.find(nameOrEmail);
     if (r == null) {
-      throw new Failure(new NoSuchAccountException(nameOrEmail));
+      switch (authType) {
+        case HTTP_LDAP:
+        case CLIENT_SSL_CERT_LDAP:
+        case LDAP:
+          r = createAccountByLdap(nameOrEmail);
+          break;
+        default:
+      }
+      if (r == null) {
+        throw new Failure(new NoSuchAccountException(nameOrEmail));
+      }
     }
     return r;
   }
 
+  private Account createAccountByLdap(String user) {
+    if (!user.matches(Account.USER_NAME_PATTERN)) {
+      return null;
+    }
+
+    try {
+      final AuthRequest req = AuthRequest.forUser(user);
+      req.setSkipAuthentication(true);
+      return accountCache.get(accountManager.authenticate(req).getAccountId())
+          .getAccount();
+    } catch (AccountException e) {
+      return null;
+    }
+  }
+
   private AccountGroup findGroup(final String name) throws OrmException,
       Failure {
     final AccountGroup g = groupCache.get(new AccountGroup.NameKey(name));
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/MyGroupsFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/MyGroupsFactory.java
index fd274fd..33ce371 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/MyGroupsFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/MyGroupsFactory.java
@@ -14,9 +14,9 @@
 
 package com.google.gerrit.httpd.rpc.account;
 
-import com.google.gerrit.common.data.GroupDetail;
 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.server.IdentifiedUser;
 import com.google.gerrit.server.account.VisibleGroups;
 import com.google.gwtorm.server.OrmException;
@@ -24,7 +24,7 @@
 
 import java.util.List;
 
-class MyGroupsFactory extends Handler<List<GroupDetail>> {
+class MyGroupsFactory extends Handler<List<AccountGroup>> {
   interface Factory {
     MyGroupsFactory create();
   }
@@ -39,7 +39,7 @@
   }
 
   @Override
-  public List<GroupDetail> call() throws OrmException, NoSuchGroupException {
+  public List<AccountGroup> call() throws OrmException, NoSuchGroupException {
     final VisibleGroups visibleGroups = visibleGroupsFactory.create();
     return visibleGroups.get(user).getGroups();
   }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/RenameGroup.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/RenameGroup.java
index 09dc582..24faf9e 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/RenameGroup.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/RenameGroup.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.httpd.rpc.account;
 
 import com.google.gerrit.common.data.GroupDetail;
+import com.google.gerrit.common.errors.InvalidNameException;
 import com.google.gerrit.common.errors.NameAlreadyUsedException;
 import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.httpd.rpc.Handler;
@@ -44,7 +45,7 @@
 
   @Override
   public GroupDetail call() throws OrmException, NameAlreadyUsedException,
-      NoSuchGroupException {
+      NoSuchGroupException, InvalidNameException {
     return performRenameGroupFactory.create().renameGroup(groupId, newName);
   }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ChangeQueryServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/change/DeprecatedChangeQueryServlet.java
similarity index 94%
rename from gerrit-httpd/src/main/java/com/google/gerrit/httpd/ChangeQueryServlet.java
rename to gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/change/DeprecatedChangeQueryServlet.java
index 9c4c598..cf443e7 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ChangeQueryServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/change/DeprecatedChangeQueryServlet.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.httpd;
+package com.google.gerrit.httpd.rpc.change;
 
 import com.google.gerrit.server.query.change.QueryProcessor;
 import com.google.gerrit.server.query.change.QueryProcessor.OutputFormat;
@@ -29,12 +29,12 @@
 import javax.servlet.http.HttpServletResponse;
 
 @Singleton
-public class ChangeQueryServlet extends HttpServlet {
+public class DeprecatedChangeQueryServlet extends HttpServlet {
   private static final long serialVersionUID = 1L;
   private final Provider<QueryProcessor> processor;
 
   @Inject
-  ChangeQueryServlet(Provider<QueryProcessor> processor) {
+  DeprecatedChangeQueryServlet(Provider<QueryProcessor> processor) {
     this.processor = processor;
   }
 
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
new file mode 100644
index 0000000..b501d43
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/change/ListChangesServlet.java
@@ -0,0 +1,86 @@
+// 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.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;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.BufferedWriter;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+@Singleton
+public class ListChangesServlet extends RestApiServlet {
+  private static final long serialVersionUID = 1L;
+  private static final Logger log = LoggerFactory.getLogger(ListChangesServlet.class);
+  private final ParameterParser paramParser;
+  private final Provider<ListChanges> factory;
+
+  @Inject
+  ListChangesServlet(final Provider<CurrentUser> currentUser,
+      ParameterParser paramParser, Provider<ListChanges> ls) {
+    super(currentUser);
+    this.paramParser = paramParser;
+    this.factory = ls;
+  }
+
+  @Override
+  protected void doGet(HttpServletRequest req, HttpServletResponse res)
+      throws IOException {
+    ListChanges 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()) {
+        buf.write(JSON_MAGIC);
+      }
+
+      Writer out = new BufferedWriter(new OutputStreamWriter(buf, "UTF-8"));
+      try {
+        impl.query(out);
+      } catch (QueryParseException e) {
+        res.setStatus(HttpServletResponse.SC_BAD_REQUEST);
+        sendText(req, res, e.getMessage());
+        return;
+      } catch (OrmException e) {
+        log.error("Error querying /changes/", e);
+        res.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
+        return;
+      }
+      out.flush();
+
+      res.setContentType(impl.getFormat().isJson() ? JSON_TYPE : "text/plain");
+      res.setCharacterEncoding("UTF-8");
+      send(req, res, buf.toByteArray());
+    }
+  }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/AbandonChangeHandler.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/AbandonChangeHandler.java
index 3aecb0c..2110885 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/AbandonChangeHandler.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/AbandonChangeHandler.java
@@ -26,8 +26,13 @@
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+
+import java.io.IOException;
+
 import javax.annotation.Nullable;
 
 class AbandonChangeHandler extends Handler<ChangeDetail> {
@@ -36,7 +41,7 @@
     AbandonChangeHandler create(PatchSet.Id patchSetId, String message);
   }
 
-  private final AbandonChange.Factory abandonChangeFactory;
+  private final Provider<AbandonChange> abandonChangeProvider;
   private final ChangeDetailFactory.Factory changeDetailFactory;
 
   private final PatchSet.Id patchSetId;
@@ -44,11 +49,11 @@
   private final String message;
 
   @Inject
-  AbandonChangeHandler(final AbandonChange.Factory abandonChangeFactory,
+  AbandonChangeHandler(final Provider<AbandonChange> abandonChangeProvider,
       final ChangeDetailFactory.Factory changeDetailFactory,
       @Assisted final PatchSet.Id patchSetId,
       @Assisted @Nullable final String message) {
-    this.abandonChangeFactory = abandonChangeFactory;
+    this.abandonChangeProvider = abandonChangeProvider;
     this.changeDetailFactory = changeDetailFactory;
 
     this.patchSetId = patchSetId;
@@ -58,9 +63,12 @@
   @Override
   public ChangeDetail call() throws NoSuchChangeException, OrmException,
       EmailException, NoSuchEntityException, InvalidChangeOperationException,
-      PatchSetInfoNotAvailableException {
-    final ReviewResult result =
-        abandonChangeFactory.create(patchSetId, message).call();
+      PatchSetInfoNotAvailableException, RepositoryNotFoundException,
+      IOException {
+    final AbandonChange abandonChange = abandonChangeProvider.get();
+    abandonChange.setChangeId(patchSetId.getParentKey());
+    abandonChange.setMessage(message);
+    final ReviewResult result = abandonChange.call();
     if (result.getErrors().size() > 0) {
       throw new NoSuchChangeException(result.getChangeId());
     }
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 47a9395..6660a3d 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
@@ -32,13 +32,17 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.ProjectUtil;
 import com.google.gerrit.server.account.AccountInfoCacheFactory;
 import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MergeOp;
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.workflow.CategoryFunction;
 import com.google.gerrit.server.workflow.FunctionState;
 import com.google.gwtorm.server.OrmException;
@@ -46,8 +50,10 @@
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Config;
 
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
@@ -70,6 +76,7 @@
   private final AccountInfoCacheFactory aic;
   private final AnonymousUser anonymousUser;
   private final ReviewDb db;
+  private final GitRepositoryManager repoManager;
 
   private final Change.Id changeId;
 
@@ -84,6 +91,7 @@
   ChangeDetailFactory(final ApprovalTypes approvalTypes,
       final FunctionState.Factory functionState,
       final PatchSetDetailFactory.Factory patchSetDetail, final ReviewDb db,
+      final GitRepositoryManager repoManager,
       final ChangeControl.Factory changeControlFactory,
       final AccountInfoCacheFactory.Factory accountInfoCacheFactory,
       final AnonymousUser anonymousUser,
@@ -94,6 +102,7 @@
     this.functionState = functionState;
     this.patchSetDetail = patchSetDetail;
     this.db = db;
+    this.repoManager = repoManager;
     this.changeControlFactory = changeControlFactory;
     this.anonymousUser = anonymousUser;
     this.aic = accountInfoCacheFactory.create();
@@ -106,7 +115,8 @@
 
   @Override
   public ChangeDetail call() throws OrmException, NoSuchEntityException,
-      PatchSetInfoNotAvailableException, NoSuchChangeException {
+      PatchSetInfoNotAvailableException, NoSuchChangeException,
+      RepositoryNotFoundException, IOException {
     control = changeControlFactory.validateFor(changeId);
     final Change change = control.getChange();
     final PatchSet patch = db.patchSets().get(change.currentPatchSetId());
@@ -122,7 +132,9 @@
 
     detail.setCanAbandon(change.getStatus() != Change.Status.DRAFT && change.getStatus().isOpen() && control.canAbandon());
     detail.setCanPublish(control.canPublish(db));
-    detail.setCanRestore(change.getStatus() == Change.Status.ABANDONED && control.canRestore());
+    detail.setCanRestore(change.getStatus() == Change.Status.ABANDONED
+        && control.canRestore()
+        && ProjectUtil.branchExists(repoManager, change.getDest()));
     detail.setCanDeleteDraft(control.canDeleteDraft(db));
     detail.setStarred(control.getCurrentUser().getStarredChanges().contains(
         changeId));
@@ -133,20 +145,21 @@
 
     detail.setCanEdit(control.getRefControl().canWrite());
 
-    if (detail.getChange().getStatus().isOpen()) {
-      List<SubmitRecord> submitRecords = control.canSubmit(db, patch.getId());
-      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()
+          && ProjectUtil.branchExists(repoManager, change.getDest())) {
+        detail.setCanSubmit(true);
+      }
     }
+    detail.setSubmitRecords(submitRecords);
 
     patchsetsById = new HashMap<PatchSet.Id, PatchSet>();
     loadPatchSets();
@@ -193,7 +206,9 @@
   }
 
   private void load() throws OrmException, NoSuchChangeException {
-    if (detail.getChange().getStatus().equals(Change.Status.NEW) && testMerge) {
+    final Change.Status status = detail.getChange().getStatus();
+    if ((status.equals(Change.Status.NEW) || status.equals(Change.Status.DRAFT)) &&
+        testMerge) {
       ChangeUtil.testMerge(opFactory, detail.getChange());
     }
 
@@ -239,6 +254,15 @@
     detail.setApprovals(ad.values());
   }
 
+  private boolean isReviewer(Change change) {
+    // Return true if the currently logged in user is a reviewer of the change.
+    try {
+      return control.isReviewer(db, new ChangeData(change));
+    } catch (OrmException e) {
+        return false;
+    }
+  }
+
   private void loadCurrentPatchSet() throws OrmException,
       NoSuchEntityException, PatchSetInfoNotAvailableException,
       NoSuchChangeException {
@@ -264,32 +288,48 @@
       }
     }
 
-    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());
+    final Set<PatchSet.Id> descendants = new HashSet<PatchSet.Id>();
+    RevId cprev;
+    for (PatchSet p : detail.getPatchSets()) {
+      cprev = p.getRevision();
+      if (cprev != null) {
+        for (PatchSetAncestor a : db.patchSetAncestors().descendantsOf(cprev)) {
+          if (descendants.add(a.getPatchSet())) {
+            changesToGet.add(a.getPatchSet().getParentKey());
+          }
         }
       }
     }
     final Map<Change.Id, Change> m =
         db.changes().toMap(db.changes().get(changesToGet));
 
+    final CurrentUser currentUser = control.getCurrentUser();
+    Account.Id currentUserId = null;
+    if (currentUser instanceof IdentifiedUser) {
+        currentUserId = ((IdentifiedUser) currentUser).getAccountId();
+    }
+
     final ArrayList<ChangeInfo> dependsOn = new ArrayList<ChangeInfo>();
     for (final Change.Id a : ancestorOrder) {
       final Change ac = m.get(a);
-      if (ac != null) {
-        dependsOn.add(newChangeInfo(ac, ancestorPatchIds));
+      if (ac != null && ac.getProject().equals(detail.getChange().getProject())) {
+        if (ac.getStatus().getCode() != Change.STATUS_DRAFT
+            || ac.getOwner().equals(currentUserId)
+            || isReviewer(ac)) {
+          dependsOn.add(newChangeInfo(ac, ancestorPatchIds));
+        }
       }
     }
 
     final ArrayList<ChangeInfo> neededBy = new ArrayList<ChangeInfo>();
-    for (final Change.Id a : descendants) {
-      final Change ac = m.get(a);
-      if (ac != null) {
-        neededBy.add(newChangeInfo(ac, null));
+    for (final PatchSet.Id a : descendants) {
+      final Change ac = m.get(a.getParentKey());
+      if (ac != null && ac.currentPatchSetId().equals(a)) {
+        if (ac.getStatus().getCode() != Change.STATUS_DRAFT
+            || ac.getOwner().equals(currentUserId)
+            || isReviewer(ac)) {
+          neededBy.add(newChangeInfo(ac, null));
+        }
       }
     }
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/DeleteDraftChange.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/DeleteDraftChange.java
index 66c11c0..ecd8b54 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/DeleteDraftChange.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/DeleteDraftChange.java
@@ -19,8 +19,8 @@
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.ReplicationQueue;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gwtjsonrpc.common.VoidResult;
@@ -38,7 +38,7 @@
   private final ChangeControl.Factory changeControlFactory;
   private final ReviewDb db;
   private final GitRepositoryManager gitManager;
-  private final ReplicationQueue replication;
+  private final GitReferenceUpdated replication;
 
   private final PatchSet.Id patchSetId;
 
@@ -46,7 +46,7 @@
   DeleteDraftChange(final ReviewDb db,
       final ChangeControl.Factory changeControlFactory,
       final GitRepositoryManager gitManager,
-      final ReplicationQueue replication,
+      final GitReferenceUpdated replication,
       @Assisted final PatchSet.Id patchSetId) {
     this.changeControlFactory = changeControlFactory;
     this.db = db;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetDetailFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetDetailFactory.java
index de3ff2f..95a8e26 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetDetailFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetDetailFactory.java
@@ -31,6 +31,7 @@
 import com.google.gerrit.server.patch.PatchList;
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.patch.PatchListKey;
+import com.google.gerrit.server.patch.PatchListNotAvailableException;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
 import com.google.gerrit.server.project.ChangeControl;
@@ -108,18 +109,19 @@
 
     final PatchList list;
 
-    if (psIdBase != null) {
-      oldId = toObjectId(psIdBase);
-      newId = toObjectId(psIdNew);
+    try {
+      if (psIdBase != null) {
+        oldId = toObjectId(psIdBase);
+        newId = toObjectId(psIdNew);
 
-      projectKey = control.getProject().getNameKey();
+        projectKey = control.getProject().getNameKey();
 
-      list = listFor(keyFor(diffPrefs.getIgnoreWhitespace()));
-    } else { // OK, means use base to compare
-      list = patchListCache.get(control.getChange(), patchSet);
-      if (list == null) {
-        throw new NoSuchEntityException();
+        list = listFor(keyFor(diffPrefs.getIgnoreWhitespace()));
+      } else { // OK, means use base to compare
+        list = patchListCache.get(control.getChange(), patchSet);
       }
+    } catch (PatchListNotAvailableException e) {
+      throw new NoSuchEntityException();
     }
 
     final List<Patch> patches = list.toPatchList(patchSet.getId());
@@ -185,7 +187,8 @@
     return new PatchListKey(projectKey, oldId, newId, whitespace);
   }
 
-  private PatchList listFor(final PatchListKey key) {
+  private PatchList listFor(PatchListKey key)
+      throws PatchListNotAvailableException {
     return patchListCache.get(key);
   }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetPublishDetailFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetPublishDetailFactory.java
index 638bfe3..50baf97 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
@@ -14,12 +14,14 @@
 
 package com.google.gerrit.httpd.rpc.changedetail;
 
+import com.google.gerrit.common.data.ApprovalDetail;
 import com.google.gerrit.common.data.ApprovalType;
 import com.google.gerrit.common.data.ApprovalTypes;
 import com.google.gerrit.common.data.PatchSetPublishDetail;
 import com.google.gerrit.common.data.PermissionRange;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.httpd.rpc.Handler;
+import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -32,6 +34,8 @@
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.workflow.CategoryFunction;
+import com.google.gerrit.server.workflow.FunctionState;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
@@ -49,6 +53,7 @@
 
   private final PatchSetInfoFactory infoFactory;
   private final ReviewDb db;
+  private final FunctionState.Factory functionState;
   private final ChangeControl.Factory changeControlFactory;
   private final ApprovalTypes approvalTypes;
   private final AccountInfoCacheFactory aic;
@@ -64,11 +69,13 @@
   PatchSetPublishDetailFactory(final PatchSetInfoFactory infoFactory,
       final ReviewDb db,
       final AccountInfoCacheFactory.Factory accountInfoCacheFactory,
+      final FunctionState.Factory functionState,
       final ChangeControl.Factory changeControlFactory,
       final ApprovalTypes approvalTypes,
       final IdentifiedUser user, @Assisted final PatchSet.Id patchSetId) {
     this.infoFactory = infoFactory;
     this.db = db;
+    this.functionState = functionState;
     this.changeControlFactory = changeControlFactory;
     this.approvalTypes = approvalTypes;
     this.aic = accountInfoCacheFactory.create();
@@ -83,7 +90,8 @@
     final Change.Id changeId = patchSetId.getParentKey();
     final ChangeControl control = changeControlFactory.validateFor(changeId);
     change = control.getChange();
-    patchSetInfo = infoFactory.get(db, patchSetId);
+    PatchSet patchSet = db.patchSets().get(patchSetId);
+    patchSetInfo = infoFactory.get(change, patchSet);
     drafts = db.patchComments().draftByPatchSetAuthor(patchSetId, user.getAccountId()).toList();
 
     aic.want(change.getOwner());
@@ -119,7 +127,7 @@
           .toList();
 
       boolean couldSubmit = false;
-      List<SubmitRecord> submitRecords = control.canSubmit(db, patchSetId);
+      List<SubmitRecord> submitRecords = control.canSubmit(db, patchSet);
       for (SubmitRecord rec : submitRecords) {
         if (rec.status == SubmitRecord.Status.OK) {
           couldSubmit = true;
@@ -129,6 +137,8 @@
           int ok = 0;
 
           for (SubmitRecord.Label lbl : rec.labels) {
+            aic.want(lbl.appliedBy);
+
             boolean canMakeOk = false;
             PermissionRange range = rangeByName.get(lbl.label);
             if (range != null) {
@@ -144,6 +154,7 @@
 
             switch (lbl.status) {
               case OK:
+              case MAY:
                 ok++;
                 break;
 
@@ -165,12 +176,60 @@
       if (couldSubmit && control.getRefControl().canSubmit()) {
         detail.setCanSubmit(true);
       }
+
+      detail.setSubmitRecords(submitRecords);
     }
 
     detail.setLabels(allowed);
     detail.setGiven(given);
+    loadApprovals(detail, control);
+
     detail.setAccounts(aic.create());
 
     return detail;
   }
+
+  private void loadApprovals(final PatchSetPublishDetail detail,
+      final ChangeControl control) throws OrmException {
+    final PatchSet.Id psId = detail.getChange().currentPatchSetId();
+    final Change.Id changeId = patchSetId.getParentKey();
+    final List<PatchSetApproval> allApprovals =
+        db.patchSetApprovals().byChange(changeId).toList();
+
+    if (detail.getChange().getStatus().isOpen()) {
+      final FunctionState fs = functionState.create(control, psId, allApprovals);
+
+      for (final ApprovalType at : approvalTypes.getApprovalTypes()) {
+        CategoryFunction.forCategory(at.getCategory()).run(at, fs);
+      }
+    }
+
+    final boolean canRemoveReviewers = detail.getChange().getStatus().isOpen() //
+        && control.getCurrentUser() instanceof IdentifiedUser;
+    final HashMap<Account.Id, ApprovalDetail> ad =
+        new HashMap<Account.Id, ApprovalDetail>();
+    for (PatchSetApproval ca : allApprovals) {
+      ApprovalDetail d = ad.get(ca.getAccountId());
+      if (d == null) {
+        d = new ApprovalDetail(ca.getAccountId());
+        d.setCanRemove(canRemoveReviewers);
+        ad.put(d.getAccount(), d);
+      }
+      if (d.canRemove()) {
+        d.setCanRemove(control.canRemoveReviewer(ca));
+      }
+      if (ca.getPatchSetId().equals(psId)) {
+        d.add(ca);
+      }
+    }
+
+    final Account.Id owner = detail.getChange().getOwner();
+    if (ad.containsKey(owner)) {
+      // Ensure the owner always sorts to the top of the table
+      ad.get(owner).sortFirst();
+    }
+
+    aic.want(ad.keySet());
+    detail.setApprovals(ad.values());
+  }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PublishAction.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PublishAction.java
index 4ea279f..f57b29c 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PublishAction.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PublishAction.java
@@ -26,6 +26,10 @@
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+
+import java.io.IOException;
+
 class PublishAction extends Handler<ChangeDetail> {
   interface Factory {
     PublishAction create(PatchSet.Id patchSetId);
@@ -49,7 +53,7 @@
   @Override
   public ChangeDetail call() throws OrmException, NoSuchEntityException,
       IllegalStateException, PatchSetInfoNotAvailableException,
-      NoSuchChangeException {
+      NoSuchChangeException, RepositoryNotFoundException, IOException {
     final ReviewResult result = publishFactory.create(patchSetId).call();
     if (result.getErrors().size() > 0) {
       throw new IllegalStateException("Cannot publish patchset");
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RebaseChange.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RebaseChange.java
index 3c29074..e71e302 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RebaseChange.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RebaseChange.java
@@ -15,17 +15,17 @@
 package com.google.gerrit.httpd.rpc.changedetail;
 
 import com.google.gerrit.common.ChangeHookRunner;
-import com.google.gerrit.common.data.ApprovalTypes;
 import com.google.gerrit.common.data.ChangeDetail;
 import com.google.gerrit.common.errors.NoSuchEntityException;
 import com.google.gerrit.httpd.rpc.Handler;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.ReplicationQueue;
 import com.google.gerrit.server.mail.EmailException;
 import com.google.gerrit.server.mail.RebasedPatchSetSender;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
@@ -54,7 +54,7 @@
   private final RebasedPatchSetSender.Factory rebasedPatchSetSenderFactory;
 
   private final ChangeDetailFactory.Factory changeDetailFactory;
-  private final ReplicationQueue replication;
+  private final GitReferenceUpdated replication;
 
   private final PatchSet.Id patchSetId;
 
@@ -65,7 +65,7 @@
 
   private final PersonIdent myIdent;
 
-  private final ApprovalTypes approvalTypes;
+  private final ApprovalsUtil approvalsUtil;
 
   @Inject
   RebaseChange(final ChangeControl.Factory changeControlFactory,
@@ -75,9 +75,9 @@
       @Assisted final PatchSet.Id patchSetId, final ChangeHookRunner hooks,
       final GitRepositoryManager gitManager,
       final PatchSetInfoFactory patchSetInfoFactory,
-      final ReplicationQueue replication,
+      final GitReferenceUpdated replication,
       @GerritPersonIdent final PersonIdent myIdent,
-      final ApprovalTypes approvalTypes) {
+      final ApprovalsUtil approvalsUtil) {
     this.changeControlFactory = changeControlFactory;
     this.db = db;
     this.currentUser = currentUser;
@@ -92,7 +92,7 @@
     this.replication = replication;
     this.myIdent = myIdent;
 
-    this.approvalTypes = approvalTypes;
+    this.approvalsUtil = approvalsUtil;
   }
 
   @Override
@@ -103,7 +103,7 @@
 
     ChangeUtil.rebaseChange(patchSetId, currentUser, db,
         rebasedPatchSetSenderFactory, hooks, gitManager, patchSetInfoFactory,
-        replication, myIdent, changeControlFactory, approvalTypes);
+        replication, myIdent, changeControlFactory, approvalsUtil);
 
     return changeDetailFactory.create(patchSetId.getParentKey()).call();
   }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RestoreChangeHandler.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RestoreChangeHandler.java
index f018750..e4571fd 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RestoreChangeHandler.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RestoreChangeHandler.java
@@ -26,8 +26,13 @@
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+
+import java.io.IOException;
+
 import javax.annotation.Nullable;
 
 class RestoreChangeHandler extends Handler<ChangeDetail> {
@@ -35,7 +40,7 @@
     RestoreChangeHandler create(PatchSet.Id patchSetId, String message);
   }
 
-  private final RestoreChange.Factory restoreChangeFactory;
+  private final Provider<RestoreChange> restoreChangeProvider;
   private final ChangeDetailFactory.Factory changeDetailFactory;
 
   private final PatchSet.Id patchSetId;
@@ -43,11 +48,11 @@
   private final String message;
 
   @Inject
-  RestoreChangeHandler(final RestoreChange.Factory restoreChangeFactory,
+  RestoreChangeHandler(final Provider<RestoreChange> restoreChangeProvider,
       final ChangeDetailFactory.Factory changeDetailFactory,
       @Assisted final PatchSet.Id patchSetId,
       @Assisted @Nullable final String message) {
-    this.restoreChangeFactory = restoreChangeFactory;
+    this.restoreChangeProvider = restoreChangeProvider;
     this.changeDetailFactory = changeDetailFactory;
 
     this.patchSetId = patchSetId;
@@ -57,9 +62,12 @@
   @Override
   public ChangeDetail call() throws NoSuchChangeException, OrmException,
       EmailException, NoSuchEntityException, InvalidChangeOperationException,
-      PatchSetInfoNotAvailableException {
-    final ReviewResult result =
-        restoreChangeFactory.create(patchSetId, message).call();
+      PatchSetInfoNotAvailableException, RepositoryNotFoundException,
+      IOException {
+    final RestoreChange restoreChange = restoreChangeProvider.get();
+    restoreChange.setChangeId(patchSetId.getParentKey());
+    restoreChange.setMessage(message);
+    final ReviewResult result = restoreChange.call();
     if (result.getErrors().size() > 0) {
       throw new NoSuchChangeException(result.getChangeId());
     }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RevertChange.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RevertChange.java
index 9fb9ae1..60a03bd 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RevertChange.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RevertChange.java
@@ -24,8 +24,8 @@
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.ReplicationQueue;
 import com.google.gerrit.server.mail.EmailException;
 import com.google.gerrit.server.mail.RevertedSender;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
@@ -54,7 +54,7 @@
   private final IdentifiedUser currentUser;
   private final RevertedSender.Factory revertedSenderFactory;
   private final ChangeDetailFactory.Factory changeDetailFactory;
-  private final ReplicationQueue replication;
+  private final GitReferenceUpdated replication;
 
   private final PatchSet.Id patchSetId;
   @Nullable
@@ -76,7 +76,7 @@
       @Assisted @Nullable final String message, final ChangeHooks hooks,
       final GitRepositoryManager gitManager,
       final PatchSetInfoFactory patchSetInfoFactory,
-      final ReplicationQueue replication,
+      final GitReferenceUpdated replication,
       @GerritPersonIdent final PersonIdent myIdent) {
     this.changeControlFactory = changeControlFactory;
     this.db = db;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/SubmitAction.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/SubmitAction.java
index 80100ad..23b21d5 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/SubmitAction.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/SubmitAction.java
@@ -27,6 +27,10 @@
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+
+import java.io.IOException;
+
 class SubmitAction extends Handler<ChangeDetail> {
   interface Factory {
     SubmitAction create(PatchSet.Id patchSetId);
@@ -50,7 +54,8 @@
   @Override
   public ChangeDetail call() throws OrmException, NoSuchEntityException,
       IllegalStateException, InvalidChangeOperationException,
-      PatchSetInfoNotAvailableException, NoSuchChangeException {
+      PatchSetInfoNotAvailableException, NoSuchChangeException,
+      RepositoryNotFoundException, IOException {
     final ReviewResult result =
         submitFactory.create(patchSetId).call();
     if (result.getErrors().size() > 0) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchDetailServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchDetailServiceImpl.java
index e90467e..40c9b84 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchDetailServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchDetailServiceImpl.java
@@ -52,6 +52,9 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+
+import java.io.IOException;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
@@ -166,6 +169,10 @@
           throw new Failure(e);
         } catch (PatchSetInfoNotAvailableException e) {
           throw new Failure(e);
+        } catch (RepositoryNotFoundException e) {
+          throw new Failure(e);
+        } catch (IOException e) {
+          throw new Failure(e);
         }
       }
     });
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptBuilder.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptBuilder.java
index ad7746f..816bbef 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptBuilder.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptBuilder.java
@@ -147,7 +147,8 @@
     } else if (diffPrefs.isIntralineDifference()) {
       IntraLineDiff d =
           patchListCache.getIntraLineDiff(new IntraLineDiffKey(a.id, a.src,
-              b.id, b.src, edits, projectKey, bId, b.path));
+              b.id, b.src, edits, projectKey, bId, b.path,
+              diffPrefs.getIgnoreWhitespace() != Whitespace.IGNORE_NONE));
       if (d != null) {
         switch (d.getStatus()) {
           case EDIT_LIST:
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 d61e6e7..97d850a 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
@@ -36,6 +36,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;
@@ -143,6 +144,9 @@
     } catch (RepositoryNotFoundException e) {
       log.error("Repository " + projectKey + " not found", e);
       throw new NoSuchChangeException(changeId, e);
+    } catch (IOException e) {
+      log.error("Cannot open repository " + projectKey, e);
+      throw new NoSuchChangeException(changeId, e);
     }
     try {
       final PatchList list = listFor(keyFor(diffPrefs.getIgnoreWhitespace()));
@@ -153,14 +157,14 @@
           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 (org.eclipse.jgit.errors.LargeObjectException err) {
-        throw new LargeObjectException("File content is too large", err);
-      }
+    } catch (PatchListNotAvailableException e) {
+      throw new NoSuchChangeException(changeId, e);
+    } catch (IOException e) {
+      log.error("File content unavailable", e);
+      throw new NoSuchChangeException(changeId, e);
+    } catch (org.eclipse.jgit.errors.LargeObjectException err) {
+      throw new LargeObjectException("File content is too large", err);
     } finally {
       git.close();
     }
@@ -170,7 +174,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);
   }
 
@@ -251,7 +256,7 @@
         break;
 
       case DELETED:
-        loadPublished(byKey, aic, oldName);
+        loadPublished(byKey, aic, newName);
         break;
 
       case COPIED:
@@ -273,7 +278,7 @@
           break;
 
         case DELETED:
-          loadDrafts(byKey, aic, me, oldName);
+          loadDrafts(byKey, aic, me, newName);
           break;
 
         case COPIED:
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/plugin/ListPluginsServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/plugin/ListPluginsServlet.java
new file mode 100644
index 0000000..5e8145c
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/plugin/ListPluginsServlet.java
@@ -0,0 +1,68 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.httpd.rpc.plugin;
+
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.httpd.RestApiServlet;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.OutputFormat;
+import com.google.gerrit.server.plugins.ListPlugins;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+@Singleton
+@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+public class ListPluginsServlet extends RestApiServlet {
+  private static final long serialVersionUID = 1L;
+  private final ParameterParser paramParser;
+  private final Provider<ListPlugins> factory;
+
+  @Inject
+  ListPluginsServlet(final Provider<CurrentUser> currentUser,
+      ParameterParser paramParser, Provider<ListPlugins> ls) {
+    super(currentUser);
+    this.paramParser = paramParser;
+    this.factory = ls;
+  }
+
+  @Override
+  protected void doGet(HttpServletRequest req, HttpServletResponse res)
+      throws IOException {
+    ListPlugins impl = factory.get();
+    if (acceptsJson(req)) {
+      impl.setFormat(OutputFormat.JSON_COMPACT);
+    }
+    if (paramParser.parse(impl, req, res)) {
+      ByteArrayOutputStream buf = new ByteArrayOutputStream();
+      if (impl.getFormat().isJson()) {
+        res.setContentType(JSON_TYPE);
+        buf.write(JSON_MAGIC);
+      } else {
+        res.setContentType("text/plain");
+      }
+      impl.display(buf);
+      res.setCharacterEncoding("UTF-8");
+      send(req, res, buf.toByteArray());
+    }
+  }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/AddBranch.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/AddBranch.java
index 44c3141..97e9bb4 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/AddBranch.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/AddBranch.java
@@ -22,8 +22,8 @@
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.ReplicationQueue;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.project.RefControl;
@@ -59,7 +59,7 @@
   private final ListBranches.Factory listBranchesFactory;
   private final IdentifiedUser identifiedUser;
   private final GitRepositoryManager repoManager;
-  private final ReplicationQueue replication;
+  private final GitReferenceUpdated referenceUpdated;
   private final ChangeHooks hooks;
 
   private final Project.NameKey projectName;
@@ -71,7 +71,7 @@
       final ListBranches.Factory listBranchesFactory,
       final IdentifiedUser identifiedUser,
       final GitRepositoryManager repoManager,
-      final ReplicationQueue replication,
+      GitReferenceUpdated referenceUpdated,
       final ChangeHooks hooks,
 
       @Assisted Project.NameKey projectName,
@@ -81,7 +81,7 @@
     this.listBranchesFactory = listBranchesFactory;
     this.identifiedUser = identifiedUser;
     this.repoManager = repoManager;
-    this.replication = replication;
+    this.referenceUpdated = referenceUpdated;
     this.hooks = hooks;
 
     this.projectName = projectName;
@@ -144,7 +144,7 @@
           case FAST_FORWARD:
           case NEW:
           case NO_CHANGE:
-            replication.scheduleUpdate(name.getParentKey(), refname);
+            referenceUpdated.fire(name.getParentKey(), refname);
             hooks.doRefUpdatedHook(name, u, identifiedUser.getAccount());
             break;
           default: {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectAccess.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectAccess.java
index 777329b..72b5e3a 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
@@ -15,41 +15,26 @@
 package com.google.gerrit.httpd.rpc.project;
 
 import com.google.gerrit.common.data.AccessSection;
-import com.google.gerrit.common.data.GroupReference;
-import com.google.gerrit.common.data.Permission;
-import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.common.data.ProjectAccess;
-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.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectControl;
-import com.google.gerrit.server.project.RefControl;
-import com.google.gwtorm.server.OrmConcurrencyException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.ObjectId;
 
 import java.io.IOException;
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.LinkedHashMap;
 import java.util.List;
-import java.util.Map;
-import java.util.Set;
 
 import javax.annotation.Nullable;
 
-class ChangeProjectAccess extends Handler<ProjectAccess> {
+class ChangeProjectAccess extends ProjectAccessHandler<ProjectAccess> {
   interface Factory {
     ChangeProjectAccess create(@Assisted Project.NameKey projectName,
         @Nullable @Assisted ObjectId base,
@@ -58,152 +43,29 @@
   }
 
   private final ProjectAccessFactory.Factory projectAccessFactory;
-  private final ProjectControl.Factory projectControlFactory;
   private final ProjectCache projectCache;
-  private final GroupCache groupCache;
-  private final MetaDataUpdate.User metaDataUpdateFactory;
-
-  private final Project.NameKey projectName;
-  private final ObjectId base;
-  private List<AccessSection> sectionList;
-  private String message;
 
   @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,
       @Nullable @Assisted final ObjectId base,
       @Assisted List<AccessSection> sectionList,
       @Nullable @Assisted String message) {
+    super(projectControlFactory, groupBackend, metaDataUpdateFactory,
+        projectName, base, sectionList, message, true);
     this.projectAccessFactory = projectAccessFactory;
-    this.projectControlFactory = projectControlFactory;
     this.projectCache = projectCache;
-    this.groupCache = groupCache;
-    this.metaDataUpdateFactory = metaDataUpdateFactory;
-
-    this.projectName = projectName;
-    this.base = base;
-    this.sectionList = sectionList;
-    this.message = message;
   }
 
   @Override
-  public ProjectAccess call() throws NoSuchProjectException, IOException,
-      ConfigInvalidException, InvalidNameException, NoSuchGroupException,
-      OrmConcurrencyException {
-    final ProjectControl projectControl =
-        projectControlFactory.controlFor(projectName);
-
-    final MetaDataUpdate md;
-    try {
-      md = metaDataUpdateFactory.create(projectName);
-    } catch (RepositoryNotFoundException notFound) {
-      throw new NoSuchProjectException(projectName);
-    }
-    try {
-      ProjectConfig config = ProjectConfig.read(md, base);
-      Set<String> toDelete = scanSectionNames(config);
-
-      for (AccessSection section : mergeSections(sectionList)) {
-        String name = section.getName();
-
-        if (AccessSection.GLOBAL_CAPABILITIES.equals(name)) {
-          if (!projectControl.isOwner()) {
-            continue;
-          }
-          replace(config, toDelete, section);
-
-        } else if (AccessSection.isValid(name)) {
-          if (!projectControl.controlForRef(name).isOwner()) {
-            continue;
-          }
-
-          RefControl.validateRefPattern(name);
-
-          replace(config, toDelete, section);
-        }
-      }
-
-      for (String name : toDelete) {
-        if (AccessSection.GLOBAL_CAPABILITIES.equals(name)) {
-          if (projectControl.isOwner()) {
-            config.remove(config.getAccessSection(name));
-          }
-
-        } else if (projectControl.controlForRef(name).isOwner()) {
-          config.remove(config.getAccessSection(name));
-        }
-      }
-
-      if (message != null && !message.isEmpty()) {
-        if (!message.endsWith("\n")) {
-          message += "\n";
-        }
-        md.setMessage(message);
-      } else {
-        md.setMessage("Modify access rules\n");
-      }
-
-      if (config.commit(md)) {
-        projectCache.evict(config.getProject());
-        return projectAccessFactory.create(projectName).call();
-
-      } else {
-        throw new OrmConcurrencyException("Cannot update " + projectName);
-      }
-    } finally {
-      md.close();
-    }
-  }
-
-  private void replace(ProjectConfig config, Set<String> toDelete,
-      AccessSection section) throws NoSuchGroupException {
-    for (Permission permission : section.getPermissions()) {
-      for (PermissionRule rule : permission.getRules()) {
-        lookupGroup(rule);
-      }
-    }
-    config.replace(section);
-    toDelete.remove(section.getName());
-  }
-
-  private static List<AccessSection> mergeSections(List<AccessSection> src) {
-    Map<String, AccessSection> map = new LinkedHashMap<String, AccessSection>();
-    for (AccessSection section : src) {
-      if (section.getPermissions().isEmpty()) {
-        continue;
-      }
-
-      AccessSection prior = map.get(section.getName());
-      if (prior != null) {
-        prior.mergeFrom(section);
-      } else {
-        map.put(section.getName(), section);
-      }
-    }
-    return new ArrayList<AccessSection>(map.values());
-  }
-
-  private static Set<String> scanSectionNames(ProjectConfig config) {
-    Set<String> names = new HashSet<String>();
-    for (AccessSection section : config.getAccessSections()) {
-      names.add(section.getName());
-    }
-    return names;
-  }
-
-  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);
-      if (group == null) {
-        throw new NoSuchGroupException(name);
-      }
-      ref.setUUID(group.getGroupUUID());
-    }
+  protected ProjectAccess updateProjectConfig(ProjectConfig config,
+      MetaDataUpdate md) throws IOException, NoSuchProjectException, ConfigInvalidException {
+    config.commit(md);
+    projectCache.evict(config.getProject());
+    return projectAccessFactory.create(projectName).call();
   }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectSettings.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectSettings.java
index bd6e460..41354aa 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectSettings.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectSettings.java
@@ -65,7 +65,8 @@
   }
 
   @Override
-  public ProjectDetail call() throws NoSuchProjectException, OrmException {
+  public ProjectDetail call() throws NoSuchProjectException, OrmException,
+      IOException {
     final Project.NameKey projectName = update.getNameKey();
     projectControlFactory.ownerFor(projectName);
 
@@ -74,6 +75,8 @@
       md = metaDataUpdateFactory.create(projectName);
     } catch (RepositoryNotFoundException notFound) {
       throw new NoSuchProjectException(projectName);
+    } catch (IOException e) {
+      throw new OrmException(e);
     }
     try {
       // TODO We really should take advantage of the Git commit DAG and
@@ -83,10 +86,11 @@
       config.getProject().copySettingsFrom(update);
 
       md.setMessage("Modified project settings\n");
-      if (config.commit(md)) {
+      try {
+        config.commit(md);
         mgr.setProjectDescription(projectName, update.getDescription());
         userCache.get().evict(config.getProject());
-      } else {
+      } catch (IOException e) {
         throw new OrmConcurrencyException("Cannot update " + projectName);
       }
     } catch (ConfigInvalidException err) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/CreateProjectHandler.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/CreateProjectHandler.java
index 039a301..141f332 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/CreateProjectHandler.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/CreateProjectHandler.java
@@ -26,7 +26,7 @@
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
-import org.eclipse.jgit.lib.Constants;
+import java.util.Collections;
 
 public class CreateProjectHandler extends Handler<VoidResult> {
 
@@ -74,7 +74,7 @@
     }
     args.projectDescription = "";
     args.submitType = SubmitType.MERGE_IF_NECESSARY;
-    args.branch = Constants.MASTER;
+    args.branch = Collections.emptyList();
     args.createEmptyCommit = emptyCommit;
     args.permissionsOnly = permissionsOnly;
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/DeleteBranches.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/DeleteBranches.java
index 073e2d7..f2b3ca3 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/DeleteBranches.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/DeleteBranches.java
@@ -18,11 +18,13 @@
 import com.google.gerrit.httpd.rpc.Handler;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.ReplicationQueue;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectControl;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -34,6 +36,7 @@
 
 import java.io.IOException;
 import java.util.HashSet;
+import java.util.Iterator;
 import java.util.Set;
 
 class DeleteBranches extends Handler<Set<Branch.NameKey>> {
@@ -47,9 +50,10 @@
 
   private final ProjectControl.Factory projectControlFactory;
   private final GitRepositoryManager repoManager;
-  private final ReplicationQueue replication;
+  private final GitReferenceUpdated replication;
   private final IdentifiedUser identifiedUser;
   private final ChangeHooks hooks;
+  private final ReviewDb db;
 
   private final Project.NameKey projectName;
   private final Set<Branch.NameKey> toRemove;
@@ -57,9 +61,10 @@
   @Inject
   DeleteBranches(final ProjectControl.Factory projectControlFactory,
       final GitRepositoryManager repoManager,
-      final ReplicationQueue replication,
+      final GitReferenceUpdated replication,
       final IdentifiedUser identifiedUser,
       final ChangeHooks hooks,
+      final ReviewDb db,
 
       @Assisted Project.NameKey name, @Assisted Set<Branch.NameKey> toRemove) {
     this.projectControlFactory = projectControlFactory;
@@ -67,6 +72,7 @@
     this.replication = replication;
     this.identifiedUser = identifiedUser;
     this.hooks = hooks;
+    this.db = db;
 
     this.projectName = name;
     this.toRemove = toRemove;
@@ -74,17 +80,23 @@
 
   @Override
   public Set<Branch.NameKey> call() throws NoSuchProjectException,
-      RepositoryNotFoundException {
+      RepositoryNotFoundException, OrmException, IOException {
     final ProjectControl projectControl =
         projectControlFactory.controlFor(projectName);
 
-    for (Branch.NameKey k : toRemove) {
+    final Iterator<Branch.NameKey> branchIt = toRemove.iterator();
+    while (branchIt.hasNext()) {
+      final Branch.NameKey k = branchIt.next();
       if (!projectName.equals(k.getParentKey())) {
         throw new IllegalArgumentException("All keys must be from same project");
       }
       if (!projectControl.controlForRef(k).canDelete()) {
         throw new IllegalStateException("Cannot delete " + k.getShortName());
       }
+
+      if (db.changes().byBranchOpenAll(k).iterator().hasNext()) {
+        branchIt.remove();
+      }
     }
 
     final Set<Branch.NameKey> deleted = new HashSet<Branch.NameKey>();
@@ -109,7 +121,7 @@
           case FAST_FORWARD:
           case FORCED:
             deleted.add(branchKey);
-            replication.scheduleUpdate(projectName, refname);
+            replication.fire(projectName, refname);
             hooks.doRefUpdatedHook(branchKey, u, identifiedUser.getAccount());
             break;
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ListBranches.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ListBranches.java
index 68fde45..2366423 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ListBranches.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ListBranches.java
@@ -62,7 +62,7 @@
   }
 
   @Override
-  public ListBranchesResult call() throws NoSuchProjectException {
+  public ListBranchesResult call() throws NoSuchProjectException, IOException {
     final ProjectControl pctl = projectControlFactory.validateFor( //
         projectName, //
         ProjectControl.OWNER | ProjectControl.VISIBLE);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ListProjectsServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ListProjectsServlet.java
new file mode 100644
index 0000000..d327d35
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ListProjectsServlet.java
@@ -0,0 +1,70 @@
+// 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.project;
+
+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;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+import java.net.URLDecoder;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+@Singleton
+public class ListProjectsServlet extends RestApiServlet {
+  private static final long serialVersionUID = 1L;
+  private final ParameterParser paramParser;
+  private final Provider<ListProjects> factory;
+
+  @Inject
+  ListProjectsServlet(final Provider<CurrentUser> currentUser,
+      ParameterParser paramParser, Provider<ListProjects> ls) {
+    super(currentUser);
+    this.paramParser = paramParser;
+    this.factory = ls;
+  }
+
+  @Override
+  protected void doGet(HttpServletRequest req, HttpServletResponse res)
+      throws IOException {
+    ListProjects impl = factory.get();
+    if (!Strings.isNullOrEmpty(req.getPathInfo())) {
+      impl.setMatchPrefix(URLDecoder.decode(req.getPathInfo(), "UTF-8"));
+    }
+    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/ProjectAccessFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java
index 7ac4ec3..7b3b8e7 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,12 +94,11 @@
     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());
-          pc = open();
-        }
+        config.commit(md);
+        projectCache.evict(config.getProject());
+        pc = open();
       } else if (config.getRevision() != null
           && !config.getRevision().equals(
               pc.getProjectState().getConfig().getRevision())) {
@@ -110,6 +109,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 =
@@ -129,6 +129,9 @@
           local.add(section);
           ownerOf.add(name);
 
+        } else if (metaConfigControl.isVisible()) {
+          local.add(section);
+
         } else if (rc.isVisible()) {
           // Filter the section to only add rules describing groups that
           // are visible to the current-user. This includes any group the
@@ -195,8 +198,9 @@
 
     detail.setLocal(local);
     detail.setOwnerOf(ownerOf);
-    detail.setConfigVisible(pc.isOwner()
-        || pc.controlForRef(GitRepositoryManager.REF_CONFIG).isVisible());
+    detail.setCanUpload(pc.isOwner()
+        || (metaConfigControl.isVisible() && metaConfigControl.canUpload()));
+    detail.setConfigVisible(pc.isOwner() || metaConfigControl.isVisible());
     return detail;
   }
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java
new file mode 100644
index 0000000..02b84b0
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java
@@ -0,0 +1,190 @@
+// 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.project;
+
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.common.errors.InvalidNameException;
+import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.httpd.rpc.Handler;
+import com.google.gerrit.reviewdb.client.Project;
+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;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.project.RefControl;
+import com.google.gwtorm.server.OrmException;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.ObjectId;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public abstract class ProjectAccessHandler<T> extends Handler<T> {
+
+  private final ProjectControl.Factory projectControlFactory;
+  protected final GroupBackend groupBackend;
+  private final MetaDataUpdate.User metaDataUpdateFactory;
+
+  protected final Project.NameKey projectName;
+  protected final ObjectId base;
+  private List<AccessSection> sectionList;
+  protected String message;
+  private boolean checkIfOwner;
+
+  protected ProjectAccessHandler(
+      final ProjectControl.Factory projectControlFactory,
+      final GroupBackend groupBackend,
+      final MetaDataUpdate.User metaDataUpdateFactory,
+      final Project.NameKey projectName, final ObjectId base,
+      final List<AccessSection> sectionList, final String message,
+      final boolean checkIfOwner) {
+    this.projectControlFactory = projectControlFactory;
+    this.groupBackend = groupBackend;
+    this.metaDataUpdateFactory = metaDataUpdateFactory;
+
+    this.projectName = projectName;
+    this.base = base;
+    this.sectionList = sectionList;
+    this.message = message;
+    this.checkIfOwner = checkIfOwner;
+  }
+
+  @Override
+  public final T call() throws NoSuchProjectException, IOException,
+      ConfigInvalidException, InvalidNameException, NoSuchGroupException,
+      OrmException {
+    final ProjectControl projectControl =
+        projectControlFactory.controlFor(projectName);
+
+    final MetaDataUpdate md;
+    try {
+      md = metaDataUpdateFactory.create(projectName);
+    } catch (RepositoryNotFoundException notFound) {
+      throw new NoSuchProjectException(projectName);
+    }
+    try {
+      ProjectConfig config = ProjectConfig.read(md, base);
+      Set<String> toDelete = scanSectionNames(config);
+
+      for (AccessSection section : mergeSections(sectionList)) {
+        String name = section.getName();
+
+        if (AccessSection.GLOBAL_CAPABILITIES.equals(name)) {
+          if (checkIfOwner && !projectControl.isOwner()) {
+            continue;
+          }
+          replace(config, toDelete, section);
+
+        } else if (AccessSection.isValid(name)) {
+          if (checkIfOwner && !projectControl.controlForRef(name).isOwner()) {
+            continue;
+          }
+
+          RefControl.validateRefPattern(name);
+
+          replace(config, toDelete, section);
+        }
+      }
+
+      for (String name : toDelete) {
+        if (AccessSection.GLOBAL_CAPABILITIES.equals(name)) {
+          if (!checkIfOwner || projectControl.isOwner()) {
+            config.remove(config.getAccessSection(name));
+          }
+
+        } else if (!checkIfOwner ||  projectControl.controlForRef(name).isOwner()) {
+          config.remove(config.getAccessSection(name));
+        }
+      }
+
+      if (message != null && !message.isEmpty()) {
+        if (!message.endsWith("\n")) {
+          message += "\n";
+        }
+        md.setMessage(message);
+      } else {
+        md.setMessage("Modify access rules\n");
+      }
+
+      return updateProjectConfig(config, md);
+    } finally {
+      md.close();
+    }
+  }
+
+  protected abstract T updateProjectConfig(ProjectConfig config,
+      MetaDataUpdate md) throws IOException, NoSuchProjectException,
+      ConfigInvalidException, OrmException;
+
+  private void replace(ProjectConfig config, Set<String> toDelete,
+      AccessSection section) throws NoSuchGroupException {
+    for (Permission permission : section.getPermissions()) {
+      for (PermissionRule rule : permission.getRules()) {
+        lookupGroup(rule);
+      }
+    }
+    config.replace(section);
+    toDelete.remove(section.getName());
+  }
+
+  private static List<AccessSection> mergeSections(List<AccessSection> src) {
+    Map<String, AccessSection> map = new LinkedHashMap<String, AccessSection>();
+    for (AccessSection section : src) {
+      if (section.getPermissions().isEmpty()) {
+        continue;
+      }
+
+      AccessSection prior = map.get(section.getName());
+      if (prior != null) {
+        prior.mergeFrom(section);
+      } else {
+        map.put(section.getName(), section);
+      }
+    }
+    return new ArrayList<AccessSection>(map.values());
+  }
+
+  private static Set<String> scanSectionNames(ProjectConfig config) {
+    Set<String> names = new HashSet<String>();
+    for (AccessSection section : config.getAccessSections()) {
+      names.add(section.getName());
+    }
+    return names;
+  }
+
+  private void lookupGroup(PermissionRule rule) throws NoSuchGroupException {
+    GroupReference ref = rule.getGroup();
+    if (ref.getUUID() == null) {
+      final GroupReference group =
+          GroupBackends.findBestSuggestion(groupBackend, ref.getName());
+      if (group == null) {
+        throw new NoSuchGroupException(ref.getName());
+      }
+      ref.setUUID(group.getUUID());
+    }
+  }
+}
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..983f1fc 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,8 +19,8 @@
 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.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.gwtjsonrpc.common.VoidResult;
@@ -34,44 +34,36 @@
 class ProjectAdminServiceImpl implements ProjectAdminService {
   private final AddBranch.Factory addBranchFactory;
   private final ChangeProjectAccess.Factory changeProjectAccessFactory;
+  private final ReviewProjectAccess.Factory reviewProjectAccessFactory;
   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,
       final ChangeProjectAccess.Factory changeProjectAccessFactory,
+      final ReviewProjectAccess.Factory reviewProjectAccessFactory,
       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.reviewProjectAccessFactory = reviewProjectAccessFactory;
     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 +72,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);
@@ -102,17 +89,25 @@
     changeProjectSettingsFactory.create(update).to(callback);
   }
 
+  private static ObjectId getBase(final String baseRevision) {
+    if (baseRevision != null && !baseRevision.isEmpty()) {
+      return ObjectId.fromString(baseRevision);
+    }
+    return null;
+  }
+
   @Override
   public void changeProjectAccess(Project.NameKey projectName,
       String baseRevision, String msg, List<AccessSection> sections,
       AsyncCallback<ProjectAccess> cb) {
-    ObjectId base;
-    if (baseRevision != null && !baseRevision.isEmpty()) {
-      base = ObjectId.fromString(baseRevision);
-    } else {
-      base = null;
-    }
-    changeProjectAccessFactory.create(projectName, base, sections, msg).to(cb);
+    changeProjectAccessFactory.create(projectName, getBase(baseRevision), sections, msg).to(cb);
+  }
+
+  @Override
+  public void reviewProjectAccess(Project.NameKey projectName,
+      String baseRevision, String msg, List<AccessSection> sections,
+      AsyncCallback<Change.Id> cb) {
+    reviewProjectAccessFactory.create(projectName, getBase(baseRevision), sections, msg).to(cb);
   }
 
   @Override
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectDetailFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectDetailFactory.java
index d782da4..2741640 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectDetailFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectDetailFactory.java
@@ -51,7 +51,7 @@
   }
 
   @Override
-  public ProjectDetail call() throws NoSuchProjectException {
+  public ProjectDetail call() throws NoSuchProjectException, IOException {
     final ProjectControl pc =
         projectControlFactory.validateFor(projectName, ProjectControl.OWNER
             | ProjectControl.VISIBLE);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectModule.java
index 2eb55b3..e943e3fc 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
@@ -30,15 +30,14 @@
       protected void configure() {
         factory(AddBranch.Factory.class);
         factory(ChangeProjectAccess.Factory.class);
+        factory(ReviewProjectAccess.Factory.class);
         factory(CreateProjectHandler.Factory.class);
         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/ReviewProjectAccess.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java
new file mode 100644
index 0000000..69a283a
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java
@@ -0,0 +1,147 @@
+// 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.project;
+
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetAncestor;
+import com.google.gerrit.reviewdb.client.PatchSetInfo;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.patch.AddReviewer;
+import com.google.gerrit.server.patch.PatchSetInfoFactory;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.revwalk.RevCommit;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+public class ReviewProjectAccess extends ProjectAccessHandler<Change.Id> {
+  interface Factory {
+    ReviewProjectAccess create(@Assisted Project.NameKey projectName,
+        @Nullable @Assisted ObjectId base,
+        @Assisted List<AccessSection> sectionList,
+        @Nullable @Assisted String message);
+  }
+
+  private final ReviewDb db;
+  private final IdentifiedUser user;
+  private final PatchSetInfoFactory patchSetInfoFactory;
+  private final AddReviewer.Factory addReviewerFactory;
+
+  @Inject
+  ReviewProjectAccess(final ProjectControl.Factory projectControlFactory,
+      final GroupBackend groupBackend,
+      final MetaDataUpdate.User metaDataUpdateFactory, final ReviewDb db,
+      final IdentifiedUser user, final PatchSetInfoFactory patchSetInfoFactory,
+      final AddReviewer.Factory addReviewerFactory,
+
+      @Assisted final Project.NameKey projectName,
+      @Nullable @Assisted final ObjectId base,
+      @Assisted List<AccessSection> sectionList,
+      @Nullable @Assisted String message) {
+    super(projectControlFactory, groupBackend, metaDataUpdateFactory,
+        projectName, base, sectionList, message, false);
+    this.db = db;
+    this.user = user;
+    this.patchSetInfoFactory = patchSetInfoFactory;
+    this.addReviewerFactory = addReviewerFactory;
+  }
+
+  @Override
+  protected Change.Id updateProjectConfig(ProjectConfig config, MetaDataUpdate md)
+      throws IOException, OrmException {
+    Change.Id changeId = new Change.Id(db.nextChangeId());
+    PatchSet ps = new PatchSet(new PatchSet.Id(changeId, 1));
+    RevCommit commit = config.commitToNewRef(md, ps.getRefName());
+    if (commit.getId().equals(base)) {
+      return null;
+    }
+
+    Change change = new Change(
+        new Change.Key("I" + commit.name()),
+        changeId,
+        user.getAccountId(),
+        new Branch.NameKey(
+            config.getProject().getNameKey(),
+            GitRepositoryManager.REF_CONFIG));
+    change.nextPatchSetId();
+
+    ps.setCreatedOn(change.getCreatedOn());
+    ps.setUploader(change.getOwner());
+    ps.setRevision(new RevId(commit.name()));
+
+    PatchSetInfo info = patchSetInfoFactory.get(commit, ps.getId());
+    change.setCurrentPatchSet(info);
+    ChangeUtil.updated(change);
+
+    db.changes().beginTransaction(changeId);
+    try {
+      insertAncestors(ps.getId(), commit);
+      db.patchSets().insert(Collections.singleton(ps));
+      db.changes().insert(Collections.singleton(change));
+      addProjectOwnersAsReviewers(changeId);
+      db.commit();
+    } finally {
+      db.rollback();
+    }
+    return changeId;
+  }
+
+  private void insertAncestors(PatchSet.Id id, RevCommit src)
+      throws OrmException {
+    final int cnt = src.getParentCount();
+    List<PatchSetAncestor> toInsert = new ArrayList<PatchSetAncestor>(cnt);
+    for (int p = 0; p < cnt; p++) {
+      PatchSetAncestor a;
+
+      a = new PatchSetAncestor(new PatchSetAncestor.Id(id, p + 1));
+      a.setAncestorRevision(new RevId(src.getParent(p).name()));
+      toInsert.add(a);
+    }
+    db.patchSetAncestors().insert(toInsert);
+  }
+
+  private void addProjectOwnersAsReviewers(final Change.Id changeId) {
+    final String projectOwners =
+        groupBackend.get(AccountGroup.PROJECT_OWNERS).getName();
+    try {
+      addReviewerFactory.create(changeId, Collections.singleton(projectOwners),
+          false).call();
+    } catch (Exception e) {
+      // one of the owner groups is not visible to the user and this it why it
+      // can't be added as reviewer
+    }
+  }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/SuggestParentCandidatesHandler.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/SuggestParentCandidatesHandler.java
deleted file mode 100644
index ba0e4cd..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/SuggestParentCandidatesHandler.java
+++ /dev/null
@@ -1,40 +0,0 @@
-// Copyright (C) 2011 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License
-
-package com.google.gerrit.httpd.rpc.project;
-
-import com.google.gerrit.httpd.rpc.Handler;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.project.SuggestParentCandidates;
-import com.google.inject.Inject;
-
-import java.util.List;
-
-public class SuggestParentCandidatesHandler extends Handler<List<Project>> {
-  interface Factory {
-    SuggestParentCandidatesHandler create();
-  }
-
-  private final SuggestParentCandidates suggestParentCandidates;
-
-  @Inject
-  SuggestParentCandidatesHandler(final SuggestParentCandidates suggestParentCandidates) {
-    this.suggestParentCandidates = suggestParentCandidates;
-  }
-
-  @Override
-  public List<Project> call() throws Exception {
-    return suggestParentCandidates.getProjects();
-  }
-}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/VisibleProjectDetails.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/VisibleProjectDetails.java
index e12cfb1..1c22d83 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/VisibleProjectDetails.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/VisibleProjectDetails.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.httpd.rpc.project;
 
-
 import com.google.gerrit.common.data.ProjectDetail;
 import com.google.gerrit.httpd.rpc.Handler;
 import com.google.gerrit.reviewdb.client.Project;
@@ -22,6 +21,7 @@
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.inject.Inject;
 
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
@@ -50,6 +50,7 @@
       try {
         result.add(projectDetailFactory.create(projectName).call());
       } catch (NoSuchProjectException e) {
+      } catch (IOException e) {
       }
     }
     Collections.sort(result, new Comparator<ProjectDetail>() {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/VisibleProjects.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/VisibleProjects.java
deleted file mode 100644
index ba65617..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/VisibleProjects.java
+++ /dev/null
@@ -1,75 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.httpd.rpc.project;
-
-import com.google.gerrit.common.data.ProjectList;
-import com.google.gerrit.httpd.rpc.Handler;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectControl;
-import com.google.inject.Inject;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.List;
-
-class VisibleProjects extends Handler<ProjectList> {
-  interface Factory {
-    VisibleProjects create();
-  }
-
-  private final ProjectControl.Factory projectControlFactory;
-  private final ProjectCache projectCache;
-  private final CurrentUser user;
-
-  @Inject
-  VisibleProjects(final ProjectControl.Factory projectControlFactory,
-      final ProjectCache projectCache, final CurrentUser user) {
-    this.projectControlFactory = projectControlFactory;
-    this.projectCache = projectCache;
-    this.user = user;
-  }
-
-  @Override
-  public ProjectList call() {
-    ProjectList result = new ProjectList();
-    result.setProjects(getProjects());
-    result.setCanCreateProject(user.getCapabilities().canCreateProject());
-    return result;
-  }
-
-  private List<Project> getProjects() {
-    List<Project> result = new ArrayList<Project>();
-    for (Project.NameKey p : projectCache.all()) {
-      try {
-        ProjectControl c = projectControlFactory.controlFor(p);
-        if (c.isVisible() || c.isOwner()) {
-          result.add(c.getProject());
-        }
-      } catch (NoSuchProjectException e) {
-        continue;
-      }
-    }
-    Collections.sort(result, new Comparator<Project>() {
-      public int compare(final Project a, final Project b) {
-        return a.getName().compareTo(b.getName());
-      }
-    });
-    return result;
-  }
-}
diff --git a/gerrit-launcher/.gitignore b/gerrit-launcher/.gitignore
index 194bedc..980a6b1 100644
--- a/gerrit-launcher/.gitignore
+++ b/gerrit-launcher/.gitignore
@@ -2,4 +2,5 @@
 /.classpath
 /.project
 /.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-launcher.iml
\ No newline at end of file
diff --git a/gerrit-launcher/.settings/org.eclipse.core.resources.prefs b/gerrit-launcher/.settings/org.eclipse.core.resources.prefs
index c780f44..e9441bb 100644
--- a/gerrit-launcher/.settings/org.eclipse.core.resources.prefs
+++ b/gerrit-launcher/.settings/org.eclipse.core.resources.prefs
@@ -1,4 +1,3 @@
-#Thu Jul 28 11:02:36 PDT 2011
 eclipse.preferences.version=1
 encoding//src/main/java=UTF-8
 encoding/<project>=UTF-8
diff --git a/gerrit-launcher/pom.xml b/gerrit-launcher/pom.xml
index 7d07652..e700351 100644
--- a/gerrit-launcher/pom.xml
+++ b/gerrit-launcher/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.4-SNAPSHOT</version>
+    <version>2.5-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-launcher</artifactId>
diff --git a/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java b/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java
index 7f2007e..9cca559 100644
--- a/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java
+++ b/gerrit-launcher/src/main/java/com/google/gerrit/launcher/GerritLauncher.java
@@ -31,9 +31,10 @@
 import java.net.URLClassLoader;
 import java.security.CodeSource;
 import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
 import java.util.Enumeration;
+import java.util.List;
+import java.util.SortedMap;
+import java.util.TreeMap;
 import java.util.jar.Attributes;
 import java.util.jar.JarFile;
 import java.util.jar.Manifest;
@@ -196,7 +197,7 @@
       throw e;
     }
 
-    final ArrayList<URL> jars = new ArrayList<URL>();
+    final SortedMap<String, URL> jars = new TreeMap<String, URL>();
     try {
       final ZipFile zf = new ZipFile(path);
       try {
@@ -208,6 +209,7 @@
           }
 
           if (ze.getName().startsWith("WEB-INF/lib/")) {
+            String name = ze.getName().substring("WEB-INF/lib/".length());
             final File tmp = createTempFile(safeName(ze), ".jar");
             final FileOutputStream out = new FileOutputStream(tmp);
             try {
@@ -224,7 +226,7 @@
             } finally {
               out.close();
             }
-            jars.add(tmp.toURI().toURL());
+            jars.put(name, tmp.toURI().toURL());
           }
         }
       } finally {
@@ -237,13 +239,38 @@
     if (jars.isEmpty()) {
       return GerritLauncher.class.getClassLoader();
     }
-    Collections.sort(jars, new Comparator<URL>() {
-      public int compare(URL o1, URL o2) {
-        return o1.toString().compareTo(o2.toString());
-      }
-    });
 
-    return new URLClassLoader(jars.toArray(new URL[jars.size()]));
+    // The extension API needs to be its own ClassLoader, along
+    // with a few of its dependencies. Try to construct this first.
+    List<URL> extapi = new ArrayList<URL>();
+    move(jars, "gerrit-extension-api-", extapi);
+    move(jars, "guice-", extapi);
+    move(jars, "javax.inject-1.jar", extapi);
+    move(jars, "aopalliance-1.0.jar", extapi);
+    move(jars, "guice-servlet-", extapi);
+    move(jars, "servlet-api-", extapi);
+
+    ClassLoader parent = ClassLoader.getSystemClassLoader();
+    if (!extapi.isEmpty()) {
+      parent = new URLClassLoader(
+          extapi.toArray(new URL[extapi.size()]),
+          parent);
+    }
+    return new URLClassLoader(
+        jars.values().toArray(new URL[jars.size()]),
+        parent);
+  }
+
+  private static void move(SortedMap<String, URL> jars,
+      String prefix,
+      List<URL> extapi) {
+    SortedMap<String, URL> matches = jars.tailMap(prefix);
+    if (!matches.isEmpty()) {
+      String first = matches.firstKey();
+      if (first.startsWith(prefix)) {
+        extapi.add(jars.remove(first));
+      }
+    }
   }
 
   private static String safeName(final ZipEntry ze) {
@@ -423,36 +450,42 @@
   }
 
   private static File tmproot() {
-    // Try to find the user's home directory. If we can't find it
-    // return null so the JVM's default temporary directory is used
-    // instead. This is probably /tmp or /var/tmp.
-    //
-    String userHome = System.getProperty("user.home");
-    if (userHome == null || "".equals(userHome)) {
-      userHome = System.getenv("HOME");
+    File tmp;
+    String gerritTemp = System.getenv("GERRIT_TMP");
+    if (gerritTemp != null && gerritTemp.length() > 0) {
+      tmp = new File(gerritTemp);
+    } else {
+      // Try to find the user's home directory. If we can't find it
+      // return null so the JVM's default temporary directory is used
+      // instead. This is probably /tmp or /var/tmp.
+      //
+      String userHome = System.getProperty("user.home");
       if (userHome == null || "".equals(userHome)) {
-        System.err.println("warning: cannot determine home directory");
-        System.err.println("warning: using system temporary directory instead");
-        return null;
+        userHome = System.getenv("HOME");
+        if (userHome == null || "".equals(userHome)) {
+          System.err.println("warning: cannot determine home directory");
+          System.err.println("warning: using system temporary directory instead");
+          return null;
+        }
       }
-    }
 
-    // Ensure the home directory exists. If it doesn't, try to make it.
-    //
-    final File home = new File(userHome);
-    if (!home.exists()) {
-      if (home.mkdirs()) {
-        System.err.println("warning: created " + home.getAbsolutePath());
-      } else {
-        System.err.println("warning: " + home.getAbsolutePath() + " not found");
-        System.err.println("warning: using system temporary directory instead");
-        return null;
+      // Ensure the home directory exists. If it doesn't, try to make it.
+      //
+      final File home = new File(userHome);
+      if (!home.exists()) {
+        if (home.mkdirs()) {
+          System.err.println("warning: created " + home.getAbsolutePath());
+        } else {
+          System.err.println("warning: " + home.getAbsolutePath() + " not found");
+          System.err.println("warning: using system temporary directory instead");
+          return null;
+        }
       }
-    }
 
-    // Use $HOME/.gerritcodereview/tmp for our temporary file area.
-    //
-    final File tmp = new File(new File(home, ".gerritcodereview"), "tmp");
+      // Use $HOME/.gerritcodereview/tmp for our temporary file area.
+      //
+      tmp = new File(new File(home, ".gerritcodereview"), "tmp");
+    }
     if (!tmp.exists() && !tmp.mkdirs()) {
       System.err.println("warning: cannot create " + tmp.getAbsolutePath());
       System.err.println("warning: using system temporary directory instead");
diff --git a/gerrit-main/.gitignore b/gerrit-main/.gitignore
index 194bedc..c847710 100644
--- a/gerrit-main/.gitignore
+++ b/gerrit-main/.gitignore
@@ -2,4 +2,5 @@
 /.classpath
 /.project
 /.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-main.iml
\ No newline at end of file
diff --git a/gerrit-main/.settings/org.eclipse.core.resources.prefs b/gerrit-main/.settings/org.eclipse.core.resources.prefs
index c780f44..e9441bb 100644
--- a/gerrit-main/.settings/org.eclipse.core.resources.prefs
+++ b/gerrit-main/.settings/org.eclipse.core.resources.prefs
@@ -1,4 +1,3 @@
-#Thu Jul 28 11:02:36 PDT 2011
 eclipse.preferences.version=1
 encoding//src/main/java=UTF-8
 encoding/<project>=UTF-8
diff --git a/gerrit-main/pom.xml b/gerrit-main/pom.xml
index 9d8320c..bb2d763 100644
--- a/gerrit-main/pom.xml
+++ b/gerrit-main/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.4-SNAPSHOT</version>
+    <version>2.5-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-main</artifactId>
diff --git a/gerrit-openid/.gitignore b/gerrit-openid/.gitignore
index 194bedc..158faf1 100644
--- a/gerrit-openid/.gitignore
+++ b/gerrit-openid/.gitignore
@@ -2,4 +2,5 @@
 /.classpath
 /.project
 /.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-openid.iml
\ No newline at end of file
diff --git a/gerrit-openid/.settings/org.eclipse.core.resources.prefs b/gerrit-openid/.settings/org.eclipse.core.resources.prefs
index fc11c3f..f9fe345 100644
--- a/gerrit-openid/.settings/org.eclipse.core.resources.prefs
+++ b/gerrit-openid/.settings/org.eclipse.core.resources.prefs
@@ -1,4 +1,3 @@
-#Thu Jul 28 11:02:36 PDT 2011
 eclipse.preferences.version=1
 encoding//src/main/java=UTF-8
 encoding//src/test/java=UTF-8
diff --git a/gerrit-openid/pom.xml b/gerrit-openid/pom.xml
index ed2625e..01e2e3e 100644
--- a/gerrit-openid/pom.xml
+++ b/gerrit-openid/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.4-SNAPSHOT</version>
+    <version>2.5-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-openid</artifactId>
@@ -51,8 +51,7 @@
 
     <dependency>
       <groupId>org.openid4java</groupId>
-      <artifactId>openid4java-consumer</artifactId>
-      <type>pom</type>
+      <artifactId>openid4java</artifactId>
     </dependency>
 
     <dependency>
diff --git a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java
index 0593bce..09a5d10 100644
--- a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java
+++ b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java
@@ -26,6 +26,7 @@
 import com.google.gerrit.server.UrlEncoded;
 import com.google.gerrit.server.account.AccountException;
 import com.google.gerrit.server.account.AccountManager;
+import com.google.gerrit.server.account.AuthMethod;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.ConfigUtil;
@@ -416,7 +417,7 @@
             lastId.setMaxAge(0);
           }
           rsp.addCookie(lastId);
-          webSession.get().login(arsp, remember);
+          webSession.get().login(arsp, AuthMethod.COOKIE, remember);
           if (arsp.isNew() && claimedIdentifier != null) {
             final com.google.gerrit.server.account.AuthRequest linkReq =
                 new com.google.gerrit.server.account.AuthRequest(
@@ -430,7 +431,7 @@
 
         case LINK_IDENTIY: {
           arsp = accountManager.link(identifiedUser.get().getAccountId(), areq);
-          webSession.get().login(arsp, remember);
+          webSession.get().login(arsp, AuthMethod.COOKIE, remember);
           callback(false, req, rsp);
           break;
         }
diff --git a/gerrit-ehcache/.gitignore b/gerrit-package-plugins/.gitignore
similarity index 79%
copy from gerrit-ehcache/.gitignore
copy to gerrit-package-plugins/.gitignore
index 20251d4..c96b05c 100644
--- a/gerrit-ehcache/.gitignore
+++ b/gerrit-package-plugins/.gitignore
@@ -1,5 +1,6 @@
 /target
 /.classpath
 /.project
-/.settings/org.eclipse.m2e.core.prefs
 /.settings/org.maven.ide.eclipse.prefs
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-package-plugins.iml
diff --git a/gerrit-package-plugins/pom.xml b/gerrit-package-plugins/pom.xml
new file mode 100644
index 0000000..c072719
--- /dev/null
+++ b/gerrit-package-plugins/pom.xml
@@ -0,0 +1,90 @@
+<?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>
+
+  <groupId>com.google.gerrit</groupId>
+  <artifactId>gerrit-package-plugins</artifactId>
+  <packaging>war</packaging>
+  <version>2.5-SNAPSHOT</version>
+
+  <name>Gerrit Code Review - Package Plugins</name>
+  <url>http://code.google.com/p/gerrit/</url>
+
+  <properties>
+    <project.build.sourceEncoding>
+      UTF-8
+    </project.build.sourceEncoding>
+  </properties>
+
+  <dependencies>
+    <dependency>
+      <groupId>com.google.gerrit</groupId>
+      <artifactId>gerrit-war</artifactId>
+      <version>${project.version}</version>
+      <type>war</type>
+    </dependency>
+    <dependency>
+      <groupId>com.googlesource.gerrit.plugins.replication</groupId>
+      <artifactId>replication</artifactId>
+      <version>1.0</version>
+      <scope>provided</scope>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-dependency-plugin</artifactId>
+        <version>2.1</version>
+        <executions>
+          <execution>
+            <phase>process-resources</phase>
+            <goals>
+              <goal>copy-dependencies</goal>
+            </goals>
+            <configuration>
+              <includeTypes>jar</includeTypes>
+              <stripVersion>true</stripVersion>
+              <outputDirectory>${project.build.directory}/${project.build.finalName}/WEB-INF/plugins</outputDirectory>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-war-plugin</artifactId>
+        <version>2.1.1</version>
+        <configuration>
+          <warName>gerrit-full-${project.version}</warName>
+          <archive>
+            <addMavenDescriptor>false</addMavenDescriptor>
+            <manifestEntries>
+              <Main-Class>Main</Main-Class>
+              <Implementation-Title>Gerrit Code Review</Implementation-Title>
+              <Implementation-Version>${project.version}</Implementation-Version>
+            </manifestEntries>
+          </archive>
+        </configuration>
+      </plugin>
+    </plugins>
+  </build>
+</project>
diff --git a/gerrit-patch-commonsnet/.gitignore b/gerrit-patch-commonsnet/.gitignore
index 194bedc..121f8e90 100644
--- a/gerrit-patch-commonsnet/.gitignore
+++ b/gerrit-patch-commonsnet/.gitignore
@@ -2,4 +2,5 @@
 /.classpath
 /.project
 /.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-patch-commonsnet.iml
\ No newline at end of file
diff --git a/gerrit-patch-commonsnet/.settings/org.eclipse.core.resources.prefs b/gerrit-patch-commonsnet/.settings/org.eclipse.core.resources.prefs
index 589908f..e9441bb 100644
--- a/gerrit-patch-commonsnet/.settings/org.eclipse.core.resources.prefs
+++ b/gerrit-patch-commonsnet/.settings/org.eclipse.core.resources.prefs
@@ -1,4 +1,3 @@
-#Thu Jul 28 11:02:35 PDT 2011
 eclipse.preferences.version=1
 encoding//src/main/java=UTF-8
 encoding/<project>=UTF-8
diff --git a/gerrit-patch-commonsnet/pom.xml b/gerrit-patch-commonsnet/pom.xml
index 75ee12e..f1a8b3e 100644
--- a/gerrit-patch-commonsnet/pom.xml
+++ b/gerrit-patch-commonsnet/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.4-SNAPSHOT</version>
+    <version>2.5-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-patch-commonsnet</artifactId>
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-patch-jgit/.gitignore b/gerrit-patch-jgit/.gitignore
index 194bedc..7c4c433 100644
--- a/gerrit-patch-jgit/.gitignore
+++ b/gerrit-patch-jgit/.gitignore
@@ -2,4 +2,5 @@
 /.classpath
 /.project
 /.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-patch-jgit.iml
\ No newline at end of file
diff --git a/gerrit-patch-jgit/.settings/org.eclipse.core.resources.prefs b/gerrit-patch-jgit/.settings/org.eclipse.core.resources.prefs
index 589908f..e9441bb 100644
--- a/gerrit-patch-jgit/.settings/org.eclipse.core.resources.prefs
+++ b/gerrit-patch-jgit/.settings/org.eclipse.core.resources.prefs
@@ -1,4 +1,3 @@
-#Thu Jul 28 11:02:35 PDT 2011
 eclipse.preferences.version=1
 encoding//src/main/java=UTF-8
 encoding/<project>=UTF-8
diff --git a/gerrit-patch-jgit/pom.xml b/gerrit-patch-jgit/pom.xml
index f8190f5..65223fb 100644
--- a/gerrit-patch-jgit/pom.xml
+++ b/gerrit-patch-jgit/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.4-SNAPSHOT</version>
+    <version>2.5-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-patch-jgit</artifactId>
diff --git a/gerrit-patch-jgit/src/main/java/org/eclipse/jgit/diff/EditDeserializer.java b/gerrit-patch-jgit/src/main/java/org/eclipse/jgit/diff/EditDeserializer.java
index 1df89b7..9a55e0f 100644
--- a/gerrit-patch-jgit/src/main/java/org/eclipse/jgit/diff/EditDeserializer.java
+++ b/gerrit-patch-jgit/src/main/java/org/eclipse/jgit/diff/EditDeserializer.java
@@ -76,7 +76,7 @@
   public JsonElement serialize(final Edit src, final Type typeOfSrc,
       final JsonSerializationContext context) {
     if (src == null) {
-      return new JsonNull();
+      return JsonNull.INSTANCE;
     }
     final JsonArray a = new JsonArray();
     add(a, src);
diff --git a/gerrit-pgm/.gitignore b/gerrit-pgm/.gitignore
index 194bedc..dafe355 100644
--- a/gerrit-pgm/.gitignore
+++ b/gerrit-pgm/.gitignore
@@ -2,4 +2,5 @@
 /.classpath
 /.project
 /.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-pgm.iml
\ No newline at end of file
diff --git a/gerrit-pgm/.settings/org.eclipse.core.resources.prefs b/gerrit-pgm/.settings/org.eclipse.core.resources.prefs
index 9df523e..839d647 100644
--- a/gerrit-pgm/.settings/org.eclipse.core.resources.prefs
+++ b/gerrit-pgm/.settings/org.eclipse.core.resources.prefs
@@ -1,4 +1,3 @@
-#Thu Jul 28 11:02:36 PDT 2011
 eclipse.preferences.version=1
 encoding//src/main/java=UTF-8
 encoding//src/main/resources=UTF-8
diff --git a/gerrit-pgm/pom.xml b/gerrit-pgm/pom.xml
index 1463d15..a015219 100644
--- a/gerrit-pgm/pom.xml
+++ b/gerrit-pgm/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.4-SNAPSHOT</version>
+    <version>2.5-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-pgm</artifactId>
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 25b7699..a826c88 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,13 +17,16 @@
 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.AllRequestFilter;
 import com.google.gerrit.httpd.CacheBasedWebSession;
 import com.google.gerrit.httpd.GitOverHttpModule;
 import com.google.gerrit.httpd.HttpCanonicalWebUrlProvider;
+import com.google.gerrit.httpd.RequestContextFilter;
+import com.google.gerrit.httpd.SignedTokenRestTokenVerifier;
 import com.google.gerrit.httpd.WebModule;
 import com.google.gerrit.httpd.WebSshGlueModule;
 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.pgm.http.jetty.GetUserFilter;
 import com.google.gerrit.pgm.http.jetty.JettyEnv;
@@ -34,27 +37,40 @@
 import com.google.gerrit.pgm.util.RuntimeShutdown;
 import com.google.gerrit.pgm.util.SiteProgram;
 import com.google.gerrit.reviewdb.client.AuthType;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.cache.h2.DefaultCacheFactory;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.AuthConfigModule;
 import com.google.gerrit.server.config.CanonicalWebUrlModule;
 import com.google.gerrit.server.config.CanonicalWebUrlProvider;
 import com.google.gerrit.server.config.GerritGlobalModule;
+import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.MasterNodeStartup;
 import com.google.gerrit.server.contact.HttpContactStoreConnection;
-import com.google.gerrit.server.git.PushReplication;
 import com.google.gerrit.server.git.ReceiveCommitsExecutorModule;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.mail.SignedTokenEmailTokenVerifier;
 import com.google.gerrit.server.mail.SmtpEmailSender;
+import com.google.gerrit.server.plugins.PluginGuiceEnvironment;
+import com.google.gerrit.server.plugins.PluginModule;
+import com.google.gerrit.server.schema.SchemaUpdater;
 import com.google.gerrit.server.schema.SchemaVersionCheck;
+import com.google.gerrit.server.schema.UpdateUI;
 import com.google.gerrit.server.ssh.NoSshModule;
 import com.google.gerrit.sshd.SshModule;
 import com.google.gerrit.sshd.commands.MasterCommandModule;
 import com.google.gerrit.sshd.commands.SlaveCommandModule;
+import com.google.gwtorm.jdbc.JdbcExecutor;
+import com.google.gwtorm.jdbc.JdbcSchema;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.gwtorm.server.StatementExecutor;
+import com.google.inject.Inject;
 import com.google.inject.Injector;
 import com.google.inject.Module;
 import com.google.inject.Provider;
 
+import org.eclipse.jgit.lib.Config;
 import org.kohsuke.args4j.Option;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -140,6 +156,9 @@
       dbInjector = createDbInjector(MULTI_USER);
       cfgInjector = createCfgInjector();
       sysInjector = createSysInjector();
+      sysInjector.getInstance(PluginGuiceEnvironment.class)
+        .setCfgInjector(cfgInjector);
+      sysInjector.getInstance(SchemaUpgrade.class).upgradeSchema();
       manager.add(dbInjector, cfgInjector, sysInjector);
 
       if (sshd) {
@@ -152,6 +171,7 @@
 
       manager.start();
       RuntimeShutdown.add(new Runnable() {
+        @Override
         public void run() {
           log.info("caught shutdown, cleaning up");
           if (runId != null) {
@@ -186,6 +206,74 @@
     }
   }
 
+  static class SchemaUpgrade {
+
+    private final Config config;
+    private final SchemaUpdater updater;
+    private final SchemaFactory<ReviewDb> schema;
+
+    @Inject
+    SchemaUpgrade(@GerritServerConfig Config config, SchemaUpdater updater,
+        SchemaFactory<ReviewDb> schema) {
+      this.config = config;
+      this.updater = updater;
+      this.schema = schema;
+    }
+
+    void upgradeSchema() throws OrmException {
+      SchemaUpgradePolicy policy =
+          config.getEnum("site", null, "upgradeSchemaOnStartup",
+              SchemaUpgradePolicy.OFF);
+      if (policy == SchemaUpgradePolicy.AUTO
+          || policy == SchemaUpgradePolicy.AUTO_NO_PRUNE) {
+        final List<String> pruneList = new ArrayList<String>();
+        updater.update(new UpdateUI() {
+          @Override
+          public void message(String msg) {
+            log.info(msg);
+          }
+
+          @Override
+          public boolean yesno(boolean def, String msg) {
+            return true;
+          }
+
+          @Override
+          public boolean isBatch() {
+            return true;
+          }
+
+          @Override
+          public void pruneSchema(StatementExecutor e, List<String> prune) {
+            for (String p : prune) {
+              if (!pruneList.contains(p)) {
+                pruneList.add(p);
+              }
+            }
+          }
+        });
+
+        if (!pruneList.isEmpty() && policy == SchemaUpgradePolicy.AUTO) {
+          log.info("Pruning: " + pruneList.toString());
+          final JdbcSchema db = (JdbcSchema) schema.open();
+          try {
+            final JdbcExecutor e = new JdbcExecutor(db);
+            try {
+              for (String sql : pruneList) {
+                e.execute(sql);
+              }
+            } finally {
+              e.close();
+            }
+          } finally {
+            db.close();
+          }
+        }
+      }
+    }
+  }
+
+
   private String myVersion() {
     return com.google.gerrit.common.Version.getVersion();
   }
@@ -204,10 +292,11 @@
     modules.add(new ChangeHookRunner.Module());
     modules.add(new ReceiveCommitsExecutorModule());
     modules.add(cfgInjector.getInstance(GerritGlobalModule.class));
-    modules.add(new EhcachePoolImpl.Module());
+    modules.add(new DefaultCacheFactory.Module());
     modules.add(new SmtpEmailSender.Module());
     modules.add(new SignedTokenEmailTokenVerifier.Module());
-    modules.add(new PushReplication.Module());
+    modules.add(new SignedTokenRestTokenVerifier.Module());
+    modules.add(new PluginModule());
     if (httpd) {
       modules.add(new CanonicalWebUrlModule() {
         @Override
@@ -231,13 +320,15 @@
 
   private void initSshd() {
     sshInjector = createSshInjector();
+    sysInjector.getInstance(PluginGuiceEnvironment.class)
+        .setSshInjector(sshInjector);
     manager.add(sshInjector);
   }
 
   private Injector createSshInjector() {
     final List<Module> modules = new ArrayList<Module>();
     if (sshd) {
-      modules.add(new SshModule());
+      modules.add(sysInjector.getInstance(SshModule.class));
       if (slave) {
         modules.add(new SlaveCommandModule());
       } else {
@@ -252,6 +343,9 @@
   private void initHttpd() {
     webInjector = createWebInjector();
 
+    sysInjector.getInstance(PluginGuiceEnvironment.class)
+        .setHttpInjector(webInjector);
+
     sysInjector.getInstance(HttpCanonicalWebUrlProvider.class)
         .setHttpServletRequest(
             webInjector.getProvider(HttpServletRequest.class));
@@ -262,19 +356,25 @@
 
   private Injector createWebInjector() {
     final List<Module> modules = new ArrayList<Module>();
+    if (sshd) {
+      modules.add(new ProjectQoSFilter.Module());
+    }
+    modules.add(RequestContextFilter.module());
+    modules.add(AllRequestFilter.module());
     modules.add(CacheBasedWebSession.module());
     modules.add(HttpContactStoreConnection.module());
     modules.add(sysInjector.getInstance(GitOverHttpModule.class));
     modules.add(sysInjector.getInstance(WebModule.class));
+    modules.add(new HttpPluginModule());
     if (sshd) {
       modules.add(sshInjector.getInstance(WebSshGlueModule.class));
-      modules.add(new ProjectQoSFilter.Module());
     } else {
       modules.add(new NoSshModule());
     }
 
     AuthConfig authConfig = cfgInjector.getInstance(AuthConfig.class);
-    if (authConfig.getAuthType() == AuthType.OPENID) {
+    if (authConfig.getAuthType() == AuthType.OPENID ||
+        authConfig.getAuthType() == AuthType.OPENID_SSO) {
       modules.add(new OpenIdModule());
     }
     modules.add(sysInjector.getInstance(GetUserFilter.Module.class));
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ExportReviewNotes.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ExportReviewNotes.java
index 5f0bc80..525360d 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,17 +17,15 @@
 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;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.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;
@@ -36,6 +34,7 @@
 import com.google.gerrit.server.git.CreateCodeReviewNotes;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.LocalDiskRepositoryManager;
+import com.google.gerrit.server.git.NotesBranchUtil;
 import com.google.gerrit.server.schema.SchemaVersionCheck;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.SchemaFactory;
@@ -45,7 +44,6 @@
 import com.google.inject.Scopes;
 
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.lib.TextProgressMonitor;
 import org.eclipse.jgit.lib.ThreadSafeProgressMonitor;
@@ -100,11 +98,12 @@
 
         install(AccountCacheImpl.module());
         install(GroupCacheImpl.module());
-        install(new EhcachePoolImpl.Module());
+        install(new DefaultCacheFactory.Module());
         install(new FactoryModule() {
           @Override
           protected void configure() {
             factory(CreateCodeReviewNotes.Factory.class);
+            factory(NotesBranchUtil.Factory.class);
           }
         });
         install(new LifecycleModule() {
@@ -173,21 +172,8 @@
     }
     try {
       CreateCodeReviewNotes notes = codeReviewNotesFactory.create(db, git);
-      try {
-        notes.loadBase();
-        for (Change change : changes) {
-          monitor.update(1);
-          PatchSet ps = db.patchSets().get(change.currentPatchSetId());
-          if (ps == null) {
-            continue;
-          }
-          notes.add(change, ObjectId.fromString(ps.getRevision().get()));
-        }
-        notes.commit("Exported prior reviews from Gerrit Code Review\n");
-        notes.updateRef();
-      } finally {
-        notes.release();
-      }
+      notes.create(changes, null,
+          "Exported prior reviews from Gerrit Code Review\n", monitor);
     } finally {
       git.close();
     }
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..95b8487f 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
@@ -141,12 +141,12 @@
       }
 
       final StringBuilder buf = new StringBuilder();
-      buf.append(why.getMessage());
-      why = why.getCause();
       while (why != null) {
-        buf.append("\n  caused by ");
-        buf.append(why.toString());
+        buf.append(why.getMessage());
         why = why.getCause();
+        if (why != null) {
+          buf.append("\n  caused by ");
+        }
       }
       throw die(buf.toString(), new RuntimeException("InitInjector failed", ce));
     }
@@ -191,6 +191,11 @@
         }
 
         @Override
+        public boolean isBatch() {
+          return ui.isBatch();
+        }
+
+        @Override
         public void pruneSchema(StatementExecutor e, List<String> prune) {
           for (String p : prune) {
             if (!pruneList.contains(p)) {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Rulec.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Rulec.java
index 451ed30..cabdc64 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Rulec.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Rulec.java
@@ -43,7 +43,7 @@
   @Option(name = "--all", usage = "recompile all rules")
   private boolean all;
 
-  @Option(name = "--quiet", usage = "supress some messsages")
+  @Option(name = "--quiet", usage = "suppress some messages")
   private boolean quiet;
 
   @Argument(index = 0, multiValued = true, metaVar = "PROJECT", usage = "project to compile rules for")
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ScanTrackingIds.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ScanTrackingIds.java
index c09329a..795ba5b 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ScanTrackingIds.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ScanTrackingIds.java
@@ -33,7 +33,6 @@
 
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.MissingObjectException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.lib.TextProgressMonitor;
@@ -109,7 +108,7 @@
     final Repository git;
     try {
       git = gitManager.openRepository(project);
-    } catch (RepositoryNotFoundException e) {
+    } catch (IOException e) {
       return;
     }
     try {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/SchemaUpgradePolicy.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/SchemaUpgradePolicy.java
new file mode 100644
index 0000000..67f5c91
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/SchemaUpgradePolicy.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.pgm;
+
+/** Policy for auto upgrading schema on server startup */
+public enum SchemaUpgradePolicy {
+
+  /** Perform schema migration if necessary and prune unused objects */
+  AUTO,
+
+  /** Like AUTO but don't prune unused objects */
+  AUTO_NO_PRUNE,
+
+  /** No automatic schema upgrade */
+  OFF
+}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
index 0a0a3cc..5823940 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/JettyServer.java
@@ -17,8 +17,8 @@
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static java.util.concurrent.TimeUnit.SECONDS;
 
+import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.launcher.GerritLauncher;
-import com.google.gerrit.lifecycle.LifecycleListener;
 import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
@@ -38,8 +38,10 @@
 import org.eclipse.jetty.server.handler.ContextHandlerCollection;
 import org.eclipse.jetty.server.handler.RequestLogHandler;
 import org.eclipse.jetty.server.nio.SelectChannelConnector;
+import org.eclipse.jetty.server.session.SessionHandler;
 import org.eclipse.jetty.server.ssl.SslSelectChannelConnector;
 import org.eclipse.jetty.servlet.DefaultServlet;
+import org.eclipse.jetty.servlet.FilterHolder;
 import org.eclipse.jetty.servlet.FilterMapping;
 import org.eclipse.jetty.servlet.ServletContextHandler;
 import org.eclipse.jetty.servlet.ServletHolder;
@@ -314,6 +316,11 @@
       final JettyEnv env) throws MalformedURLException, IOException {
     final ServletContextHandler app = new ServletContextHandler();
 
+    // This enables the use of sessions in Jetty, feature available
+    // for Gerrit plug-ins to enable user-level sessions.
+    //
+    app.setSessionHandler(new SessionHandler());
+
     // This is the path we are accessed by clients within our domain.
     //
     app.setContextPath(contextPath);
@@ -328,7 +335,8 @@
     // of using the listener to create the injector pass the one we
     // already have built.
     //
-    app.addFilter(GuiceFilter.class, "/*", FilterMapping.DEFAULT);
+    GuiceFilter filter = env.webInjector.getInstance(GuiceFilter.class);
+    app.addFilter(new FilterHolder(filter), "/*", FilterMapping.DEFAULT);
     app.addEventListener(new GuiceServletContextListener() {
       @Override
       protected Injector getInjector() {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/ProjectQoSFilter.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/ProjectQoSFilter.java
index ee7c794..8d320d4 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/ProjectQoSFilter.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/ProjectQoSFilter.java
@@ -71,7 +71,7 @@
   private static final String CANCEL = ATT_SPACE + "/CANCEL";
 
   private static final String FILTER_RE =
-      "^/p/(.*)/(git-upload-pack|git-receive-pack)$";
+      "^/(.*)/(git-upload-pack|git-receive-pack)$";
   private static final Pattern URI_PATTERN = Pattern.compile(FILTER_RE);
 
   public static class Module extends ServletModule {
@@ -97,7 +97,7 @@
     this.userProvider = userProvider;
     this.queue = queue;
     this.context = context;
-    this.maxWait = getTimeUnit(cfg, "httpd", null, "maxwait", 5, MINUTES);
+    this.maxWait = MINUTES.toMillis(getTimeUnit(cfg, "httpd", null, "maxwait", 5, MINUTES));
   }
 
   @Override
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAuth.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAuth.java
index f809c73..fa4dc14 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAuth.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAuth.java
@@ -85,5 +85,9 @@
     if (auth.getSecure("registerEmailPrivateKey") == null) {
       auth.setSecure("registerEmailPrivateKey", SignedToken.generateRandomKey());
     }
+
+    if (auth.getSecure("restTokenPrivateKey") == null) {
+      auth.setSecure("restTokenPrivateKey", SignedToken.generateRandomKey());
+    }
   }
 }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitModule.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitModule.java
index a55cea5..8b3d87e 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitModule.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitModule.java
@@ -43,6 +43,7 @@
     step().to(InitSshd.class);
     step().to(InitHttpd.class);
     step().to(InitCache.class);
+    step().to(InitPlugins.class);
   }
 
   protected LinkedBindingBuilder<InitStep> step() {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPlugins.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPlugins.java
new file mode 100644
index 0000000..155fe4c
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPlugins.java
@@ -0,0 +1,140 @@
+// 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.pgm.init;
+
+import com.google.gerrit.launcher.GerritLauncher;
+import com.google.gerrit.pgm.util.ConsoleUI;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.plugins.PluginLoader;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Enumeration;
+import java.util.jar.Attributes;
+import java.util.jar.JarFile;
+import java.util.jar.Manifest;
+import java.util.zip.ZipEntry;
+import java.util.zip.ZipFile;
+
+@Singleton
+public class InitPlugins implements InitStep {
+  private final static String PLUGIN_DIR = "WEB-INF/plugins/";
+  private final static String JAR = ".jar";
+
+  private final ConsoleUI ui;
+  private final SitePaths site;
+
+  @Inject
+  InitPlugins(final ConsoleUI ui, final SitePaths site) {
+    this.ui = ui;
+    this.site = site;
+  }
+
+  @Override
+  public void run() throws Exception {
+    ui.header("Plugins");
+
+    final File myWar;
+    try {
+      myWar = GerritLauncher.getDistributionArchive();
+    } catch (FileNotFoundException e) {
+      System.err.println("warn: Cannot find gerrit.war");
+      return;
+    }
+
+    boolean foundPlugin = false;
+    try {
+      final ZipFile zf = new ZipFile(myWar);
+      try {
+        final Enumeration<? extends ZipEntry> e = zf.entries();
+        while (e.hasMoreElements()) {
+          final ZipEntry ze = e.nextElement();
+          if (ze.isDirectory()) {
+            continue;
+          }
+
+          if (ze.getName().startsWith(PLUGIN_DIR) && ze.getName().endsWith(JAR)) {
+            if (!foundPlugin) {
+              if (!ui.yesno(false, "Prompt to install core plugins")) {
+                return;
+              }
+              foundPlugin = true;
+            }
+
+            final String pluginJarName = new File(ze.getName()).getName();
+            final String pluginName = pluginJarName.substring(0,  pluginJarName.length() - JAR.length());
+
+            final InputStream in = zf.getInputStream(ze);
+            try {
+              final File tmpPlugin = PluginLoader.storeInTemp(pluginName, in, site);
+              final String pluginVersion = getVersion(tmpPlugin);
+
+              if (!ui.yesno(false, "Install plugin %s version %s", pluginName,
+                  pluginVersion)) {
+                tmpPlugin.delete();
+                continue;
+              }
+
+              final File plugin = new File(site.plugins_dir, pluginJarName);
+              if (plugin.exists()) {
+                final String installedPluginVersion = getVersion(plugin);
+                if (!ui.yesno(false,
+                    "version %s is already installed, overwrite it",
+                    installedPluginVersion)) {
+                  tmpPlugin.delete();
+                  continue;
+                }
+                if (!plugin.delete()) {
+                  throw new IOException("Failed to delete plugin " + pluginName
+                      + ": " + plugin.getAbsolutePath());
+                }
+              }
+              if (!tmpPlugin.renameTo(plugin)) {
+                throw new IOException("Failed to install plugin " + pluginName
+                    + ": " + tmpPlugin.getAbsolutePath() + " -> "
+                    + plugin.getAbsolutePath());
+              }
+            } finally {
+              in.close();
+            }
+          }
+        }
+      } finally {
+        zf.close();
+      }
+    } catch (IOException e) {
+      throw new IOException("Failure during plugin installation", e);
+    }
+
+    if (!foundPlugin) {
+      ui.message("No plugins found.");
+    }
+  }
+
+  private static String getVersion(final File plugin) throws IOException {
+    final JarFile jarFile = new JarFile(plugin);
+    try {
+      final Manifest manifest = jarFile.getManifest();
+      final Attributes main = manifest.getMainAttributes();
+      return main.getValue(Attributes.Name.IMPLEMENTATION_VERSION);
+    } finally {
+      jarFile.close();
+    }
+  }
+}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java
index dae0893..f8ef637 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java
@@ -67,9 +67,12 @@
     mkdir(site.bin_dir);
     mkdir(site.etc_dir);
     mkdir(site.lib_dir);
+    mkdir(site.tmp_dir);
     mkdir(site.logs_dir);
     mkdir(site.mail_dir);
     mkdir(site.static_dir);
+    mkdir(site.plugins_dir);
+    mkdir(site.data_dir);
 
     for (InitStep step : steps) {
       step.run();
@@ -78,12 +81,9 @@
     savePublic(flags.cfg);
     saveSecure(flags.sec);
 
-    if (!site.replication_config.exists()) {
-      site.replication_config.createNewFile();
-    }
-
     extract(site.gerrit_sh, Init.class, "gerrit.sh");
     chmod(0755, site.gerrit_sh);
+    chmod(0700, site.tmp_dir);
 
     extractMailExample("Abandoned.vm");
     extractMailExample("ChangeFooter.vm");
@@ -92,8 +92,11 @@
     extractMailExample("Merged.vm");
     extractMailExample("MergeFail.vm");
     extractMailExample("NewChange.vm");
+    extractMailExample("RebasedPatchSet.vm");
     extractMailExample("RegisterNewEmail.vm");
     extractMailExample("ReplacePatchSet.vm");
+    extractMailExample("Restored.vm");
+    extractMailExample("Reverted.vm");
 
     if (!ui.isBatch()) {
       System.err.println();
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ConsoleUI.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ConsoleUI.java
index 1e93651..b4e0fad 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ConsoleUI.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ConsoleUI.java
@@ -61,6 +61,9 @@
   /** Display a header message before a series of prompts. */
   public abstract void header(String fmt, Object... args);
 
+  /** Display a message. */
+  public abstract void message(String fmt, Object... args);
+
   /** Request the user to answer a yes/no question. */
   public abstract boolean yesno(Boolean def, String fmt, Object... args);
 
@@ -215,6 +218,11 @@
       fmt = fmt.replaceAll("\n", "\n*** ");
       console.printf("\n*** " + fmt + "\n*** \n\n", args);
     }
+
+    @Override
+    public void message(String fmt, Object... args) {
+      console.printf(fmt, args);
+    }
   }
 
   private static class Batch extends ConsoleUI {
@@ -250,5 +258,9 @@
     @Override
     public void header(String fmt, Object... args) {
     }
+
+    @Override
+    public void message(String fmt, Object... args) {
+    }
   }
 }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ErrorLogFile.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ErrorLogFile.java
index 14c8d9f..68762bb 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ErrorLogFile.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ErrorLogFile.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.pgm.util;
 
-import com.google.gerrit.lifecycle.LifecycleListener;
+import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.server.config.SitePaths;
 
 import org.apache.log4j.Appender;
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/LogFileCompressor.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/LogFileCompressor.java
index 23f36a1..57cc7c4 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/LogFileCompressor.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/LogFileCompressor.java
@@ -16,7 +16,7 @@
 
 import static java.util.concurrent.TimeUnit.HOURS;
 
-import com.google.gerrit.lifecycle.LifecycleListener;
+import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.git.WorkQueue;
diff --git a/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/gerrit.sh b/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/gerrit.sh
index 4148847..3857ebd 100755
--- a/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/gerrit.sh
+++ b/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/gerrit.sh
@@ -176,6 +176,8 @@
 
 GERRIT_PID="$GERRIT_SITE/logs/gerrit.pid"
 GERRIT_RUN="$GERRIT_SITE/logs/gerrit.run"
+GERRIT_TMP="$GERRIT_SITE/tmp"
+export GERRIT_TMP
 
 ##################################################
 # Check for JAVA_HOME
@@ -302,7 +304,7 @@
   done
 fi
 if test -z "$GERRIT_WAR" ; then
-  echo >&2 "** ERROR: Cannot find gerrit.war (try setting gerrit.war)"
+  echo >&2 "** ERROR: Cannot find gerrit.war (try setting \$GERRIT_WAR)"
   exit 1
 fi
 
@@ -492,6 +494,7 @@
     echo "  GERRIT_SITE     =  $GERRIT_SITE"
     echo "  GERRIT_CONFIG   =  $GERRIT_CONFIG"
     echo "  GERRIT_PID      =  $GERRIT_PID"
+    echo "  GERRIT_TMP      =  $GERRIT_TMP"
     echo "  GERRIT_WAR      =  $GERRIT_WAR"
     echo "  GERRIT_FDS      =  $GERRIT_FDS"
     echo "  GERRIT_USER     =  $GERRIT_USER"
diff --git a/gerrit-plugin-api/.gitignore b/gerrit-plugin-api/.gitignore
new file mode 100644
index 0000000..e87ebb8
--- /dev/null
+++ b/gerrit-plugin-api/.gitignore
@@ -0,0 +1,8 @@
+/target
+/.classpath
+/.project
+/.settings/org.maven.ide.eclipse.prefs
+/.settings/org.eclipse.m2e.core.prefs
+/.settings/org.eclipse.core.resources.prefs
+/.settings/org.eclipse.jdt.core.prefs
+/gerrit-plugin-api.iml
diff --git a/gerrit-plugin-api/pom.xml b/gerrit-plugin-api/pom.xml
new file mode 100644
index 0000000..84f6f7b
--- /dev/null
+++ b/gerrit-plugin-api/pom.xml
@@ -0,0 +1,110 @@
+<?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-api</artifactId>
+  <name>Gerrit Code Review - Plugin API</name>
+
+  <description>
+    API for tightly coupled plugins to compile against
+  </description>
+
+  <dependencies>
+    <dependency>
+      <groupId>com.google.gerrit</groupId>
+      <artifactId>gerrit-sshd</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>com.google.gerrit</groupId>
+      <artifactId>gerrit-httpd</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>org.apache.tomcat</groupId>
+      <artifactId>servlet-api</artifactId>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-shade-plugin</artifactId>
+        <configuration>
+          <createSourcesJar>true</createSourcesJar>
+          <artifactSet>
+            <excludes>
+              <exclude>gwtexpui:gwtexpui</exclude>
+              <exclude>gwtjsonrpc:gwtjsonrpc</exclude>
+              <exclude>com.google.gerrit:gerrit-prettify</exclude>
+              <exclude>com.google.gerrit:gerrit-patch-commonsnet</exclude>
+              <exclude>com.google.gerrit:gerrit-patch-jgit</exclude>
+              <exclude>com.google.gerrit:gerrit-util-ssl</exclude>
+              <exclude>com.google.gerrit:juniversalchardet</exclude>
+
+              <exclude>com.googlecode.prolog-cafe:PrologCafe</exclude>
+              <exclude>org.slf4j:slf4j-log4j12</exclude>
+              <exclude>log4j:log4j</exclude>
+
+              <exclude>commons-collections:commons-collections</exclude>
+              <exclude>commons-codec:commons-codec</exclude>
+              <exclude>commons-dbcp:commons-dbcp</exclude>
+              <exclude>commons-lang:commons-lang</exclude>
+              <exclude>commons-net:commons-net</exclude>
+              <exclude>commons-pool:commons-pool</exclude>
+
+              <exclude>asm:asm</exclude>
+              <exclude>eu.medsea.mimeutil:mime-util</exclude>
+              <exclude>org.antlr:antlr</exclude>
+              <exclude>org.antlr:antlr-runtime</exclude>
+              <exclude>org.apache.mina:mina-core</exclude>
+              <exclude>oro:oro</exclude>
+            </excludes>
+          </artifactSet>
+          <filters>
+            <filter>
+              <artifact>com.google.gerrit:gerrit-server</artifact>
+              <excludes>
+                <exclude>gerrit/**</exclude>
+              </excludes>
+            </filter>
+          </filters>
+        </configuration>
+        <executions>
+          <execution>
+            <phase>package</phase>
+            <goals>
+              <goal>shade</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+</project>
diff --git a/gerrit-ehcache/.gitignore b/gerrit-plugin-archetype/.gitignore
similarity index 99%
rename from gerrit-ehcache/.gitignore
rename to gerrit-plugin-archetype/.gitignore
index 20251d4..80d6257 100644
--- a/gerrit-ehcache/.gitignore
+++ b/gerrit-plugin-archetype/.gitignore
@@ -1,5 +1,5 @@
 /target
 /.classpath
 /.project
-/.settings/org.eclipse.m2e.core.prefs
 /.settings/org.maven.ide.eclipse.prefs
+/.settings/org.eclipse.m2e.core.prefs
diff --git a/gerrit-plugin-archetype/.settings/org.eclipse.core.resources.prefs b/gerrit-plugin-archetype/.settings/org.eclipse.core.resources.prefs
new file mode 100644
index 0000000..abdea9ac
--- /dev/null
+++ b/gerrit-plugin-archetype/.settings/org.eclipse.core.resources.prefs
@@ -0,0 +1,4 @@
+eclipse.preferences.version=1
+encoding//src/main/java=UTF-8
+encoding//src/main/resources=UTF-8
+encoding/<project>=UTF-8
diff --git a/gerrit-ehcache/.settings/org.eclipse.jdt.core.prefs b/gerrit-plugin-archetype/.settings/org.eclipse.jdt.core.prefs
similarity index 99%
copy from gerrit-ehcache/.settings/org.eclipse.jdt.core.prefs
copy to gerrit-plugin-archetype/.settings/org.eclipse.jdt.core.prefs
index e89c048..470942d 100644
--- a/gerrit-ehcache/.settings/org.eclipse.jdt.core.prefs
+++ b/gerrit-plugin-archetype/.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-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..ce8fa1a
--- /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="Gerrit Plugin">
+  <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 99%
copy from gerrit-ehcache/.gitignore
copy to gerrit-plugin-archetype/src/main/resources/archetype-resources/.gitignore
index 20251d4..80d6257 100644
--- a/gerrit-ehcache/.gitignore
+++ b/gerrit-plugin-archetype/src/main/resources/archetype-resources/.gitignore
@@ -1,5 +1,5 @@
 /target
 /.classpath
 /.project
-/.settings/org.eclipse.m2e.core.prefs
 /.settings/org.maven.ide.eclipse.prefs
+/.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%
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/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-prettify/.gitignore b/gerrit-prettify/.gitignore
index 194bedc..8cf95ef 100644
--- a/gerrit-prettify/.gitignore
+++ b/gerrit-prettify/.gitignore
@@ -2,4 +2,5 @@
 /.classpath
 /.project
 /.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-prettify.iml
\ No newline at end of file
diff --git a/gerrit-prettify/.settings/org.eclipse.core.resources.prefs b/gerrit-prettify/.settings/org.eclipse.core.resources.prefs
index e7d6680..abdea9ac 100644
--- a/gerrit-prettify/.settings/org.eclipse.core.resources.prefs
+++ b/gerrit-prettify/.settings/org.eclipse.core.resources.prefs
@@ -1,4 +1,3 @@
-#Thu Jul 28 11:02:35 PDT 2011
 eclipse.preferences.version=1
 encoding//src/main/java=UTF-8
 encoding//src/main/resources=UTF-8
diff --git a/gerrit-prettify/pom.xml b/gerrit-prettify/pom.xml
index f5bd3d6..9354274 100644
--- a/gerrit-prettify/pom.xml
+++ b/gerrit-prettify/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.4-SNAPSHOT</version>
+    <version>2.5-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-prettify</artifactId>
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettifyConstants.java b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettifyConstants.java
index e14063a..df60305 100644
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettifyConstants.java
+++ b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettifyConstants.java
@@ -23,4 +23,5 @@
   String wseTabAfterSpace();
   String wseTrailingSpace();
   String wseBareCR();
+  String leCR();
 }
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettifyConstants.properties b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettifyConstants.properties
index d440c65..97ab0cf 100644
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettifyConstants.properties
+++ b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettifyConstants.properties
@@ -1,3 +1,4 @@
 wseTabAfterSpace=Whitespace error: Tab after space
 wseTrailingSpace=Whitespace error: Trailing space at end of line
-wseBareCR=Whitespace error: CR without LF
+wseBareCR=CR without LF
+leCR=Carriage Return
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettyFormatter.java b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettyFormatter.java
index c9a9ab8..151149b 100644
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettyFormatter.java
+++ b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettyFormatter.java
@@ -335,6 +335,10 @@
       html = showTrailingWhitespace(html);
     }
 
+    if (diffPrefs.isShowLineEndings()){
+      html = showLineEndings(html);
+    }
+
     if (diffPrefs.isShowTabs()) {
       String t = 1 < diffPrefs.getTabSize() ? "\t" : "";
       html = html.replaceAll("\t", "<span class=\"vt\">\u00BB</span>" + t);
@@ -449,12 +453,11 @@
 
       } else if (end) {
         if (cr == src.length() - 1) {
-          buf.append(src.substring(0, cr));
+          buf.append(src.substring(0, cr + 1));
           return;
         }
       } else if (cr == src.length() - 2 && src.charAt(cr + 1) == '\n') {
-        buf.append(src.substring(0, cr));
-        buf.append('\n');
+        buf.append(src);
         return;
       }
 
@@ -499,6 +502,14 @@
     return src;
   }
 
+  private SafeHtml showLineEndings(SafeHtml src) {
+    final String r = "<span class=\"lecr\""
+        + " title=\"" + PrettifyConstants.C.leCR() + "\"" //
+        + ">\\\\r</span>";
+    src = src.replaceAll("\r", r);
+    return src;
+  }
+
   private String expandTabs(String html) {
     StringBuilder tmp = new StringBuilder();
     int i = 0;
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/SparseFileContent.java b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/SparseFileContent.java
index a5373b8..1a5468c 100644
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/SparseFileContent.java
+++ b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/SparseFileContent.java
@@ -249,7 +249,7 @@
     range.lines = lines;
 
     SparseFileContent r = new SparseFileContent();
-    r.setSize(size());
+    r.setSize(lines.size());
     r.setMissingNewlineAtEnd(isMissingNewlineAtEnd());
     r.setPath(getPath());
     r.ranges.add(range);
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/gerrit.css b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/gerrit.css
index 3478e68..23e7e46 100644
--- a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/gerrit.css
+++ b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/gerrit.css
@@ -14,6 +14,7 @@
  */
 
 @external .wse;
+@external .lecr;
 @external .vt;
 @external .wdd;
 @external .wdi;
@@ -35,6 +36,19 @@
   cursor: pointer;
 }
 
+.lecr {
+  border-bottom: #aaaaaa 1px dashed;
+  border-left: #aaaaaa 1px dashed;
+  padding-bottom: 0px;
+  margin: 0px 2px;
+  padding-left: 2px;
+  padding-right: 2px;
+  border-top: #aaaaaa 1px dashed;
+  border-right: #aaaaaa 1px dashed;
+  padding-top: 0px;
+  cursor: pointer;
+}
+
 .vt,
 .vt .str,
 .vt .kwd,
diff --git a/gerrit-reviewdb/.gitignore b/gerrit-reviewdb/.gitignore
index 194bedc..812ddd0 100644
--- a/gerrit-reviewdb/.gitignore
+++ b/gerrit-reviewdb/.gitignore
@@ -2,4 +2,5 @@
 /.classpath
 /.project
 /.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-reviewdb.iml
\ No newline at end of file
diff --git a/gerrit-reviewdb/.settings/org.eclipse.core.resources.prefs b/gerrit-reviewdb/.settings/org.eclipse.core.resources.prefs
index e7d6680..abdea9ac 100644
--- a/gerrit-reviewdb/.settings/org.eclipse.core.resources.prefs
+++ b/gerrit-reviewdb/.settings/org.eclipse.core.resources.prefs
@@ -1,4 +1,3 @@
-#Thu Jul 28 11:02:35 PDT 2011
 eclipse.preferences.version=1
 encoding//src/main/java=UTF-8
 encoding//src/main/resources=UTF-8
diff --git a/gerrit-reviewdb/pom.xml b/gerrit-reviewdb/pom.xml
index 24d6a1b..f9fb49e 100644
--- a/gerrit-reviewdb/pom.xml
+++ b/gerrit-reviewdb/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.4-SNAPSHOT</version>
+    <version>2.5-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-reviewdb</artifactId>
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AbstractAgreement.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AbstractAgreement.java
deleted file mode 100644
index 0ed7410..0000000
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AbstractAgreement.java
+++ /dev/null
@@ -1,59 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.reviewdb.client;
-
-import java.sql.Timestamp;
-
-/** Base for {@link AccountAgreement} or {@link AccountGroupAgreement}. */
-public interface AbstractAgreement {
-  public static enum Status {
-    NEW('n'),
-
-    VERIFIED('V'),
-
-    REJECTED('R');
-
-    private final char code;
-
-    private Status(final char c) {
-      code = c;
-    }
-
-    public char getCode() {
-      return code;
-    }
-
-    public static Status forCode(final char c) {
-      for (final Status s : Status.values()) {
-        if (s.code == c) {
-          return s;
-        }
-      }
-      return null;
-    }
-  }
-
-  public ContributorAgreement.Id getAgreementId();
-
-  public Timestamp getAcceptedOn();
-
-  public Status getStatus();
-
-  public Timestamp getReviewedOn();
-
-  public Account.Id getReviewedBy();
-
-  public String getReviewComments();
-}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountAgreement.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountAgreement.java
deleted file mode 100644
index baa9b5c..0000000
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountAgreement.java
+++ /dev/null
@@ -1,122 +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.reviewdb.client;
-
-import com.google.gwtorm.client.Column;
-import com.google.gwtorm.client.CompoundKey;
-
-import java.sql.Timestamp;
-
-/** Electronic acceptance of a {@link ContributorAgreement} by {@link Account} */
-public final class AccountAgreement implements AbstractAgreement {
-  public static class Key extends CompoundKey<Account.Id> {
-    private static final long serialVersionUID = 1L;
-
-    @Column(id = 1)
-    protected Account.Id accountId;
-
-    @Column(id = 2)
-    protected ContributorAgreement.Id claId;
-
-    protected Key() {
-      accountId = new Account.Id();
-      claId = new ContributorAgreement.Id();
-    }
-
-    public Key(final Account.Id account, final ContributorAgreement.Id cla) {
-      this.accountId = account;
-      this.claId = cla;
-    }
-
-    @Override
-    public Account.Id getParentKey() {
-      return accountId;
-    }
-
-    public ContributorAgreement.Id getContributorAgreementId() {
-      return claId;
-    }
-
-    @Override
-    public com.google.gwtorm.client.Key<?>[] members() {
-      return new com.google.gwtorm.client.Key<?>[] {claId};
-    }
-  }
-
-  @Column(id = 1, name = Column.NONE)
-  protected Key key;
-
-  @Column(id = 2)
-  protected Timestamp acceptedOn;
-
-  @Column(id = 3)
-  protected char status;
-
-  @Column(id = 4, notNull = false)
-  protected Account.Id reviewedBy;
-
-  @Column(id = 5, notNull = false)
-  protected Timestamp reviewedOn;
-
-  @Column(id = 6, notNull = false, length = Integer.MAX_VALUE)
-  protected String reviewComments;
-
-  protected AccountAgreement() {
-  }
-
-  public AccountAgreement(final AccountAgreement.Key k) {
-    key = k;
-    acceptedOn = new Timestamp(System.currentTimeMillis());
-    status = Status.NEW.getCode();
-  }
-
-  public AccountAgreement.Key getKey() {
-    return key;
-  }
-
-  public ContributorAgreement.Id getAgreementId() {
-    return key.claId;
-  }
-
-  public Timestamp getAcceptedOn() {
-    return acceptedOn;
-  }
-
-  public Status getStatus() {
-    return Status.forCode(status);
-  }
-
-  public Timestamp getReviewedOn() {
-    return reviewedOn;
-  }
-
-  public Account.Id getReviewedBy() {
-    return reviewedBy;
-  }
-
-  public String getReviewComments() {
-    return reviewComments;
-  }
-
-  public void setReviewComments(final String s) {
-    reviewComments = s;
-  }
-
-  public void review(final Status newStatus, final Account.Id by) {
-    status = newStatus.getCode();
-    reviewedBy = by;
-    reviewedOn = new Timestamp(System.currentTimeMillis());
-  }
-}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountDiffPreference.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountDiffPreference.java
index 3b04725..afafd46 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountDiffPreference.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountDiffPreference.java
@@ -62,6 +62,7 @@
     p.setLineLength(100);
     p.setSyntaxHighlighting(true);
     p.setShowWhitespaceErrors(true);
+    p.setShowLineEndings(true);
     p.setIntralineDifference(true);
     p.setShowTabs(true);
     p.setContext(DEFAULT_CONTEXT);
@@ -112,6 +113,9 @@
   @Column(id = 14)
   protected boolean manualReview;
 
+  @Column(id = 15)
+  protected boolean showLineEndings;
+
   protected AccountDiffPreference() {
   }
 
@@ -126,6 +130,7 @@
     this.lineLength = p.lineLength;
     this.syntaxHighlighting = p.syntaxHighlighting;
     this.showWhitespaceErrors = p.showWhitespaceErrors;
+    this.showLineEndings = p.showLineEndings;
     this.intralineDifference = p.intralineDifference;
     this.showTabs = p.showTabs;
     this.skipDeleted = p.skipDeleted;
@@ -180,6 +185,14 @@
     this.showWhitespaceErrors = showWhitespaceErrors;
   }
 
+  public boolean isShowLineEndings() {
+    return showLineEndings;
+  }
+
+  public void setShowLineEndings(boolean showLineEndings) {
+    this.showLineEndings = showLineEndings;
+  }
+
   public boolean isIntralineDifference() {
     return intralineDifference;
   }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGeneralPreferences.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGeneralPreferences.java
index b9ff4fb..6f121ee 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGeneralPreferences.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGeneralPreferences.java
@@ -32,7 +32,7 @@
 
   /** Preferred method to download a change. */
   public static enum DownloadCommand {
-    REPO_DOWNLOAD, PULL, CHECKOUT, CHERRY_PICK, FORMAT_PATCH;
+    REPO_DOWNLOAD, PULL, CHECKOUT, CHERRY_PICK, FORMAT_PATCH, DEFAULT_DOWNLOADS;
   }
 
   public static enum DateFormat {
@@ -118,10 +118,10 @@
    * (show latest patch set on top).
    */
   @Column(id = 10)
-  protected boolean displayPatchSetsInReverseOrder;
+  protected boolean reversePatchSetOrder;
 
   @Column(id = 11)
-  protected boolean displayPersonNameInReviewCategory;
+  protected boolean showUsernameInReviewCategory;
 
   public AccountGeneralPreferences() {
   }
@@ -188,20 +188,20 @@
     copySelfOnEmail = includeSelfOnEmail;
   }
 
-  public boolean isDisplayPatchSetsInReverseOrder() {
-    return displayPatchSetsInReverseOrder;
+  public boolean isReversePatchSetOrder() {
+    return reversePatchSetOrder;
   }
 
-  public void setDisplayPatchSetsInReverseOrder(final boolean displayPatchSetsInReverseOrder) {
-    this.displayPatchSetsInReverseOrder = displayPatchSetsInReverseOrder;
+  public void setReversePatchSetOrder(final boolean reversePatchSetOrder) {
+    this.reversePatchSetOrder = reversePatchSetOrder;
   }
 
-  public boolean isDisplayPersonNameInReviewCategory() {
-    return displayPersonNameInReviewCategory;
+  public boolean isShowUsernameInReviewCategory() {
+    return showUsernameInReviewCategory;
   }
 
-  public void setDisplayPersonNameInReviewCategory(final boolean displayPersonNameInReviewCategory) {
-    this.displayPersonNameInReviewCategory = displayPersonNameInReviewCategory;
+  public void setShowUsernameInReviewCategory(final boolean showUsernameInReviewCategory) {
+    this.showUsernameInReviewCategory = showUsernameInReviewCategory;
   }
 
   public DateFormat getDateFormat() {
@@ -231,8 +231,8 @@
     showSiteHeader = true;
     useFlashClipboard = true;
     copySelfOnEmail = false;
-    displayPatchSetsInReverseOrder = false;
-    displayPersonNameInReviewCategory = false;
+    reversePatchSetOrder = false;
+    showUsernameInReviewCategory = false;
     downloadUrl = null;
     downloadCommand = null;
     dateFormat = null;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroup.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroup.java
index 8e2541a..061ef3e 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroup.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroup.java
@@ -51,7 +51,7 @@
       StringKey<com.google.gwtorm.client.Key<?>> {
     private static final long serialVersionUID = 1L;
 
-    @Column(id = 1, length = 40)
+    @Column(id = 1)
     protected String uuid;
 
     protected UUID() {
@@ -79,30 +79,10 @@
     }
   }
 
-  /** Distinguished name, within organization directory server. */
-  public static class ExternalNameKey extends
-      StringKey<com.google.gwtorm.client.Key<?>> {
-    private static final long serialVersionUID = 1L;
-
-    @Column(id = 1)
-    protected String name;
-
-    protected ExternalNameKey() {
-    }
-
-    public ExternalNameKey(final String n) {
-      name = n;
-    }
-
-    @Override
-    public String get() {
-      return name;
-    }
-
-    @Override
-    protected void set(String newValue) {
-      name = newValue;
-    }
+  /** @return true if the UUID is for a group managed within Gerrit. */
+  public static boolean isInternalGroup(AccountGroup.UUID uuid) {
+    return uuid.get().startsWith("global:")
+        || uuid.get().matches("^[0-9a-f]{40}$");
   }
 
   /** Synthetic key to link to within the database */
@@ -157,20 +137,7 @@
      * who is a member of the owner group. These groups are not treated special
      * in the code.
      */
-    INTERNAL,
-
-    /**
-     * Group defined by external LDAP database.
-     * <p>
-     * A group whose membership is determined by the LDAP directory that we
-     * connect to for user and group information. In UI contexts the membership
-     * of the group is not displayed, as it may be exceedingly large, or might
-     * contain users who have never logged into this server before (and thus
-     * have no matching account record). Adding or removing users from an LDAP
-     * group requires making edits through the LDAP directory, and cannot be
-     * done through our UI.
-     */
-    LDAP;
+    INTERNAL;
   }
 
   /** Common UUID assigned to the "Project Owners" placeholder group. */
@@ -193,14 +160,6 @@
   @Column(id = 2)
   protected Id groupId;
 
-  /**
-   * Identity of the group whose members can manage this group.
-   * <p>
-   * This can be a self-reference to indicate the group's members manage itself.
-   */
-  @Column(id = 3)
-  protected Id ownerGroupId;
-
   /** A textual description of the group's purpose. */
   @Column(id = 4, length = Integer.MAX_VALUE, notNull = false)
   protected String description;
@@ -209,10 +168,6 @@
   @Column(id = 5, length = 8)
   protected String groupType;
 
-  /** Distinguished name in the directory server. */
-  @Column(id = 6, notNull = false)
-  protected ExternalNameKey externalName;
-
   @Column(id = 7)
   protected boolean visibleToAll;
 
@@ -220,6 +175,14 @@
   @Column(id = 9)
   protected UUID groupUUID;
 
+  /**
+   * Identity of the group whose members can manage this group.
+   * <p>
+   * This can be a self-reference to indicate the group's members manage itself.
+   */
+  @Column(id = 10)
+  protected UUID ownerGroupUUID;
+
   protected AccountGroup() {
   }
 
@@ -227,9 +190,9 @@
       final AccountGroup.Id newId, final AccountGroup.UUID uuid) {
     name = newName;
     groupId = newId;
-    ownerGroupId = groupId;
     visibleToAll = false;
     groupUUID = uuid;
+    ownerGroupUUID = groupUUID;
     setType(Type.INTERNAL);
   }
 
@@ -257,12 +220,12 @@
     description = d;
   }
 
-  public AccountGroup.Id getOwnerGroupId() {
-    return ownerGroupId;
+  public AccountGroup.UUID getOwnerGroupUUID() {
+    return ownerGroupUUID;
   }
 
-  public void setOwnerGroupId(final AccountGroup.Id id) {
-    ownerGroupId = id;
+  public void setOwnerGroupUUID(final AccountGroup.UUID uuid) {
+    ownerGroupUUID = uuid;
   }
 
   public Type getType() {
@@ -273,14 +236,6 @@
     groupType = t.name();
   }
 
-  public ExternalNameKey getExternalNameKey() {
-    return externalName;
-  }
-
-  public void setExternalNameKey(final ExternalNameKey k) {
-    externalName = k;
-  }
-
   public void setVisibleToAll(final boolean visibleToAll) {
     this.visibleToAll = visibleToAll;
   }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupAgreement.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupAgreement.java
deleted file mode 100644
index c712b3d..0000000
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupAgreement.java
+++ /dev/null
@@ -1,118 +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.reviewdb.client;
-
-import com.google.gwtorm.client.Column;
-import com.google.gwtorm.client.CompoundKey;
-
-import java.sql.Timestamp;
-
-/**
- * Acceptance of a {@link ContributorAgreement} by an {@link AccountGroup}.
- */
-public final class AccountGroupAgreement implements AbstractAgreement {
-  public static class Key extends CompoundKey<AccountGroup.Id> {
-    private static final long serialVersionUID = 1L;
-
-    @Column(id = 1)
-    protected AccountGroup.Id groupId;
-
-    @Column(id = 2)
-    protected ContributorAgreement.Id claId;
-
-    protected Key() {
-      groupId = new AccountGroup.Id();
-      claId = new ContributorAgreement.Id();
-    }
-
-    public Key(final AccountGroup.Id group, final ContributorAgreement.Id cla) {
-      this.groupId = group;
-      this.claId = cla;
-    }
-
-    @Override
-    public AccountGroup.Id getParentKey() {
-      return groupId;
-    }
-
-    public ContributorAgreement.Id getContributorAgreementId() {
-      return claId;
-    }
-
-    @Override
-    public com.google.gwtorm.client.Key<?>[] members() {
-      return new com.google.gwtorm.client.Key<?>[] {claId};
-    }
-  }
-
-  @Column(id = 1, name = Column.NONE)
-  protected Key key;
-
-  @Column(id = 2)
-  protected Timestamp acceptedOn;
-
-  @Column(id = 3)
-  protected char status;
-
-  @Column(id = 4, notNull = false)
-  protected Account.Id reviewedBy;
-
-  @Column(id = 5, notNull = false)
-  protected Timestamp reviewedOn;
-
-  @Column(id = 6, notNull = false, length = Integer.MAX_VALUE)
-  protected String reviewComments;
-
-  protected AccountGroupAgreement() {
-  }
-
-  public AccountGroupAgreement(final AccountGroupAgreement.Key k) {
-    key = k;
-    acceptedOn = new Timestamp(System.currentTimeMillis());
-    status = Status.NEW.getCode();
-  }
-
-  public AccountGroupAgreement.Key getKey() {
-    return key;
-  }
-
-  public ContributorAgreement.Id getAgreementId() {
-    return key.claId;
-  }
-
-  public Timestamp getAcceptedOn() {
-    return acceptedOn;
-  }
-
-  public Status getStatus() {
-    return Status.forCode(status);
-  }
-
-  public Timestamp getReviewedOn() {
-    return reviewedOn;
-  }
-
-  public Account.Id getReviewedBy() {
-    return reviewedBy;
-  }
-
-  public String getReviewComments() {
-    return reviewComments;
-  }
-
-  public void setReviewComments(final String s) {
-    reviewComments = s;
-  }
-}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupMemberAudit.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupMemberAudit.java
index 8ba8912..523134b 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupMemberAudit.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupMemberAudit.java
@@ -80,9 +80,14 @@
 
   public AccountGroupMemberAudit(final AccountGroupMember m,
       final Account.Id adder) {
+    this(m, adder, now());
+  }
+
+  public AccountGroupMemberAudit(final AccountGroupMember m,
+      final Account.Id adder, Timestamp addedOn) {
     final Account.Id who = m.getAccountId();
     final AccountGroup.Id group = m.getAccountGroupId();
-    key = new AccountGroupMemberAudit.Key(who, group, now());
+    key = new AccountGroupMemberAudit.Key(who, group, addedOn);
     addedBy = adder;
   }
 
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountProjectWatch.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountProjectWatch.java
index 93e6fb3..e592101 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountProjectWatch.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountProjectWatch.java
@@ -22,7 +22,7 @@
 public final class AccountProjectWatch {
 
   public enum NotifyType {
-    NEW_CHANGES, ALL_COMMENTS, SUBMITTED_CHANGES
+    NEW_CHANGES, ALL_COMMENTS, SUBMITTED_CHANGES, ALL
   }
 
   public static final String FILTER_ALL = "*";
@@ -159,6 +159,12 @@
       case SUBMITTED_CHANGES:
         notifySubmittedChanges = v;
         break;
+
+      case ALL:
+        notifyNewChanges = v;
+        notifyAllComments = v;
+        notifySubmittedChanges = v;
+        break;
     }
   }
 }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/ApprovalCategory.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/ApprovalCategory.java
index bb25265..7d61975 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/ApprovalCategory.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/ApprovalCategory.java
@@ -105,7 +105,8 @@
         char c = name.charAt(i);
         if (('0' <= c && c <= '9') //
             || ('a' <= c && c <= 'z') //
-            || ('A' <= c && c <= 'Z')) {
+            || ('A' <= c && c <= 'Z') //
+            || (c == '-')) {
           r.append(c);
         } else if (c == ' ') {
           r.append('-');
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AuthType.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AuthType.java
index 962426b..b615fc5 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AuthType.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AuthType.java
@@ -18,6 +18,9 @@
   /** Login relies upon the OpenID standard: {@link "http://openid.net/"} */
   OPENID,
 
+  /** Login relies upon the OpenID standard: {@link "http://openid.net/"} in Single Sign On mode */
+  OPENID_SSO,
+
   /**
    * Login relies upon the container/web server security.
    * <p>
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
index 61e6a97..bfbacc0 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
@@ -491,6 +491,10 @@
     --nbrPatchSets;
   }
 
+  public void updateNumberOfPatchSets(int max) {
+    nbrPatchSets = Math.max(nbrPatchSets, max);
+  }
+
   public PatchSet.Id currPatchSetId() {
     return new PatchSet.Id(changeId, nbrPatchSets);
   }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/ContributorAgreement.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/ContributorAgreement.java
deleted file mode 100644
index 12e7459..0000000
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/ContributorAgreement.java
+++ /dev/null
@@ -1,140 +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.reviewdb.client;
-
-import com.google.gwtorm.client.Column;
-import com.google.gwtorm.client.IntKey;
-
-/**
- * An agreement {@link Account} must acknowledge to contribute changes.
- *
- * @see AccountAgreement
- */
-public final class ContributorAgreement {
-  public static class Id extends IntKey<com.google.gwtorm.client.Key<?>> {
-    private static final long serialVersionUID = 1L;
-
-    @Column(id = 1, name = "cla_id")
-    protected int id;
-
-    protected Id() {
-    }
-
-    public Id(final int id) {
-      this.id = id;
-    }
-
-    @Override
-    public int get() {
-      return id;
-    }
-
-    @Override
-    protected void set(int newValue) {
-      id = newValue;
-    }
-  }
-
-  @Column(id = 1)
-  protected Id id;
-
-  /** Is this an active agreement contributors can use. */
-  @Column(id = 2)
-  protected boolean active;
-
-  /** Does this agreement require the {@link Account} to have contact details? */
-  @Column(id = 3)
-  protected boolean requireContactInformation;
-
-  /** Does this agreement automatically verify new accounts? */
-  @Column(id = 4)
-  protected boolean autoVerify;
-
-  /** A short name for the agreement. */
-  @Column(id = 5, length = 40)
-  protected String shortName;
-
-  /** A short one-line description text to appear next to the name. */
-  @Column(id = 6, notNull = false)
-  protected String shortDescription;
-
-  /** Web address of the agreement documentation. */
-  @Column(id = 7)
-  protected String agreementUrl;
-
-  protected ContributorAgreement() {
-  }
-
-  /**
-   * Create a new agreement.
-   *
-   * @param newId unique id, see
-   *        {@link com.google.gerrit.reviewdb.server.ReviewDb#nextAccountId()}.
-   * @param name a short title/name for the agreement.
-   */
-  public ContributorAgreement(final ContributorAgreement.Id newId,
-      final String name) {
-    id = newId;
-    shortName = name;
-  }
-
-  public ContributorAgreement.Id getId() {
-    return id;
-  }
-
-  public boolean isActive() {
-    return active;
-  }
-
-  public void setActive(final boolean a) {
-    active = a;
-  }
-
-  public boolean isAutoVerify() {
-    return autoVerify;
-  }
-
-  public void setAutoVerify(final boolean g) {
-    autoVerify = g;
-  }
-
-  public boolean isRequireContactInformation() {
-    return requireContactInformation;
-  }
-
-  public void setRequireContactInformation(final boolean r) {
-    requireContactInformation = r;
-  }
-
-  public String getShortName() {
-    return shortName;
-  }
-
-  public String getShortDescription() {
-    return shortDescription;
-  }
-
-  public void setShortDescription(final String d) {
-    shortDescription = d;
-  }
-
-  public String getAgreementUrl() {
-    return agreementUrl;
-  }
-
-  public void setAgreementUrl(final String h) {
-    agreementUrl = h;
-  }
-}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Patch.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Patch.java
index 0d0ea88..00e282b 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Patch.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Patch.java
@@ -82,7 +82,10 @@
     RENAMED('R'),
 
     /** Path was copied from {@link Patch#getSourceFileName()}. */
-    COPIED('C');
+    COPIED('C'),
+
+    /** Sufficient amount of content changed to claim the file was written. */
+    REWRITE('W');
 
     private final char code;
 
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/TrackingId.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/TrackingId.java
index e73dd73..c664285 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/TrackingId.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/TrackingId.java
@@ -20,7 +20,7 @@
 
 /** External tracking id associated with a {@link Change} */
 public final class TrackingId {
-  public static final int TRACKING_ID_MAX_CHAR = 20;
+  public static final int TRACKING_ID_MAX_CHAR = 32;
   public static final int TRACKING_SYSTEM_MAX_CHAR = 10;
 
   /** External tracking id */
@@ -80,20 +80,20 @@
     protected Change.Id changeId;
 
     @Column(id = 2)
-    protected Id trackingId;
+    protected Id trackingKey;
 
     @Column(id = 3)
     protected System trackingSystem;
 
     protected Key() {
       changeId = new Change.Id();
-      trackingId = new Id();
+      trackingKey = new Id();
       trackingSystem = new System();
     }
 
     protected Key(final Change.Id ch, final Id id, final System s) {
       changeId = ch;
-      trackingId = id;
+      trackingKey = id;
       trackingSystem = s;
     }
 
@@ -103,7 +103,7 @@
     }
 
     public TrackingId.Id getTrackingId() {
-      return trackingId;
+      return trackingKey;
     }
 
     public TrackingId.System getTrackingSystem() {
@@ -112,7 +112,7 @@
 
     @Override
     public com.google.gwtorm.client.Key<?>[] members() {
-      return new com.google.gwtorm.client.Key<?>[] {trackingId, trackingSystem};
+      return new com.google.gwtorm.client.Key<?>[] {trackingKey, trackingSystem};
     }
   }
 
@@ -140,7 +140,7 @@
   }
 
   public String getTrackingId() {
-    return key.trackingId.get();
+    return key.trackingKey.get();
   }
 
   public String getSystem() {
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountAccess.java
index 2b23b7a..29a426b 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountAccess.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.reviewdb.server;
 
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Account.Id;
 import com.google.gwtorm.server.Access;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountAgreementAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountAgreementAccess.java
deleted file mode 100644
index 86ea372..0000000
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountAgreementAccess.java
+++ /dev/null
@@ -1,34 +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.reviewdb.server;
-
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountAgreement;
-import com.google.gerrit.reviewdb.client.Account.Id;
-import com.google.gerrit.reviewdb.client.AccountAgreement.Key;
-import com.google.gwtorm.server.Access;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.PrimaryKey;
-import com.google.gwtorm.server.Query;
-import com.google.gwtorm.server.ResultSet;
-
-public interface AccountAgreementAccess extends
-    Access<AccountAgreement, AccountAgreement.Key> {
-  @PrimaryKey("key")
-  AccountAgreement get(AccountAgreement.Key key) throws OrmException;
-
-  @Query("WHERE key.accountId = ? ORDER BY acceptedOn")
-  ResultSet<AccountAgreement> byAccount(Account.Id id) throws OrmException;
-}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountDiffPreferenceAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountDiffPreferenceAccess.java
index 8574758..fffcf9e 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountDiffPreferenceAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountDiffPreferenceAccess.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountDiffPreference;
-import com.google.gerrit.reviewdb.client.Account.Id;
 import com.google.gwtorm.server.Access;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountExternalIdAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountExternalIdAccess.java
index b263552..bf6c0ef 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountExternalIdAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountExternalIdAccess.java
@@ -16,8 +16,6 @@
 
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
-import com.google.gerrit.reviewdb.client.Account.Id;
-import com.google.gerrit.reviewdb.client.AccountExternalId.Key;
 import com.google.gwtorm.server.Access;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.PrimaryKey;
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-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupAgreementAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupAgreementAccess.java
deleted file mode 100644
index ecab70d..0000000
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupAgreementAccess.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.reviewdb.server;
-
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupAgreement;
-import com.google.gerrit.reviewdb.client.AccountGroup.Id;
-import com.google.gerrit.reviewdb.client.AccountGroupAgreement.Key;
-import com.google.gwtorm.server.Access;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.PrimaryKey;
-import com.google.gwtorm.server.Query;
-import com.google.gwtorm.server.ResultSet;
-
-public interface AccountGroupAgreementAccess extends
-    Access<AccountGroupAgreement, AccountGroupAgreement.Key> {
-  @PrimaryKey("key")
-  AccountGroupAgreement get(AccountGroupAgreement.Key key) throws OrmException;
-
-  @Query("WHERE key.groupId = ? ORDER BY acceptedOn")
-  ResultSet<AccountGroupAgreement> byGroup(AccountGroup.Id id)
-      throws OrmException;
-}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeAccess.java
index c8c7b40..3ee4ba0 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeAccess.java
@@ -16,8 +16,6 @@
 
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountGroupInclude;
-import com.google.gerrit.reviewdb.client.AccountGroup.Id;
-import com.google.gerrit.reviewdb.client.AccountGroupInclude.Key;
 import com.google.gwtorm.server.Access;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeAuditAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeAuditAccess.java
index 3cf24f3..b3f4f88 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeAuditAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeAuditAccess.java
@@ -16,8 +16,6 @@
 
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountGroupIncludeAudit;
-import com.google.gerrit.reviewdb.client.AccountGroup.Id;
-import com.google.gerrit.reviewdb.client.AccountGroupIncludeAudit.Key;
 import com.google.gwtorm.server.Access;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupMemberAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupMemberAccess.java
index 53ecaae..e070d69 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupMemberAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupMemberAccess.java
@@ -17,8 +17,6 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountGroupMember;
-import com.google.gerrit.reviewdb.client.AccountGroup.Id;
-import com.google.gerrit.reviewdb.client.AccountGroupMember.Key;
 import com.google.gwtorm.server.Access;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupMemberAuditAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupMemberAuditAccess.java
index f26996d..48c4e2d 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupMemberAuditAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupMemberAuditAccess.java
@@ -17,8 +17,6 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
-import com.google.gerrit.reviewdb.client.AccountGroup.Id;
-import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit.Key;
 import com.google.gwtorm.server.Access;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupNameAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupNameAccess.java
index d0bc419..30e685c 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupNameAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupNameAccess.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountGroupName;
-import com.google.gerrit.reviewdb.client.AccountGroup.NameKey;
 import com.google.gwtorm.server.Access;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountPatchReviewAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountPatchReviewAccess.java
index ef8133b..80b2dc4 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountPatchReviewAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountPatchReviewAccess.java
@@ -17,8 +17,6 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountPatchReview;
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Account.Id;
-import com.google.gerrit.reviewdb.client.AccountPatchReview.Key;
 import com.google.gwtorm.server.Access;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountProjectWatchAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountProjectWatchAccess.java
index 046d5a5..c073468 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountProjectWatchAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountProjectWatchAccess.java
@@ -17,9 +17,6 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountProjectWatch;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.Account.Id;
-import com.google.gerrit.reviewdb.client.AccountProjectWatch.Key;
-import com.google.gerrit.reviewdb.client.Project.NameKey;
 import com.google.gwtorm.server.Access;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountSshKeyAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountSshKeyAccess.java
index d756a44..b31b5b6 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountSshKeyAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountSshKeyAccess.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountSshKey;
-import com.google.gerrit.reviewdb.client.Account.Id;
 import com.google.gwtorm.server.Access;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ApprovalCategoryAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ApprovalCategoryAccess.java
index 31e6c62..db9886e 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ApprovalCategoryAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ApprovalCategoryAccess.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.reviewdb.server;
 
 import com.google.gerrit.reviewdb.client.ApprovalCategory;
-import com.google.gerrit.reviewdb.client.ApprovalCategory.Id;
 import com.google.gwtorm.server.Access;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ApprovalCategoryValueAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ApprovalCategoryValueAccess.java
index acdda5e8..0bc9981 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ApprovalCategoryValueAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ApprovalCategoryValueAccess.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.reviewdb.client.ApprovalCategory;
 import com.google.gerrit.reviewdb.client.ApprovalCategoryValue;
-import com.google.gerrit.reviewdb.client.ApprovalCategory.Id;
 import com.google.gwtorm.server.Access;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ChangeAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ChangeAccess.java
index 4660291..66df78a 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ChangeAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ChangeAccess.java
@@ -18,9 +18,6 @@
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.Change.Id;
-import com.google.gerrit.reviewdb.client.Change.Key;
-import com.google.gerrit.reviewdb.client.Project.NameKey;
 import com.google.gwtorm.server.Access;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ChangeMessageAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ChangeMessageAccess.java
index 6db2675..0126a31 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ChangeMessageAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ChangeMessageAccess.java
@@ -17,8 +17,6 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Change.Id;
-import com.google.gerrit.reviewdb.client.ChangeMessage.Key;
 import com.google.gwtorm.server.Access;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ContributorAgreementAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ContributorAgreementAccess.java
deleted file mode 100644
index 3c7f47a..0000000
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ContributorAgreementAccess.java
+++ /dev/null
@@ -1,33 +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.reviewdb.server;
-
-import com.google.gerrit.reviewdb.client.ContributorAgreement;
-import com.google.gerrit.reviewdb.client.ContributorAgreement.Id;
-import com.google.gwtorm.server.Access;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.PrimaryKey;
-import com.google.gwtorm.server.Query;
-import com.google.gwtorm.server.ResultSet;
-
-/** Access interface for {@link ContributorAgreement}. */
-public interface ContributorAgreementAccess extends
-    Access<ContributorAgreement, ContributorAgreement.Id> {
-  @PrimaryKey("id")
-  ContributorAgreement get(ContributorAgreement.Id key) throws OrmException;
-
-  @Query("WHERE active = true ORDER BY shortName")
-  ResultSet<ContributorAgreement> active() throws OrmException;
-}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchLineCommentAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchLineCommentAccess.java
index 3be8563..a5842de 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchLineCommentAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchLineCommentAccess.java
@@ -18,8 +18,6 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Change.Id;
-import com.google.gerrit.reviewdb.client.PatchLineComment.Key;
 import com.google.gwtorm.server.Access;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAccess.java
index c04fa06..7e0b90c 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAccess.java
@@ -17,7 +17,6 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.RevId;
-import com.google.gerrit.reviewdb.client.Change.Id;
 import com.google.gwtorm.server.Access;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAncestorAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAncestorAccess.java
index e163bc4..f2b1cb7 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAncestorAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAncestorAccess.java
@@ -17,7 +17,6 @@
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetAncestor;
 import com.google.gerrit.reviewdb.client.RevId;
-import com.google.gerrit.reviewdb.client.PatchSetAncestor.Id;
 import com.google.gwtorm.server.Access;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetApprovalAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetApprovalAccess.java
index b30c5bfa..dae8e6d 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetApprovalAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetApprovalAccess.java
@@ -18,8 +18,6 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.reviewdb.client.Change.Id;
-import com.google.gerrit.reviewdb.client.PatchSetApproval.Key;
 import com.google.gwtorm.server.Access;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDb.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDb.java
index 616a656..149626b 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDb.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDb.java
@@ -49,9 +49,6 @@
   @Relation(id = 4)
   ApprovalCategoryValueAccess approvalCategoryValues();
 
-  @Relation(id = 5)
-  ContributorAgreementAccess contributorAgreements();
-
   @Relation(id = 6)
   AccountAccess accounts();
 
@@ -61,9 +58,6 @@
   @Relation(id = 8)
   AccountSshKeyAccess accountSshKeys();
 
-  @Relation(id = 9)
-  AccountAgreementAccess accountAgreements();
-
   @Relation(id = 10)
   AccountGroupAccess accountGroups();
 
@@ -82,9 +76,6 @@
   @Relation(id = 15)
   AccountGroupIncludeAuditAccess accountGroupIncludesAudit();
 
-  @Relation(id = 16)
-  AccountGroupAgreementAccess accountGroupAgreements();
-
   @Relation(id = 17)
   AccountDiffPreferenceAccess accountDiffPreferences();
 
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SchemaVersionAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SchemaVersionAccess.java
index 07f255c..470f8c6 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SchemaVersionAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SchemaVersionAccess.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.reviewdb.server;
 
 import com.google.gerrit.reviewdb.client.CurrentSchemaVersion;
-import com.google.gerrit.reviewdb.client.CurrentSchemaVersion.Key;
 import com.google.gwtorm.server.Access;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/StarredChangeAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/StarredChangeAccess.java
index 88e703a..4010dae 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/StarredChangeAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/StarredChangeAccess.java
@@ -17,8 +17,6 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.StarredChange;
-import com.google.gerrit.reviewdb.client.Change.Id;
-import com.google.gerrit.reviewdb.client.StarredChange.Key;
 import com.google.gwtorm.server.Access;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SubmoduleSubscriptionAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SubmoduleSubscriptionAccess.java
index e1aa907..0090df8 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SubmoduleSubscriptionAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SubmoduleSubscriptionAccess.java
@@ -16,8 +16,6 @@
 
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.SubmoduleSubscription;
-import com.google.gerrit.reviewdb.client.Branch.NameKey;
-import com.google.gerrit.reviewdb.client.SubmoduleSubscription.Key;
 import com.google.gwtorm.server.Access;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SystemConfigAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SystemConfigAccess.java
index 3bb07cb..4b2ed74 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SystemConfigAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SystemConfigAccess.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.reviewdb.server;
 
 import com.google.gerrit.reviewdb.client.SystemConfig;
-import com.google.gerrit.reviewdb.client.SystemConfig.Key;
 import com.google.gwtorm.server.Access;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.PrimaryKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/TrackingIdAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/TrackingIdAccess.java
index 51babb3..d8b2cee 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/TrackingIdAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/TrackingIdAccess.java
@@ -16,8 +16,6 @@
 
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.TrackingId;
-import com.google.gerrit.reviewdb.client.Change.Id;
-import com.google.gerrit.reviewdb.client.TrackingId.Key;
 import com.google.gwtorm.server.Access;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.PrimaryKey;
@@ -31,7 +29,7 @@
   @Query("WHERE key.changeId = ?")
   ResultSet<TrackingId> byChange(Change.Id change) throws OrmException;
 
-  @Query("WHERE key.trackingId = ?")
+  @Query("WHERE key.trackingKey = ?")
   ResultSet<TrackingId> byTrackingId(TrackingId.Id trackingId)
       throws OrmException;
 }
diff --git a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql
index 5d0e15c..9d453fc 100644
--- a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql
+++ b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql
@@ -16,11 +16,6 @@
 
 
 -- *********************************************************************
--- AccountAgreementAccess
---    @PrimaryKey covers: byAccount
-
-
--- *********************************************************************
 -- AccountExternalIdAccess
 --    covers:             byAccount
 CREATE INDEX account_external_ids_byAccount
@@ -32,12 +27,6 @@
 
 
 -- *********************************************************************
--- AccountGroupAccess
-CREATE INDEX account_groups_ownedByGroup
-ON account_groups (owner_group_id);
-
-
--- *********************************************************************
 -- AccountGroupMemberAccess
 --    @PrimaryKey covers: byAccount
 CREATE INDEX account_group_members_byGroup
@@ -133,13 +122,9 @@
 -- ChangeMessageAccess
 --    @PrimaryKey covers: byChange
 
-
--- *********************************************************************
--- ContributorAgreementAccess
---    covers:             active
-CREATE INDEX contributor_agreements_active
-ON contributor_agreements (active, short_name);
-
+--    covers:             byPatchSet
+CREATE INDEX change_messages_byPatchset
+ON change_messages (patchset_change_id, patchset_patch_set_id);
 
 -- *********************************************************************
 -- PatchLineCommentAccess
@@ -164,8 +149,8 @@
 -- *********************************************************************
 -- TrackingIdAccess
 --
-CREATE INDEX tracking_ids_byTrkId
-ON tracking_ids (tracking_id);
+CREATE INDEX tracking_ids_byTrkKey
+ON tracking_ids (tracking_key);
 
 
 -- *********************************************************************
diff --git a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
index 97ad126..2c91db4 100644
--- a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
+++ b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
@@ -88,11 +88,6 @@
 
 
 -- *********************************************************************
--- AccountAgreementAccess
---    @PrimaryKey covers: byAccount
-
-
--- *********************************************************************
 -- AccountExternalIdAccess
 --    covers:             byAccount
 CREATE INDEX account_external_ids_byAccount
@@ -104,12 +99,6 @@
 
 
 -- *********************************************************************
--- AccountGroupAccess
-CREATE INDEX account_groups_ownedByGroup
-ON account_groups (owner_group_id);
-
-
--- *********************************************************************
 -- AccountGroupMemberAccess
 --    @PrimaryKey covers: byAccount
 CREATE INDEX account_group_members_byGroup
@@ -214,13 +203,9 @@
 -- ChangeMessageAccess
 --    @PrimaryKey covers: byChange
 
-
--- *********************************************************************
--- ContributorAgreementAccess
---    covers:             active
-CREATE INDEX contributor_agreements_active
-ON contributor_agreements (active, short_name);
-
+--    covers:             byPatchSet
+CREATE INDEX change_messages_byPatchset
+ON change_messages (patchset_change_id, patchset_patch_set_id);
 
 -- *********************************************************************
 -- PatchLineCommentAccess
@@ -247,8 +232,8 @@
 -- *********************************************************************
 -- TrackingIdAccess
 --
-CREATE INDEX tracking_ids_byTrkId
-ON tracking_ids (tracking_id);
+CREATE INDEX tracking_ids_byTrkKey
+ON tracking_ids (tracking_key);
 
 
 -- *********************************************************************
diff --git a/gerrit-server/.gitignore b/gerrit-server/.gitignore
index 194bedc..9324efe 100644
--- a/gerrit-server/.gitignore
+++ b/gerrit-server/.gitignore
@@ -2,4 +2,5 @@
 /.classpath
 /.project
 /.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-server.iml
\ No newline at end of file
diff --git a/gerrit-server/.settings/org.eclipse.core.resources.prefs b/gerrit-server/.settings/org.eclipse.core.resources.prefs
index 7d5f965..29abf99 100644
--- a/gerrit-server/.settings/org.eclipse.core.resources.prefs
+++ b/gerrit-server/.settings/org.eclipse.core.resources.prefs
@@ -1,4 +1,3 @@
-#Thu Jul 28 11:02:36 PDT 2011
 eclipse.preferences.version=1
 encoding//src/main/java=UTF-8
 encoding//src/main/resources=UTF-8
diff --git a/gerrit-server/pom.xml b/gerrit-server/pom.xml
index 58e43cf..af18173 100644
--- a/gerrit-server/pom.xml
+++ b/gerrit-server/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.4-SNAPSHOT</version>
+    <version>2.5-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-server</artifactId>
@@ -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>
@@ -123,6 +128,12 @@
 
     <dependency>
       <groupId>com.google.gerrit</groupId>
+      <artifactId>gerrit-extension-api</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>com.google.gerrit</groupId>
       <artifactId>gerrit-util-cli</artifactId>
       <version>${project.version}</version>
     </dependency>
@@ -164,6 +175,11 @@
       <groupId>com.googlecode.prolog-cafe</groupId>
       <artifactId>PrologCafe</artifactId>
     </dependency>
+
+    <dependency>
+      <groupId>org.pegdown</groupId>
+      <artifactId>pegdown</artifactId>
+    </dependency>
   </dependencies>
 
   <build>
diff --git a/gerrit-server/src/main/java/com/google/gerrit/audit/AuditEvent.java b/gerrit-server/src/main/java/com/google/gerrit/audit/AuditEvent.java
new file mode 100644
index 0000000..364df80
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/audit/AuditEvent.java
@@ -0,0 +1,172 @@
+// 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.audit;
+
+import com.google.common.base.Preconditions;
+import com.google.gerrit.server.CurrentUser;
+
+import java.util.Collections;
+import java.util.List;
+
+public class AuditEvent {
+
+  public static final String UNKNOWN_SESSION_ID = "000000000000000000000000000";
+  private static final Object UNKNOWN_RESULT = "N/A";
+
+  public final String sessionId;
+  public final CurrentUser who;
+  public final long when;
+  public final String what;
+  public final List<?> params;
+  public final Object result;
+  public final long timeAtStart;
+  public final long elapsed;
+  public final UUID uuid;
+
+  public static class UUID {
+
+    protected final String uuid;
+
+    protected UUID() {
+      uuid = String.format("audit:%s", java.util.UUID.randomUUID().toString());
+    }
+
+    public UUID(final String n) {
+      uuid = n;
+    }
+
+    public String get() {
+      return uuid;
+    }
+
+    @Override
+    public int hashCode() {
+      return uuid.hashCode();
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      if (this == obj) {
+        return true;
+      }
+      if (obj == null) {
+        return false;
+      }
+      if (!(obj instanceof UUID)) {
+        return false;
+      }
+
+      return uuid.equals(((UUID) obj).uuid);
+    }
+  }
+
+  /**
+   * Creates a new audit event.
+   *
+   * @param sessionId session id the event belongs to
+   * @param who principal that has generated the event
+   * @param what object of the event
+   * @param params parameters of the event
+   */
+  public AuditEvent(String sessionId, CurrentUser who, String what, List<?> params) {
+    this(sessionId, who, what, System.currentTimeMillis(), params,
+        UNKNOWN_RESULT);
+  }
+
+  /**
+   * Creates a new audit event with results
+   *
+   * @param sessionId session id the event belongs to
+   * @param who principal that has generated the event
+   * @param what object of the event
+   * @param when time-stamp of when the event started
+   * @param params parameters of the event
+   * @param result result of the event
+   */
+  public AuditEvent(String sessionId, CurrentUser who, String what, long when,
+      List<?> params, Object result) {
+    Preconditions.checkNotNull(what, "what is a mandatory not null param !");
+
+    this.sessionId = getValueWithDefault(sessionId, UNKNOWN_SESSION_ID);
+    this.who = who;
+    this.what = what;
+    this.when = when;
+    this.timeAtStart = this.when;
+    this.params = getValueWithDefault(params, Collections.emptyList());
+    this.uuid = new UUID();
+    this.result = result;
+    this.elapsed = System.currentTimeMillis() - timeAtStart;
+  }
+
+  private <T> T getValueWithDefault(T value, T defaultValueIfNull) {
+    if (value == null) {
+      return defaultValueIfNull;
+    } else {
+      return value;
+    }
+  }
+
+  @Override
+  public int hashCode() {
+    return uuid.hashCode();
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (this == obj) return true;
+    if (obj == null) return false;
+    if (getClass() != obj.getClass()) return false;
+
+    AuditEvent other = (AuditEvent) obj;
+    return this.uuid.equals(other.uuid);
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder sb = new StringBuilder();
+    sb.append(uuid.toString());
+    sb.append("|");
+    sb.append(sessionId);
+    sb.append('|');
+    sb.append(who);
+    sb.append('|');
+    sb.append(when);
+    sb.append('|');
+    sb.append(what);
+    sb.append('|');
+    sb.append(elapsed);
+    sb.append('|');
+    if (params != null) {
+      sb.append('[');
+      for (int i = 0; i < params.size(); i++) {
+        if (i > 0) sb.append(',');
+
+        Object param = params.get(i);
+        if (param == null) {
+          sb.append("null");
+        } else {
+          sb.append(param);
+        }
+      }
+      sb.append(']');
+    }
+    sb.append('|');
+    if (result != null) {
+      sb.append(result);
+    }
+
+    return sb.toString();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CachePool.java b/gerrit-server/src/main/java/com/google/gerrit/audit/AuditListener.java
similarity index 68%
copy from gerrit-server/src/main/java/com/google/gerrit/server/cache/CachePool.java
copy to gerrit-server/src/main/java/com/google/gerrit/audit/AuditListener.java
index 3370b08..0aab248 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CachePool.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/audit/AuditListener.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 com.google.gerrit.audit;
 
-public interface CachePool {
-  public <K, V> ProxyCache<K, V> register(CacheProvider<K, V> provider);
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+
+@ExtensionPoint
+public interface AuditListener {
+
+  void onAuditableAction(AuditEvent action);
+
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/audit/AuditModule.java b/gerrit-server/src/main/java/com/google/gerrit/audit/AuditModule.java
new file mode 100644
index 0000000..dc870ac
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/audit/AuditModule.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.audit;
+
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.inject.AbstractModule;
+
+public class AuditModule extends AbstractModule {
+
+  @Override
+  protected void configure() {
+    DynamicSet.setOf(binder(), AuditListener.class);
+    bind(AuditService.class);
+  }
+
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/audit/AuditService.java b/gerrit-server/src/main/java/com/google/gerrit/audit/AuditService.java
new file mode 100644
index 0000000..a992aa1
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/audit/AuditService.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.audit;
+
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+@Singleton
+public class AuditService {
+  private final DynamicSet<AuditListener> auditListeners;
+
+  @Inject
+  public AuditService(DynamicSet<AuditListener> auditListeners) {
+    this.auditListeners = auditListeners;
+  }
+
+  public void dispatch(AuditEvent action) {
+    for (AuditListener auditListener : auditListeners) {
+      auditListener.onAuditableAction(action);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java
index 31837b0..37be293 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java
@@ -16,12 +16,12 @@
 
 import com.google.gerrit.common.data.ApprovalType;
 import com.google.gerrit.common.data.ApprovalTypes;
+import com.google.gerrit.common.data.ContributorAgreement;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.ApprovalCategory;
 import com.google.gerrit.reviewdb.client.ApprovalCategoryValue;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.ContributorAgreement;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -35,8 +35,9 @@
 import com.google.gerrit.server.events.ChangeAbandonedEvent;
 import com.google.gerrit.server.events.ChangeEvent;
 import com.google.gerrit.server.events.ChangeMergedEvent;
-import com.google.gerrit.server.events.ChangeRestoreEvent;
+import com.google.gerrit.server.events.ChangeRestoredEvent;
 import com.google.gerrit.server.events.CommentAddedEvent;
+import com.google.gerrit.server.events.DraftPublishedEvent;
 import com.google.gerrit.server.events.EventFactory;
 import com.google.gerrit.server.events.PatchSetCreatedEvent;
 import com.google.gerrit.server.events.RefUpdatedEvent;
@@ -50,7 +51,6 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.RefUpdate;
@@ -99,6 +99,9 @@
     /** Filename of the new patchset hook. */
     private final File patchsetCreatedHook;
 
+    /** Filename of the draft published hook. */
+    private final File draftPublishedHook;
+
     /** Filename of the new comments hook. */
     private final File commentAddedHook;
 
@@ -108,7 +111,7 @@
     /** Filename of the change abandoned hook. */
     private final File changeAbandonedHook;
 
-    /** Filename of the change abandoned hook. */
+    /** Filename of the change restored hook. */
     private final File changeRestoredHook;
 
     /** Filename of the ref updated hook. */
@@ -133,6 +136,8 @@
 
     private final EventFactory eventFactory;
 
+    private final SitePaths sitePaths;
+
     /**
      * Create a new ChangeHookRunner.
      *
@@ -149,7 +154,7 @@
       final @AnonymousCowardName String anonymousCowardName,
       final SitePaths sitePath, final ProjectCache projectCache,
       final AccountCache accountCache, final ApprovalTypes approvalTypes,
-      final EventFactory eventFactory) {
+      final EventFactory eventFactory, final SitePaths sitePaths) {
         this.anonymousCowardName = anonymousCowardName;
         this.repoManager = repoManager;
         this.hookQueue = queue.createQueue(1, "hook");
@@ -157,10 +162,12 @@
         this.accountCache = accountCache;
         this.approvalTypes = approvalTypes;
         this.eventFactory = eventFactory;
+        this.sitePaths = sitePath;
 
         final File hooksPath = sitePath.resolve(getValue(config, "hooks", "path", sitePath.hooks_dir.getAbsolutePath()));
 
         patchsetCreatedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "patchsetCreatedHook", "patchset-created")).getPath());
+        draftPublishedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "draftPublishedHook", "draft-published")).getPath());
         commentAddedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "commentAddedHook", "comment-added")).getPath());
         changeMergedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "changeMergedHook", "change-merged")).getPath());
         changeAbandonedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "changeAbandonedHook", "change-abandoned")).getPath());
@@ -192,16 +199,6 @@
     }
 
     /**
-     * Get the Repository for the given change, or null on error.
-     *
-     * @param change Change to get repo for,
-     * @return Repository or null.
-     */
-    private Repository openRepository(final Change change) {
-        return openRepository(change.getProject());
-    }
-
-    /**
      * Get the Repository for the given project name, or null on error.
      *
      * @param name Project to get repo for,
@@ -210,7 +207,7 @@
     private Repository openRepository(final Project.NameKey name) {
         try {
             return repoManager.openRepository(name);
-        } catch (RepositoryNotFoundException err) {
+        } catch (IOException err) {
             log.warn("Cannot open repository " + name.get(), err);
             return null;
         }
@@ -235,9 +232,11 @@
 
         final List<String> args = new ArrayList<String>();
         addArg(args, "--change", event.change.id);
+        addArg(args, "--is-draft", patchSet.isDraft() ? "true" : "false");
         addArg(args, "--change-url", event.change.url);
         addArg(args, "--project", event.change.project);
         addArg(args, "--branch", event.change.branch);
+        addArg(args, "--topic", event.change.topic);
         addArg(args, "--uploader", getDisplayName(uploader.getAccount()));
         addArg(args, "--commit", event.patchSet.revision);
         addArg(args, "--patchset", event.patchSet.number);
@@ -245,6 +244,29 @@
         runHook(change.getProject(), patchsetCreatedHook, args);
     }
 
+    public void doDraftPublishedHook(final Change change, final PatchSet patchSet,
+          final ReviewDb db) throws OrmException {
+        final DraftPublishedEvent event = new DraftPublishedEvent();
+        final AccountState uploader = accountCache.get(patchSet.getUploader());
+
+        event.change = eventFactory.asChangeAttribute(change);
+        event.patchSet = eventFactory.asPatchSetAttribute(patchSet);
+        event.uploader = eventFactory.asAccountAttribute(uploader.getAccount());
+        fireEvent(change, event, db);
+
+        final List<String> args = new ArrayList<String>();
+        addArg(args, "--change", event.change.id);
+        addArg(args, "--change-url", event.change.url);
+        addArg(args, "--project", event.change.project);
+        addArg(args, "--branch", event.change.branch);
+        addArg(args, "--topic", event.change.topic);
+        addArg(args, "--uploader", getDisplayName(uploader.getAccount()));
+        addArg(args, "--commit", event.patchSet.revision);
+        addArg(args, "--patchset", event.patchSet.number);
+
+        runHook(change.getProject(), draftPublishedHook, args);
+    }
+
     public void doCommentAddedHook(final Change change, final Account account,
           final PatchSet patchSet, final String comment, final Map<ApprovalCategory.Id,
           ApprovalCategoryValue.Id> approvals, final ReviewDb db) throws OrmException {
@@ -270,6 +292,7 @@
         addArg(args, "--change-url", event.change.url);
         addArg(args, "--project", event.change.project);
         addArg(args, "--branch", event.change.branch);
+        addArg(args, "--topic", event.change.topic);
         addArg(args, "--author", getDisplayName(account));
         addArg(args, "--commit", event.patchSet.revision);
         addArg(args, "--comment", comment == null ? "" : comment);
@@ -294,6 +317,7 @@
         addArg(args, "--change-url", event.change.url);
         addArg(args, "--project", event.change.project);
         addArg(args, "--branch", event.change.branch);
+        addArg(args, "--topic", event.change.topic);
         addArg(args, "--submitter", getDisplayName(account));
         addArg(args, "--commit", event.patchSet.revision);
 
@@ -314,15 +338,16 @@
         addArg(args, "--change-url", event.change.url);
         addArg(args, "--project", event.change.project);
         addArg(args, "--branch", event.change.branch);
+        addArg(args, "--topic", event.change.topic);
         addArg(args, "--abandoner", getDisplayName(account));
         addArg(args, "--reason", reason == null ? "" : reason);
 
         runHook(change.getProject(), changeAbandonedHook, args);
     }
 
-    public void doChangeRestoreHook(final Change change, final Account account,
+    public void doChangeRestoredHook(final Change change, final Account account,
           final String reason, final ReviewDb db) throws OrmException {
-        final ChangeRestoreEvent event = new ChangeRestoreEvent();
+        final ChangeRestoredEvent event = new ChangeRestoredEvent();
 
         event.change = eventFactory.asChangeAttribute(change);
         event.restorer = eventFactory.asAccountAttribute(account);
@@ -334,6 +359,7 @@
         addArg(args, "--change-url", event.change.url);
         addArg(args, "--project", event.change.project);
         addArg(args, "--branch", event.change.branch);
+        addArg(args, "--topic", event.change.topic);
         addArg(args, "--restorer", getDisplayName(account));
         addArg(args, "--reason", reason == null ? "" : reason);
 
@@ -370,7 +396,7 @@
         final List<String> args = new ArrayList<String>();
         addArg(args, "--submitter", getDisplayName(account));
         addArg(args, "--user-id", account.getId().toString());
-        addArg(args, "--cla-id", cla.getId().toString());
+        addArg(args, "--cla-name", cla.getName());
 
         runHook(claSignedHook, args);
       }
@@ -491,10 +517,12 @@
           repo = openRepository(project);
         }
 
+        final Map<String, String> env = pb.environment();
+        env.put("GERRIT_SITE", sitePaths.site_path.getAbsolutePath());
+
         if (repo != null) {
           pb.directory(repo.getDirectory());
 
-          final Map<String, String> env = pb.environment();
           env.put("GIT_DIR", repo.getDirectory().getAbsolutePath());
         }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHooks.java b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHooks.java
index c424a26..134057d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHooks.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHooks.java
@@ -14,12 +14,12 @@
 
 package com.google.gerrit.common;
 
+import com.google.gerrit.common.data.ContributorAgreement;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.ApprovalCategory;
 import com.google.gerrit.reviewdb.client.ApprovalCategoryValue;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.ContributorAgreement;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
@@ -47,11 +47,21 @@
       ReviewDb db) throws OrmException;
 
   /**
+   * Fire the Draft Published Hook.
+   *
+   * @param change The change itself.
+   * @param patchSet The Patchset that was created.
+   * @throws OrmException
+   */
+  public void doDraftPublishedHook(Change change, PatchSet patchSet,
+      ReviewDb db) throws OrmException;
+
+  /**
    * Fire the Comment Added Hook.
    *
    * @param change The change itself.
    * @param patchSet The patchset this comment is related to.
-   * @param account The gerrit user who commited the change.
+   * @param account The gerrit user who added the comment.
    * @param comment The comment given.
    * @param approvals Map of Approval Categories and Scores
    * @throws OrmException
@@ -65,7 +75,7 @@
    * Fire the Change Merged Hook.
    *
    * @param change The change itself.
-   * @param account The gerrit user who commited the change.
+   * @param account The gerrit user who submitted the change.
    * @param patchSet The patchset that was merged.
    * @throws OrmException
    */
@@ -91,7 +101,7 @@
    * @param reason Reason for restoring the change.
    * @throws OrmException
    */
-  public void doChangeRestoreHook(Change change, Account account,
+  public void doChangeRestoredHook(Change change, Account account,
       String reason, ReviewDb db) throws OrmException;
 
   /**
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java b/gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java
index 5544216..f30f5ea 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java
@@ -14,11 +14,11 @@
 
 package com.google.gerrit.common;
 
+import com.google.gerrit.common.data.ContributorAgreement;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.ApprovalCategory;
 import com.google.gerrit.reviewdb.client.ApprovalCategoryValue;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.ContributorAgreement;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.Branch.NameKey;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -46,7 +46,7 @@
   }
 
   @Override
-  public void doChangeRestoreHook(Change change, Account account,
+  public void doChangeRestoredHook(Change change, Account account,
       String reason, ReviewDb db) {
   }
 
@@ -66,6 +66,11 @@
   }
 
   @Override
+  public void doDraftPublishedHook(Change change, PatchSet patchSet,
+      ReviewDb db) {
+  }
+
+  @Override
   public void doRefUpdatedHook(NameKey refName, RefUpdate refUpdate,
       Account account) {
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/lifecycle/LifecycleManager.java b/gerrit-server/src/main/java/com/google/gerrit/lifecycle/LifecycleManager.java
index b720a25..1a3ad9b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/lifecycle/LifecycleManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/lifecycle/LifecycleManager.java
@@ -14,75 +14,90 @@
 
 package com.google.gerrit.lifecycle;
 
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Lists;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.extensions.registration.RegistrationHandle;
 import com.google.inject.Binding;
 import com.google.inject.Injector;
+import com.google.inject.Provider;
 import com.google.inject.TypeLiteral;
+import com.google.inject.util.Providers;
 
 import org.slf4j.LoggerFactory;
 
-import java.util.ArrayList;
-import java.util.LinkedHashMap;
 import java.util.List;
 
 /** Tracks and executes registered {@link LifecycleListener}s. */
 public class LifecycleManager {
-  private final LinkedHashMap<LifecycleListener, Boolean> listeners =
-      new LinkedHashMap<LifecycleListener, Boolean>();
+  private final List<Provider<LifecycleListener>> listeners = newList();
+  private final List<RegistrationHandle> handles = newList();
 
-  private boolean started;
+  /** Index of the last listener to start successfully; -1 when not started. */
+  private int startedIndex = -1;
+
+  /** Add a handle that must be cleared during stop. */
+  public void add(RegistrationHandle handle) {
+    handles.add(handle);
+  }
 
   /** Add a single listener. */
-  public void add(final LifecycleListener listener) {
-    listeners.put(listener, true);
+  public void add(LifecycleListener listener) {
+    listeners.add(Providers.of(listener));
+  }
+
+  /** Add a single listener. */
+  public void add(Provider<LifecycleListener> listener) {
+    listeners.add(listener);
   }
 
   /** Add all {@link LifecycleListener}s registered in the Injector. */
-  public void add(final Injector injector) {
-    if (started) {
-      throw new IllegalStateException("Already started");
-    }
-    for (final Binding<LifecycleListener> binding : get(injector)) {
-      add(binding.getProvider().get());
+  public void add(Injector injector) {
+    Preconditions.checkState(startedIndex < 0, "Already started");
+    for (Binding<LifecycleListener> binding : get(injector)) {
+      add(binding.getProvider());
     }
   }
 
   /** Add all {@link LifecycleListener}s registered in the Injectors. */
-  public void add(final Injector... injectors) {
-    for (final Injector i : injectors) {
+  public void add(Injector... injectors) {
+    for (Injector i : injectors) {
       add(i);
     }
   }
 
   /** Start all listeners, in the order they were registered. */
   public void start() {
-    if (!started) {
-      started = true;
-      for (LifecycleListener obj : listeners.keySet()) {
-        obj.start();
-      }
+    for (int i = startedIndex + 1; i < listeners.size(); i++) {
+      LifecycleListener listener = listeners.get(i).get();
+      startedIndex = i;
+      listener.start();
     }
   }
 
   /** Stop all listeners, in the reverse order they were registered. */
   public void stop() {
-    if (started) {
-      final List<LifecycleListener> t =
-          new ArrayList<LifecycleListener>(listeners.keySet());
+    for (int i = handles.size() - 1; 0 <= i; i--) {
+      handles.get(i).remove();
+    }
+    handles.clear();
 
-      for (int i = t.size() - 1; 0 <= i; i--) {
-        final LifecycleListener obj = t.get(i);
-        try {
-          obj.stop();
-        } catch (Throwable err) {
-          LoggerFactory.getLogger(obj.getClass()).warn("Failed to stop", err);
-        }
+    for (int i = startedIndex; 0 <= i; i--) {
+      LifecycleListener obj = listeners.get(i).get();
+      try {
+        obj.stop();
+      } catch (Throwable err) {
+        LoggerFactory.getLogger(obj.getClass()).warn("Failed to stop", err);
       }
-
-      started = false;
+      startedIndex = i - 1;
     }
   }
 
   private static List<Binding<LifecycleListener>> get(Injector i) {
     return i.findBindingsByType(new TypeLiteral<LifecycleListener>() {});
   }
+
+  private static <T> List<T> newList() {
+    return Lists.newArrayListWithCapacity(4);
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/lifecycle/LifecycleModule.java b/gerrit-server/src/main/java/com/google/gerrit/lifecycle/LifecycleModule.java
index dcc8a35..04682f5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/lifecycle/LifecycleModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/lifecycle/LifecycleModule.java
@@ -1,5 +1,6 @@
 package com.google.gerrit.lifecycle;
 
+import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.inject.AbstractModule;
 import com.google.inject.Singleton;
 import com.google.inject.binder.LinkedBindingBuilder;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/rules/RulesCache.java b/gerrit-server/src/main/java/com/google/gerrit/rules/RulesCache.java
index fd72e0c..fbee145 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/rules/RulesCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/rules/RulesCache.java
@@ -189,6 +189,8 @@
       git = gitMgr.openRepository(project);
     } catch (RepositoryNotFoundException e) {
       throw new CompileException("Cannot open repository " + project, e);
+    } catch (IOException e) {
+      throw new CompileException("Cannot open repository " + project, e);
     }
     try {
       ObjectLoader ldr = git.open(rulesId, Constants.OBJ_BLOB);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java b/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java
index 8ab9471..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
@@ -16,19 +16,25 @@
 
 import static com.google.gerrit.rules.StoredValue.create;
 
+import com.google.common.collect.Maps;
+import com.google.gerrit.reviewdb.client.Account;
+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.PatchSetInfo;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.AnonymousUser;
+import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.git.GitRepositoryManager;
 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;
+import com.google.gerrit.server.query.change.ChangeData;
 
 import com.googlecode.prolog_cafe.lang.Prolog;
 import com.googlecode.prolog_cafe.lang.SystemException;
@@ -37,21 +43,26 @@
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
 
+import java.io.IOException;
+import java.util.Map;
+
 public final class StoredValues {
   public static final StoredValue<ReviewDb> REVIEW_DB = create(ReviewDb.class);
   public static final StoredValue<Change> CHANGE = create(Change.class);
-  public static final StoredValue<PatchSet.Id> PATCH_SET_ID = create(PatchSet.Id.class);
+  public static final StoredValue<ChangeData> CHANGE_DATA = create(ChangeData.class);
+  public static final StoredValue<PatchSet> PATCH_SET = create(PatchSet.class);
   public static final StoredValue<ChangeControl> CHANGE_CONTROL = create(ChangeControl.class);
 
   public static final StoredValue<PatchSetInfo> PATCH_SET_INFO = new StoredValue<PatchSetInfo>() {
     @Override
     public PatchSetInfo createValue(Prolog engine) {
-      PatchSet.Id patchSetId = StoredValues.PATCH_SET_ID.get(engine);
+      Change change = StoredValues.CHANGE.get(engine);
+      PatchSet ps = StoredValues.PATCH_SET.get(engine);
       PrologEnvironment env = (PrologEnvironment) engine.control;
       PatchSetInfoFactory patchInfoFactory =
           env.getInjector().getInstance(PatchSetInfoFactory.class);
       try {
-        return patchInfoFactory.get(REVIEW_DB.get(engine), patchSetId);
+        return patchInfoFactory.get(change, ps);
       } catch (PatchSetInfoNotAvailableException e) {
         throw new SystemException(e.getMessage());
       }
@@ -70,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;
@@ -91,6 +104,8 @@
         repo = gitMgr.openRepository(projectKey);
       } catch (RepositoryNotFoundException e) {
         throw new SystemException(e.getMessage());
+      } catch (IOException e) {
+        throw new SystemException(e.getMessage());
       }
       env.addToCleanup(new Runnable() {
         @Override
@@ -102,6 +117,23 @@
     }
   };
 
+  public static final StoredValue<AnonymousUser> ANONYMOUS_USER =
+      new StoredValue<AnonymousUser>() {
+        @Override
+        protected AnonymousUser createValue(Prolog engine) {
+          PrologEnvironment env = (PrologEnvironment) engine.control;
+          return env.getInjector().getInstance(AnonymousUser.class);
+        }
+      };
+
+  public static final StoredValue<Map<Account.Id, IdentifiedUser>> USERS =
+      new StoredValue<Map<Account.Id, IdentifiedUser>>() {
+        @Override
+        protected Map<Account.Id, IdentifiedUser> createValue(Prolog engine) {
+          return Maps.newHashMap();
+        }
+      };
+
   private StoredValues() {
   }
-}
\ No newline at end of file
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/AccessPath.java b/gerrit-server/src/main/java/com/google/gerrit/server/AccessPath.java
index 4d1d631..ae76a6e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/AccessPath.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/AccessPath.java
@@ -26,8 +26,5 @@
   SSH_COMMAND,
 
   /** Access from a Git client using any Git protocol. */
-  GIT,
-
-  /** Access through replication */
-  REPLICATION;
+  GIT;
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
index 3417111..a295c49 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
@@ -14,23 +14,51 @@
 
 package com.google.gerrit.server;
 
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
 import com.google.gerrit.common.data.ApprovalType;
 import com.google.gerrit.common.data.ApprovalTypes;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Account.Id;
 import com.google.gerrit.reviewdb.client.ApprovalCategory;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.PatchSetInfo;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
 
 import java.io.IOException;
 import java.util.Collections;
 import java.util.List;
+import java.util.Set;
 
+/**
+ * Utility functions to manipulate patchset approvals.
+ * <p>
+ * Approvals are overloaded, they represent both approvals and reviewers
+ * which should be CCed on a change.  To ensure that reviewers are not lost
+ * there must always be an approval on each patchset for each reviewer,
+ * even if the reviewer hasn't actually given a score to the change.  To
+ * mark the "no score" case, a dummy approval, which may live in any of
+ * the available categories, with a score of 0 is used.
+ */
 public class ApprovalsUtil {
-  /* Resync the changeOpen status which is cached in the approvals table for
-     performance reasons*/
-  public static void syncChangeStatus(final ReviewDb db, final Change change)
+  private final ReviewDb db;
+  private final ApprovalTypes approvalTypes;
+
+  @Inject
+  ApprovalsUtil(ReviewDb db, ApprovalTypes approvalTypes) {
+    this.db = db;
+    this.approvalTypes = approvalTypes;
+  }
+
+  /**
+   * Resync the changeOpen status which is cached in the approvals table for
+   * performance reasons
+   */
+  public void syncChangeStatus(final Change change)
       throws OrmException {
     final List<PatchSetApproval> approvals =
         db.patchSetApprovals().byChange(change.getId()).toList();
@@ -44,14 +72,13 @@
    * Moves the PatchSetApprovals to the last PatchSet on the change while
    * keeping the vetos.
    *
-   * @param db The review database
    * @param change Change to update
-   * @param approvalTypes The approval types
    * @throws OrmException
    * @throws IOException
+   * @return List<PatchSetApproval> The previous approvals
    */
-  public static void copyVetosToLatestPatchSet(final ReviewDb db, Change change,
-      ApprovalTypes approvalTypes) throws OrmException, IOException {
+  public List<PatchSetApproval> copyVetosToLatestPatchSet(Change change)
+      throws OrmException, IOException {
     PatchSet.Id source;
     if (change.getNumberOfPatchSets() > 1) {
       source = new PatchSet.Id(change.getId(), change.getNumberOfPatchSets() - 1);
@@ -60,15 +87,65 @@
     }
 
     PatchSet.Id dest = change.currPatchSetId();
-    for (PatchSetApproval a : db.patchSetApprovals().byPatchSet(source)) {
+    List<PatchSetApproval> patchSetApprovals = db.patchSetApprovals().byChange(change.getId()).toList();
+    for (PatchSetApproval a : patchSetApprovals) {
       // ApprovalCategory.SUBMIT is still in db but not relevant in git-store
       if (!ApprovalCategory.SUBMIT.equals(a.getCategoryId())) {
         final ApprovalType type = approvalTypes.byId(a.getCategoryId());
-        if (type.getCategory().isCopyMinScore() && type.isMaxNegative(a)) {
+        if (a.getPatchSetId().equals(source) &&
+            type.getCategory().isCopyMinScore() &&
+            type.isMaxNegative(a)) {
           db.patchSetApprovals().insert(
               Collections.singleton(new PatchSetApproval(dest, a)));
         }
       }
     }
+    return patchSetApprovals;
+  }
+
+
+  /** Attach reviewers to a change. */
+  public void addReviewers(Change change, PatchSet ps, PatchSetInfo info,
+      Set<Account.Id> wantReviewers) throws OrmException {
+    Set<Id> existing = Sets.<Account.Id> newHashSet();
+    addReviewers(change, ps, info, wantReviewers, existing);
+  }
+
+  /** Attach reviewers to a change. */
+  public void addReviewers(Change change, PatchSet ps, PatchSetInfo info,
+      Set<Account.Id> wantReviewers, Set<Account.Id> existingReviewers)
+      throws OrmException {
+    List<ApprovalType> allTypes = approvalTypes.getApprovalTypes();
+    if (allTypes.isEmpty()) {
+      return;
+    }
+
+    Set<Account.Id> need = Sets.newHashSet(wantReviewers);
+    Account.Id authorId = info.getAuthor() != null
+        ? info.getAuthor().getAccount()
+        : null;
+    if (authorId != null && !ps.isDraft()) {
+      need.add(authorId);
+    }
+
+    Account.Id committerId = info.getCommitter() != null
+        ? info.getCommitter().getAccount()
+        : null;
+    if (committerId != null && !ps.isDraft()) {
+      need.add(committerId);
+    }
+    need.remove(change.getOwner());
+    need.removeAll(existingReviewers);
+
+    List<PatchSetApproval> cells = Lists.newArrayListWithCapacity(need.size());
+    ApprovalCategory.Id catId = allTypes.get(allTypes.size() - 1).getCategory().getId();
+    for (Account.Id account : need) {
+      PatchSetApproval psa = new PatchSetApproval(
+          new PatchSetApproval.Key(ps.getId(), account, catId),
+          (short) 0);
+      psa.cache(change);
+      cells.add(psa);
+    }
+    db.patchSetApprovals().insert(cells);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
index 9dc572d..535ad8e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ChangeUtil.java
@@ -14,9 +14,9 @@
 
 package com.google.gerrit.server;
 
+import com.google.common.collect.Sets;
 import com.google.gerrit.common.ChangeHookRunner;
 import com.google.gerrit.common.ChangeHooks;
-import com.google.gerrit.common.data.ApprovalTypes;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Change.Status;
@@ -30,16 +30,15 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.config.TrackingFooter;
 import com.google.gerrit.server.config.TrackingFooters;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MergeOp;
-import com.google.gerrit.server.git.ReplicationQueue;
 import com.google.gerrit.server.mail.EmailException;
 import com.google.gerrit.server.mail.RebasedPatchSetSender;
 import com.google.gerrit.server.mail.ReplacePatchSetSender;
 import com.google.gerrit.server.mail.ReplyToChangeSender;
 import com.google.gerrit.server.mail.RevertedSender;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
-import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.NoSuchChangeException;
@@ -63,9 +62,13 @@
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.util.Base64;
+import org.eclipse.jgit.util.ChangeIdUtil;
 import org.eclipse.jgit.util.NB;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
+import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
@@ -75,6 +78,9 @@
 import java.util.regex.Matcher;
 
 public class ChangeUtil {
+
+  private static final Logger log = LoggerFactory.getLogger(ChangeUtil.class);
+
   private static int uuidPrefix;
   private static int uuidSeq;
 
@@ -191,13 +197,15 @@
    * Rebases a commit
    *
    * @param git Repository to find commits in
+   * @param inserter inserter to handle new trees and blobs.
    * @param original The commit to rebase
    * @param base Base to rebase against
    * @return CommitBuilder the newly rebased commit
    * @throws IOException Merged failed
    */
-  public static CommitBuilder rebaseCommit(Repository git, RevCommit original,
-      RevCommit base, PersonIdent committerIdent) throws IOException {
+  public static CommitBuilder rebaseCommit(Repository git,
+      final ObjectInserter inserter, RevCommit original, RevCommit base,
+      PersonIdent committerIdent) throws IOException {
 
     if (original.getParentCount() == 0) {
       throw new IOException(
@@ -217,6 +225,20 @@
     }
 
     final ThreeWayMerger merger = MergeStrategy.RESOLVE.newMerger(git, true);
+    merger.setObjectInserter(new ObjectInserter.Filter() {
+      @Override
+      protected ObjectInserter delegate() {
+        return inserter;
+      }
+
+      @Override
+      public void flush() {
+      }
+
+      @Override
+      public void release() {
+      }
+    });
     merger.setBase(parentCommit);
     merger.merge(original, base);
 
@@ -241,12 +263,12 @@
       RebasedPatchSetSender.Factory rebasedPatchSetSenderFactory,
       final ChangeHookRunner hooks, GitRepositoryManager gitManager,
       final PatchSetInfoFactory patchSetInfoFactory,
-      final ReplicationQueue replication, PersonIdent myIdent,
+      final GitReferenceUpdated replication, PersonIdent myIdent,
       final ChangeControl.Factory changeControlFactory,
-      final ApprovalTypes approvalTypes) throws NoSuchChangeException,
+      final ApprovalsUtil approvalsUtil) throws NoSuchChangeException,
       EmailException, OrmException, MissingObjectException,
       IncorrectObjectTypeException, IOException,
-      PatchSetInfoNotAvailableException, InvalidChangeOperationException {
+      InvalidChangeOperationException {
 
     final Change.Id changeId = patchSetId.getParentKey();
     final ChangeControl changeControl =
@@ -314,104 +336,119 @@
           branchTipCommit = revWalk.parseCommit(destRef.getObjectId());
         }
 
-        final RevCommit originalCommit =
-            revWalk.parseCommit(ObjectId.fromString(originalPatchSet
-                .getRevision().get()));
-
-        CommitBuilder rebasedCommitBuilder =
-            rebaseCommit(git, originalCommit, branchTipCommit, myIdent);
-
+        final RevCommit rebasedCommit;
         final ObjectInserter oi = git.newObjectInserter();
-        final ObjectId rebasedCommitId;
         try {
-          rebasedCommitId = oi.insert(rebasedCommitBuilder);
+          ObjectId oldId = ObjectId.fromString(originalPatchSet.getRevision().get());
+          ObjectId newId = oi.insert(rebaseCommit(
+              git, oi, revWalk.parseCommit(oldId), branchTipCommit, myIdent));
           oi.flush();
+          rebasedCommit = revWalk.parseCommit(newId);
         } finally {
           oi.release();
         }
 
-        Change updatedChange =
-            db.changes().atomicUpdate(changeId, new AtomicUpdate<Change>() {
-              @Override
-              public Change update(Change change) {
-                if (change.getStatus().isOpen()) {
-                  change.nextPatchSetId();
-                  return change;
-                } else {
-                  return null;
-                }
-              }
-            });
+        change.nextPatchSetId();
+        final PatchSet newPatchSet = new PatchSet(change.currPatchSetId());
+        newPatchSet.setCreatedOn(new Timestamp(System.currentTimeMillis()));
+        newPatchSet.setUploader(user.getAccountId());
+        newPatchSet.setRevision(new RevId(rebasedCommit.name()));
+        newPatchSet.setDraft(originalPatchSet.isDraft());
 
-        if (updatedChange == null) {
-          throw new InvalidChangeOperationException("Change is closed: "
-              + change.toString());
-        } else {
-          change = updatedChange;
-        }
-
-        final PatchSet rebasedPatchSet = new PatchSet(change.currPatchSetId());
-        rebasedPatchSet.setCreatedOn(change.getCreatedOn());
-        rebasedPatchSet.setUploader(user.getAccountId());
-        rebasedPatchSet.setRevision(new RevId(rebasedCommitId.getName()));
-
-        insertAncestors(db, rebasedPatchSet.getId(),
-            revWalk.parseCommit(rebasedCommitId));
-
-        db.patchSets().insert(Collections.singleton(rebasedPatchSet));
         final PatchSetInfo info =
-            patchSetInfoFactory.get(db, rebasedPatchSet.getId());
+            patchSetInfoFactory.get(rebasedCommit, newPatchSet.getId());
 
-        change =
-            db.changes().atomicUpdate(change.getId(),
-                new AtomicUpdate<Change>() {
-                  @Override
-                  public Change update(Change change) {
-                    change.setCurrentPatchSet(info);
-                    ChangeUtil.updated(change);
-                    return change;
-                  }
-                });
-
-        final RefUpdate ru = git.updateRef(rebasedPatchSet.getRefName());
-        ru.setNewObjectId(rebasedCommitId);
+        RefUpdate ru = git.updateRef(newPatchSet.getRefName());
+        ru.setExpectedOldObjectId(ObjectId.zeroId());
+        ru.setNewObjectId(rebasedCommit);
         ru.disableRefLog();
         if (ru.update(revWalk) != RefUpdate.Result.NEW) {
-          throw new IOException("Failed to create ref "
-              + rebasedPatchSet.getRefName() + " in " + git.getDirectory()
-              + ": " + ru.getResult());
+          throw new IOException(String.format(
+              "Failed to create ref %s in %s: %s", newPatchSet.getRefName(),
+              change.getDest().getParentKey().get(), ru.getResult()));
         }
+        replication.fire(change.getProject(), ru.getName());
 
-        replication.scheduleUpdate(change.getProject(), ru.getName());
+        final Set<Account.Id> oldReviewers = Sets.newHashSet();
+        final Set<Account.Id> oldCC = Sets.newHashSet();
+        db.changes().beginTransaction(change.getId());
+        try {
+          Change updatedChange;
 
-        ApprovalsUtil.copyVetosToLatestPatchSet(db, change, approvalTypes);
-
-        final ChangeMessage cmsg =
-            new ChangeMessage(new ChangeMessage.Key(changeId,
-                ChangeUtil.messageUUID(db)), user.getAccountId(), patchSetId);
-        cmsg.setMessage("Patch Set " + patchSetId.get() + ": Rebased");
-        db.changeMessages().insert(Collections.singleton(cmsg));
-
-        final Set<Account.Id> oldReviewers = new HashSet<Account.Id>();
-        final Set<Account.Id> oldCC = new HashSet<Account.Id>();
-
-        for (PatchSetApproval a : db.patchSetApprovals().byChange(change.getId())) {
-          if (a.getValue() != 0) {
-            oldReviewers.add(a.getAccountId());
+          updatedChange = db.changes().atomicUpdate(changeId,
+              new AtomicUpdate<Change>() {
+                @Override
+                public Change update(Change change) {
+                  if (change.getStatus().isOpen()) {
+                    change.updateNumberOfPatchSets(newPatchSet.getPatchSetId());
+                    return change;
+                  } else {
+                    return null;
+                  }
+                }
+              });
+          if (updatedChange != null) {
+            change = updatedChange;
           } else {
-            oldCC.add(a.getAccountId());
+            throw new InvalidChangeOperationException(
+                String.format("Change %s is closed", change.getId()));
           }
+
+          insertAncestors(db, newPatchSet.getId(), rebasedCommit);
+          db.patchSets().insert(Collections.singleton(newPatchSet));
+          updatedChange = db.changes().atomicUpdate(changeId,
+              new AtomicUpdate<Change>() {
+                @Override
+                public Change update(Change change) {
+                  if (change.getStatus().isClosed()) {
+                    return null;
+                  }
+                  if (!change.currentPatchSetId().equals(patchSetId)) {
+                    return null;
+                  }
+                  if (change.getStatus() != Change.Status.DRAFT) {
+                    change.setStatus(Change.Status.NEW);
+                  }
+                  change.setLastSha1MergeTested(null);
+                  change.setCurrentPatchSet(info);
+                  ChangeUtil.updated(change);
+                  return change;
+                }
+              });
+          if (updatedChange != null) {
+            change = updatedChange;
+          } else {
+            throw new InvalidChangeOperationException(
+                String.format("Change %s was modified", change.getId()));
+          }
+
+          for (PatchSetApproval a : approvalsUtil.copyVetosToLatestPatchSet(change)) {
+            if (a.getValue() != 0) {
+              oldReviewers.add(a.getAccountId());
+            } else {
+              oldCC.add(a.getAccountId());
+            }
+          }
+
+          final ChangeMessage cmsg =
+              new ChangeMessage(new ChangeMessage.Key(changeId,
+                  ChangeUtil.messageUUID(db)), user.getAccountId(), patchSetId);
+          cmsg.setMessage("Patch Set " + patchSetId.get() + ": Rebased");
+          db.changeMessages().insert(Collections.singleton(cmsg));
+          db.commit();
+        } finally {
+          db.rollback();
         }
 
         final ReplacePatchSetSender cm =
             rebasedPatchSetSenderFactory.create(change);
         cm.setFrom(user.getAccountId());
-        cm.setPatchSet(rebasedPatchSet);
+        cm.setPatchSet(newPatchSet);
         cm.addReviewers(oldReviewers);
         cm.addExtraCC(oldCC);
         cm.send();
 
-        hooks.doPatchsetCreatedHook(change, rebasedPatchSet, db);
+        hooks.doPatchsetCreatedHook(change, newPatchSet, db);
       } finally {
         revWalk.release();
       }
@@ -425,11 +462,9 @@
       final RevertedSender.Factory revertedSenderFactory,
       final ChangeHooks hooks, GitRepositoryManager gitManager,
       final PatchSetInfoFactory patchSetInfoFactory,
-      final ReplicationQueue replication, PersonIdent myIdent)
+      final GitReferenceUpdated replication, PersonIdent myIdent)
       throws NoSuchChangeException, EmailException, OrmException,
-      MissingObjectException, IncorrectObjectTypeException, IOException,
-      PatchSetInfoNotAvailableException {
-
+      MissingObjectException, IncorrectObjectTypeException, IOException {
     final Change.Id changeId = patchSetId.getParentKey();
     final PatchSet patch = db.patchSets().get(patchSetId);
     if (patch == null) {
@@ -441,7 +476,7 @@
       git = gitManager.openRepository(db.changes().get(changeId).getProject());
     } catch (RepositoryNotFoundException e) {
       throw new NoSuchChangeException(changeId, e);
-    };
+    }
 
     final RevWalk revWalk = new RevWalk(git);
     try {
@@ -454,50 +489,62 @@
       RevCommit parentToCommitToRevert = commitToRevert.getParent(0);
       revWalk.parseHeaders(parentToCommitToRevert);
 
-      CommitBuilder revertCommit = new CommitBuilder();
-      revertCommit.addParentId(commitToRevert);
-      revertCommit.setTreeId(parentToCommitToRevert.getTree());
-      revertCommit.setAuthor(authorIdent);
-      revertCommit.setCommitter(myIdent);
-      revertCommit.setMessage(message);
+      CommitBuilder revertCommitBuilder = new CommitBuilder();
+      revertCommitBuilder.addParentId(commitToRevert);
+      revertCommitBuilder.setTreeId(parentToCommitToRevert.getTree());
+      revertCommitBuilder.setAuthor(authorIdent);
+      revertCommitBuilder.setCommitter(myIdent);
 
-      final ObjectInserter oi = git.newObjectInserter();;
-      ObjectId id;
+      final ObjectId computedChangeId =
+          ChangeIdUtil.computeChangeId(parentToCommitToRevert.getTree(),
+              commitToRevert, authorIdent, myIdent, message);
+      revertCommitBuilder.setMessage(ChangeIdUtil.insertId(message, computedChangeId, true));
+
+      RevCommit revertCommit;
+      final ObjectInserter oi = git.newObjectInserter();
       try {
-        id = oi.insert(revertCommit);
+        ObjectId id = oi.insert(revertCommitBuilder);
         oi.flush();
+        revertCommit = revWalk.parseCommit(id);
       } finally {
         oi.release();
       }
 
-      Change.Key changeKey = new Change.Key("I" + id.name());
-      final Change change =
-          new Change(changeKey, new Change.Id(db.nextChangeId()),
-              user.getAccountId(), db.changes().get(changeId).getDest());
+      final Change change = new Change(
+          new Change.Key("I" + computedChangeId.name()),
+          new Change.Id(db.nextChangeId()),
+          user.getAccountId(),
+          db.changes().get(changeId).getDest());
       change.nextPatchSetId();
 
       final PatchSet ps = new PatchSet(change.currPatchSetId());
       ps.setCreatedOn(change.getCreatedOn());
-      ps.setUploader(user.getAccountId());
-      ps.setRevision(new RevId(id.getName()));
+      ps.setUploader(change.getOwner());
+      ps.setRevision(new RevId(revertCommit.name()));
 
-      db.patchSets().insert(Collections.singleton(ps));
-
-      final PatchSetInfo info =
-          patchSetInfoFactory.get(revWalk.parseCommit(id), ps.getId());
-      change.setCurrentPatchSet(info);
+      change.setCurrentPatchSet(patchSetInfoFactory.get(revertCommit, ps.getId()));
       ChangeUtil.updated(change);
-      db.changes().insert(Collections.singleton(change));
 
       final RefUpdate ru = git.updateRef(ps.getRefName());
-      ru.setNewObjectId(id);
+      ru.setExpectedOldObjectId(ObjectId.zeroId());
+      ru.setNewObjectId(revertCommit);
       ru.disableRefLog();
       if (ru.update(revWalk) != RefUpdate.Result.NEW) {
-        throw new IOException("Failed to create ref " + ps.getRefName()
-            + " in " + git.getDirectory() + ": " + ru.getResult());
+        throw new IOException(String.format(
+            "Failed to create ref %s in %s: %s", ps.getRefName(),
+            change.getDest().getParentKey().get(), ru.getResult()));
       }
-      replication.scheduleUpdate(db.changes().get(changeId).getProject(),
-          ru.getName());
+      replication.fire(change.getProject(), ru.getName());
+
+      db.changes().beginTransaction(change.getId());
+      try {
+        insertAncestors(db, ps.getId(), revertCommit);
+        db.patchSets().insert(Collections.singleton(ps));
+        db.changes().insert(Collections.singleton(change));
+        db.commit();
+      } finally {
+        db.rollback();
+      }
 
       final ChangeMessage cmsg =
           new ChangeMessage(new ChangeMessage.Key(changeId,
@@ -505,7 +552,7 @@
       final StringBuilder msgBuf =
           new StringBuilder("Patch Set " + patchSetId.get() + ": Reverted");
       msgBuf.append("\n\n");
-      msgBuf.append("This patchset was reverted in change: " + changeKey.get());
+      msgBuf.append("This patchset was reverted in change: " + change.getKey().get());
 
       cmsg.setMessage(msgBuf.toString());
       db.changeMessages().insert(Collections.singleton(cmsg));
@@ -526,7 +573,7 @@
 
   public static void deleteDraftChange(final PatchSet.Id patchSetId,
       GitRepositoryManager gitManager,
-      final ReplicationQueue replication, final ReviewDb db)
+      final GitReferenceUpdated replication, final ReviewDb db)
       throws NoSuchChangeException, OrmException, IOException {
     final Change.Id changeId = patchSetId.getParentKey();
     final Change change = db.changes().get(changeId);
@@ -547,7 +594,7 @@
 
   public static void deleteOnlyDraftPatchSet(final PatchSet patch,
       final Change change, GitRepositoryManager gitManager,
-      final ReplicationQueue replication, final ReviewDb db)
+      final GitReferenceUpdated replication, final ReviewDb db)
       throws NoSuchChangeException, OrmException, IOException {
     final PatchSet.Id patchSetId = patch.getId();
     if (patch == null || !patch.isDraft()) {
@@ -570,7 +617,7 @@
           throw new IOException("Failed to delete ref " + patch.getRefName() +
               " in " + repo.getDirectory() + ": " + update.getResult());
       }
-      replication.scheduleUpdate(change.getProject(), update.getName());
+      replication.fire(change.getProject(), update.getName());
     } finally {
       repo.close();
     }
@@ -586,21 +633,21 @@
 
   public static <T extends ReplyToChangeSender> void updatedChange(
       final ReviewDb db, final IdentifiedUser user, final Change change,
-      final ChangeMessage cmsg, ReplyToChangeSender.Factory<T> senderFactory,
-      final String err) throws NoSuchChangeException,
-      InvalidChangeOperationException, EmailException, OrmException {
-    if (change == null) {
-      throw new InvalidChangeOperationException(err);
-    }
+      final ChangeMessage cmsg, ReplyToChangeSender.Factory<T> senderFactory)
+      throws OrmException {
     db.changeMessages().insert(Collections.singleton(cmsg));
 
-    ApprovalsUtil.syncChangeStatus(db, change);
+    new ApprovalsUtil(db, null).syncChangeStatus(change);
 
     // Email the reviewers
-    final ReplyToChangeSender cm = senderFactory.create(change);
-    cm.setFrom(user.getAccountId());
-    cm.setChangeMessage(cmsg);
-    cm.send();
+    try {
+      final ReplyToChangeSender cm = senderFactory.create(change);
+      cm.setFrom(user.getAccountId());
+      cm.setChangeMessage(cmsg);
+      cm.send();
+    } catch (Exception e) {
+      log.error("Cannot email update for change " + change.getChangeId(), e);
+    }
   }
 
   public static String sortKey(long lastUpdated, int id){
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..89cbac1 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,8 @@
 
 package com.google.gerrit.server;
 
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.common.data.AccountInfo;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountDiffPreference;
 import com.google.gerrit.reviewdb.client.AccountGroup;
@@ -24,8 +26,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 +49,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 +70,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 +79,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 +96,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 +121,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 +133,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 +143,7 @@
       this.canonicalUrl = canonicalUrl;
       this.realm = realm;
       this.accountCache = accountCache;
-      this.groupMembershipFactory = groupMembershipFactory;
+      this.groupBackend = groupBackend;
 
       this.remotePeerProvider = remotePeerProvider;
       this.dbProvider = dbProvider;
@@ -153,40 +153,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 +192,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 +206,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());
     }
@@ -268,16 +250,23 @@
     return emailAddresses;
   }
 
+  public String getName() {
+    return new AccountInfo(getAccount()).getName(anonymousCowardName);
+  }
+
+  public String getNameEmail() {
+    return new AccountInfo(getAccount()).getNameEmail(anonymousCowardName);
+  }
+
   @Override
   public GroupMembership getEffectiveGroups() {
     if (effectiveGroups == null) {
       if (authConfig.isIdentityTrustable(state().getExternalIds())) {
-        effectiveGroups = realm.groups(state());
+        effectiveGroups = groupBackend.membershipsOf(this);
       } else {
-        effectiveGroups = groupMembershipFactory.create(registeredGroups);
+        effectiveGroups = registeredGroups;
       }
     }
-
     return effectiveGroups;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/InternalUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/InternalUser.java
new file mode 100644
index 0000000..eba59c8
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/InternalUser.java
@@ -0,0 +1,64 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server;
+
+import com.google.gerrit.reviewdb.client.AccountProjectWatch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.account.CapabilityControl;
+import com.google.gerrit.server.account.GroupMembership;
+import com.google.inject.Inject;
+
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Set;
+
+/**
+ * User identity for plugin code that needs an identity.
+ * <p>
+ * An InternalUser has no real identity, it acts as the server and can access
+ * anything it wants, anytime it wants, given the JVM's own direct access to
+ * data. Plugins may use this when they need to have a CurrentUser with read
+ * permission on anything.
+ */
+public class InternalUser extends CurrentUser {
+  public interface Factory {
+    InternalUser create();
+  }
+
+  @Inject
+  protected InternalUser(CapabilityControl.Factory capabilityControlFactory) {
+    super(capabilityControlFactory, AccessPath.UNKNOWN);
+  }
+
+  @Override
+  public GroupMembership getEffectiveGroups() {
+    return GroupMembership.EMPTY;
+  }
+
+  @Override
+  public Set<Change.Id> getStarredChanges() {
+    return Collections.emptySet();
+  }
+
+  @Override
+  public Collection<AccountProjectWatch> getNotificationFilters() {
+    return Collections.emptySet();
+  }
+
+  @Override
+  public String toString() {
+    return "InternalUser";
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/OutputFormat.java b/gerrit-server/src/main/java/com/google/gerrit/server/OutputFormat.java
new file mode 100644
index 0000000..7e1ec4b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/OutputFormat.java
@@ -0,0 +1,71 @@
+// 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.gson.FieldNamingPolicy;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gwtjsonrpc.server.SqlTimestampDeserializer;
+
+import java.sql.Timestamp;
+
+/** Standard output format used by an API call. */
+public enum OutputFormat {
+  /**
+   * The output is a human readable text format. It may also be regular enough
+   * to be machine readable. Whether or not the text format is machine readable
+   * and will be committed to as a long term format that tools can build upon is
+   * specific to each API call.
+   */
+  TEXT,
+
+  /**
+   * Pretty-printed JSON format. This format uses whitespace to make the output
+   * readable by a human, but is also machine readable with a JSON library. The
+   * structure of the output is a long term format that tools can rely upon.
+   */
+  JSON,
+
+  /**
+   * Same as {@link #JSON}, but with unnecessary whitespace removed to save
+   * generation time and copy costs. Typically JSON_COMPACT format is used by a
+   * browser based HTML client running over the network.
+   */
+  JSON_COMPACT;
+
+  /** @return true when the format is either JSON or JSON_COMPACT. */
+  public boolean isJson() {
+    return this == JSON_COMPACT || this == JSON;
+  }
+
+  /** @return a new Gson instance configured according to the format. */
+  public GsonBuilder newGsonBuilder() {
+    if (!isJson()) {
+      throw new IllegalStateException(String.format("%s is not JSON", this));
+    }
+    GsonBuilder gb = new GsonBuilder()
+      .setFieldNamingPolicy(FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES)
+      .registerTypeAdapter(Timestamp.class, new SqlTimestampDeserializer());
+    if (this == OutputFormat.JSON) {
+      gb.setPrettyPrinting();
+    }
+    return gb;
+  }
+
+  /** @return a new Gson instance configured according to the format. */
+  public Gson newGson() {
+    return newGsonBuilder().create();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/PeerDaemonUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/PeerDaemonUser.java
index 352f540..75ba0d7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/PeerDaemonUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/PeerDaemonUser.java
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.server;
 
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountProjectWatch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.account.CapabilityControl;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ProjectUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/ProjectUtil.java
new file mode 100644
index 0000000..ea1f1d1
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ProjectUtil.java
@@ -0,0 +1,53 @@
+// 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.Branch;
+import com.google.gerrit.server.git.GitRepositoryManager;
+
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Repository;
+
+import java.io.IOException;
+
+public class ProjectUtil {
+
+  /**
+   * Checks whether the specified branch exists.
+   *
+   * @param repoManager Git repository manager to open the git repository
+   * @param branch the branch for which it should be checked if it exists
+   * @return <code>true</code> if the specified branch exists or if
+   *         <code>HEAD</code> points to this branch, otherwise
+   *         <code>false</code>
+   * @throws RepositoryNotFoundException the repository of the branch's project
+   *         does not exist.
+   * @throws IOException error while retrieving the branch from the repository.
+   */
+  public static boolean branchExists(final GitRepositoryManager repoManager,
+      final Branch.NameKey branch) throws RepositoryNotFoundException,
+      IOException {
+    final Repository repo = repoManager.openRepository(branch.getParentKey());
+    try {
+      boolean exists = repo.getRef(branch.get()) != null;
+      if (!exists) {
+        exists = repo.getFullBranch().equals(branch.get());
+      }
+      return exists;
+    } finally {
+      repo.close();
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ReplicationUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/ReplicationUser.java
deleted file mode 100644
index fc6ac9c..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ReplicationUser.java
+++ /dev/null
@@ -1,66 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server;
-
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountProjectWatch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.server.account.CapabilityControl;
-import com.google.gerrit.server.account.GroupMembership;
-import com.google.gerrit.server.account.ListGroupMembership;
-import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
-
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Set;
-
-public class ReplicationUser extends CurrentUser {
-  /** Magic set of groups enabling read of any project and reference. */
-  public static final GroupMembership EVERYTHING_VISIBLE =
-      new ListGroupMembership(Collections.<AccountGroup.UUID>emptySet());
-
-  public interface Factory {
-    ReplicationUser create(@Assisted GroupMembership authGroups);
-  }
-
-  private final GroupMembership effectiveGroups;
-
-  @Inject
-  protected ReplicationUser(CapabilityControl.Factory capabilityControlFactory,
-      @Assisted GroupMembership authGroups) {
-    super(capabilityControlFactory, AccessPath.REPLICATION);
-    effectiveGroups = authGroups;
-  }
-
-  @Override
-  public GroupMembership getEffectiveGroups() {
-    return effectiveGroups;
-  }
-
-  @Override
-  public Set<Change.Id> getStarredChanges() {
-    return Collections.emptySet();
-  }
-
-  @Override
-  public Collection<AccountProjectWatch> getNotificationFilters() {
-    return Collections.emptySet();
-  }
-
-  public boolean isEverythingVisible() {
-    return getEffectiveGroups() == EVERYTHING_VISIBLE;
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/StringUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/StringUtil.java
new file mode 100644
index 0000000..fe1072d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/StringUtil.java
@@ -0,0 +1,54 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server;
+
+public class StringUtil {
+  /**
+   * An array of the string representations that should be used in place
+   * of the non-printable characters in the beginning of the ASCII table
+   * when escaping a string. The index of each element in the array
+   * corresponds to its ASCII value, i.e. the string representation of
+   * ASCII 0 is found in the first element of this array.
+   */
+  static String[] NON_PRINTABLE_CHARS =
+    { "\\x00", "\\x01", "\\x02", "\\x03", "\\x04", "\\x05", "\\x06", "\\a",
+      "\\b",   "\\t",   "\\n",   "\\v",   "\\f",   "\\r",   "\\x0e", "\\x0f",
+      "\\x10", "\\x11", "\\x12", "\\x13", "\\x14", "\\x15", "\\x16", "\\x17",
+      "\\x18", "\\x19", "\\x1a", "\\x1b", "\\x1c", "\\x1d", "\\x1e", "\\x1f" };
+
+  /**
+   * Escapes the input string so that all non-printable characters
+   * (0x00-0x1f) are represented as a hex escape (\x00, \x01, ...)
+   * or as a C-style escape sequence (\a, \b, \t, \n, \v, \f, or \r).
+   * Backslashes in the input string are doubled (\\).
+   */
+  public static String escapeString(final String str) {
+    // Allocate a buffer big enough to cover the case with a string needed
+    // very excessive escaping without having to reallocate the buffer.
+    final StringBuilder result = new StringBuilder(3 * str.length());
+
+    for (int i = 0; i < str.length(); i++) {
+      char c = str.charAt(i);
+      if (c < NON_PRINTABLE_CHARS.length) {
+        result.append(NON_PRINTABLE_CHARS[c]);
+      } else if (c == '\\') {
+        result.append("\\\\");
+      } else {
+        result.append(c);
+      }
+    }
+    return result.toString();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java
index 72fb2e8..4827ed5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java
@@ -14,12 +14,14 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.cache.Cache;
 import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.cache.EntryCreator;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Module;
@@ -27,45 +29,58 @@
 import com.google.inject.TypeLiteral;
 import com.google.inject.name.Named;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import java.util.Collections;
-import java.util.HashSet;
 import java.util.Set;
+import java.util.concurrent.ExecutionException;
 
 /** Translates an email address to a set of matching accounts. */
 @Singleton
 public class AccountByEmailCacheImpl implements AccountByEmailCache {
+  private static final Logger log = LoggerFactory
+      .getLogger(AccountByEmailCacheImpl.class);
   private static final String CACHE_NAME = "accounts_byemail";
 
   public static Module module() {
     return new CacheModule() {
       @Override
       protected void configure() {
-        final TypeLiteral<Cache<String, Set<Account.Id>>> type =
-            new TypeLiteral<Cache<String, Set<Account.Id>>>() {};
-        core(type, CACHE_NAME).populateWith(Loader.class);
+        cache(CACHE_NAME,
+            String.class,
+            new TypeLiteral<Set<Account.Id>>() {})
+          .loader(Loader.class);
         bind(AccountByEmailCacheImpl.class);
         bind(AccountByEmailCache.class).to(AccountByEmailCacheImpl.class);
       }
     };
   }
 
-  private final Cache<String, Set<Account.Id>> cache;
+  private final LoadingCache<String, Set<Account.Id>> cache;
 
   @Inject
   AccountByEmailCacheImpl(
-      @Named(CACHE_NAME) final Cache<String, Set<Account.Id>> cache) {
+      @Named(CACHE_NAME) LoadingCache<String, Set<Account.Id>> cache) {
     this.cache = cache;
   }
 
   public Set<Account.Id> get(final String email) {
-    return cache.get(email);
+    try {
+      return cache.get(email);
+    } catch (ExecutionException e) {
+      log.warn("Cannot resolve accounts by email", e);
+      return Collections.emptySet();
+    }
   }
 
   public void evict(final String email) {
-    cache.remove(email);
+    if (email != null) {
+      cache.invalidate(email);
+    }
   }
 
-  static class Loader extends EntryCreator<String, Set<Account.Id>> {
+  static class Loader extends CacheLoader<String, Set<Account.Id>> {
     private final SchemaFactory<ReviewDb> schema;
 
     @Inject
@@ -74,10 +89,10 @@
     }
 
     @Override
-    public Set<Account.Id> createEntry(final String email) throws Exception {
+    public Set<Account.Id> load(String email) throws Exception {
       final ReviewDb db = schema.open();
       try {
-        final HashSet<Account.Id> r = new HashSet<Account.Id>();
+        Set<Account.Id> r = Sets.newHashSet();
         for (Account a : db.accounts().byPreferredEmail(email)) {
           r.add(a.getId());
         }
@@ -85,30 +100,10 @@
             .byEmailAddress(email)) {
           r.add(a.getAccountId());
         }
-        return pack(r);
+        return ImmutableSet.copyOf(r);
       } finally {
         db.close();
       }
     }
-
-    @Override
-    public Set<Account.Id> missing(final String key) {
-      return Collections.emptySet();
-    }
-
-    private static Set<Account.Id> pack(final Set<Account.Id> c) {
-      switch (c.size()) {
-        case 0:
-          return Collections.emptySet();
-        case 1:
-          return one(c);
-        default:
-          return Collections.unmodifiableSet(new HashSet<Account.Id>(c));
-      }
-    }
-
-    private static <T> Set<T> one(final Set<T> c) {
-      return Collections.singleton(c.iterator().next());
-    }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java
index 819ec31..4217f9f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java
@@ -14,14 +14,16 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.common.base.Optional;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountGroupMember;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.cache.Cache;
 import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.cache.EntryCreator;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
@@ -30,14 +32,21 @@
 import com.google.inject.TypeLiteral;
 import com.google.inject.name.Named;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.Set;
+import java.util.concurrent.ExecutionException;
 
 /** Caches important (but small) account state to avoid database hits. */
 @Singleton
 public class AccountCacheImpl implements AccountCache {
+  private static final Logger log = LoggerFactory
+      .getLogger(AccountCacheImpl.class);
+
   private static final String BYID_NAME = "accounts";
   private static final String BYUSER_NAME = "accounts_byname";
 
@@ -45,13 +54,13 @@
     return new CacheModule() {
       @Override
       protected void configure() {
-        final TypeLiteral<Cache<Account.Id, AccountState>> byIdType =
-            new TypeLiteral<Cache<Account.Id, AccountState>>() {};
-        core(byIdType, BYID_NAME).populateWith(ByIdLoader.class);
+        cache(BYID_NAME, Account.Id.class, AccountState.class)
+          .loader(ByIdLoader.class);
 
-        final TypeLiteral<Cache<String, Account.Id>> byUsernameType =
-            new TypeLiteral<Cache<String, Account.Id>>() {};
-        core(byUsernameType, BYUSER_NAME).populateWith(ByNameLoader.class);
+        cache(BYUSER_NAME,
+            String.class,
+            new TypeLiteral<Optional<Account.Id>>() {})
+          .loader(ByNameLoader.class);
 
         bind(AccountCacheImpl.class);
         bind(AccountCache.class).to(AccountCacheImpl.class);
@@ -59,54 +68,76 @@
     };
   }
 
-  private final Cache<Account.Id, AccountState> byId;
-  private final Cache<String, Account.Id> byName;
+  private final LoadingCache<Account.Id, AccountState> byId;
+  private final LoadingCache<String, Optional<Account.Id>> byName;
 
   @Inject
-  AccountCacheImpl(@Named(BYID_NAME) Cache<Account.Id, AccountState> byId,
-      @Named(BYUSER_NAME) Cache<String, Account.Id> byUsername) {
+  AccountCacheImpl(@Named(BYID_NAME) LoadingCache<Account.Id, AccountState> byId,
+      @Named(BYUSER_NAME) LoadingCache<String, Optional<Account.Id>> byUsername) {
     this.byId = byId;
     this.byName = byUsername;
   }
 
-  public AccountState get(final Account.Id accountId) {
-    return byId.get(accountId);
+  public AccountState get(Account.Id accountId) {
+    try {
+      return byId.get(accountId);
+    } catch (ExecutionException e) {
+      log.warn("Cannot load AccountState for " + accountId, e);
+      return missing(accountId);
+    }
   }
 
   @Override
   public AccountState getByUsername(String username) {
-    Account.Id id = byName.get(username);
-    return id != null ? byId.get(id) : null;
+    try {
+      Optional<Account.Id> id = byName.get(username);
+      return id != null && id.isPresent() ? byId.get(id.get()) : null;
+    } catch (ExecutionException e) {
+      log.warn("Cannot load AccountState for " + username, e);
+      return null;
+    }
   }
 
-  public void evict(final Account.Id accountId) {
-    byId.remove(accountId);
+  public void evict(Account.Id accountId) {
+    if (accountId != null) {
+      byId.invalidate(accountId);
+    }
   }
 
   public void evictByUsername(String username) {
-    byName.remove(username);
+    if (username != null) {
+      byName.invalidate(username);
+    }
   }
 
-  static class ByIdLoader extends EntryCreator<Account.Id, AccountState> {
+  private static AccountState missing(Account.Id accountId) {
+    Account account = new Account(accountId);
+    Collection<AccountExternalId> ids = Collections.emptySet();
+    Set<AccountGroup.UUID> anon = ImmutableSet.of(AccountGroup.ANONYMOUS_USERS);
+    return new AccountState(account, anon, ids);
+  }
+
+  static class ByIdLoader extends CacheLoader<Account.Id, AccountState> {
     private final SchemaFactory<ReviewDb> schema;
     private final GroupCache groupCache;
-    private final Cache<String, Account.Id> byName;
+    private final LoadingCache<String, Optional<Account.Id>> byName;
 
     @Inject
     ByIdLoader(SchemaFactory<ReviewDb> sf, GroupCache groupCache,
-        @Named(BYUSER_NAME) Cache<String, Account.Id> byUsername) {
+        @Named(BYUSER_NAME) LoadingCache<String, Optional<Account.Id>> byUsername) {
       this.schema = sf;
       this.groupCache = groupCache;
       this.byName = byUsername;
     }
 
     @Override
-    public AccountState createEntry(final Account.Id key) throws Exception {
+    public AccountState load(Account.Id key) throws Exception {
       final ReviewDb db = schema.open();
       try {
         final AccountState state = load(db, key);
-        if (state.getUserName() != null) {
-          byName.put(state.getUserName(), state.getAccount().getId());
+        String user = state.getUserName();
+        if (user != null) {
+          byName.put(user, Optional.of(state.getAccount().getId()));
         }
         return state;
       } finally {
@@ -142,18 +173,9 @@
 
       return new AccountState(account, internalGroups, externalIds);
     }
-
-    @Override
-    public AccountState missing(final Account.Id accountId) {
-      final Account account = new Account(accountId);
-      final Collection<AccountExternalId> ids = Collections.emptySet();
-      final Set<AccountGroup.UUID> anonymous =
-          Collections.singleton(AccountGroup.ANONYMOUS_USERS);
-      return new AccountState(account, anonymous, ids);
-    }
   }
 
-  static class ByNameLoader extends EntryCreator<String, Account.Id> {
+  static class ByNameLoader extends CacheLoader<String, Optional<Account.Id>> {
     private final SchemaFactory<ReviewDb> schema;
 
     @Inject
@@ -162,14 +184,17 @@
     }
 
     @Override
-    public Account.Id createEntry(final String username) throws Exception {
+    public Optional<Account.Id> load(String username) throws Exception {
       final ReviewDb db = schema.open();
       try {
         final AccountExternalId.Key key = new AccountExternalId.Key( //
             AccountExternalId.SCHEME_USERNAME, //
             username);
         final AccountExternalId id = db.accountExternalIds().get(key);
-        return id != null ? id.getAccountId() : null;
+        if (id != null) {
+          return Optional.of(id.getAccountId());
+        }
+        return Optional.absent();
       } finally {
         db.close();
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountControl.java
index b297ed8..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
@@ -14,11 +14,14 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.git.AccountsSection;
+import com.google.gerrit.server.project.ProjectCache;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
@@ -27,16 +30,19 @@
 /** Access control management for one account's access to other accounts. */
 public class AccountControl {
   public static class Factory {
+    private final ProjectCache projectCache;
     private final GroupControl.Factory groupControlFactory;
     private final Provider<CurrentUser> user;
     private final IdentifiedUser.GenericFactory userFactory;
     private final AccountVisibility accountVisibility;
 
     @Inject
-    Factory(final GroupControl.Factory groupControlFactory,
+    Factory(final ProjectCache projectCache,
+        final GroupControl.Factory groupControlFactory,
         final Provider<CurrentUser> user,
         final IdentifiedUser.GenericFactory userFactory,
         final AccountVisibility accountVisibility) {
+      this.projectCache = projectCache;
       this.groupControlFactory = groupControlFactory;
       this.user = user;
       this.userFactory = userFactory;
@@ -44,20 +50,24 @@
     }
 
     public AccountControl get() {
-      return new AccountControl(groupControlFactory, user.get(), userFactory,
-          accountVisibility);
+      return new AccountControl(projectCache, groupControlFactory, user.get(),
+          userFactory, accountVisibility);
     }
   }
 
+  private final AccountsSection accountsSection;
   private final GroupControl.Factory groupControlFactory;
   private final CurrentUser currentUser;
   private final IdentifiedUser.GenericFactory userFactory;
   private final AccountVisibility accountVisibility;
 
-  AccountControl(final GroupControl.Factory groupControlFactory,
+  AccountControl(final ProjectCache projectCache,
+        final GroupControl.Factory groupControlFactory,
         final CurrentUser currentUser,
         final IdentifiedUser.GenericFactory userFactory,
         final AccountVisibility accountVisibility) {
+    this.accountsSection =
+        projectCache.getAllProjects().getConfig().getAccountsSection();
     this.groupControlFactory = groupControlFactory;
     this.currentUser = currentUser;
     this.userFactory = userFactory;
@@ -73,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;
     }
 
@@ -87,6 +108,12 @@
         Set<AccountGroup.UUID> usersGroups = groupsOf(otherUser);
         usersGroups.remove(AccountGroup.ANONYMOUS_USERS);
         usersGroups.remove(AccountGroup.REGISTERED_USERS);
+        for (PermissionRule rule : accountsSection.getSameGroupVisibility()) {
+          if (rule.isBlock() || rule.isDeny()) {
+            usersGroups.remove(rule.getGroup().getUUID());
+          }
+        }
+
         if (currentUser.getEffectiveGroups().containsAnyOf(usersGroups)) {
           return true;
         }
@@ -115,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/AccountManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
index 6c216a8..e469c34 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountManager.java
@@ -426,6 +426,56 @@
     }
   }
 
+  /**
+   * Unlink an authentication identity from an existing account.
+   *
+   * @param from account to unlink the identity from.
+   * @param who the identity to delete
+   * @return the result of unlinking the identity from the user.
+   * @throws AccountException the identity belongs to a different account, or it
+   *         cannot be unlinked at this time.
+   */
+  public AuthResult unlink(final Account.Id from, AuthRequest who)
+      throws AccountException {
+    try {
+      final ReviewDb db = schema.open();
+      try {
+        who = realm.unlink(db, from, who);
+
+        final AccountExternalId.Key key = id(who);
+        AccountExternalId extId = db.accountExternalIds().get(key);
+        if (extId != null) {
+          if (!extId.getAccountId().equals(from)) {
+            throw new AccountException("Identity in use by another account");
+          }
+          db.accountExternalIds().delete(Collections.singleton(extId));
+
+          if (who.getEmailAddress() != null) {
+            final Account a = db.accounts().get(from);
+            if (a.getPreferredEmail() != null
+                && a.getPreferredEmail().equals(who.getEmailAddress())) {
+              a.setPreferredEmail(null);
+              db.accounts().update(Collections.singleton(a));
+            }
+            byEmailCache.evict(who.getEmailAddress());
+            byIdCache.evict(from);
+          }
+
+        } else {
+          throw new AccountException("Identity not found");
+        }
+
+        return new AuthResult(from, key, false);
+
+      } finally {
+        db.close();
+      }
+    } catch (OrmException e) {
+      throw new AccountException("Cannot unlink identity", e);
+    }
+  }
+
+
   private static AccountExternalId.Key id(final AuthRequest who) {
     return new AccountExternalId.Key(who.getExternalId());
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResolver.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResolver.java
index 3a2ac56..abdf29e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResolver.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResolver.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.account;
 
+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;
@@ -115,7 +116,21 @@
     final int lt = nameOrEmail.indexOf('<');
     final int gt = nameOrEmail.indexOf('>');
     if (lt >= 0 && gt > lt && nameOrEmail.contains("@")) {
-      return byEmail.get(nameOrEmail.substring(lt + 1, gt));
+      Set<Account.Id> ids = byEmail.get(nameOrEmail.substring(lt + 1, gt));
+      if (ids.isEmpty() || ids.size() == 1) {
+        return ids;
+      }
+
+      // more than one match, try to return the best one
+      String name = nameOrEmail.substring(0, lt - 1);
+      Set<Account.Id> nameMatches = Sets.newHashSet();
+      for (Account.Id id : ids) {
+        Account a = byId.get(id).getAccount();
+        if (name.equals(a.getFullName())) {
+          nameMatches.add(id);
+        }
+      }
+      return nameMatches.isEmpty() ? ids : nameMatches;
     }
 
     if (nameOrEmail.contains("@")) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthMethod.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthMethod.java
new file mode 100644
index 0000000..fdaabd2
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthMethod.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.account;
+
+/** Method by which a user has authenticated for a given request. */
+public enum AuthMethod {
+  /** The user is not authenticated */
+  NONE,
+
+  /** The user is authenticated via a cookie. */
+  COOKIE,
+
+  /** The user authenticated with a password for this request. */
+  PASSWORD,
+
+  /** The user has used a credentialess development feature to login. */
+  BACKDOOR;
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
index eb42921..1524185 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
@@ -83,7 +83,7 @@
       || canAdministrateServer();
   }
 
-  /** @return true if the user can create a group. */
+  /** @return true if the user can create a project. */
   public boolean canCreateProject() {
     return canPerform(GlobalCapability.CREATE_PROJECT)
       || canAdministrateServer();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/DefaultRealm.java
index 56a95ba..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,26 +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.List;
 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
@@ -57,12 +51,12 @@
   }
 
   @Override
-  public void onCreateAccount(final AuthRequest who, final Account account) {
+  public AuthRequest unlink(ReviewDb db, Account.Id from, AuthRequest who) {
+    return who;
   }
 
   @Override
-  public GroupMembership groups(final AccountState who) {
-    return groupMembershipFactory.create(who.getInternalGroups());
+  public void onCreateAccount(final AuthRequest who, final Account account) {
   }
 
   @Override
@@ -75,9 +69,4 @@
     }
     return null;
   }
-
-  @Override
-  public Set<AccountGroup.ExternalNameKey> lookupGroups(String name) {
-    return Collections.emptySet();
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupBackend.java
new file mode 100644
index 0000000..b4e770f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupBackend.java
@@ -0,0 +1,51 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import com.google.gerrit.common.data.GroupDescription;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.IdentifiedUser;
+
+import java.util.Collection;
+
+import javax.annotation.Nullable;
+
+/**
+ * Implementations of GroupBackend provide lookup and membership accessors
+ * to a group system.
+ */
+@ExtensionPoint
+public interface GroupBackend {
+  /** @return {@code true} if the backend can operate on the UUID. */
+  boolean handles(AccountGroup.UUID uuid);
+
+  /**
+   * Looks up a group in the backend. If the group does not exist, null is
+   * returned.
+   *
+   * @param uuid the group identifier
+   * @return the group
+   */
+  @Nullable
+  GroupDescription.Basic get(AccountGroup.UUID uuid);
+
+  /** @return suggestions for the group name sorted by name. */
+  Collection<GroupReference> suggest(String name);
+
+  /** @return the group membership checker for the backend. */
+  GroupMembership membershipsOf(IdentifiedUser user);
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupBackends.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupBackends.java
new file mode 100644
index 0000000..cdbb0e4
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupBackends.java
@@ -0,0 +1,89 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import com.google.common.collect.Iterables;
+import com.google.gerrit.common.data.GroupReference;
+
+import java.util.Collection;
+import java.util.Comparator;
+
+import javax.annotation.Nullable;
+
+/**
+ * Utility class for dealing with a GroupBackend.
+ */
+public class GroupBackends {
+
+  public static final Comparator<GroupReference> GROUP_REF_NAME_COMPARATOR =
+      new Comparator<GroupReference>() {
+    @Override
+    public int compare(GroupReference a, GroupReference b) {
+      return a.getName().compareTo(b.getName());
+    }
+  };
+
+  /**
+   * Runs {@link GroupBackend#suggest(String)} and filters the result to return
+   * the best suggestion, or null if one does not exist.
+   *
+   * @param groupBackend the group backend
+   * @param name the name for which to suggest groups
+   * @return the best single GroupReference suggestion
+   */
+  @Nullable
+  public static GroupReference findBestSuggestion(
+      GroupBackend groupBackend, String name) {
+    Collection<GroupReference> refs = groupBackend.suggest(name);
+    if (refs.size() == 1) {
+      return Iterables.getOnlyElement(refs);
+    }
+
+    for (GroupReference ref : refs) {
+      if (isExactSuggestion(ref, name)) {
+        return ref;
+      }
+    }
+    return null;
+  }
+
+  /**
+   * Runs {@link GroupBackend#suggest(String)} and filters the result to return
+   * the exact suggestion, or null if one does not exist.
+   *
+   * @param groupBackend the group backend
+   * @param name the name for which to suggest groups
+   * @return the exact single GroupReference suggestion
+   */
+  @Nullable
+  public static GroupReference findExactSuggestion(
+      GroupBackend groupBackend, String name) {
+    Collection<GroupReference> refs = groupBackend.suggest(name);
+    for (GroupReference ref : refs) {
+      if (isExactSuggestion(ref, name)) {
+        return ref;
+      }
+    }
+    return null;
+  }
+
+  /** Returns whether the GroupReference is an exact suggestion for the name. */
+  public static boolean isExactSuggestion(GroupReference ref, String name) {
+    return ref.getName().equalsIgnoreCase(name) || ref.getUUID().get().equals(name);
+  }
+
+  private GroupBackends() {
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCache.java
index b092ac4..3b9e85f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCache.java
@@ -16,8 +16,6 @@
 
 import com.google.gerrit.reviewdb.client.AccountGroup;
 
-import java.util.Collection;
-
 import javax.annotation.Nullable;
 
 /** Tracks group objects in memory for efficient access. */
@@ -34,8 +32,6 @@
   @Nullable
   public AccountGroup get(AccountGroup.UUID uuid);
 
-  public Collection<AccountGroup> get(AccountGroup.ExternalNameKey externalName);
-
   /** @return sorted iteration of groups. */
   public abstract Iterable<AccountGroup> all();
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java
index d29a5e5..b301839 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java
@@ -14,12 +14,15 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.common.base.Optional;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountGroupName;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.cache.Cache;
 import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.cache.EntryCreator;
+import com.google.gwtorm.server.OrmDuplicateKeyException;
+import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Module;
@@ -27,48 +30,41 @@
 import com.google.inject.TypeLiteral;
 import com.google.inject.name.Named;
 
-import java.util.ArrayList;
-import java.util.Collection;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import java.util.Collections;
 import java.util.List;
-import java.util.SortedSet;
-import java.util.TreeSet;
-import java.util.concurrent.locks.Lock;
-import java.util.concurrent.locks.ReentrantLock;
+import java.util.concurrent.ExecutionException;
 
 /** Tracks group objects in memory for efficient access. */
 @Singleton
 public class GroupCacheImpl implements GroupCache {
+  private static final Logger log = LoggerFactory
+      .getLogger(GroupCacheImpl.class);
+
   private static final String BYID_NAME = "groups";
   private static final String BYNAME_NAME = "groups_byname";
   private static final String BYUUID_NAME = "groups_byuuid";
-  private static final String BYEXT_NAME = "groups_byext";
-  private static final String BYNAME_LIST = "groups_byname_list";
 
   public static Module module() {
     return new CacheModule() {
       @Override
       protected void configure() {
-        final TypeLiteral<Cache<AccountGroup.Id, AccountGroup>> byId =
-            new TypeLiteral<Cache<AccountGroup.Id, AccountGroup>>() {};
-        core(byId, BYID_NAME).populateWith(ByIdLoader.class);
+        cache(BYID_NAME,
+            AccountGroup.Id.class,
+            new TypeLiteral<Optional<AccountGroup>>() {})
+          .loader(ByIdLoader.class);
 
-        final TypeLiteral<Cache<AccountGroup.NameKey, AccountGroup>> byName =
-            new TypeLiteral<Cache<AccountGroup.NameKey, AccountGroup>>() {};
-        core(byName, BYNAME_NAME).populateWith(ByNameLoader.class);
+        cache(BYNAME_NAME,
+            String.class,
+            new TypeLiteral<Optional<AccountGroup>>() {})
+          .loader(ByNameLoader.class);
 
-        final TypeLiteral<Cache<AccountGroup.UUID, AccountGroup>> byUUID =
-            new TypeLiteral<Cache<AccountGroup.UUID, AccountGroup>>() {};
-        core(byUUID, BYUUID_NAME).populateWith(ByUUIDLoader.class);
-
-        final TypeLiteral<Cache<AccountGroup.ExternalNameKey, Collection<AccountGroup>>> byExternalName =
-            new TypeLiteral<Cache<AccountGroup.ExternalNameKey, Collection<AccountGroup>>>() {};
-        core(byExternalName, BYEXT_NAME) //
-            .populateWith(ByExternalNameLoader.class);
-
-        final TypeLiteral<Cache<ListKey, SortedSet<AccountGroup.NameKey>>> listType =
-          new TypeLiteral<Cache<ListKey, SortedSet<AccountGroup.NameKey>>>() {};
-        core(listType, BYNAME_LIST).populateWith(Lister.class);
+        cache(BYUUID_NAME,
+            String.class,
+            new TypeLiteral<Optional<AccountGroup>>() {})
+          .loader(ByUUIDLoader.class);
 
         bind(GroupCacheImpl.class);
         bind(GroupCache.class).to(GroupCacheImpl.class);
@@ -76,94 +72,113 @@
     };
   }
 
-  private final Cache<AccountGroup.Id, AccountGroup> byId;
-  private final Cache<AccountGroup.NameKey, AccountGroup> byName;
-  private final Cache<AccountGroup.UUID, AccountGroup> byUUID;
-  private final Cache<AccountGroup.ExternalNameKey, Collection<AccountGroup>> byExternalName;
-  private final Cache<ListKey,SortedSet<AccountGroup.NameKey>> list;
-  private final Lock listLock;
+  private final LoadingCache<AccountGroup.Id, Optional<AccountGroup>> byId;
+  private final LoadingCache<String, Optional<AccountGroup>> byName;
+  private final LoadingCache<String, Optional<AccountGroup>> byUUID;
+  private final SchemaFactory<ReviewDb> schema;
 
   @Inject
   GroupCacheImpl(
-      @Named(BYID_NAME) Cache<AccountGroup.Id, AccountGroup> byId,
-      @Named(BYNAME_NAME) Cache<AccountGroup.NameKey, AccountGroup> byName,
-      @Named(BYUUID_NAME) Cache<AccountGroup.UUID, AccountGroup> byUUID,
-      @Named(BYEXT_NAME) Cache<AccountGroup.ExternalNameKey, Collection<AccountGroup>> byExternalName,
-      @Named(BYNAME_LIST) final Cache<ListKey, SortedSet<AccountGroup.NameKey>> list) {
+      @Named(BYID_NAME) LoadingCache<AccountGroup.Id, Optional<AccountGroup>> byId,
+      @Named(BYNAME_NAME) LoadingCache<String, Optional<AccountGroup>> byName,
+      @Named(BYUUID_NAME) LoadingCache<String, Optional<AccountGroup>> byUUID,
+      SchemaFactory<ReviewDb> schema) {
     this.byId = byId;
     this.byName = byName;
     this.byUUID = byUUID;
-    this.byExternalName = byExternalName;
-    this.list = list;
-    this.listLock = new ReentrantLock(true /* fair */);
+    this.schema = schema;
   }
 
+  @Override
   public AccountGroup get(final AccountGroup.Id groupId) {
-    return byId.get(groupId);
+    try {
+      Optional<AccountGroup> g = byId.get(groupId);
+      return g.isPresent() ? g.get() : missing(groupId);
+    } catch (ExecutionException e) {
+      log.warn("Cannot load group "+groupId, e);
+      return missing(groupId);
+    }
   }
 
+  @Override
   public void evict(final AccountGroup group) {
-    byId.remove(group.getId());
-    byName.remove(group.getNameKey());
-    byUUID.remove(group.getGroupUUID());
-    byExternalName.remove(group.getExternalNameKey());
+    if (group.getId() != null) {
+      byId.invalidate(group.getId());
+    }
+    if (group.getNameKey() != null) {
+      byName.invalidate(group.getNameKey().get());
+    }
+    if (group.getGroupUUID() != null) {
+      byUUID.invalidate(group.getGroupUUID().get());
+    }
   }
 
+  @Override
   public void evictAfterRename(final AccountGroup.NameKey oldName,
       final AccountGroup.NameKey newName) {
-    byName.remove(oldName);
-    updateGroupList(oldName, newName);
+    if (oldName != null) {
+      byName.invalidate(oldName.get());
+    }
+    if (newName != null) {
+      byName.invalidate(newName.get());
+    }
   }
 
-  public AccountGroup get(final AccountGroup.NameKey name) {
-    return byName.get(name);
+  @Override
+  public AccountGroup get(AccountGroup.NameKey name) {
+    if (name == null) {
+      return null;
+    }
+    try {
+      return byName.get(name.get()).orNull();
+    } catch (ExecutionException e) {
+      log.warn(String.format("Cannot lookup group %s by name", name.get()), e);
+      return null;
+    }
   }
 
-  public AccountGroup get(final AccountGroup.UUID uuid) {
-    return byUUID.get(uuid);
-  }
-
-  public Collection<AccountGroup> get(
-      final AccountGroup.ExternalNameKey externalName) {
-    return byExternalName.get(externalName);
+  @Override
+  public AccountGroup get(AccountGroup.UUID uuid) {
+    if (uuid == null) {
+      return null;
+    }
+    try {
+      return byUUID.get(uuid.get()).orNull();
+    } catch (ExecutionException e) {
+      log.warn(String.format("Cannot lookup group %s by name", uuid.get()), e);
+      return null;
+    }
   }
 
   @Override
   public Iterable<AccountGroup> all() {
-    final List<AccountGroup> groups = new ArrayList<AccountGroup>();
-    for (final AccountGroup.NameKey groupName : list.get(ListKey.ALL)) {
-      final AccountGroup group = get(groupName);
-      if (group != null) {
-        groups.add(group);
+    try {
+      ReviewDb db = schema.open();
+      try {
+        return Collections.unmodifiableList(db.accountGroups().all().toList());
+      } finally {
+        db.close();
       }
+    } catch (OrmException e) {
+      log.warn("Cannot list internal groups", e);
+      return Collections.emptyList();
     }
-    return Collections.unmodifiableList(groups);
   }
 
   @Override
-  public void onCreateGroup(final AccountGroup.NameKey newGroupName) {
-    updateGroupList(null, newGroupName);
+  public void onCreateGroup(AccountGroup.NameKey newGroupName) {
+    byName.invalidate(newGroupName.get());
   }
 
-  private void updateGroupList(final AccountGroup.NameKey nameToRemove,
-      final AccountGroup.NameKey nameToAdd) {
-    listLock.lock();
-    try {
-      SortedSet<AccountGroup.NameKey> n = list.get(ListKey.ALL);
-      n = new TreeSet<AccountGroup.NameKey>(n);
-      if (nameToRemove != null) {
-        n.remove(nameToRemove);
-      }
-      if (nameToAdd != null) {
-        n.add(nameToAdd);
-      }
-      list.put(ListKey.ALL, Collections.unmodifiableSortedSet(n));
-    } finally {
-      listLock.unlock();
-    }
+  private static AccountGroup missing(AccountGroup.Id key) {
+    AccountGroup.NameKey name = new AccountGroup.NameKey("Deleted Group" + key);
+    AccountGroup g = new AccountGroup(name, key, null);
+    g.setType(AccountGroup.Type.SYSTEM);
+    return g;
   }
 
-  static class ByIdLoader extends EntryCreator<AccountGroup.Id, AccountGroup> {
+  static class ByIdLoader extends
+      CacheLoader<AccountGroup.Id, Optional<AccountGroup>> {
     private final SchemaFactory<ReviewDb> schema;
 
     @Inject
@@ -172,32 +187,18 @@
     }
 
     @Override
-    public AccountGroup createEntry(final AccountGroup.Id key) throws Exception {
+    public Optional<AccountGroup> load(final AccountGroup.Id key)
+        throws Exception {
       final ReviewDb db = schema.open();
       try {
-        final AccountGroup group = db.accountGroups().get(key);
-        if (group != null) {
-          return group;
-        } else {
-          return missing(key);
-        }
+        return Optional.fromNullable(db.accountGroups().get(key));
       } finally {
         db.close();
       }
     }
-
-    @Override
-    public AccountGroup missing(final AccountGroup.Id key) {
-      final AccountGroup.NameKey name =
-          new AccountGroup.NameKey("Deleted Group" + key.toString());
-      final AccountGroup g = new AccountGroup(name, key, null);
-      g.setType(AccountGroup.Type.SYSTEM);
-      return g;
-    }
   }
 
-  static class ByNameLoader extends
-      EntryCreator<AccountGroup.NameKey, AccountGroup> {
+  static class ByNameLoader extends CacheLoader<String, Optional<AccountGroup>> {
     private final SchemaFactory<ReviewDb> schema;
 
     @Inject
@@ -206,25 +207,23 @@
     }
 
     @Override
-    public AccountGroup createEntry(final AccountGroup.NameKey key)
+    public Optional<AccountGroup> load(String name)
         throws Exception {
-      final AccountGroupName r;
       final ReviewDb db = schema.open();
       try {
-        r = db.accountGroupNames().get(key);
+        AccountGroup.NameKey key = new AccountGroup.NameKey(name);
+        AccountGroupName r = db.accountGroupNames().get(key);
         if (r != null) {
-          return db.accountGroups().get(r.getId());
-        } else {
-          return null;
+          return Optional.fromNullable(db.accountGroups().get(r.getId()));
         }
+        return Optional.absent();
       } finally {
         db.close();
       }
     }
   }
 
-  static class ByUUIDLoader extends
-      EntryCreator<AccountGroup.UUID, AccountGroup> {
+  static class ByUUIDLoader extends CacheLoader<String, Optional<AccountGroup>> {
     private final SchemaFactory<ReviewDb> schema;
 
     @Inject
@@ -233,74 +232,23 @@
     }
 
     @Override
-    public AccountGroup createEntry(final AccountGroup.UUID uuid)
+    public Optional<AccountGroup> load(String uuid)
         throws Exception {
       final ReviewDb db = schema.open();
       try {
-        List<AccountGroup> r = db.accountGroups().byUUID(uuid).toList();
+        List<AccountGroup> r;
+
+        r = db.accountGroups().byUUID(new AccountGroup.UUID(uuid)).toList();
         if (r.size() == 1) {
-          return r.get(0);
+          return Optional.of(r.get(0));
+        } else if (r.size() == 0) {
+          return Optional.absent();
         } else {
-          return null;
+          throw new OrmDuplicateKeyException("Duplicate group UUID " + uuid);
         }
       } finally {
         db.close();
       }
     }
   }
-
-  static class ByExternalNameLoader extends
-      EntryCreator<AccountGroup.ExternalNameKey, Collection<AccountGroup>> {
-    private final SchemaFactory<ReviewDb> schema;
-
-    @Inject
-    ByExternalNameLoader(final SchemaFactory<ReviewDb> sf) {
-      schema = sf;
-    }
-
-    @Override
-    public Collection<AccountGroup> createEntry(
-        final AccountGroup.ExternalNameKey key) throws Exception {
-      final ReviewDb db = schema.open();
-      try {
-        return db.accountGroups().byExternalName(key).toList();
-      } finally {
-        db.close();
-      }
-    }
-  }
-
-  static class ListKey {
-    static final ListKey ALL = new ListKey();
-
-    private ListKey() {
-    }
-  }
-
-  static class Lister extends EntryCreator<ListKey, SortedSet<AccountGroup.NameKey>> {
-    private final SchemaFactory<ReviewDb> schema;
-
-    @Inject
-    Lister(final SchemaFactory<ReviewDb> sf) {
-      schema = sf;
-    }
-
-    @Override
-    public SortedSet<AccountGroup.NameKey> createEntry(ListKey key)
-        throws Exception {
-      final ReviewDb db = schema.open();
-      try {
-        final List<AccountGroupName> groupNames =
-            db.accountGroupNames().all().toList();
-        final SortedSet<AccountGroup.NameKey> groups =
-            new TreeSet<AccountGroup.NameKey>();
-        for (final AccountGroupName groupName : groupNames) {
-          groups.add(groupName.getNameKey());
-        }
-        return Collections.unmodifiableSortedSet(groups);
-      } finally {
-        db.close();
-      }
-    }
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java
index f7451a8..d9b12ac 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.gerrit.common.data.GroupDescription;
+import com.google.gerrit.common.data.GroupDescriptions;
 import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
@@ -27,11 +29,14 @@
   public static class Factory {
     private final GroupCache groupCache;
     private final Provider<CurrentUser> user;
+    private final GroupBackend groupBackend;
 
     @Inject
-    Factory(final GroupCache gc, final Provider<CurrentUser> cu) {
+    Factory(final GroupCache gc, final Provider<CurrentUser> cu,
+        final GroupBackend gb) {
       groupCache = gc;
       user = cu;
+      groupBackend = gb;
     }
 
     public GroupControl controlFor(final AccountGroup.Id groupId)
@@ -40,20 +45,20 @@
       if (group == null) {
         throw new NoSuchGroupException(groupId);
       }
-      return new GroupControl(groupCache, user.get(), group);
+      return new GroupControl(user.get(), group);
     }
 
     public GroupControl controlFor(final AccountGroup.UUID groupId)
         throws NoSuchGroupException {
-      final AccountGroup group = groupCache.get(groupId);
+      final GroupDescription.Basic group = groupBackend.get(groupId);
       if (group == null) {
         throw new NoSuchGroupException(groupId);
       }
-      return new GroupControl(groupCache, user.get(), group);
+      return new GroupControl(user.get(), group);
     }
 
     public GroupControl controlFor(final AccountGroup group) {
-      return new GroupControl(groupCache, user.get(), group);
+      return new GroupControl(user.get(), group);
     }
 
     public GroupControl validateFor(final AccountGroup.Id groupId)
@@ -66,25 +71,23 @@
     }
   }
 
-  private final GroupCache groupCache;
   private final CurrentUser user;
-  private final AccountGroup group;
+  private final GroupDescription.Basic group;
   private Boolean isOwner;
 
-  GroupControl(GroupCache g, CurrentUser who, AccountGroup gc) {
-    groupCache = g;
+  GroupControl(CurrentUser who, GroupDescription.Basic gd) {
     user = who;
-    group = gc;
+    group =  gd;
+  }
+
+  GroupControl(CurrentUser who, AccountGroup ag) {
+    this(who, GroupDescriptions.forAccountGroup(ag));
   }
 
   public CurrentUser getCurrentUser() {
     return user;
   }
 
-  public AccountGroup getAccountGroup() {
-    return group;
-  }
-
   /** Can this user see this group exists? */
   public boolean isVisible() {
     return group.isVisibleToAll()
@@ -93,9 +96,11 @@
   }
 
   public boolean isOwner() {
-    if (isOwner == null) {
-      AccountGroup g = groupCache.get(group.getOwnerGroupId());
-      AccountGroup.UUID ownerUUID = g != null ? g.getGroupUUID() : null;
+    AccountGroup accountGroup = GroupDescriptions.toAccountGroup(group);
+    if (accountGroup == null) {
+      isOwner = false;
+    } else if (isOwner == null) {
+      AccountGroup.UUID ownerUUID = accountGroup.getOwnerGroupUUID();
       isOwner = getCurrentUser().getEffectiveGroups().contains(ownerUUID)
              || getCurrentUser().getCapabilities().canAdministrateServer();
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupDetailFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupDetailFactory.java
index 7f88134..2e500bd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupDetailFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupDetailFactory.java
@@ -14,7 +14,9 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.common.data.GroupDetail;
+import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
@@ -39,6 +41,7 @@
   private final ReviewDb db;
   private final GroupControl.Factory groupControl;
   private final GroupCache groupCache;
+  private final GroupBackend groupBackend;
   private final AccountInfoCacheFactory aic;
   private final GroupInfoCacheFactory gic;
 
@@ -48,12 +51,14 @@
   @Inject
   GroupDetailFactory(final ReviewDb db,
       final GroupControl.Factory groupControl, final GroupCache groupCache,
+      final GroupBackend groupBackend,
       final AccountInfoCacheFactory.Factory accountInfoCacheFactory,
       final GroupInfoCacheFactory.Factory groupInfoCacheFactory,
       @Assisted final AccountGroup.Id groupId) {
     this.db = db;
     this.groupControl = groupControl;
     this.groupCache = groupCache;
+    this.groupBackend = groupBackend;
     this.aic = accountInfoCacheFactory.create();
     this.gic = groupInfoCacheFactory.create();
 
@@ -63,10 +68,13 @@
   @Override
   public GroupDetail call() throws OrmException, NoSuchGroupException {
     control = groupControl.validateFor(groupId);
-    final AccountGroup group = control.getAccountGroup();
+    final AccountGroup group = groupCache.get(groupId);
     final GroupDetail detail = new GroupDetail();
     detail.setGroup(group);
-    detail.setOwnerGroup(groupCache.get(group.getOwnerGroupId()));
+    GroupDescription.Basic ownerGroup = groupBackend.get(group.getOwnerGroupUUID());
+    if (ownerGroup != null) {
+      detail.setOwnerGroup(GroupReference.forGroup(ownerGroup));
+    }
     switch (group.getType()) {
       case INTERNAL:
         detail.setMembers(loadMembers());
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
index 791d0f5..7fbba45 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
@@ -14,12 +14,14 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountGroupInclude;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.cache.Cache;
 import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.cache.EntryCreator;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Module;
@@ -27,24 +29,30 @@
 import com.google.inject.TypeLiteral;
 import com.google.inject.name.Named;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import java.util.Collection;
 import java.util.Collections;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
+import java.util.concurrent.ExecutionException;
 
 /** Tracks group inclusions in memory for efficient access. */
 @Singleton
 public class GroupIncludeCacheImpl implements GroupIncludeCache {
+  private static final Logger log = LoggerFactory
+      .getLogger(GroupIncludeCacheImpl.class);
   private static final String BYINCLUDE_NAME = "groups_byinclude";
 
   public static Module module() {
     return new CacheModule() {
       @Override
       protected void configure() {
-        final TypeLiteral<Cache<AccountGroup.UUID, Collection<AccountGroup.UUID>>> byInclude =
-            new TypeLiteral<Cache<AccountGroup.UUID, Collection<AccountGroup.UUID>>>() {};
-        core(byInclude, BYINCLUDE_NAME).populateWith(ByIncludeLoader.class);
+        cache(BYINCLUDE_NAME,
+            AccountGroup.UUID.class,
+            new TypeLiteral<Set<AccountGroup.UUID>>() {})
+          .loader(ByIncludeLoader.class);
 
         bind(GroupIncludeCacheImpl.class);
         bind(GroupIncludeCache.class).to(GroupIncludeCacheImpl.class);
@@ -52,24 +60,31 @@
     };
   }
 
-  private final Cache<AccountGroup.UUID, Collection<AccountGroup.UUID>> byInclude;
+  private final LoadingCache<AccountGroup.UUID, Set<AccountGroup.UUID>> byInclude;
 
   @Inject
   GroupIncludeCacheImpl(
-      @Named(BYINCLUDE_NAME) Cache<AccountGroup.UUID, Collection<AccountGroup.UUID>> byInclude) {
+      @Named(BYINCLUDE_NAME) LoadingCache<AccountGroup.UUID, Set<AccountGroup.UUID>> byInclude) {
     this.byInclude = byInclude;
   }
 
   public Collection<AccountGroup.UUID> getByInclude(AccountGroup.UUID groupId) {
-    return byInclude.get(groupId);
+    try {
+      return byInclude.get(groupId);
+    } catch (ExecutionException e) {
+      log.warn("Cannot load included groups", e);
+      return Collections.emptySet();
+    }
   }
 
   public void evictInclude(AccountGroup.UUID groupId) {
-    byInclude.remove(groupId);
+    if (groupId != null) {
+      byInclude.invalidate(groupId);
+    }
   }
 
   static class ByIncludeLoader extends
-      EntryCreator<AccountGroup.UUID, Collection<AccountGroup.UUID>> {
+      CacheLoader<AccountGroup.UUID, Set<AccountGroup.UUID>> {
     private final SchemaFactory<ReviewDb> schema;
 
     @Inject
@@ -78,32 +93,28 @@
     }
 
     @Override
-    public Collection<AccountGroup.UUID> createEntry(final AccountGroup.UUID key) throws Exception {
+    public Set<AccountGroup.UUID> load(AccountGroup.UUID key) throws Exception {
       final ReviewDb db = schema.open();
       try {
         List<AccountGroup> group = db.accountGroups().byUUID(key).toList();
         if (group.size() != 1) {
-          return Collections.emptyList();
+          return Collections.emptySet();
         }
 
-        Set<AccountGroup.Id> ids = new HashSet<AccountGroup.Id>();
-        for (AccountGroupInclude agi : db.accountGroupIncludes().byInclude(group.get(0).getId())) {
+        Set<AccountGroup.Id> ids = Sets.newHashSet();
+        for (AccountGroupInclude agi : db.accountGroupIncludes()
+            .byInclude(group.get(0).getId())) {
           ids.add(agi.getGroupId());
         }
 
-        Set<AccountGroup.UUID> groupArray = new HashSet<AccountGroup.UUID> ();
+        Set<AccountGroup.UUID> groupArray = Sets.newHashSet();
         for (AccountGroup g : db.accountGroups().get(ids)) {
           groupArray.add(g.getGroupUUID());
         }
-        return Collections.unmodifiableCollection(groupArray);
+        return ImmutableSet.copyOf(groupArray);
       } finally {
         db.close();
       }
     }
-
-    @Override
-    public Collection<AccountGroup.UUID> missing(final AccountGroup.UUID key) {
-      return Collections.emptyList();
-    }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembership.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembership.java
index 9bb571e..d536c09 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembership.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembership.java
@@ -24,7 +24,6 @@
  * the presence of a user in a particular group.
  */
 public interface GroupMembership {
-
   public static final GroupMembership EMPTY =
       new ListGroupMembership(Collections.<AccountGroup.UUID>emptySet());
 
@@ -45,7 +44,7 @@
    * This may not return all groups the {@link #contains(AccountGroup.UUID)}
    * would return {@code true} for, but will at least contain all top level
    * groups. This restriction stems from the API of some group systems, which
-   * make it expensive to enumate the members of a group.
+   * make it expensive to enumerate the members of a group.
    */
   Set<AccountGroup.UUID> getKnownGroups();
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/MaterializedGroupMembership.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/IncludingGroupMembership.java
similarity index 89%
rename from gerrit-server/src/main/java/com/google/gerrit/server/account/MaterializedGroupMembership.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/account/IncludingGroupMembership.java
index 81ff656..d448fff 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/MaterializedGroupMembership.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/IncludingGroupMembership.java
@@ -25,11 +25,12 @@
 import java.util.Set;
 
 /**
- * Creates a GroupMembership object from materialized collection of groups.
+ * Creates a GroupMembership checker for the internal group system, which
+ * starts with the seed groups and includes all child groups.
  */
-public class MaterializedGroupMembership implements GroupMembership {
+public class IncludingGroupMembership implements GroupMembership {
   public interface Factory {
-    MaterializedGroupMembership create(Iterable<AccountGroup.UUID> groupIds);
+    IncludingGroupMembership create(Iterable<AccountGroup.UUID> groupIds);
   }
 
   private final GroupIncludeCache groupIncludeCache;
@@ -37,7 +38,7 @@
   private final Queue<AccountGroup.UUID> groupQueue;
 
   @Inject
-  MaterializedGroupMembership(
+  IncludingGroupMembership(
       GroupIncludeCache groupIncludeCache,
       @Assisted Iterable<AccountGroup.UUID> seedGroups) {
     this.groupIncludeCache = groupIncludeCache;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalGroupBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalGroupBackend.java
new file mode 100644
index 0000000..ad65499
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalGroupBackend.java
@@ -0,0 +1,94 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import com.google.common.base.Function;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.gerrit.common.data.GroupDescription;
+import com.google.gerrit.common.data.GroupDescriptions;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import java.util.Collection;
+
+/**
+ * Implementation of GroupBackend for the internal group system.
+ */
+@Singleton
+public class InternalGroupBackend implements GroupBackend {
+  private static final Function<AccountGroup, GroupReference> ACT_GROUP_TO_GROUP_REF =
+      new Function<AccountGroup, GroupReference>() {
+        @Override
+        public GroupReference apply(AccountGroup group) {
+          return GroupReference.forGroup(group);
+        }
+      };
+
+  private final GroupControl.Factory groupControlFactory;
+  private final GroupCache groupCache;
+  private final IncludingGroupMembership.Factory groupMembershipFactory;
+
+
+  @Inject
+  InternalGroupBackend(GroupControl.Factory groupControlFactory,
+      GroupCache groupCache,
+      IncludingGroupMembership.Factory groupMembershipFactory) {
+    this.groupControlFactory = groupControlFactory;
+    this.groupCache = groupCache;
+    this.groupMembershipFactory = groupMembershipFactory;
+  }
+
+  @Override
+  public boolean handles(AccountGroup.UUID uuid) {
+    return AccountGroup.isInternalGroup(uuid);
+  }
+
+  @Override
+  public GroupDescription.Internal get(AccountGroup.UUID uuid) {
+    if (!handles(uuid)) {
+      return null;
+    }
+
+    AccountGroup g = groupCache.get(uuid);
+    if (g == null) {
+      return null;
+    }
+    return GroupDescriptions.forAccountGroup(g);
+  }
+
+  @Override
+  public Collection<GroupReference> suggest(final String name) {
+    Iterable<AccountGroup> filtered = Iterables.filter(groupCache.all(),
+        new Predicate<AccountGroup>() {
+          @Override
+          public boolean apply(AccountGroup group) {
+            // startsWithIgnoreCase && isVisible
+            return group.getName().regionMatches(true, 0, name, 0, name.length())
+                && groupControlFactory.controlFor(group).isVisible();
+          }
+        });
+    return Lists.newArrayList(Iterables.transform(filtered, ACT_GROUP_TO_GROUP_REF));
+  }
+
+  @Override
+  public GroupMembership membershipsOf(IdentifiedUser user) {
+    return groupMembershipFactory.create(user.state().getInternalGroups());
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/ListGroupMembership.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/ListGroupMembership.java
index 237d381..346f406 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/ListGroupMembership.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/ListGroupMembership.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.account;
 
 import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 
 import java.util.Set;
@@ -47,6 +48,6 @@
 
   @Override
   public Set<AccountGroup.UUID> getKnownGroups() {
-    return ImmutableSet.copyOf(groups);
+    return Sets.newHashSet(groups);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PerformCreateGroup.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PerformCreateGroup.java
index 5f94df2..1ff1a3e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PerformCreateGroup.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PerformCreateGroup.java
@@ -107,7 +107,10 @@
     final AccountGroup group = new AccountGroup(nameKey, groupId, uuid);
     group.setVisibleToAll(visibleToAll);
     if (ownerGroupId != null) {
-      group.setOwnerGroupId(ownerGroupId);
+      AccountGroup ownerGroup = groupCache.get(ownerGroupId);
+      if (ownerGroup != null) {
+        group.setOwnerGroupUUID(ownerGroup.getGroupUUID());
+      }
     }
     if (groupDescription != null) {
       group.setDescription(groupDescription);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PerformRenameGroup.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PerformRenameGroup.java
index 6db232a..4c72d49 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PerformRenameGroup.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PerformRenameGroup.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.account;
 
 import com.google.gerrit.common.data.GroupDetail;
+import com.google.gerrit.common.errors.InvalidNameException;
 import com.google.gerrit.common.errors.NameAlreadyUsedException;
 import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.reviewdb.client.AccountGroup;
@@ -60,7 +61,7 @@
 
   public GroupDetail renameGroup(final String groupName,
       final String newGroupName) throws OrmException, NameAlreadyUsedException,
-      NoSuchGroupException {
+      NoSuchGroupException, InvalidNameException {
     final AccountGroup.NameKey groupNameKey =
         new AccountGroup.NameKey(groupName);
     final AccountGroup group = groupCache.get(groupNameKey);
@@ -72,12 +73,15 @@
 
   public GroupDetail renameGroup(final AccountGroup.Id groupId,
       final String newName) throws OrmException, NameAlreadyUsedException,
-      NoSuchGroupException {
+      NoSuchGroupException, InvalidNameException {
     final GroupControl ctl = groupControlFactory.validateFor(groupId);
     final AccountGroup group = db.accountGroups().get(groupId);
     if (group == null || !ctl.isOwner()) {
       throw new NoSuchGroupException(groupId);
     }
+    if (newName.trim().isEmpty()) {
+      throw new InvalidNameException();
+    }
 
     final AccountGroup.NameKey old = group.getNameKey();
     final AccountGroup.NameKey key = new AccountGroup.NameKey(newName);
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 fc7c0be..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);
@@ -29,9 +26,10 @@
   public AuthRequest link(ReviewDb db, Account.Id to, AuthRequest who)
       throws AccountException;
 
-  public void onCreateAccount(AuthRequest who, Account account);
+  public AuthRequest unlink(ReviewDb db, Account.Id to, AuthRequest who)
+      throws AccountException;
 
-  public GroupMembership groups(AccountState who);
+  public void onCreateAccount(AuthRequest who, Account account);
 
   /**
    * Locate an account whose local username is the given account name.
@@ -42,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-server/src/main/java/com/google/gerrit/server/account/VisibleGroups.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/VisibleGroups.java
index 3112ed4..d3b2c83 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/VisibleGroups.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/VisibleGroups.java
@@ -14,23 +14,22 @@
 
 package com.google.gerrit.server.account;
 
-import com.google.gerrit.common.data.GroupDetail;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
 import com.google.gerrit.common.data.GroupList;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.project.ProjectControl;
-import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-import java.util.ArrayList;
 import java.util.Collection;
-import java.util.LinkedList;
+import java.util.Collections;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
-import java.util.TreeSet;
 
 public class VisibleGroups {
 
@@ -41,7 +40,6 @@
   private final Provider<IdentifiedUser> identifiedUser;
   private final GroupCache groupCache;
   private final GroupControl.Factory groupControlFactory;
-  private final GroupDetailFactory.Factory groupDetailFactory;
 
   private boolean onlyVisibleToAll;
   private AccountGroup.Type groupType;
@@ -49,12 +47,10 @@
   @Inject
   VisibleGroups(final Provider<IdentifiedUser> currentUser,
       final GroupCache groupCache,
-      final GroupControl.Factory groupControlFactory,
-      final GroupDetailFactory.Factory groupDetailFactory) {
+      final GroupControl.Factory groupControlFactory) {
     this.identifiedUser = currentUser;
     this.groupCache = groupCache;
     this.groupControlFactory = groupControlFactory;
-    this.groupDetailFactory = groupDetailFactory;
   }
 
   public void setOnlyVisibleToAll(final boolean onlyVisibleToAll) {
@@ -65,15 +61,13 @@
     this.groupType = groupType;
   }
 
-  public GroupList get() throws OrmException, NoSuchGroupException {
-    final Iterable<AccountGroup> groups = groupCache.all();
-    return createGroupList(filterGroups(groups));
+  public GroupList get() {
+    return createGroupList(filterGroups(groupCache.all()));
   }
 
   public GroupList get(final Collection<ProjectControl> projects)
-      throws OrmException, NoSuchGroupException {
-    final Set<AccountGroup> groups =
-        new TreeSet<AccountGroup>(new GroupComparator());
+      throws NoSuchGroupException {
+    Map<AccountGroup.UUID, AccountGroup> groups = Maps.newHashMap();
     for (final ProjectControl projectControl : projects) {
       final Set<GroupReference> groupsRefs = projectControl.getAllGroups();
       for (final GroupReference groupRef : groupsRefs) {
@@ -81,10 +75,10 @@
         if (group == null) {
           throw new NoSuchGroupException(groupRef.getUUID());
         }
-        groups.add(group);
+        groups.put(group.getGroupUUID(), group);
       }
     }
-    return createGroupList(filterGroups(groups));
+    return createGroupList(filterGroups(groups.values()));
   }
 
   /**
@@ -93,21 +87,18 @@
    * groups.
    * @See GroupMembership#getKnownGroups()
    */
-  public GroupList get(final IdentifiedUser user) throws OrmException,
-      NoSuchGroupException {
+  public GroupList get(final IdentifiedUser user) throws NoSuchGroupException {
     if (identifiedUser.get().getAccountId().equals(user.getAccountId())
         || identifiedUser.get().getCapabilities().canAdministrateServer()) {
-      final Set<AccountGroup.UUID> effective =
-          user.getEffectiveGroups().getKnownGroups();
-      final Set<AccountGroup> groups =
-          new TreeSet<AccountGroup>(new GroupComparator());
-      for (final AccountGroup.UUID groupId : effective) {
+      Set<AccountGroup.UUID> mine = user.getEffectiveGroups().getKnownGroups();
+      Map<AccountGroup.UUID, AccountGroup> groups = Maps.newHashMap();
+      for (final AccountGroup.UUID groupId : mine) {
         AccountGroup group = groupCache.get(groupId);
         if (group != null) {
-          groups.add(group);
+          groups.put(groupId, group);
         }
       }
-      return createGroupList(filterGroups(groups));
+      return createGroupList(filterGroups(groups.values()));
     } else {
       throw new NoSuchGroupException("Groups of user '" + user.getAccountId()
           + "' are not visible.");
@@ -115,7 +106,7 @@
   }
 
   private List<AccountGroup> filterGroups(final Iterable<AccountGroup> groups) {
-    final List<AccountGroup> filteredGroups = new LinkedList<AccountGroup>();
+    final List<AccountGroup> filteredGroups = Lists.newArrayList();
     final boolean isAdmin =
         identifiedUser.get().getCapabilities().canAdministrateServer();
     for (final AccountGroup group : groups) {
@@ -131,16 +122,12 @@
       }
       filteredGroups.add(group);
     }
+    Collections.sort(filteredGroups, new GroupComparator());
     return filteredGroups;
   }
 
-  private GroupList createGroupList(final List<AccountGroup> groups)
-      throws OrmException, NoSuchGroupException {
-    final List<GroupDetail> groupDetailList = new ArrayList<GroupDetail>();
-    for (final AccountGroup group : groups) {
-      groupDetailList.add(groupDetailFactory.create(group.getId()).call());
-    }
-    return new GroupList(groupDetailList, identifiedUser.get()
+  private GroupList createGroupList(final List<AccountGroup> groups) {
+    return new GroupList(groups, identifiedUser.get()
         .getCapabilities().canCreateGroup());
   }
 }
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-server/src/main/java/com/google/gerrit/server/args4j/ChangeIdHandler.java b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/ChangeIdHandler.java
new file mode 100644
index 0000000..9c3d052
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/ChangeIdHandler.java
@@ -0,0 +1,78 @@
+// 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.args4j;
+
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import org.kohsuke.args4j.CmdLineException;
+import org.kohsuke.args4j.CmdLineParser;
+import org.kohsuke.args4j.OptionDef;
+import org.kohsuke.args4j.spi.OptionHandler;
+import org.kohsuke.args4j.spi.Parameters;
+import org.kohsuke.args4j.spi.Setter;
+
+public class ChangeIdHandler extends OptionHandler<Change.Id> {
+
+  @Inject
+  private ReviewDb db;
+
+  @Inject
+  public ChangeIdHandler(
+      final ReviewDb db,
+      @Assisted final CmdLineParser parser, @Assisted final OptionDef option,
+      @Assisted final Setter<Change.Id> setter) {
+    super(parser, option, setter);
+    this.db = db;
+  }
+
+  @Override
+  public final int parseArguments(final Parameters params)
+      throws CmdLineException {
+    final String token = params.getParameter(0);
+    final String[] tokens = token.split(",");
+    if (tokens.length != 3) {
+      throw new CmdLineException(owner, "change should be specified as "
+                                 + "<project>,<branch>,<change-id>");
+    }
+
+    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, "refs/heads/" + tokens[1]);
+      for (final Change change : db.changes().byBranchKey(branch, key)) {
+        setter.addValue(change.getId());
+        return 1;
+      }
+    } catch (IllegalArgumentException e) {
+      throw new CmdLineException(owner, "Change-Id is not valid");
+    } catch (OrmException e) {
+      throw new CmdLineException(owner, "Database error: " + e.getMessage());
+    }
+
+    throw new CmdLineException(owner, "\"" + token + "\": change not found");
+  }
+
+  @Override
+  public final String getDefaultMetaVariable() {
+    return "CHANGE";
+  }
+}
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/ObjectIdHandler.java
similarity index 63%
copy from gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/SubcommandHandler.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/args4j/ObjectIdHandler.java
index 3df73a8..b7f2fb9 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/ObjectIdHandler.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,11 +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.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
+import org.eclipse.jgit.lib.ObjectId;
 import org.kohsuke.args4j.CmdLineException;
 import org.kohsuke.args4j.CmdLineParser;
 import org.kohsuke.args4j.OptionDef;
@@ -24,24 +25,23 @@
 import org.kohsuke.args4j.spi.Parameters;
 import org.kohsuke.args4j.spi.Setter;
 
-public class SubcommandHandler extends OptionHandler<String> {
+public class ObjectIdHandler extends OptionHandler<ObjectId> {
 
   @Inject
-  public SubcommandHandler(@Assisted final CmdLineParser parser,
-      @Assisted final OptionDef option, @Assisted final Setter<String> setter) {
+  public ObjectIdHandler(@Assisted final CmdLineParser parser,
+      @Assisted final OptionDef option, @Assisted final Setter<ObjectId> setter) {
     super(parser, option, setter);
   }
 
   @Override
-  public final int parseArguments(final Parameters params)
-      throws CmdLineException {
-    setter.addValue(params.getParameter(0));
-    owner.stopOptionParsing();
+  public int parseArguments(Parameters params) throws CmdLineException {
+    final String n = params.getParameter(0);
+    setter.addValue(ObjectId.fromString(n));
     return 1;
   }
 
   @Override
-  public final String getDefaultMetaVariable() {
-    return "COMMAND";
+  public String getDefaultMetaVariable() {
+    return "COMMIT";
   }
 }
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 94%
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..da033e7 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;
@@ -71,7 +71,7 @@
     final ProjectControl control;
     try {
       Project.NameKey nameKey = new Project.NameKey(projectName);
-      control = projectControlFactory.validateFor(nameKey);
+      control = projectControlFactory.validateFor(nameKey, ProjectControl.OWNER | ProjectControl.VISIBLE);
     } catch (NoSuchProjectException e) {
       throw new CmdLineException(owner, "'" + token + "': not a Gerrit project");
     }
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..644f5df 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
@@ -14,15 +14,17 @@
 
 package com.google.gerrit.server.auth.ldap;
 
+import com.google.common.cache.Cache;
+import com.google.common.collect.ImmutableSet;
 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;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
+import com.google.inject.name.Named;
 
 import org.eclipse.jgit.lib.Config;
 
@@ -47,7 +49,9 @@
 import javax.net.ssl.SSLSocketFactory;
 
 @Singleton class Helper {
-  private final GroupCache groupCache;
+  static final String LDAP_UUID = "ldap:";
+
+  private final Cache<String, ImmutableSet<String>> groupsByInclude;
   private final Config config;
   private final String server;
   private final String username;
@@ -58,8 +62,9 @@
   private final String readTimeOutMillis;
 
   @Inject
-  Helper(@GerritServerConfig final Config config, final GroupCache groupCache) {
-    this.groupCache = groupCache;
+  Helper(@GerritServerConfig final Config config,
+      @Named(LdapModule.GROUPS_BYINCLUDE_CACHE)
+      Cache<String, ImmutableSet<String>> groupsByInclude) {
     this.config = config;
     this.server = LdapRealm.required(config, "server");
     this.username = LdapRealm.optional(config, "username");
@@ -74,6 +79,7 @@
     } else {
       readTimeOutMillis = null;
     }
+    this.groupsByInclude = groupsByInclude;
   }
 
   private Properties createContextProperties() {
@@ -195,12 +201,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()) {
@@ -213,24 +214,31 @@
   private void recursivelyExpandGroups(final Set<String> groupDNs,
       final LdapSchema schema, final DirContext ctx, final String groupDN) {
     if (groupDNs.add(groupDN) && schema.accountMemberField != null) {
-      // Recursively identify the groups it is a member of.
-      //
-      try {
-        final Name compositeGroupName = new CompositeName().add(groupDN);
-        final Attribute in =
-            ctx.getAttributes(compositeGroupName).get(schema.accountMemberField);
-        if (in != null) {
-          final NamingEnumeration<?> groups = in.getAll();
-          try {
-            while (groups.hasMore()) {
-              final String nextDN = (String) groups.next();
-              recursivelyExpandGroups(groupDNs, schema, ctx, nextDN);
+      ImmutableSet<String> cachedGroupDNs = groupsByInclude.getIfPresent(groupDN);
+      if (cachedGroupDNs == null) {
+        // Recursively identify the groups it is a member of.
+        ImmutableSet.Builder<String> dns = ImmutableSet.builder();
+        try {
+          final Name compositeGroupName = new CompositeName().add(groupDN);
+          final Attribute in =
+              ctx.getAttributes(compositeGroupName).get(schema.accountMemberField);
+          if (in != null) {
+            final NamingEnumeration<?> groups = in.getAll();
+            try {
+              while (groups.hasMore()) {
+                dns.add((String) groups.next());
+              }
+            } catch (PartialResultException e) {
             }
-          } catch (PartialResultException e) {
           }
+        } catch (NamingException e) {
+          LdapRealm.log.warn("Could not find group " + groupDN, e);
         }
-      } catch (NamingException e) {
-        LdapRealm.log.warn("Could not find group " + groupDN, e);
+        cachedGroupDNs = dns.build();
+        groupsByInclude.put(groupDN, cachedGroupDNs);
+      }
+      for (String dn : cachedGroupDNs) {
+        recursivelyExpandGroups(groupDNs, schema, ctx, dn);
       }
     }
   }
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..f1b15f9 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,13 @@
 
 import static java.util.concurrent.TimeUnit.HOURS;
 
+import com.google.common.base.Optional;
+import com.google.common.collect.ImmutableSet;
+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 +32,37 @@
 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";
+  static final String GROUPS_BYINCLUDE_CACHE = "ldap_groups_byinclude";
+
 
   @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);
+
+    cache(GROUPS_BYINCLUDE_CACHE,
+        String.class,
+        new TypeLiteral<ImmutableSet<String>>() {})
+      .expireAfterWrite(1, HOURS);
 
     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 e085d1e..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)) {
@@ -255,66 +249,30 @@
   }
 
   @Override
+  public AuthRequest unlink(ReviewDb db, Account.Id from, AuthRequest who) {
+    return who;
+  }
+
+  @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
@@ -323,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
@@ -350,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);
@@ -363,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/UnnamedCacheBinding.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheRemovalListener.java
similarity index 69%
rename from gerrit-server/src/main/java/com/google/gerrit/server/cache/UnnamedCacheBinding.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheRemovalListener.java
index 43039e1..078f2dc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/UnnamedCacheBinding.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/cache/CacheRemovalListener.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,9 +14,10 @@
 
 package com.google.gerrit.server.cache;
 
+import com.google.common.cache.RemovalNotification;
 
-/** 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);
-}
+public interface CacheRemovalListener<K,V> {
+  public void onRemoval(String pluginName,
+    String cacheName,
+    RemovalNotification<K, V> notification);
+}
\ No newline at end of file
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/ForwardingRemovalListener.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/ForwardingRemovalListener.java
new file mode 100644
index 0000000..c98ddf9
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/cache/ForwardingRemovalListener.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.cache;
+
+import com.google.common.base.Strings;
+import com.google.common.cache.RemovalListener;
+import com.google.common.cache.RemovalNotification;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+/**
+ * This listener dispatches removal events to all other RemovalListeners
+ * attached via the DynamicSet API.
+ *
+ * @param <K>
+ * @param <V>
+ */
+@SuppressWarnings("rawtypes")
+public class ForwardingRemovalListener<K, V> implements RemovalListener<K, V> {
+  public interface Factory {
+    ForwardingRemovalListener create(String cacheName);
+  }
+
+  private final DynamicSet<CacheRemovalListener> listeners;
+  private final String cacheName;
+  private String pluginName = "gerrit";
+
+  @Inject
+  ForwardingRemovalListener(DynamicSet<CacheRemovalListener> listeners,
+      @Assisted String cacheName) {
+    this.listeners = listeners;
+    this.cacheName = cacheName;
+  }
+
+  @Inject(optional = true)
+  void setPluginName(String name) {
+    if (!Strings.isNullOrEmpty(name)) {
+      this.pluginName = name;
+    }
+  }
+
+  public void onRemoval(RemovalNotification<K, V> notification) {
+    for (CacheRemovalListener<K, V> l : listeners) {
+      l.onRemoval(pluginName, cacheName, notification);
+    }
+  }
+}
\ No newline at end of file
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/MemoryCacheFactory.java
similarity index 61%
copy from gerrit-server/src/main/java/com/google/gerrit/server/cache/UnnamedCacheBinding.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/cache/MemoryCacheFactory.java
index 43039e1..6b8b489 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/UnnamedCacheBinding.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/cache/MemoryCacheFactory.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,9 +14,14 @@
 
 package com.google.gerrit.server.cache;
 
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
 
-/** 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);
+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/cache/UnnamedCacheBinding.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/PersistentCacheFactory.java
similarity index 61%
copy from gerrit-server/src/main/java/com/google/gerrit/server/cache/UnnamedCacheBinding.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/cache/PersistentCacheFactory.java
index 43039e1..983e956 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/UnnamedCacheBinding.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/cache/PersistentCacheFactory.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,9 +14,14 @@
 
 package com.google.gerrit.server.cache;
 
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
 
-/** 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);
+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/changedetail/AbandonChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/AbandonChange.java
index 1fac8c5..dca4a83 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/AbandonChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/AbandonChange.java
@@ -31,15 +31,13 @@
 import com.google.gwtorm.server.AtomicUpdate;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
 
 import java.util.concurrent.Callable;
 
-public class AbandonChange implements Callable<ReviewResult> {
+import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.Option;
 
-  public interface Factory {
-    AbandonChange create(PatchSet.Id patchSetId, String changeComment);
-  }
+public class AbandonChange implements Callable<ReviewResult> {
 
   private final AbandonedSender.Factory abandonedSenderFactory;
   private final ChangeControl.Factory changeControlFactory;
@@ -47,33 +45,48 @@
   private final IdentifiedUser currentUser;
   private final ChangeHooks hooks;
 
-  private final PatchSet.Id patchSetId;
-  private final String changeComment;
+  @Argument(index = 0, required = true, multiValued = false, usage = "change to abandon")
+  private Change.Id changeId;
+
+  public void setChangeId(final Change.Id changeId) {
+    this.changeId = changeId;
+  }
+
+  @Option(name = "--message", aliases = {"-m"},
+          usage = "optional message to append to change")
+  private String message;
+
+  public void setMessage(final String message) {
+    this.message = message;
+  }
 
   @Inject
   AbandonChange(final AbandonedSender.Factory abandonedSenderFactory,
       final ChangeControl.Factory changeControlFactory, final ReviewDb db,
-      final IdentifiedUser currentUser, final ChangeHooks hooks,
-      @Assisted final PatchSet.Id patchSetId,
-      @Assisted final String changeComment) {
+      final IdentifiedUser currentUser, final ChangeHooks hooks) {
     this.abandonedSenderFactory = abandonedSenderFactory;
     this.changeControlFactory = changeControlFactory;
     this.db = db;
     this.currentUser = currentUser;
     this.hooks = hooks;
 
-    this.patchSetId = patchSetId;
-    this.changeComment = changeComment;
+    changeId = null;
+    message = null;
   }
 
   @Override
   public ReviewResult call() throws EmailException,
       InvalidChangeOperationException, NoSuchChangeException, OrmException {
-    final ReviewResult result = new ReviewResult();
+    if (changeId == null) {
+      throw new InvalidChangeOperationException("changeId is required");
+    }
 
-    final Change.Id changeId = patchSetId.getParentKey();
+    final ReviewResult result = new ReviewResult();
     result.setChangeId(changeId);
+
     final ChangeControl control = changeControlFactory.validateFor(changeId);
+    final Change change = db.changes().get(changeId);
+    final PatchSet.Id patchSetId = change.currentPatchSetId();
     final PatchSet patch = db.patchSets().get(patchSetId);
     if (!control.canAbandon()) {
       result.addError(new ReviewResult.Error(
@@ -88,9 +101,9 @@
           currentUser.getAccountId(), patchSetId);
       final StringBuilder msgBuf =
           new StringBuilder("Patch Set " + patchSetId.get() + ": Abandoned");
-      if (changeComment != null && changeComment.length() > 0) {
+      if (message != null && message.length() > 0) {
         msgBuf.append("\n\n");
-        msgBuf.append(changeComment);
+        msgBuf.append(message);
       }
       cmsg.setMessage(msgBuf.toString());
 
@@ -99,8 +112,7 @@
           new AtomicUpdate<Change>() {
         @Override
         public Change update(Change change) {
-          if (change.getStatus().isOpen()
-              && change.currentPatchSetId().equals(patchSetId)) {
+          if (change.getStatus().isOpen()) {
             change.setStatus(Change.Status.ABANDONED);
             ChangeUtil.updated(change);
             return change;
@@ -109,11 +121,17 @@
           }
         }
       });
-      ChangeUtil.updatedChange(
-          db, currentUser, updatedChange, cmsg, abandonedSenderFactory,
-          "Change is no longer open or patchset is not latest");
+
+      if (updatedChange == null) {
+        result.addError(new ReviewResult.Error(
+            ReviewResult.Error.Type.CHANGE_IS_CLOSED));
+        return result;
+      }
+
+      ChangeUtil.updatedChange(db, currentUser, updatedChange, cmsg,
+                               abandonedSenderFactory);
       hooks.doChangeAbandonedHook(updatedChange, currentUser.getAccount(),
-                                  changeComment, db);
+                                  message, db);
     }
 
     return result;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/DeleteDraftPatchSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/DeleteDraftPatchSet.java
index 268e118..f466231 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/DeleteDraftPatchSet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/DeleteDraftPatchSet.java
@@ -20,8 +20,8 @@
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.ReplicationQueue;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
 import com.google.gerrit.server.project.ChangeControl;
@@ -44,7 +44,7 @@
   private final ChangeControl.Factory changeControlFactory;
   private final ReviewDb db;
   private final GitRepositoryManager gitManager;
-  private final ReplicationQueue replication;
+  private final GitReferenceUpdated replication;
   private final PatchSetInfoFactory patchSetInfoFactory;
 
   private final PatchSet.Id patchSetId;
@@ -52,7 +52,7 @@
   @Inject
   DeleteDraftPatchSet(ChangeControl.Factory changeControlFactory,
       ReviewDb db, GitRepositoryManager gitManager,
-      ReplicationQueue replication, PatchSetInfoFactory patchSetInfoFactory,
+      GitReferenceUpdated replication, PatchSetInfoFactory patchSetInfoFactory,
       @Assisted final PatchSet.Id patchSetId) {
     this.changeControlFactory = changeControlFactory;
     this.db = db;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/PublishDraft.java b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/PublishDraft.java
index 028feac..a71e12e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/PublishDraft.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/PublishDraft.java
@@ -15,6 +15,7 @@
 
 package com.google.gerrit.server.changedetail;
 
+import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.common.data.ReviewResult;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -37,14 +38,17 @@
 
   private final ChangeControl.Factory changeControlFactory;
   private final ReviewDb db;
+  private final ChangeHooks hooks;
 
   private final PatchSet.Id patchSetId;
 
   @Inject
   PublishDraft(ChangeControl.Factory changeControlFactory,
-      ReviewDb db, @Assisted final PatchSet.Id patchSetId) {
+      ReviewDb db, @Assisted final PatchSet.Id patchSetId,
+      final ChangeHooks hooks) {
     this.changeControlFactory = changeControlFactory;
     this.db = db;
+    this.hooks = hooks;
 
     this.patchSetId = patchSetId;
   }
@@ -70,31 +74,29 @@
       result.addError(new ReviewResult.Error(
           ReviewResult.Error.Type.PUBLISH_NOT_PERMITTED));
     } else {
-      db.patchSets().atomicUpdate(patchSetId, new AtomicUpdate<PatchSet>() {
+      final PatchSet updatedPatch = db.patchSets().atomicUpdate(patchSetId,
+          new AtomicUpdate<PatchSet>() {
         @Override
         public PatchSet update(PatchSet patchset) {
-          if (patchset.isDraft()) {
-            patchset.setDraft(false);
-          }
-          return null;
+          patchset.setDraft(false);
+          return patchset;
         }
       });
 
-      final Change change = db.changes().get(changeId);
-      if (change.getStatus() == Change.Status.DRAFT) {
-        db.changes().atomicUpdate(changeId,
-            new AtomicUpdate<Change>() {
-          @Override
-          public Change update(Change change) {
-            if (change.getStatus() == Change.Status.DRAFT) {
-              change.setStatus(Change.Status.NEW);
-              ChangeUtil.updated(change);
-              return change;
-            } else {
-              return null;
-            }
+      final Change updatedChange = db.changes().atomicUpdate(changeId,
+          new AtomicUpdate<Change>() {
+        @Override
+        public Change update(Change change) {
+          if (change.getStatus() == Change.Status.DRAFT) {
+            change.setStatus(Change.Status.NEW);
+            ChangeUtil.updated(change);
           }
-        });
+          return change;
+        }
+      });
+
+      if (!updatedPatch.isDraft() || updatedChange.getStatus() == Change.Status.NEW) {
+        hooks.doDraftPublishedHook(updatedChange, updatedPatch, db);
       }
     }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RestoreChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RestoreChange.java
index 7232755..53da2b6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RestoreChange.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RestoreChange.java
@@ -17,12 +17,15 @@
 
 import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.common.data.ReviewResult;
+import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.ProjectUtil;
+import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.mail.EmailException;
 import com.google.gerrit.server.mail.RestoredSender;
 import com.google.gerrit.server.project.ChangeControl;
@@ -31,93 +34,124 @@
 import com.google.gwtorm.server.AtomicUpdate;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
 
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+
+import java.io.IOException;
 import java.util.concurrent.Callable;
 
-public class RestoreChange implements Callable<ReviewResult> {
+import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.Option;
 
-  public interface Factory {
-    RestoreChange create(PatchSet.Id patchSetId, String changeComment);
-  }
+public class RestoreChange implements Callable<ReviewResult> {
 
   private final RestoredSender.Factory restoredSenderFactory;
   private final ChangeControl.Factory changeControlFactory;
   private final ReviewDb db;
+  private final GitRepositoryManager repoManager;
   private final IdentifiedUser currentUser;
   private final ChangeHooks hooks;
 
-  private final PatchSet.Id patchSetId;
-  private final String changeComment;
+  @Argument(index = 0, required = true, multiValued = false,
+            usage = "change to restore", metaVar = "CHANGE")
+  private Change.Id changeId;
+  public void setChangeId(final Change.Id changeId) {
+    this.changeId = changeId;
+  }
+
+  @Option(name = "--message", aliases = {"-m"},
+          usage = "optional message to append to change")
+  private String message;
+  public void setMessage(final String message) {
+    this.message = message;
+  }
 
   @Inject
   RestoreChange(final RestoredSender.Factory restoredSenderFactory,
       final ChangeControl.Factory changeControlFactory, final ReviewDb db,
-      final IdentifiedUser currentUser, final ChangeHooks hooks,
-      @Assisted final PatchSet.Id patchSetId,
-      @Assisted final String changeComment) {
+      final GitRepositoryManager repoManager, final IdentifiedUser currentUser,
+      final ChangeHooks hooks) {
     this.restoredSenderFactory = restoredSenderFactory;
     this.changeControlFactory = changeControlFactory;
     this.db = db;
+    this.repoManager = repoManager;
     this.currentUser = currentUser;
     this.hooks = hooks;
 
-    this.patchSetId = patchSetId;
-    this.changeComment = changeComment;
+    changeId = null;
+    message = null;
   }
 
   @Override
-  public ReviewResult call() throws EmailException,
-      InvalidChangeOperationException, NoSuchChangeException, OrmException {
-    final ReviewResult result = new ReviewResult();
+  public ReviewResult call() throws EmailException, NoSuchChangeException,
+      InvalidChangeOperationException, OrmException,
+      RepositoryNotFoundException, IOException {
+    if (changeId == null) {
+      throw new InvalidChangeOperationException("changeId is required");
+    }
 
-    final Change.Id changeId = patchSetId.getParentKey();
+    final ReviewResult result = new ReviewResult();
     result.setChangeId(changeId);
+
     final ChangeControl control = changeControlFactory.validateFor(changeId);
-    final PatchSet patch = db.patchSets().get(patchSetId);
+    final Change change = db.changes().get(changeId);
+    final PatchSet.Id patchSetId = change.currentPatchSetId();
     if (!control.canRestore()) {
       result.addError(new ReviewResult.Error(
           ReviewResult.Error.Type.RESTORE_NOT_PERMITTED));
-    } else if (patch == null) {
-      throw new NoSuchChangeException(changeId);
-    } else {
-
-      // Create a message to accompany the restored change
-      final ChangeMessage cmsg =
-          new ChangeMessage(new ChangeMessage.Key(changeId, ChangeUtil
-              .messageUUID(db)), currentUser.getAccountId(), patchSetId);
-      final StringBuilder msgBuf =
-          new StringBuilder("Patch Set " + patchSetId.get() + ": Restored");
-      if (changeComment != null && changeComment.length() > 0) {
-        msgBuf.append("\n\n");
-        msgBuf.append(changeComment);
-      }
-      cmsg.setMessage(msgBuf.toString());
-
-      // Restore the change
-      final Change updatedChange = db.changes().atomicUpdate(changeId,
-          new AtomicUpdate<Change>() {
-        @Override
-        public Change update(Change change) {
-          if (change.getStatus() == Change.Status.ABANDONED
-              && change.currentPatchSetId().equals(patchSetId)) {
-            change.setStatus(Change.Status.NEW);
-            ChangeUtil.updated(change);
-            return change;
-          } else {
-            return null;
-          }
-        }
-      });
-
-      ChangeUtil.updatedChange(
-          db, currentUser, updatedChange, cmsg, restoredSenderFactory,
-         "Change is not abandoned or patchset is not latest");
-
-      hooks.doChangeRestoreHook(updatedChange, currentUser.getAccount(),
-                                changeComment, db);
+      return result;
     }
 
+    final PatchSet patch = db.patchSets().get(patchSetId);
+    if (patch == null) {
+      throw new NoSuchChangeException(changeId);
+    }
+
+    final Branch.NameKey destBranch = control.getChange().getDest();
+    if (!ProjectUtil.branchExists(repoManager, destBranch)) {
+      result.addError(new ReviewResult.Error(
+          ReviewResult.Error.Type.DEST_BRANCH_NOT_FOUND, destBranch.get()));
+      return result;
+    }
+
+    // Create a message to accompany the restored change
+    final ChangeMessage cmsg =
+        new ChangeMessage(new ChangeMessage.Key(changeId, ChangeUtil
+            .messageUUID(db)), currentUser.getAccountId(), patchSetId);
+    final StringBuilder msgBuf =
+        new StringBuilder("Patch Set " + patchSetId.get() + ": Restored");
+    if (message != null && message.length() > 0) {
+      msgBuf.append("\n\n");
+      msgBuf.append(message);
+    }
+    cmsg.setMessage(msgBuf.toString());
+
+    // Restore the change
+    final Change updatedChange = db.changes().atomicUpdate(changeId,
+        new AtomicUpdate<Change>() {
+          @Override
+          public Change update(Change change) {
+            if (change.getStatus() == Change.Status.ABANDONED) {
+              change.setStatus(Change.Status.NEW);
+              ChangeUtil.updated(change);
+              return change;
+            } else {
+              return null;
+            }
+          }
+        });
+
+    if (updatedChange == null) {
+      result.addError(new ReviewResult.Error(
+          ReviewResult.Error.Type.CHANGE_NOT_ABANDONED));
+      return result;
+    }
+
+    ChangeUtil.updatedChange(db, currentUser, updatedChange, cmsg,
+                             restoredSenderFactory);
+    hooks.doChangeRestoredHook(updatedChange, currentUser.getAccount(),
+                               message, db);
+
     return result;
   }
 }
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 abd3582..3287aa1 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
@@ -24,6 +24,8 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.ProjectUtil;
+import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MergeOp;
 import com.google.gerrit.server.git.MergeQueue;
 import com.google.gerrit.server.project.ChangeControl;
@@ -34,6 +36,7 @@
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
@@ -49,6 +52,7 @@
   private final MergeOp.Factory opFactory;
   private final MergeQueue merger;
   private final ReviewDb db;
+  private final GitRepositoryManager repoManager;
   private final IdentifiedUser currentUser;
 
   private final PatchSet.Id patchSetId;
@@ -56,12 +60,13 @@
   @Inject
   Submit(final ChangeControl.Factory changeControlFactory,
       final MergeOp.Factory opFactory, final MergeQueue merger,
-      final ReviewDb db, final IdentifiedUser currentUser,
-      @Assisted final PatchSet.Id patchSetId) {
+      final ReviewDb db, final GitRepositoryManager repoManager,
+      final IdentifiedUser currentUser, @Assisted final PatchSet.Id patchSetId) {
     this.changeControlFactory = changeControlFactory;
     this.opFactory = opFactory;
     this.merger = merger;
     this.db = db;
+    this.repoManager = repoManager;
     this.currentUser = currentUser;
 
     this.patchSetId = patchSetId;
@@ -69,7 +74,8 @@
 
   @Override
   public ReviewResult call() throws IllegalStateException,
-      InvalidChangeOperationException, NoSuchChangeException, OrmException {
+      InvalidChangeOperationException, NoSuchChangeException, OrmException,
+      IOException {
     final ReviewResult result = new ReviewResult();
 
     final PatchSet patch = db.patchSets().get(patchSetId);
@@ -80,7 +86,7 @@
       throw new NoSuchChangeException(changeId);
     }
 
-    List<SubmitRecord> submitResult = control.canSubmit(db, patchSetId);
+    List<SubmitRecord> submitResult = control.canSubmit(db, patch);
     if (submitResult.isEmpty()) {
       throw new IllegalStateException(
           "ChangeControl.canSubmit returned empty list");
@@ -113,6 +119,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
@@ -147,6 +157,14 @@
       }
     }
 
+    if (!ProjectUtil.branchExists(repoManager, control.getChange().getDest())) {
+      result.addError(new ReviewResult.Error(
+          ReviewResult.Error.Type.DEST_BRANCH_NOT_FOUND,
+          "Destination branch \"" + control.getChange().getDest().get()
+              + "\" not found."));
+      return result;
+    }
+
     // Submit the change if we can
     if (result.getErrors().isEmpty()) {
       final List<PatchSetApproval> allApprovals =
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java
index a0f0d36..9916257 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfig.java
@@ -36,12 +36,16 @@
   private final AuthType authType;
   private final String httpHeader;
   private final boolean trustContainerAuth;
+  private final boolean userNameToLowerCase;
+  private final boolean gitBasicAuth;
   private final String logoutUrl;
+  private final String openIdSsoUrl;
   private final List<OpenIdProviderPattern> trustedOpenIDs;
   private final List<OpenIdProviderPattern> allowedOpenIDs;
   private final String cookiePath;
   private final boolean cookieSecure;
   private final SignedToken emailReg;
+  private final SignedToken restToken;
 
   private final boolean allowGoogleAccountUpgrade;
 
@@ -51,11 +55,15 @@
     authType = toType(cfg);
     httpHeader = cfg.getString("auth", null, "httpheader");
     logoutUrl = cfg.getString("auth", null, "logouturl");
+    openIdSsoUrl = cfg.getString("auth", null, "openidssourl");
     trustedOpenIDs = toPatterns(cfg, "trustedOpenID");
     allowedOpenIDs = toPatterns(cfg, "allowedOpenID");
     cookiePath = cfg.getString("auth", null, "cookiepath");
     cookieSecure = cfg.getBoolean("auth", "cookiesecure", false);
     trustContainerAuth = cfg.getBoolean("auth", "trustContainerAuth", false);
+    gitBasicAuth = cfg.getBoolean("auth", "gitBasicAuth", false);
+    userNameToLowerCase = cfg.getBoolean("auth", "userNameToLowerCase", false);
+
 
     String key = cfg.getString("auth", null, "registerEmailPrivateKey");
     if (key != null && !key.isEmpty()) {
@@ -68,6 +76,15 @@
       emailReg = null;
     }
 
+    key = cfg.getString("auth", null, "restTokenPrivateKey");
+    if (key != null && !key.isEmpty()) {
+      int age = (int) ConfigUtil.getTimeUnit(cfg,
+          "auth", null, "maxRestTokenAge", 60, TimeUnit.SECONDS);
+      restToken = new SignedToken(age, key);
+    } else {
+      restToken = null;
+    }
+
     if (authType == AuthType.OPENID) {
       allowGoogleAccountUpgrade =
           cfg.getBoolean("auth", "allowgoogleaccountupgrade", false);
@@ -106,6 +123,10 @@
     return logoutUrl;
   }
 
+  public String getOpenIdSsoUrl() {
+    return openIdSsoUrl;
+  }
+
   public String getCookiePath() {
     return cookiePath;
   }
@@ -118,6 +139,10 @@
     return emailReg;
   }
 
+  public SignedToken getRestToken() {
+    return restToken;
+  }
+
   public boolean isAllowGoogleAccountUpgrade() {
     return allowGoogleAccountUpgrade;
   }
@@ -132,6 +157,16 @@
     return trustContainerAuth;
   }
 
+  /** Whether user name should be converted to lower-case before validation */
+  public boolean isUserNameToLowerCase() {
+    return userNameToLowerCase;
+  }
+
+  /** Whether git-over-http should use Gerrit basic authentication scheme. */
+  public boolean isGitBasichAuth() {
+    return gitBasicAuth;
+  }
+
   public boolean isIdentityTrustable(final Collection<AccountExternalId> ids) {
     switch (getAuthType()) {
       case DEVELOPMENT_BECOME_ANY_ACCOUNT:
@@ -146,6 +181,10 @@
         //
         return true;
 
+      case OPENID_SSO:
+        // There's only one provider in SSO mode, so it must be okay.
+        return true;
+
       case OPENID:
         // All identities must be trusted in order to trust the account.
         //
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigUtil.java
index e76249a..cc54054 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigUtil.java
@@ -16,27 +16,10 @@
 
 import static org.eclipse.jgit.util.StringUtils.equalsIgnoreCase;
 
-import com.google.common.base.Function;
-import com.google.common.base.Predicates;
-import com.google.common.collect.Iterables;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupName;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.OrmRuntimeException;
-import com.google.gwtorm.server.SchemaFactory;
-
 import org.eclipse.jgit.lib.Config;
-import org.slf4j.Logger;
-
 import java.lang.reflect.InvocationTargetException;
-import java.text.MessageFormat;
 import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.HashSet;
-import java.util.Iterator;
 import java.util.List;
-import java.util.Set;
 import java.util.concurrent.TimeUnit;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
@@ -245,7 +228,7 @@
    */
   public static long getTimeUnit(final String valueString, long defaultValue,
       TimeUnit wantUnit) {
-    Matcher m = Pattern.compile("^([1-9][0-9]*)\\s*(.*)$").matcher(valueString);
+    Matcher m = Pattern.compile("^(0|[1-9][0-9]*)\\s*(.*)$").matcher(valueString);
     if (!m.matches()) {
       return defaultValue;
     }
@@ -303,83 +286,6 @@
     }
   }
 
-  /**
-   * Resolve groups from group names, via the database. Group names not found in
-   * the database will be skipped.
-   *
-   * @param dbfactory database to resolve from.
-   * @param groupNames group names to resolve.
-   * @param log log for any warnings and errors.
-   * @param groupNotFoundWarning formatted message to output to the log for each
-   *        group name which is not found in the database. <code>{0}</code> will
-   *        be replaced with the group name.
-   * @return the actual groups resolved from the database. If no groups are
-   *         found, returns an empty {@code Set}, never {@code null}.
-   */
-  public static Set<AccountGroup.UUID> groupsFor(
-      SchemaFactory<ReviewDb> dbfactory, String[] groupNames, Logger log,
-      String groupNotFoundWarning) {
-    final Set<AccountGroup.UUID> result = new HashSet<AccountGroup.UUID>();
-    try {
-      final ReviewDb db = dbfactory.open();
-      try {
-        List<AccountGroupName> groups = db.accountGroupNames().get(
-            Iterables.transform(Arrays.asList(groupNames),
-                new Function<String, AccountGroup.NameKey>() {
-                  @Override
-                  public AccountGroup.NameKey apply(String name) {
-                    return new AccountGroup.NameKey(name);
-                  }
-            })).toList();
-
-        Iterator<AccountGroup> ags = db.accountGroups().get(
-            Iterables.transform(Iterables.filter(groups, Predicates.notNull()),
-                new Function<AccountGroupName, AccountGroup.Id>() {
-                  @Override
-                  public AccountGroup.Id apply(AccountGroupName group) {
-                    return group.getId();
-                  }
-            })).iterator();
-
-        for (int i = 0; i < groupNames.length; i++) {
-          if (groups.get(i) == null) {
-            log.warn(MessageFormat.format(groupNotFoundWarning, groupNames[i]));
-            continue;
-          }
-          AccountGroup ag = ags.next();
-          if (ag == null) {
-            log.warn(MessageFormat.format(groupNotFoundWarning, groupNames[i]));
-          } else {
-            result.add(ag.getGroupUUID());
-          }
-        }
-      } finally {
-        db.close();
-      }
-    } catch (OrmRuntimeException e) {
-      log.error("Database error, cannot load groups", e);
-    } catch (OrmException e) {
-      log.error("Database error, cannot load groups", e);
-    }
-    return result;
-  }
-
-  /**
-   * Resolve groups from group names, via the database. Group names not found in
-   * the database will be skipped.
-   *
-   * @param dbfactory database to resolve from.
-   * @param groupNames group names to resolve.
-   * @param log log for any warnings and errors.
-   * @return the actual groups resolved from the database. If no groups are
-   *         found, returns an empty {@code Set}, never {@code null}.
-   */
-  public static Set<AccountGroup.UUID> groupsFor(
-      SchemaFactory<ReviewDb> dbfactory, String[] groupNames, Logger log) {
-    return groupsFor(dbfactory, groupNames, log,
-        "Group \"{0}\" not in database, skipping.");
-  }
-
   private static boolean match(final String a, final String... cases) {
     for (final String b : cases) {
       if (equalsIgnoreCase(a, b)) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/DownloadSchemeConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/DownloadConfig.java
similarity index 66%
rename from gerrit-server/src/main/java/com/google/gerrit/server/config/DownloadSchemeConfig.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/config/DownloadConfig.java
index ecfe4f5..f259871 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/DownloadSchemeConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/DownloadConfig.java
@@ -14,8 +14,9 @@
 
 package com.google.gerrit.server.config;
 
-import com.google.gerrit.reviewdb.client.SystemConfig;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadCommand;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme;
+import com.google.gerrit.reviewdb.client.SystemConfig;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -28,22 +29,33 @@
 
 /** Download protocol from {@code gerrit.config}. */
 @Singleton
-public class DownloadSchemeConfig {
+public class DownloadConfig {
   private final Set<DownloadScheme> downloadSchemes;
+  private final Set<DownloadCommand> downloadCommands;
 
   @Inject
-  DownloadSchemeConfig(@GerritServerConfig final Config cfg,
+  DownloadConfig(@GerritServerConfig final Config cfg,
       final SystemConfig s) {
-    List<DownloadScheme> all =
+    List<DownloadScheme> allSchemes =
         ConfigUtil.getEnumList(cfg, "download", null, "scheme",
             DownloadScheme.DEFAULT_DOWNLOADS);
-
     downloadSchemes =
-        Collections.unmodifiableSet(new HashSet<DownloadScheme>(all));
+        Collections.unmodifiableSet(new HashSet<DownloadScheme>(allSchemes));
+
+    List<DownloadCommand> allCommands =
+        ConfigUtil.getEnumList(cfg, "download", null, "command",
+            DownloadCommand.DEFAULT_DOWNLOADS);
+    downloadCommands =
+        Collections.unmodifiableSet(new HashSet<DownloadCommand>(allCommands));
   }
 
   /** Scheme used to download. */
-  public Set<DownloadScheme> getDownloadScheme() {
+  public Set<DownloadScheme> getDownloadSchemes() {
     return downloadSchemes;
   }
+
+  /** Command used to download. */
+  public Set<DownloadCommand> getDownloadCommands() {
+    return downloadCommands;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 99dd54a..2a66706 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
@@ -16,14 +16,21 @@
 
 import static com.google.inject.Scopes.SINGLETON;
 
+import com.google.common.cache.Cache;
+import com.google.gerrit.audit.AuditModule;
 import com.google.gerrit.common.data.ApprovalTypes;
+import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
+import com.google.gerrit.extensions.events.NewProjectCreatedListener;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.rules.PrologModule;
 import com.google.gerrit.rules.RulesCache;
+import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.FileTypeRegistry;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.InternalUser;
 import com.google.gerrit.server.MimeUtilFileTypeRegistry;
-import com.google.gerrit.server.ReplicationUser;
 import com.google.gerrit.server.account.AccountByEmailCacheImpl;
 import com.google.gerrit.server.account.AccountCacheImpl;
 import com.google.gerrit.server.account.AccountInfoCacheFactory;
@@ -32,19 +39,23 @@
 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.cache.CacheRemovalListener;
 import com.google.gerrit.server.events.EventFactory;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.ChangeMergeQueue;
 import com.google.gerrit.server.git.GitModule;
 import com.google.gerrit.server.git.MergeQueue;
-import com.google.gerrit.server.git.PushAllProjectsOp;
 import com.google.gerrit.server.git.ReloadSubmitQueueOp;
-import com.google.gerrit.server.git.SecureCredentialsProvider;
 import com.google.gerrit.server.git.TagCache;
 import com.google.gerrit.server.git.TransferConfig;
 import com.google.gerrit.server.mail.FromAddressGenerator;
@@ -62,8 +73,10 @@
 import com.google.gerrit.server.project.SectionSortCache;
 import com.google.gerrit.server.tools.ToolsCatalog;
 import com.google.gerrit.server.util.IdGenerator;
+import com.google.gerrit.server.util.ThreadLocalRequestContext;
 import com.google.gerrit.server.workflow.FunctionState;
 import com.google.inject.Inject;
+import com.google.inject.TypeLiteral;
 
 import org.apache.velocity.runtime.RuntimeInstance;
 import org.eclipse.jgit.lib.Config;
@@ -115,26 +128,31 @@
     install(new AccessControlModule());
     install(new GitModule());
     install(new PrologModule());
+    install(ThreadLocalRequestContext.module());
 
     factory(AccountInfoCacheFactory.Factory.class);
     factory(CapabilityControl.Factory.class);
     factory(GroupInfoCacheFactory.Factory.class);
+    factory(InternalUser.Factory.class);
     factory(ProjectNode.Factory.class);
     factory(ProjectState.Factory.class);
-    factory(MaterializedGroupMembership.Factory.class);
     bind(PermissionCollection.Factory.class);
     bind(AccountVisibility.class)
         .toProvider(AccountVisibilityProvider.class)
         .in(SINGLETON);
 
+    bind(GroupControl.Factory.class).in(SINGLETON);
+    factory(IncludingGroupMembership.Factory.class);
+    bind(InternalGroupBackend.class).in(SINGLETON);
+    bind(GroupBackend.class).to(UniversalGroupBackend.class).in(SINGLETON);
+    DynamicSet.setOf(binder(), GroupBackend.class);
+    DynamicSet.bind(binder(), GroupBackend.class).to(InternalGroupBackend.class);
+
     bind(FileTypeRegistry.class).to(MimeUtilFileTypeRegistry.class);
     bind(ToolsCatalog.class);
     bind(EventFactory.class);
     bind(TransferConfig.class);
 
-    factory(SecureCredentialsProvider.Factory.class);
-    factory(PushAllProjectsOp.Factory.class);
-
     bind(ChangeMergeQueue.class).in(SINGLETON);
     bind(MergeQueue.class).to(ChangeMergeQueue.class).in(SINGLETON);
     factory(ReloadSubmitQueueOp.Factory.class);
@@ -150,6 +168,15 @@
     bind(ChangeControl.GenericFactory.class);
     bind(ProjectControl.GenericFactory.class);
     factory(FunctionState.Factory.class);
-    factory(ReplicationUser.Factory.class);
+
+    install(new AuditModule());
+
+    bind(GitReferenceUpdated.class);
+    DynamicMap.mapOf(binder(), new TypeLiteral<Cache<?, ?>>() {});
+    DynamicSet.setOf(binder(), CacheRemovalListener.class);
+    DynamicSet.setOf(binder(), GitReferenceUpdatedListener.class);
+    DynamicSet.setOf(binder(), NewProjectCreatedListener.class);
+
+    bind(AnonymousUser.class);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritRequestModule.java
index 71bcc6c..ba54c56 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,26 +17,25 @@
 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;
 import com.google.gerrit.server.account.PerformRenameGroup;
 import com.google.gerrit.server.account.VisibleGroups;
-import com.google.gerrit.server.changedetail.AbandonChange;
 import com.google.gerrit.server.changedetail.DeleteDraftPatchSet;
 import com.google.gerrit.server.changedetail.PublishDraft;
-import com.google.gerrit.server.changedetail.RestoreChange;
 import com.google.gerrit.server.changedetail.Submit;
 import com.google.gerrit.server.git.AsyncReceiveCommits;
+import com.google.gerrit.server.git.BanCommit;
 import com.google.gerrit.server.git.CreateCodeReviewNotes;
 import com.google.gerrit.server.git.MergeOp;
 import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.NotesBranchUtil;
 import com.google.gerrit.server.git.SubmoduleOp;
 import com.google.gerrit.server.mail.AbandonedSender;
 import com.google.gerrit.server.mail.AddReviewerSender;
@@ -73,11 +72,10 @@
     bind(AccountResolver.class);
     bind(ChangeQueryRewriter.class);
     bind(ListProjects.class);
+    bind(ApprovalsUtil.class);
 
-    bind(AnonymousUser.class).in(RequestScoped.class);
     bind(PerRequestProjectControlCache.class).in(RequestScoped.class);
     bind(ChangeControl.Factory.class).in(SINGLETON);
-    bind(GroupControl.Factory.class).in(SINGLETON);
     bind(ProjectControl.Factory.class).in(SINGLETON);
     bind(AccountControl.Factory.class).in(SINGLETON);
 
@@ -85,12 +83,12 @@
     factory(SubmoduleOp.Factory.class);
     factory(MergeOp.Factory.class);
     factory(CreateCodeReviewNotes.Factory.class);
+    factory(NotesBranchUtil.Factory.class);
     install(new AsyncReceiveCommits.Module());
 
     // Not really per-request, but dammit, I don't know where else to
     // easily park this stuff.
     //
-    factory(AbandonChange.Factory.class);
     factory(AddReviewer.Factory.class);
     factory(AddReviewerSender.Factory.class);
     factory(CreateChangeSender.Factory.class);
@@ -101,7 +99,6 @@
     factory(RebasedPatchSetSender.Factory.class);
     factory(AbandonedSender.Factory.class);
     factory(RemoveReviewer.Factory.class);
-    factory(RestoreChange.Factory.class);
     factory(RestoredSender.Factory.class);
     factory(RevertedSender.Factory.class);
     factory(CommentSender.Factory.class);
@@ -115,5 +112,6 @@
     factory(CreateProject.Factory.class);
     factory(Submit.Factory.class);
     factory(SuggestParentCandidates.Factory.class);
+    factory(BanCommit.Factory.class);
   }
 }
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 9992f18..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,8 +15,7 @@
 package com.google.gerrit.server.config;
 
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gwtorm.server.SchemaFactory;
+import com.google.gerrit.server.account.GroupBackend;
 import com.google.inject.Inject;
 
 import org.eclipse.jgit.lib.Config;
@@ -25,9 +24,9 @@
 
 public class GitReceivePackGroupsProvider extends GroupSetProvider {
   @Inject
-  public GitReceivePackGroupsProvider(@GerritServerConfig Config config,
-      SchemaFactory<ReviewDb> db) {
-    super(config, db, "receive", null, "allowGroup");
+  public GitReceivePackGroupsProvider(GroupBackend gb,
+      @GerritServerConfig Config config) {
+    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 76d8844..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,8 +15,7 @@
 package com.google.gerrit.server.config;
 
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gwtorm.server.SchemaFactory;
+import com.google.gerrit.server.account.GroupBackend;
 import com.google.inject.Inject;
 
 import org.eclipse.jgit.lib.Config;
@@ -26,9 +25,9 @@
 
 public class GitUploadPackGroupsProvider extends GroupSetProvider {
   @Inject
-  public GitUploadPackGroupsProvider(@GerritServerConfig Config config,
-      SchemaFactory<ReviewDb> db) {
-    super(config, db, "upload", null, "allowGroup");
+  public GitUploadPackGroupsProvider(GroupBackend gb,
+      @GerritServerConfig Config config) {
+    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 15711af..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
@@ -14,12 +14,11 @@
 
 package com.google.gerrit.server.config;
 
-import static com.google.gerrit.server.config.ConfigUtil.groupsFor;
-import static java.util.Collections.unmodifiableSet;
-
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gwtorm.server.SchemaFactory;
+import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.account.GroupBackends;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
@@ -37,10 +36,20 @@
   protected Set<AccountGroup.UUID> groupIds;
 
   @Inject
-  protected GroupSetProvider(@GerritServerConfig Config config,
-      SchemaFactory<ReviewDb> db, String section, String subsection, String name) {
+  protected GroupSetProvider(GroupBackend groupBackend,
+      @GerritServerConfig Config config, String section,
+      String subsection, String name) {
     String[] groupNames = config.getStringList(section, subsection, name);
-    groupIds = unmodifiableSet(groupsFor(db, groupNames, log));
+    ImmutableSet.Builder<AccountGroup.UUID> builder = ImmutableSet.builder();
+    for (String n : groupNames) {
+      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();
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/MasterNodeStartup.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/MasterNodeStartup.java
index 26c76c5..bb95dca 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/MasterNodeStartup.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/MasterNodeStartup.java
@@ -14,14 +14,11 @@
 
 package com.google.gerrit.server.config;
 
-import com.google.gerrit.lifecycle.LifecycleListener;
+import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.lifecycle.LifecycleModule;
-import com.google.gerrit.server.git.PushAllProjectsOp;
 import com.google.gerrit.server.git.ReloadSubmitQueueOp;
 import com.google.inject.Inject;
 
-import org.eclipse.jgit.lib.Config;
-
 import java.util.concurrent.TimeUnit;
 
 /** Configuration for a master node in a cluster of servers. */
@@ -32,26 +29,15 @@
   }
 
   static class OnStart implements LifecycleListener {
-    private final PushAllProjectsOp.Factory pushAll;
     private final ReloadSubmitQueueOp.Factory submit;
-    private final boolean replicateOnStartup;
 
     @Inject
-    OnStart(final PushAllProjectsOp.Factory pushAll,
-        final ReloadSubmitQueueOp.Factory submit,
-        final @GerritServerConfig Config cfg) {
-      this.pushAll = pushAll;
+    OnStart(final ReloadSubmitQueueOp.Factory submit) {
       this.submit = submit;
-
-      replicateOnStartup = cfg.getBoolean("gerrit", "replicateOnStartup", true);
     }
 
     @Override
     public void start() {
-      if (replicateOnStartup) {
-        pushAll.create(null).start(30, TimeUnit.SECONDS);
-      }
-
       submit.create().start(15, TimeUnit.SECONDS);
     }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java
index b279086..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,8 +14,7 @@
 
 package com.google.gerrit.server.config;
 
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gwtorm.server.SchemaFactory;
+import com.google.gerrit.server.account.GroupBackend;
 import com.google.inject.Inject;
 
 import org.eclipse.jgit.lib.Config;
@@ -33,8 +32,8 @@
  */
 public class ProjectOwnerGroupsProvider extends GroupSetProvider {
   @Inject
-  public ProjectOwnerGroupsProvider(
-      @GerritServerConfig final Config config, final SchemaFactory<ReviewDb> db) {
-    super(config, db, "repository", "*", "ownerGroup");
+  public ProjectOwnerGroupsProvider(GroupBackend gb,
+      @GerritServerConfig final Config config) {
+    super(gb, config, "repository", "*", "ownerGroup");
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java
index ab52a9d..8d76e90 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePaths.java
@@ -28,7 +28,10 @@
   public final File bin_dir;
   public final File etc_dir;
   public final File lib_dir;
+  public final File tmp_dir;
   public final File logs_dir;
+  public final File plugins_dir;
+  public final File data_dir;
   public final File mail_dir;
   public final File hooks_dir;
   public final File static_dir;
@@ -38,7 +41,6 @@
 
   public final File gerrit_config;
   public final File secure_config;
-  public final File replication_config;
   public final File contact_information_pub;
 
   public final File ssl_keystore;
@@ -62,6 +64,9 @@
     bin_dir = new File(site_path, "bin");
     etc_dir = new File(site_path, "etc");
     lib_dir = new File(site_path, "lib");
+    tmp_dir = new File(site_path, "tmp");
+    plugins_dir = new File(site_path, "plugins");
+    data_dir = new File(site_path, "data");
     logs_dir = new File(site_path, "logs");
     mail_dir = new File(etc_dir, "mail");
     hooks_dir = new File(site_path, "hooks");
@@ -72,7 +77,6 @@
 
     gerrit_config = new File(etc_dir, "gerrit.config");
     secure_config = new File(etc_dir, "secure.config");
-    replication_config = new File(etc_dir, "replication.config");
     contact_information_pub = new File(etc_dir, "contact_information.pub");
 
     ssl_keystore = new File(etc_dir, "keystore");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/documentation/MarkdownFormatter.java b/gerrit-server/src/main/java/com/google/gerrit/server/documentation/MarkdownFormatter.java
new file mode 100644
index 0000000..6bb7ba8
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/documentation/MarkdownFormatter.java
@@ -0,0 +1,145 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.documentation;
+
+import static org.pegdown.Extensions.ALL;
+import static org.pegdown.Extensions.HARDWRAPS;
+
+import com.google.common.base.Strings;
+
+import org.eclipse.jgit.util.RawParseUtils;
+import org.eclipse.jgit.util.TemporaryBuffer;
+import org.pegdown.LinkRenderer;
+import org.pegdown.PegDownProcessor;
+import org.pegdown.ToHtmlSerializer;
+import org.pegdown.ast.HeaderNode;
+import org.pegdown.ast.Node;
+import org.pegdown.ast.RootNode;
+import org.pegdown.ast.TextNode;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.net.URL;
+import java.nio.charset.Charset;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+public class MarkdownFormatter {
+  private static final Logger log =
+      LoggerFactory.getLogger(MarkdownFormatter.class);
+
+  private static final String css;
+
+  static {
+    AtomicBoolean file = new AtomicBoolean();
+    String src;
+    try {
+      src = readPegdownCss(file);
+    } catch (IOException err) {
+      log.warn("Cannot load pegdown.css", err);
+      src = "";
+    }
+    css = file.get() ? null : src;
+  }
+
+  private static String readCSS() {
+    if (css != null) {
+      return css;
+    }
+    try {
+      return readPegdownCss(new AtomicBoolean());
+    } catch (IOException err) {
+      log.warn("Cannot load pegdown.css", err);
+      return "";
+    }
+  }
+
+  public byte[] markdownToDocHtml(String md, String charEnc)
+      throws UnsupportedEncodingException {
+    RootNode root = parseMarkdown(md);
+    String title = findTitle(root);
+
+    StringBuilder html = new StringBuilder();
+    html.append("<html>");
+    html.append("<head>");
+    if (!Strings.isNullOrEmpty(title)) {
+      html.append("<title>").append(title).append("</title>");
+    }
+    html.append("<style type=\"text/css\">\n")
+        .append(readCSS())
+        .append("\n</style>");
+    html.append("</head>");
+    html.append("<body>\n");
+    html.append(new ToHtmlSerializer(new LinkRenderer()).toHtml(root));
+    html.append("\n</body></html>");
+    return html.toString().getBytes(charEnc);
+  }
+
+  public String extractTitleFromMarkdown(byte[] data, String charEnc) {
+    String md = RawParseUtils.decode(Charset.forName(charEnc), data);
+    return findTitle(parseMarkdown(md));
+  }
+
+  private String findTitle(Node root) {
+    if (root instanceof HeaderNode) {
+      HeaderNode h = (HeaderNode) root;
+      if (h.getLevel() == 1
+          && h.getChildren() != null
+          && !h.getChildren().isEmpty()) {
+        StringBuilder b = new StringBuilder();
+        for (Node n : root.getChildren()) {
+          if (n instanceof TextNode) {
+            b.append(((TextNode) n).getText());
+          }
+        }
+        return b.toString();
+      }
+    }
+
+    for (Node n : root.getChildren()) {
+      String title = findTitle(n);
+      if (title != null) {
+        return title;
+      }
+    }
+    return null;
+  }
+
+  private RootNode parseMarkdown(String md) {
+    return new PegDownProcessor(ALL & ~(HARDWRAPS))
+        .parseMarkdown(md.toCharArray());
+  }
+
+  private static String readPegdownCss(AtomicBoolean file)
+      throws IOException {
+    String name = "pegdown.css";
+    URL url = MarkdownFormatter.class.getResource(name);
+    if (url == null) {
+      throw new FileNotFoundException("Resource " + name);
+    }
+    file.set("file".equals(url.getProtocol()));
+    InputStream in = url.openStream();
+    try {
+      TemporaryBuffer.Heap tmp = new TemporaryBuffer.Heap(128 * 1024);
+      tmp.copy(in);
+      return new String(tmp.toByteArray(), "UTF-8");
+    } finally {
+      in.close();
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/AccountAttribute.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/AccountAttribute.java
index 2ad7ffe..2d88b83 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/AccountAttribute.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/AccountAttribute.java
@@ -17,4 +17,5 @@
 public class AccountAttribute {
     public String name;
     public String email;
+    public String username;
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeAttribute.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeAttribute.java
index 9810f59..5150b48 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeAttribute.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeAttribute.java
@@ -42,4 +42,5 @@
 
     public List<DependencyAttribute> dependsOn;
     public List<DependencyAttribute> neededBy;
+    public List<SubmitRecordAttribute> submitRecords;
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeRestoreEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeRestoredEvent.java
similarity index 93%
rename from gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeRestoreEvent.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeRestoredEvent.java
index 1a2922b..717e23c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeRestoreEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeRestoredEvent.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.events;
 
-public class ChangeRestoreEvent extends ChangeEvent {
+public class ChangeRestoredEvent extends ChangeEvent {
     public final String type = "change-restored";
     public ChangeAttribute change;
     public PatchSetAttribute patchSet;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeRestoreEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/DraftPublishedEvent.java
similarity index 75%
copy from gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeRestoreEvent.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/events/DraftPublishedEvent.java
index 1a2922b..c90ac90 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/ChangeRestoreEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/DraftPublishedEvent.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.
@@ -14,10 +14,9 @@
 
 package com.google.gerrit.server.events;
 
-public class ChangeRestoreEvent extends ChangeEvent {
-    public final String type = "change-restored";
+public class DraftPublishedEvent extends ChangeEvent {
+    public final String type = "draft-published";
     public ChangeAttribute change;
     public PatchSetAttribute patchSet;
-    public AccountAttribute restorer;
-    public String reason;
+    public AccountAttribute uploader;
 }
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 4d34b71..f07ae0c 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
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.common.data.ApprovalType;
 import com.google.gerrit.common.data.ApprovalTypes;
+import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
@@ -27,11 +28,13 @@
 import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.reviewdb.client.TrackingId;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 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;
@@ -39,31 +42,39 @@
 import com.google.inject.Singleton;
 
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Map;
+import java.util.List;
 
 import javax.annotation.Nullable;
 
 @Singleton
 public class EventFactory {
+  private static final Logger log = LoggerFactory.getLogger(EventFactory.class);
   private final AccountCache accountCache;
   private final Provider<String> urlProvider;
   private final ApprovalTypes approvalTypes;
   private final PatchListCache patchListCache;
   private final SchemaFactory<ReviewDb> schema;
+  private final PersonIdent myIdent;
 
   @Inject
   EventFactory(AccountCache accountCache,
       @CanonicalWebUrl @Nullable Provider<String> urlProvider,
       ApprovalTypes approvalTypes,
-      PatchListCache patchListCache, SchemaFactory<ReviewDb> schema) {
+      PatchListCache patchListCache, SchemaFactory<ReviewDb> schema,
+      @GerritPersonIdent PersonIdent myIdent) {
     this.accountCache = accountCache;
     this.urlProvider = urlProvider;
     this.approvalTypes = approvalTypes;
     this.patchListCache = patchListCache;
     this.schema = schema;
+    this.myIdent = myIdent;
   }
 
   /**
@@ -117,6 +128,47 @@
     a.status = change.getStatus();
   }
 
+  /**
+   * Add submitRecords to an existing ChangeAttribute.
+   *
+   * @param ca
+   * @param submitRecords
+   */
+  public void addSubmitRecords(ChangeAttribute ca,
+      List<SubmitRecord> submitRecords) {
+    ca.submitRecords = new ArrayList<SubmitRecordAttribute>();
+
+    for (SubmitRecord submitRecord : submitRecords) {
+      SubmitRecordAttribute sa = new SubmitRecordAttribute();
+      sa.status = submitRecord.status.name();
+      if (submitRecord.status != SubmitRecord.Status.RULE_ERROR) {
+        addSubmitRecordLabels(submitRecord, sa);
+      }
+      ca.submitRecords.add(sa);
+    }
+    // Remove empty lists so a confusing label won't be displayed in the output.
+    if (ca.submitRecords.isEmpty()) {
+      ca.submitRecords = null;
+    }
+  }
+
+  private void addSubmitRecordLabels(SubmitRecord submitRecord,
+      SubmitRecordAttribute sa) {
+    if (submitRecord.labels != null && !submitRecord.labels.isEmpty()) {
+      sa.labels = new ArrayList<SubmitLabelAttribute>();
+      for (SubmitRecord.Label lbl : submitRecord.labels) {
+        SubmitLabelAttribute la = new SubmitLabelAttribute();
+        la.label = lbl.label;
+        la.status = lbl.status.name();
+        if(lbl.appliedBy != null) {
+          Account a = accountCache.get(lbl.appliedBy).getAccount();
+          la.by = asAccountAttribute(a);
+        }
+        sa.labels.add(la);
+      }
+    }
+  }
+
   public void addDependencies(ChangeAttribute ca, Change change) {
     ca.dependsOn = new ArrayList<DependencyAttribute>();
     ca.neededBy = new ArrayList<DependencyAttribute>();
@@ -229,16 +281,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) {
     }
   }
 
@@ -273,6 +328,20 @@
     p.ref = patchSet.getRefName();
     p.uploader = asAccountAttribute(patchSet.getUploader());
     p.createdOn = patchSet.getCreatedOn().getTime() / 1000L;
+    try {
+      final ReviewDb db = schema.open();
+      try {
+        p.parents = new ArrayList<String>();
+        for (PatchSetAncestor a : db.patchSetAncestors().ancestorsOf(
+            patchSet.getId())) {
+          p.parents.add(a.getAncestorRevision().get());
+        }
+      } finally {
+        db.close();
+      }
+    } catch (OrmException e) {
+      log.error("Cannot load patch set data for " + patchSet.getId(), e);
+    }
     return p;
   }
 
@@ -321,6 +390,21 @@
     AccountAttribute who = new AccountAttribute();
     who.name = account.getFullName();
     who.email = account.getPreferredEmail();
+    who.username = account.getUserName();
+    return who;
+  }
+
+  /**
+   * Create an AuthorAttribute for the given person ident suitable for
+   * serialization to JSON.
+   *
+   * @param ident
+   * @return object suitable for serialization to JSON
+   */
+  public AccountAttribute asAccountAttribute(PersonIdent ident) {
+    AccountAttribute who = new AccountAttribute();
+    who.name = ident.getName();
+    who.email = ident.getEmailAddress();
     return who;
   }
 
@@ -348,7 +432,9 @@
   public MessageAttribute asMessageAttribute(ChangeMessage message) {
     MessageAttribute a = new MessageAttribute();
     a.timestamp = message.getWrittenOn().getTime() / 1000L;
-    a.reviewer = asAccountAttribute(message.getAuthor());
+    a.reviewer =
+        message.getAuthor() != null ? asAccountAttribute(message.getAuthor())
+            : asAccountAttribute(myIdent);
     a.message = message.getMessage();
     return a;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/PatchSetAttribute.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/PatchSetAttribute.java
index dca4438..f726ce3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/PatchSetAttribute.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/PatchSetAttribute.java
@@ -17,13 +17,14 @@
 import java.util.List;
 
 public class PatchSetAttribute {
-    public String number;
-    public String revision;
-    public String ref;
-    public AccountAttribute uploader;
-    public Long createdOn;
+  public String number;
+  public String revision;
+  public List<String> parents;
+  public String ref;
+  public AccountAttribute uploader;
+  public Long createdOn;
 
-    public List<ApprovalAttribute> approvals;
-    public List<PatchSetCommentAttribute> comments;
-    public List<PatchAttribute> files;
+  public List<ApprovalAttribute> approvals;
+  public List<PatchSetCommentAttribute> comments;
+  public List<PatchAttribute> files;
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CachePool.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/SubmitLabelAttribute.java
similarity index 71%
rename from gerrit-server/src/main/java/com/google/gerrit/server/cache/CachePool.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/events/SubmitLabelAttribute.java
index 3370b08..99d0350 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CachePool.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/SubmitLabelAttribute.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,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.cache;
+package com.google.gerrit.server.events;
 
-public interface CachePool {
-  public <K, V> ProxyCache<K, V> register(CacheProvider<K, V> provider);
+public class SubmitLabelAttribute {
+    public String label;
+    public String status;
+    public AccountAttribute by;
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CachePool.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/SubmitRecordAttribute.java
similarity index 70%
copy from gerrit-server/src/main/java/com/google/gerrit/server/cache/CachePool.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/events/SubmitRecordAttribute.java
index 3370b08..04b76e1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CachePool.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/SubmitRecordAttribute.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,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.cache;
+package com.google.gerrit.server.events;
 
-public interface CachePool {
-  public <K, V> ProxyCache<K, V> register(CacheProvider<K, V> provider);
+import java.util.List;
+
+public class SubmitRecordAttribute {
+    public String status;
+    public List<SubmitLabelAttribute> labels;
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java
new file mode 100644
index 0000000..833b611
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java
@@ -0,0 +1,73 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.extensions.events;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.inject.Inject;
+
+import java.util.Collections;
+import java.util.List;
+
+public class GitReferenceUpdated {
+  public static final GitReferenceUpdated DISABLED = new GitReferenceUpdated(
+      Collections.<GitReferenceUpdatedListener> emptyList());
+
+  private final Iterable<GitReferenceUpdatedListener> listeners;
+
+  @Inject
+  GitReferenceUpdated(DynamicSet<GitReferenceUpdatedListener> listeners) {
+    this.listeners = listeners;
+  }
+
+  GitReferenceUpdated(Iterable<GitReferenceUpdatedListener> listeners) {
+    this.listeners = listeners;
+  }
+
+  public void fire(Project.NameKey project, String ref) {
+    Event event = new Event(project, ref);
+    for (GitReferenceUpdatedListener l : listeners) {
+      l.onGitReferenceUpdated(event);
+    }
+  }
+
+  private static class Event implements GitReferenceUpdatedListener.Event {
+    private final String projectName;
+    private final String ref;
+
+    Event(Project.NameKey project, String ref) {
+      this.projectName = project.get();
+      this.ref = ref;
+    }
+
+    @Override
+    public String getProjectName() {
+      return projectName;
+    }
+
+    @Override
+    public List<GitReferenceUpdatedListener.Update> getUpdates() {
+      GitReferenceUpdatedListener.Update update =
+          new GitReferenceUpdatedListener.Update() {
+            public String getRefName() {
+              return ref;
+            }
+          };
+      return ImmutableList.of(update);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/AccountsSection.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/AccountsSection.java
new file mode 100644
index 0000000..7d868bf
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/AccountsSection.java
@@ -0,0 +1,35 @@
+// 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.git;
+
+import com.google.gerrit.common.data.PermissionRule;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class AccountsSection {
+  protected List<PermissionRule> sameGroupVisibility;
+
+  public List<PermissionRule> getSameGroupVisibility() {
+    if (sameGroupVisibility == null) {
+      sameGroupVisibility = new ArrayList<PermissionRule>();
+    }
+    return sameGroupVisibility;
+  }
+
+  public void setSameGroupVisibility(List<PermissionRule> sameGroupVisibility) {
+    this.sameGroupVisibility = sameGroupVisibility;
+  }
+}
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
new file mode 100644
index 0000000..c0e00aa
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommit.java
@@ -0,0 +1,156 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git;
+
+import static com.google.gerrit.server.git.GitRepositoryManager.REF_REJECT_COMMITS;
+
+import com.google.gerrit.common.errors.PermissionDeniedException;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.notes.Note;
+import org.eclipse.jgit.notes.NoteMap;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.util.Date;
+import java.util.List;
+import java.util.TimeZone;
+
+public class BanCommit {
+  public interface Factory {
+    BanCommit create();
+  }
+
+  private final Provider<IdentifiedUser> currentUser;
+  private final GitRepositoryManager repoManager;
+  private final PersonIdent gerritIdent;
+  private NotesBranchUtil.Factory notesBranchUtilFactory;
+
+  @Inject
+  BanCommit(final Provider<IdentifiedUser> currentUser,
+      final GitRepositoryManager repoManager,
+      @GerritPersonIdent final PersonIdent gerritIdent,
+      final NotesBranchUtil.Factory notesBranchUtilFactory) {
+    this.currentUser = currentUser;
+    this.repoManager = repoManager;
+    this.gerritIdent = gerritIdent;
+    this.notesBranchUtilFactory = notesBranchUtilFactory;
+  }
+
+  public BanCommitResult ban(final ProjectControl projectControl,
+      final List<ObjectId> commitsToBan, final String reason)
+      throws PermissionDeniedException, IOException,
+      InterruptedException, MergeException, ConcurrentRefUpdateException {
+    if (!projectControl.isOwner()) {
+      throw new PermissionDeniedException(
+          "No project owner: not permitted to ban commits");
+    }
+
+    final BanCommitResult result = new BanCommitResult();
+    NoteMap banCommitNotes = NoteMap.newEmptyMap();
+    // add a note for each banned commit to notes
+    final Repository repo =
+        repoManager.openRepository(projectControl.getProject().getNameKey());
+    try {
+      final RevWalk revWalk = new RevWalk(repo);
+      final ObjectInserter inserter = repo.newObjectInserter();
+      try {
+        for (final ObjectId commitToBan : commitsToBan) {
+          try {
+            revWalk.parseCommit(commitToBan);
+          } catch (MissingObjectException e) {
+            // ignore exception, also not existing commits can be banned
+          } catch (IncorrectObjectTypeException e) {
+            result.notACommit(commitToBan, e.getMessage());
+            continue;
+          }
+          banCommitNotes.set(commitToBan, createNoteContent(reason, inserter));
+        }
+        inserter.flush();
+        NotesBranchUtil notesBranchUtil = notesBranchUtilFactory.create(repo);
+        NoteMap newlyCreated =
+            notesBranchUtil.commitNewNotes(banCommitNotes, REF_REJECT_COMMITS,
+                createPersonIdent(), buildCommitMessage(commitsToBan, reason));
+
+        for (Note n : banCommitNotes) {
+          if (newlyCreated.contains(n)) {
+            result.commitBanned(n);
+          } else {
+            result.commitAlreadyBanned(n);
+          }
+        }
+        return result;
+      } finally {
+        revWalk.release();
+        inserter.release();
+      }
+    } finally {
+      repo.close();
+    }
+  }
+
+  private ObjectId createNoteContent(String reason, ObjectInserter inserter)
+      throws UnsupportedEncodingException, IOException {
+    String noteContent = reason != null ? reason : "";
+    if (noteContent.length() > 0 && !noteContent.endsWith("\n")) {
+      noteContent = noteContent + "\n";
+    }
+    return inserter.insert(Constants.OBJ_BLOB, noteContent.getBytes("UTF-8"));
+  }
+
+  private PersonIdent createPersonIdent() {
+    Date now = new Date();
+    TimeZone tz = gerritIdent.getTimeZone();
+    return currentUser.get().newCommitterIdent(now, tz);
+  }
+
+  private static String buildCommitMessage(final List<ObjectId> bannedCommits,
+      final String reason) {
+    final StringBuilder commitMsg = new StringBuilder();
+    commitMsg.append("Banning ");
+    commitMsg.append(bannedCommits.size());
+    commitMsg.append(" ");
+    commitMsg.append(bannedCommits.size() == 1 ? "commit" : "commits");
+    commitMsg.append("\n\n");
+    if (reason != null) {
+      commitMsg.append("Reason: ");
+      commitMsg.append(reason);
+      commitMsg.append("\n\n");
+    }
+    commitMsg.append("The following commits are banned:\n");
+    final StringBuilder commitList = new StringBuilder();
+    for (final ObjectId c : bannedCommits) {
+      if (commitList.length() > 0) {
+        commitList.append(",\n");
+      }
+      commitList.append(c.getName());
+    }
+    commitMsg.append(commitList);
+    return commitMsg.toString();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommitResult.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommitResult.java
new file mode 100644
index 0000000..1b48455
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommitResult.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.git;
+
+import org.eclipse.jgit.lib.ObjectId;
+
+import java.util.LinkedList;
+import java.util.List;
+
+public class BanCommitResult {
+
+  private final List<ObjectId> newlyBannedCommits = new LinkedList<ObjectId>();
+  private final List<ObjectId> alreadyBannedCommits = new LinkedList<ObjectId>();
+  private final List<ObjectId> ignoredObjectIds = new LinkedList<ObjectId>();
+
+  public BanCommitResult() {
+  }
+
+  public void commitBanned(final ObjectId commitId) {
+    newlyBannedCommits.add(commitId);
+  }
+
+  public void commitAlreadyBanned(final ObjectId commitId) {
+    alreadyBannedCommits.add(commitId);
+  }
+
+  public void notACommit(final ObjectId id, final String message) {
+    ignoredObjectIds.add(id);
+  }
+
+  public List<ObjectId> getNewlyBannedCommits() {
+    return newlyBannedCommits;
+  }
+
+  public List<ObjectId> getAlreadyBannedCommits() {
+    return alreadyBannedCommits;
+  }
+
+  public List<ObjectId> getIgnoredObjectIds() {
+    return ignoredObjectIds;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeMergeQueue.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeMergeQueue.java
index 86e0740..6d1b155 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeMergeQueue.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeMergeQueue.java
@@ -20,16 +20,17 @@
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.RemotePeer;
 import com.google.gerrit.server.config.GerritRequestModule;
 import com.google.gerrit.server.ssh.SshInfo;
+import com.google.gerrit.server.util.RequestContext;
 import com.google.gerrit.server.util.RequestScopePropagator;
 import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
 import com.google.inject.OutOfScopeException;
 import com.google.inject.Provider;
+import com.google.inject.Provides;
 import com.google.inject.Singleton;
 import com.google.inject.servlet.RequestScoped;
 
@@ -43,6 +44,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.Callable;
 import java.util.concurrent.TimeUnit;
 
 @Singleton
@@ -57,6 +59,7 @@
 
   private final WorkQueue workQueue;
   private final Provider<MergeOp.Factory> bgFactory;
+  private final PerThreadRequestScope.Scoper threadScoper;
 
   @Inject
   ChangeMergeQueue(final WorkQueue wq, Injector parent) {
@@ -68,15 +71,9 @@
         bindScope(RequestScoped.class, PerThreadRequestScope.REQUEST);
         bind(RequestScopePropagator.class)
             .to(PerThreadRequestScope.Propagator.class);
+        bind(PerThreadRequestScope.Propagator.class);
         install(new GerritRequestModule());
 
-        bind(CurrentUser.class).to(IdentifiedUser.class);
-        bind(IdentifiedUser.class).toProvider(new Provider<IdentifiedUser>() {
-          @Override
-          public IdentifiedUser get() {
-            throw new OutOfScopeException("No user on merge thread");
-          }
-        });
         bind(SocketAddress.class).annotatedWith(RemotePeer.class).toProvider(
             new Provider<SocketAddress>() {
               @Override
@@ -91,8 +88,26 @@
           }
         });
       }
+
+      @Provides
+      public PerThreadRequestScope.Scoper provideScoper(
+          final PerThreadRequestScope.Propagator propagator) {
+        final RequestContext requestContext = new RequestContext() {
+          @Override
+          public CurrentUser getCurrentUser() {
+            throw new OutOfScopeException("No user on merge thread");
+          }
+        };
+        return new PerThreadRequestScope.Scoper() {
+          @Override
+          public <T> Callable<T> scope(Callable<T> callable) {
+            return propagator.scope(requestContext, callable);
+          }
+        };
+      }
     });
     bgFactory = child.getProvider(MergeOp.Factory.class);
+    threadScoper = child.getInstance(PerThreadRequestScope.Scoper.class);
   }
 
   @Override
@@ -186,19 +201,15 @@
     }
   }
 
-  private void mergeImpl(Branch.NameKey branch) {
+  private void mergeImpl(final Branch.NameKey branch) {
     try {
-      PerThreadRequestScope ctx = new PerThreadRequestScope();
-      PerThreadRequestScope old = PerThreadRequestScope.set(ctx);
-      try {
-        try {
+      threadScoper.scope(new Callable<Void>(){
+        @Override
+        public Void call() throws Exception {
           bgFactory.get().create(branch).merge();
-        } finally {
-          ctx.cleanup.run();
+          return null;
         }
-      } finally {
-        PerThreadRequestScope.set(old);
-      }
+      }).call();
     } catch (Throwable e) {
       log.error("Merge attempt for " + branch + " failed", e);
     } finally {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/CommitMergeStatus.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/CommitMergeStatus.java
index 8790351..6b53121 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/CommitMergeStatus.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/CommitMergeStatus.java
@@ -51,7 +51,26 @@
   /** */
   NOT_FAST_FORWARD("Project policy requires all submissions to be a fast-forward.\n"
                   + "\n"
-                  + "Please rebase the change locally and upload again for review.");
+                  + "Please rebase the change locally and upload again for review."),
+
+  /** */
+  INVALID_PROJECT_CONFIGURATION("Change contains an invalid project configuration."),
+
+  /** */
+  INVALID_PROJECT_CONFIGURATION_PARENT_PROJECT_NOT_FOUND(
+      "Change contains an invalid project configuration:\n"
+          + "Parent project does not exist."),
+
+  /** */
+  INVALID_PROJECT_CONFIGURATION_ROOT_PROJECT_CANNOT_HAVE_PARENT(
+      "Change contains an invalid project configuration:\n"
+          + "The root project cannot have a parent."),
+
+  /** */
+  SETTING_PARENT_PROJECT_ONLY_ALLOWED_BY_ADMIN(
+      "Change contains a project configuration that changes the parent project.\n"
+          + "The change must be submitted by a Gerrit administrator.");
+
 
   private String message;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/CreateCodeReviewNotes.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/CreateCodeReviewNotes.java
index 6fea8f1..9a0fe17 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/CreateCodeReviewNotes.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/CreateCodeReviewNotes.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.common.data.ApprovalTypes;
 import com.google.gerrit.reviewdb.client.ApprovalCategory;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.GerritPersonIdent;
@@ -31,23 +32,15 @@
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
-import org.eclipse.jgit.errors.CorruptObjectException;
-import org.eclipse.jgit.errors.IncorrectObjectTypeException;
-import org.eclipse.jgit.errors.MissingObjectException;
-import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
 import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.NullProgressMonitor;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectInserter;
-import org.eclipse.jgit.lib.ObjectReader;
 import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.RefUpdate;
-import org.eclipse.jgit.lib.RefUpdate.Result;
+import org.eclipse.jgit.lib.ProgressMonitor;
 import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.notes.Note;
 import org.eclipse.jgit.notes.NoteMap;
-import org.eclipse.jgit.notes.NoteMapMerger;
-import org.eclipse.jgit.notes.NoteMerger;
 import org.eclipse.jgit.revwalk.FooterKey;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
@@ -68,30 +61,22 @@
     CreateCodeReviewNotes create(ReviewDb reviewDb, Repository db);
   }
 
-  private static final int MAX_LOCK_FAILURE_CALLS = 10;
-  private static final int SLEEP_ON_LOCK_FAILURE_MS = 25;
   private static final FooterKey CHANGE_ID = new FooterKey("Change-Id");
 
-  private final ReviewDb schema;
-  private final PersonIdent gerritIdent;
   private final AccountCache accountCache;
   private final ApprovalTypes approvalTypes;
   private final String canonicalWebUrl;
   private final String anonymousCowardName;
+  private final ReviewDb schema;
   private final Repository db;
-  private final RevWalk revWalk;
-  private final ObjectInserter inserter;
-  private final ObjectReader reader;
 
-  private RevCommit baseCommit;
-  private NoteMap base;
-
-  private RevCommit oursCommit;
-  private NoteMap ours;
-
-  private List<CodeReviewCommit> commits;
   private PersonIdent author;
 
+  private RevWalk revWalk;
+  private ObjectInserter inserter;
+
+  private final NotesBranchUtil.Factory notesBranchUtilFactory;
+
   @Inject
   CreateCodeReviewNotes(
       @GerritPersonIdent final PersonIdent gerritIdent,
@@ -99,90 +84,89 @@
       final ApprovalTypes approvalTypes,
       final @Nullable @CanonicalWebUrl String canonicalWebUrl,
       final @AnonymousCowardName String anonymousCowardName,
+      final NotesBranchUtil.Factory notesBranchUtilFactory,
       final @Assisted  ReviewDb reviewDb,
       final @Assisted  Repository db) {
-    schema = reviewDb;
     this.author = gerritIdent;
-    this.gerritIdent = gerritIdent;
     this.accountCache = accountCache;
     this.approvalTypes = approvalTypes;
     this.canonicalWebUrl = canonicalWebUrl;
     this.anonymousCowardName = anonymousCowardName;
+    this.notesBranchUtilFactory = notesBranchUtilFactory;
+    schema = reviewDb;
     this.db = db;
-
-    revWalk = new RevWalk(db);
-    inserter = db.newObjectInserter();
-    reader = db.newObjectReader();
   }
 
   public void create(List<CodeReviewCommit> commits, PersonIdent author)
       throws CodeReviewNoteCreationException {
     try {
-      this.commits = commits;
-      this.author = author;
-      loadBase();
-      applyNotes();
-      updateRef();
+      revWalk = new RevWalk(db);
+      inserter = db.newObjectInserter();
+      if (author != null) {
+        this.author = author;
+      }
+
+      NoteMap notes = NoteMap.newEmptyMap();
+      StringBuilder message =
+          new StringBuilder("Update notes for submitted changes\n\n");
+      for (CodeReviewCommit c : commits) {
+        notes.set(c, createNoteContent(c.change, c));
+        message.append("* ").append(c.getShortMessage()).append("\n");
+      }
+
+      NotesBranchUtil notesBranchUtil = notesBranchUtilFactory.create(db);
+      notesBranchUtil.commitAllNotes(notes, REFS_NOTES_REVIEW, author,
+          message.toString());
+      inserter.flush();
     } catch (IOException e) {
       throw new CodeReviewNoteCreationException(e);
-    } catch (InterruptedException e) {
+    } catch (ConcurrentRefUpdateException e) {
       throw new CodeReviewNoteCreationException(e);
     } finally {
-      release();
+      revWalk.release();
+      inserter.release();
     }
   }
 
-  public void loadBase() throws IOException {
-    Ref notesBranch = db.getRef(REFS_NOTES_REVIEW);
-    if (notesBranch != null) {
-      baseCommit = revWalk.parseCommit(notesBranch.getObjectId());
-      base = NoteMap.read(revWalk.getObjectReader(), baseCommit);
-    }
-    if (baseCommit != null) {
-      ours = NoteMap.read(db.newObjectReader(), baseCommit);
-    } else {
-      ours = NoteMap.newEmptyMap();
+  public void create(List<Change> changes, PersonIdent author,
+      String commitMessage, ProgressMonitor monitor) throws OrmException,
+      IOException, CodeReviewNoteCreationException {
+    try {
+      revWalk = new RevWalk(db);
+      inserter = db.newObjectInserter();
+      if (author != null) {
+        this.author = author;
+      }
+      if (monitor == null) {
+        monitor = NullProgressMonitor.INSTANCE;
+      }
+
+      NoteMap notes = NoteMap.newEmptyMap();
+      for (Change c : changes) {
+        monitor.update(1);
+        PatchSet ps = schema.patchSets().get(c.currentPatchSetId());
+        ObjectId commitId = ObjectId.fromString(ps.getRevision().get());
+        notes.set(commitId, createNoteContent(c, commitId));
+      }
+
+      NotesBranchUtil notesBranchUtil = notesBranchUtilFactory.create(db);
+      notesBranchUtil.commitAllNotes(notes, REFS_NOTES_REVIEW, author,
+          commitMessage);
+      inserter.flush();
+    } catch (ConcurrentRefUpdateException e) {
+      throw new CodeReviewNoteCreationException(e);
+    } finally {
+      revWalk.release();
+      inserter.release();
     }
   }
 
-  private void applyNotes() throws IOException, CodeReviewNoteCreationException {
-    StringBuilder message =
-        new StringBuilder("Update notes for submitted changes\n\n");
-    for (CodeReviewCommit c : commits) {
-      add(c.change, c);
-      message.append("* ").append(c.getShortMessage()).append("\n");
-    }
-    commit(message.toString());
-  }
-
-  public void commit(String message) throws IOException {
-    if (baseCommit != null) {
-      oursCommit = createCommit(ours, author, message, baseCommit);
-    } else {
-      oursCommit = createCommit(ours, author, message);
-    }
-  }
-
-  public void add(Change change, ObjectId commit)
-      throws MissingObjectException, IncorrectObjectTypeException, IOException,
-      CodeReviewNoteCreationException {
+  private ObjectId createNoteContent(Change change, ObjectId commit)
+      throws CodeReviewNoteCreationException, IOException  {
     if (!(commit instanceof RevCommit)) {
       commit = revWalk.parseCommit(commit);
     }
-
-    RevCommit c = (RevCommit) commit;
-    ObjectId noteContent = createNoteContent(change, c);
-    if (ours.contains(c)) {
-      // merge the existing and the new note as if they are both new
-      // means: base == null
-      // there is not really a common ancestry for these two note revisions
-      // use the same NoteMerger that is used from the NoteMapMerger
-      NoteMerger noteMerger = new ReviewNoteMerger();
-      Note newNote = new Note(c, noteContent);
-      noteContent = noteMerger.merge(null, newNote, ours.getNote(c),
-          reader, inserter).getData();
-    }
-    ours.set(c, noteContent);
+    return createNoteContent(change, (RevCommit) commit);
   }
 
   private ObjectId createNoteContent(Change change, RevCommit commit)
@@ -227,83 +211,4 @@
       throw new CodeReviewNoteCreationException(commit, e);
     }
   }
-
-  public void updateRef() throws IOException, InterruptedException,
-      CodeReviewNoteCreationException, MissingObjectException,
-      IncorrectObjectTypeException, CorruptObjectException {
-    if (baseCommit != null && oursCommit.getTree().equals(baseCommit.getTree())) {
-      // If the trees are identical, there is no change in the notes.
-      // Avoid saving this commit as it has no new information.
-      return;
-    }
-
-    int remainingLockFailureCalls = MAX_LOCK_FAILURE_CALLS;
-    RefUpdate refUpdate = createRefUpdate(oursCommit, baseCommit);
-
-    for (;;) {
-      Result result = refUpdate.update();
-
-      if (result == Result.LOCK_FAILURE) {
-        if (--remainingLockFailureCalls > 0) {
-          Thread.sleep(SLEEP_ON_LOCK_FAILURE_MS);
-        } else {
-          throw new CodeReviewNoteCreationException(
-              "Failed to lock the ref: " + REFS_NOTES_REVIEW);
-        }
-
-      } else if (result == Result.REJECTED) {
-        RevCommit theirsCommit =
-            revWalk.parseCommit(refUpdate.getOldObjectId());
-        NoteMap theirs =
-            NoteMap.read(revWalk.getObjectReader(), theirsCommit);
-        NoteMapMerger merger = new NoteMapMerger(db);
-        NoteMap merged = merger.merge(base, ours, theirs);
-        RevCommit mergeCommit =
-            createCommit(merged, gerritIdent, "Merged note commits\n",
-                theirsCommit, oursCommit);
-        refUpdate = createRefUpdate(mergeCommit, theirsCommit);
-        remainingLockFailureCalls = MAX_LOCK_FAILURE_CALLS;
-
-      } else if (result == Result.IO_FAILURE) {
-        throw new CodeReviewNoteCreationException(
-            "Couldn't create code review notes because of IO_FAILURE");
-      } else {
-        break;
-      }
-    }
-  }
-
-  public void release() {
-    reader.release();
-    inserter.release();
-    revWalk.release();
-  }
-
-  private RevCommit createCommit(NoteMap map, PersonIdent author,
-      String message, RevCommit... parents) throws IOException {
-    CommitBuilder b = new CommitBuilder();
-    b.setTreeId(map.writeTree(inserter));
-    b.setAuthor(author != null ? author : gerritIdent);
-    b.setCommitter(gerritIdent);
-    if (parents.length > 0) {
-      b.setParentIds(parents);
-    }
-    b.setMessage(message);
-    ObjectId commitId = inserter.insert(b);
-    inserter.flush();
-    return revWalk.parseCommit(commitId);
-  }
-
-
-  private RefUpdate createRefUpdate(ObjectId newObjectId,
-      ObjectId expectedOldObjectId) throws IOException {
-    RefUpdate refUpdate = db.updateRef(REFS_NOTES_REVIEW);
-    refUpdate.setNewObjectId(newObjectId);
-    if (expectedOldObjectId == null) {
-      refUpdate.setExpectedOldObjectId(ObjectId.zeroId());
-    } else {
-      refUpdate.setExpectedOldObjectId(expectedOldObjectId);
-    }
-    return refUpdate;
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/GitRepositoryManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/GitRepositoryManager.java
index a2b0495..1bf157b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/GitRepositoryManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/GitRepositoryManager.java
@@ -59,10 +59,11 @@
    * @return the cached Repository instance. Caller must call {@code close()}
    *         when done to decrement the resource handle.
    * @throws RepositoryNotFoundException the name does not denote an existing
-   *         repository, or the name cannot be read as a repository.
+   *         repository.
+   * @throws IOException the name cannot be read as a repository.
    */
   public abstract Repository openRepository(Project.NameKey name)
-      throws RepositoryNotFoundException;
+      throws RepositoryNotFoundException, IOException;
 
   /**
    * Create (and open) a repository by name.
@@ -73,9 +74,11 @@
    * @throws RepositoryCaseMismatchException the name collides with an existing
    *         repository name, but only in case of a character within the name.
    * @throws RepositoryNotFoundException the name is invalid.
+   * @throws IOException the repository cannot be created.
    */
   public abstract Repository createRepository(Project.NameKey name)
-      throws RepositoryCaseMismatchException, RepositoryNotFoundException;
+      throws RepositoryCaseMismatchException, RepositoryNotFoundException,
+      IOException;
 
   /** @return set of all known projects, sorted by natural NameKey order. */
   public abstract SortedSet<Project.NameKey> list();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
index 9fa45e1..4e3f324 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/LocalDiskRepositoryManager.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.git;
 
-import com.google.gerrit.lifecycle.LifecycleListener;
+import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.config.GerritServerConfig;
@@ -23,6 +23,8 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
+import com.jcraft.jsch.Session;
+
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ConfigConstants;
@@ -34,6 +36,9 @@
 import org.eclipse.jgit.storage.file.LockFile;
 import org.eclipse.jgit.storage.file.WindowCache;
 import org.eclipse.jgit.storage.file.WindowCacheConfig;
+import org.eclipse.jgit.transport.JschConfigSessionFactory;
+import org.eclipse.jgit.transport.OpenSshConfig;
+import org.eclipse.jgit.transport.SshSessionFactory;
 import org.eclipse.jgit.util.FS;
 import org.eclipse.jgit.util.IO;
 import org.eclipse.jgit.util.RawParseUtils;
@@ -82,6 +87,15 @@
 
     @Override
     public void start() {
+      // Install our own factory which always runs in batch mode, as we
+      // have no UI available for interactive prompting.
+      SshSessionFactory.setInstance(new JschConfigSessionFactory() {
+        @Override
+        protected void configure(OpenSshConfig.Host hc, Session session) {
+          // Default configuration is batch mode.
+        }
+      });
+
       final WindowCacheConfig c = new WindowCacheConfig();
       c.fromConfig(cfg);
       WindowCache.reconfigure(c);
@@ -122,7 +136,15 @@
       throw new RepositoryNotFoundException("Invalid name: " + name);
     }
     if (!names.contains(name)) {
-      throw new RepositoryNotFoundException(gitDirOf(name));
+      // The this.names list does not hold the project-name but it can still exist
+      // on disk; for instance when the project has been created directly on the
+      // file-system through replication.
+      //
+      if (FileKey.resolve(gitDirOf(name), FS.DETECTED) != null) {
+        onCreateProject(name);
+      } else {
+        throw new RepositoryNotFoundException(gitDirOf(name));
+      }
     }
     final FileKey loc = FileKey.lenient(gitDirOf(name), FS.DETECTED);
     try {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeException.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeException.java
index 44becb5..1997c13 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeException.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeException.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.git;
 
 /** Indicates the current branch's queue cannot be processed at this time. */
-class MergeException extends Exception {
+public class MergeException extends Exception {
   private static final long serialVersionUID = 1L;
 
   MergeException(final String msg) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeOp.java
index 4773680..2e8f183 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
@@ -36,7 +36,9 @@
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.mail.MergeFailSender;
 import com.google.gerrit.server.mail.MergedSender;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
@@ -134,7 +136,7 @@
   private final SchemaFactory<ReviewDb> schemaFactory;
   private final ProjectCache projectCache;
   private final FunctionState.Factory functionState;
-  private final ReplicationQueue replication;
+  private final GitReferenceUpdated replication;
   private final MergedSender.Factory mergedSenderFactory;
   private final MergeFailSender.Factory mergeFailSenderFactory;
   private final Provider<String> urlProvider;
@@ -158,6 +160,7 @@
   private CodeReviewCommit mergeTip;
   private Set<RevCommit> alreadyAccepted;
   private RefUpdate branchUpdate;
+  private ObjectInserter inserter;
 
   private final ChangeHooks hooks;
   private final AccountCache accountCache;
@@ -166,11 +169,12 @@
   private final SubmoduleOp.Factory subOpFactory;
   private final WorkQueue workQueue;
   private final RequestScopePropagator requestScopePropagator;
+  private final AllProjectsName allProjectsName;
 
   @Inject
   MergeOp(final GitRepositoryManager grm, final SchemaFactory<ReviewDb> sf,
       final ProjectCache pc, final FunctionState.Factory fs,
-      final ReplicationQueue rq, final MergedSender.Factory msf,
+      final GitReferenceUpdated rq, final MergedSender.Factory msf,
       final MergeFailSender.Factory mfsf,
       @CanonicalWebUrl @Nullable final Provider<String> cwu,
       final ApprovalTypes approvalTypes, final PatchSetInfoFactory psif,
@@ -182,7 +186,8 @@
       final TagCache tagCache, final CreateCodeReviewNotes.Factory crnf,
       final SubmoduleOp.Factory subOpFactory,
       final WorkQueue workQueue,
-      final RequestScopePropagator requestScopePropagator) {
+      final RequestScopePropagator requestScopePropagator,
+      final AllProjectsName allProjectsName) {
     repoManager = grm;
     schemaFactory = sf;
     functionState = fs;
@@ -203,6 +208,7 @@
     this.subOpFactory = subOpFactory;
     this.workQueue = workQueue;
     this.requestScopePropagator = requestScopePropagator;
+    this.allProjectsName = allProjectsName;
     this.myIdent = myIdent;
     destBranch = branch;
     toMerge = new ArrayList<CodeReviewCommit>();
@@ -247,10 +253,12 @@
       log.error("Test merge attempt for change: " + change.getId()
           + " failed", e);
     } finally {
+      if (repo != null) {
+        repo.close();
+      }
       if (db != null) {
         db.close();
       }
-      db = null;
     }
   }
 
@@ -281,14 +289,18 @@
     } catch (OrmException e) {
       throw new MergeException("Cannot query the database", e);
     } finally {
+      if (inserter != null) {
+        inserter.release();
+      }
       if (rw != null) {
         rw.release();
       }
       if (repo != null) {
         repo.close();
       }
-      db.close();
-      db = null;
+      if (db != null) {
+        db.close();
+      }
     }
   }
 
@@ -319,6 +331,9 @@
     } catch (RepositoryNotFoundException notGit) {
       final String m = "Repository \"" + name.get() + "\" unknown.";
       throw new MergeException(m, notGit);
+    } catch (IOException err) {
+      final String m = "Error opening repository \"" + name.get() + '"';
+      throw new MergeException(m, err);
     }
 
     rw = new RevWalk(repo) {
@@ -330,6 +345,8 @@
     rw.sort(RevSort.TOPO);
     rw.sort(RevSort.COMMIT_TIME_DESC, true);
     CAN_MERGE = rw.newFlag("CAN_MERGE");
+
+    inserter = repo.newObjectInserter();
   }
 
   private void openBranch() throws MergeException {
@@ -345,6 +362,21 @@
         branchTip = null;
       }
 
+      try {
+        final Ref destRef = repo.getRef(destBranch.get());
+        if (destRef != null) {
+          branchUpdate.setExpectedOldObjectId(destRef.getObjectId());
+        } else if (repo.getFullBranch().equals(destBranch.get())) {
+          branchUpdate.setExpectedOldObjectId(ObjectId.zeroId());
+        } else {
+          throw new MergeException("Destination branch \""
+              + branchUpdate.getRef().getName() + "\" does not exist");
+        }
+      } catch (IOException e) {
+        throw new MergeException(
+            "Failed to check existence of destination branch", e);
+      }
+
       for (final Ref r : repo.getAllRefs().values()) {
         if (r.getName().startsWith(Constants.R_HEADS)
             || r.getName().startsWith(Constants.R_TAGS)) {
@@ -423,6 +455,50 @@
         continue;
       }
 
+      if (GitRepositoryManager.REF_CONFIG.equals(branchUpdate.getName())) {
+        final Project.NameKey newParent;
+        try {
+          ProjectConfig cfg = new ProjectConfig(destProject.getNameKey());
+          cfg.load(repo, commit);
+          newParent = cfg.getProject().getParent(allProjectsName);
+        } catch (Exception e) {
+          commits.put(changeId, CodeReviewCommit
+              .error(CommitMergeStatus.INVALID_PROJECT_CONFIGURATION));
+          continue;
+        }
+        final Project.NameKey oldParent = destProject.getParent(allProjectsName);
+        if (oldParent == null) {
+          // update of the 'All-Projects' project
+          if (newParent != null) {
+            commits.put(changeId, CodeReviewCommit
+                .error(CommitMergeStatus.INVALID_PROJECT_CONFIGURATION_ROOT_PROJECT_CANNOT_HAVE_PARENT));
+            continue;
+          }
+        } else {
+          if (!oldParent.equals(newParent)) {
+            final PatchSetApproval psa = getSubmitter(db, ps.getId());
+            if (psa == null) {
+              commits.put(changeId, CodeReviewCommit
+                  .error(CommitMergeStatus.SETTING_PARENT_PROJECT_ONLY_ALLOWED_BY_ADMIN));
+              continue;
+            }
+            final IdentifiedUser submitter =
+                identifiedUserFactory.create(psa.getAccountId());
+            if (!submitter.getCapabilities().canAdministrateServer()) {
+              commits.put(changeId, CodeReviewCommit
+                  .error(CommitMergeStatus.SETTING_PARENT_PROJECT_ONLY_ALLOWED_BY_ADMIN));
+              continue;
+            }
+
+            if (projectCache.get(newParent) == null) {
+              commits.put(changeId, CodeReviewCommit
+                  .error(CommitMergeStatus.INVALID_PROJECT_CONFIGURATION_PARENT_PROJECT_NOT_FOUND));
+              continue;
+            }
+          }
+        }
+      }
+
       commit.change = chg;
       commit.patchsetId = ps.getId();
       commit.originalOrder = commitOrder++;
@@ -501,21 +577,10 @@
   }
 
   private void mergeOneCommit(final CodeReviewCommit n) throws MergeException {
-    final ThreeWayMerger m;
-    if (destProject.isUseContentMerge()) {
-      // Settings for this project allow us to try and
-      // automatically resolve conflicts within files if needed.
-      // Use ResolveMerge and instruct to operate in core.
-      m = MergeStrategy.RESOLVE.newMerger(repo, true);
-    } else {
-      // No auto conflict resolving allowed. If any of the
-      // affected files was modified, merge will fail.
-      m = MergeStrategy.SIMPLE_TWO_WAY_IN_CORE.newMerger(repo);
-    }
-
+    final ThreeWayMerger m = newThreeWayMerger();
     try {
       if (m.merge(new AnyObjectId[] {mergeTip, n})) {
-        writeMergeCommit(m, n);
+        writeMergeCommit(m.getResultTreeId(), n);
 
       } else {
         failed(n, CommitMergeStatus.PATH_CONFLICT);
@@ -533,6 +598,35 @@
     }
   }
 
+  private ThreeWayMerger newThreeWayMerger() {
+    ThreeWayMerger m;
+    if (destProject.isUseContentMerge()) {
+      // Settings for this project allow us to try and
+      // automatically resolve conflicts within files if needed.
+      // Use ResolveMerge and instruct to operate in core.
+      m = MergeStrategy.RESOLVE.newMerger(repo, true);
+    } else {
+      // No auto conflict resolving allowed. If any of the
+      // affected files was modified, merge will fail.
+      m = MergeStrategy.SIMPLE_TWO_WAY_IN_CORE.newMerger(repo);
+    }
+    m.setObjectInserter(new ObjectInserter.Filter() {
+      @Override
+      protected ObjectInserter delegate() {
+        return inserter;
+      }
+
+      @Override
+      public void flush() {
+      }
+
+      @Override
+      public void release() {
+      }
+    });
+    return m;
+  }
+
   private CodeReviewCommit failed(final CodeReviewCommit n,
       final CommitMergeStatus failure) throws MissingObjectException,
       IncorrectObjectTypeException, IOException {
@@ -546,7 +640,7 @@
     return failed;
   }
 
-  private void writeMergeCommit(final Merger m, final CodeReviewCommit n)
+  private void writeMergeCommit(ObjectId treeId, CodeReviewCommit n)
       throws IOException, MissingObjectException, IncorrectObjectTypeException {
     final List<CodeReviewCommit> merged = new ArrayList<CodeReviewCommit>();
     rw.reset();
@@ -595,13 +689,13 @@
     PersonIdent authorIdent = computeAuthor(merged);
 
     final CommitBuilder mergeCommit = new CommitBuilder();
-    mergeCommit.setTreeId(m.getResultTreeId());
+    mergeCommit.setTreeId(treeId);
     mergeCommit.setParentIds(mergeTip, n);
     mergeCommit.setAuthor(authorIdent);
     mergeCommit.setCommitter(myIdent);
     mergeCommit.setMessage(msgbuf.toString());
 
-    mergeTip = (CodeReviewCommit) rw.parseCommit(commit(m, mergeCommit));
+    mergeTip = (CodeReviewCommit) rw.parseCommit(commit(mergeCommit));
   }
 
   private PersonIdent computeAuthor(
@@ -627,6 +721,12 @@
           identifiedUserFactory.create(submitter.getAccountId());
       Set<String> emails = new HashSet<String>();
       for (RevCommit c : codeReviewCommits) {
+        try {
+          rw.parseBody(c);
+        } catch (IOException e) {
+          log.warn("Cannot parse commit " + c.name() + " in " + destBranch, e);
+          continue;
+        }
         emails.add(c.getAuthorIdent().getEmailAddress());
       }
 
@@ -687,19 +787,7 @@
   private void cherryPickChanges() throws MergeException, OrmException {
     while (!toMerge.isEmpty()) {
       final CodeReviewCommit n = toMerge.remove(0);
-      final ThreeWayMerger m;
-
-      if (destProject.isUseContentMerge()) {
-        // Settings for this project allow us to try and
-        // automatically resolve conflicts within files if needed.
-        // Use ResolveMerge and instruct to operate in core.
-        m = MergeStrategy.RESOLVE.newMerger(repo, true);
-      } else {
-        // No auto conflict resolving allowed. If any of the
-        // affected files was modified, merge will fail.
-        m = MergeStrategy.SIMPLE_TWO_WAY_IN_CORE.newMerger(repo);
-      }
-
+      final ThreeWayMerger m = newThreeWayMerger();
       try {
         if (mergeTip == null) {
           // The branch is unborn. Take a fast-forward resolution to
@@ -894,42 +982,60 @@
     mergeCommit.setCommitter(toCommitterIdent(submitAudit));
     mergeCommit.setMessage(msgbuf.toString());
 
-    final ObjectId id = commit(m, mergeCommit);
+    final ObjectId id = commit(mergeCommit);
     final CodeReviewCommit newCommit = (CodeReviewCommit) rw.parseCommit(id);
 
-    n.change =
-        db.changes().atomicUpdate(n.change.getId(),
-            new AtomicUpdate<Change>() {
-              @Override
-              public Change update(Change change) {
-                change.nextPatchSetId();
-                return change;
-              }
-            });
+    if (submitAudit != null) {
+      final Change oldChange = n.change;
 
-    final PatchSet ps = new PatchSet(n.change.currPatchSetId());
-    ps.setCreatedOn(new Timestamp(System.currentTimeMillis()));
-    ps.setUploader(submitAudit.getAccountId());
-    ps.setRevision(new RevId(id.getName()));
-    insertAncestors(ps.getId(), newCommit);
-    db.patchSets().insert(Collections.singleton(ps));
+      n.change =
+          db.changes().atomicUpdate(n.change.getId(),
+              new AtomicUpdate<Change>() {
+                @Override
+                public Change update(Change change) {
+                  change.nextPatchSetId();
+                  return change;
+                }
+              });
 
-    n.change =
-        db.changes().atomicUpdate(n.change.getId(),
-            new AtomicUpdate<Change>() {
-              @Override
-              public Change update(Change change) {
-                change.setCurrentPatchSet(patchSetInfoFactory.get(newCommit,
-                    ps.getId()));
-                return change;
-              }
-            });
+      final PatchSet ps = new PatchSet(n.change.currPatchSetId());
+      ps.setCreatedOn(new Timestamp(System.currentTimeMillis()));
+      ps.setUploader(submitAudit.getAccountId());
+      ps.setRevision(new RevId(id.getName()));
+      insertAncestors(ps.getId(), newCommit);
+      db.patchSets().insert(Collections.singleton(ps));
 
-    if (approvalList != null) {
-      for (PatchSetApproval a : approvalList) {
-        db.patchSetApprovals().insert(
-            Collections.singleton(new PatchSetApproval(ps.getId(), a)));
+      n.change =
+          db.changes().atomicUpdate(n.change.getId(),
+              new AtomicUpdate<Change>() {
+                @Override
+                public Change update(Change change) {
+                  change.setCurrentPatchSet(patchSetInfoFactory.get(newCommit,
+                      ps.getId()));
+                  return change;
+                }
+              });
+
+      this.submitted.remove(oldChange);
+      this.submitted.add(n.change);
+
+      if (approvalList != null) {
+        for (PatchSetApproval a : approvalList) {
+          db.patchSetApprovals().insert(
+              Collections.singleton(new PatchSetApproval(ps.getId(), a)));
+        }
       }
+
+      final RefUpdate ru = repo.updateRef(ps.getRefName());
+      ru.setExpectedOldObjectId(ObjectId.zeroId());
+      ru.setNewObjectId(newCommit);
+      ru.disableRefLog();
+      if (ru.update(rw) != RefUpdate.Result.NEW) {
+        throw new IOException(String.format(
+            "Failed to create ref %s in %s: %s", ps.getRefName(), n.change
+                .getDest().getParentKey().get(), ru.getResult()));
+      }
+      replication.fire(n.change.getProject(), ru.getName());
     }
 
     newCommit.copyFrom(n);
@@ -953,16 +1059,11 @@
     db.patchSetAncestors().insert(toInsert);
   }
 
-  private ObjectId commit(final Merger m, final CommitBuilder mergeCommit)
+  private ObjectId commit(CommitBuilder mergeCommit)
       throws IOException, UnsupportedEncodingException {
-    ObjectInserter oi = m.getObjectInserter();
-    try {
-      ObjectId id = oi.insert(mergeCommit);
-      oi.flush();
-      return id;
-    } finally {
-      oi.release();
-    }
+    ObjectId id = inserter.insert(mergeCommit);
+    inserter.flush();
+    return id;
   }
 
   private boolean contains(List<FooterLine> footers, FooterKey key, String val) {
@@ -1026,8 +1127,7 @@
                   ps.getProject().getDescription());
             }
 
-            replication.scheduleUpdate(destBranch.getParentKey(), branchUpdate
-                .getName());
+            replication.fire(destBranch.getParentKey(), branchUpdate.getName());
 
             Account account = null;
             final PatchSetApproval submitter = getSubmitter(db, mergeTip.patchsetId);
@@ -1096,7 +1196,11 @@
         case PATH_CONFLICT:
         case CRISS_CROSS_MERGE:
         case CANNOT_CHERRY_PICK_ROOT:
-        case NOT_FAST_FORWARD: {
+        case NOT_FAST_FORWARD:
+        case INVALID_PROJECT_CONFIGURATION:
+        case INVALID_PROJECT_CONFIGURATION_PARENT_PROJECT_NOT_FOUND:
+        case INVALID_PROJECT_CONFIGURATION_ROOT_PROJECT_CANNOT_HAVE_PARENT:
+        case SETTING_PARENT_PROJECT_ONLY_ALLOWED_BY_ADMIN: {
           setNew(c, message(c, txt));
           break;
         }
@@ -1122,7 +1226,7 @@
     } catch (CodeReviewNoteCreationException e) {
       log.error(e.getMessage());
     }
-    replication.scheduleUpdate(destBranch.getParentKey(),
+    replication.fire(destBranch.getParentKey(),
         GitRepositoryManager.REFS_NOTES_REVIEW);
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MetaDataUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MetaDataUpdate.java
index fbe9d16..86dc19c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MetaDataUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MetaDataUpdate.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -25,6 +26,8 @@
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Repository;
 
+import java.io.IOException;
+
 /** Helps with the updating of a {@link VersionedMetaData}. */
 public class MetaDataUpdate {
   public static class User {
@@ -49,7 +52,7 @@
     }
 
     public MetaDataUpdate create(Project.NameKey name)
-        throws RepositoryNotFoundException {
+        throws RepositoryNotFoundException, IOException {
       MetaDataUpdate md = factory.create(name, mgr.openRepository(name));
       md.getCommitBuilder().setAuthor(userIdent);
       md.getCommitBuilder().setCommitter(serverIdent);
@@ -71,7 +74,7 @@
     }
 
     public MetaDataUpdate create(Project.NameKey name)
-        throws RepositoryNotFoundException {
+        throws RepositoryNotFoundException, IOException {
       MetaDataUpdate md = factory.create(name, mgr.openRepository(name));
       md.getCommitBuilder().setAuthor(serverIdent);
       md.getCommitBuilder().setCommitter(serverIdent);
@@ -84,13 +87,13 @@
         @Assisted Repository db);
   }
 
-  private final ReplicationQueue replication;
+  private final GitReferenceUpdated replication;
   private final Project.NameKey projectName;
   private final Repository db;
   private final CommitBuilder commit;
 
   @Inject
-  public MetaDataUpdate(ReplicationQueue replication,
+  public MetaDataUpdate(GitReferenceUpdated replication,
       @Assisted Project.NameKey projectName, @Assisted Repository db) {
     this.replication = replication;
     this.projectName = projectName;
@@ -121,8 +124,6 @@
   }
 
   void replicate(String ref) {
-    if (replication.isEnabled()) {
-      replication.scheduleUpdate(projectName, ref);
-    }
+    replication.fire(projectName, ref);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MultiProgressMonitor.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MultiProgressMonitor.java
index dbb849c..23d8dad 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MultiProgressMonitor.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MultiProgressMonitor.java
@@ -17,6 +17,7 @@
 import static java.util.concurrent.TimeUnit.NANOSECONDS;
 
 import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ProgressMonitor;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -58,7 +59,7 @@
   private static final char NO_SPINNER = ' ';
 
   /** Handle for a sub-task. */
-  public class Task {
+  public class Task implements ProgressMonitor {
     private final String name;
     private final int total;
     private volatile int count;
@@ -76,6 +77,7 @@
      *
      * @param completed number of work units completed.
      */
+    @Override
     public void update(final int completed) {
       count += completed;
       if (total != UNKNOWN) {
@@ -97,6 +99,23 @@
         wakeUp();
       }
     }
+
+    @Override
+    public void start(int totalTasks) {
+    }
+
+    @Override
+    public void beginTask(String title, int totalWork) {
+    }
+
+    @Override
+    public void endTask() {
+    }
+
+    @Override
+    public boolean isCancelled() {
+      return false;
+    }
   }
 
   private final OutputStream out;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/NoReplication.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/NoReplication.java
deleted file mode 100644
index c5ee262..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/NoReplication.java
+++ /dev/null
@@ -1,37 +0,0 @@
-// Copyright (C) 2011 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.git;
-
-import com.google.gerrit.reviewdb.client.Project;
-
-/** A disabled {@link ReplicationQueue}. */
-public final class NoReplication implements ReplicationQueue {
-  @Override
-  public boolean isEnabled() {
-    return false;
-  }
-
-  @Override
-  public void scheduleUpdate(Project.NameKey project, String ref) {
-  }
-
-  @Override
-  public void scheduleFullSync(Project.NameKey project, String urlMatch) {
-  }
-
-  @Override
-  public void replicateNewProject(Project.NameKey project, String head) {
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/NotesBranchUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/NotesBranchUtil.java
new file mode 100644
index 0000000..17cfea8
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/NotesBranchUtil.java
@@ -0,0 +1,271 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git;
+
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
+import org.eclipse.jgit.errors.CorruptObjectException;
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.RefUpdate.Result;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.merge.MergeStrategy;
+import org.eclipse.jgit.notes.Note;
+import org.eclipse.jgit.notes.NoteMap;
+import org.eclipse.jgit.notes.NoteMapMerger;
+import org.eclipse.jgit.notes.NoteMerger;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+import java.io.IOException;
+
+/**
+ * A utility class for updating a notes branch with automatic merge of note
+ * trees.
+ */
+public class NotesBranchUtil {
+  public interface Factory {
+    NotesBranchUtil create(Repository db);
+  }
+
+  private static final int MAX_LOCK_FAILURE_CALLS = 10;
+  private static final int SLEEP_ON_LOCK_FAILURE_MS = 25;
+
+  private PersonIdent gerritIdent;
+  private final Repository db;
+
+  private RevCommit baseCommit;
+  private NoteMap base;
+
+  private RevCommit oursCommit;
+  private NoteMap ours;
+
+  private RevWalk revWalk;
+  private ObjectInserter inserter;
+  private ObjectReader reader;
+  private boolean overwrite;
+
+  private ReviewNoteMerger noteMerger;
+
+  @Inject
+  public NotesBranchUtil(@GerritPersonIdent final PersonIdent gerritIdent,
+      @Assisted Repository db) {
+    this.gerritIdent = gerritIdent;
+    this.db = db;
+  }
+
+  /**
+   * Create a new commit in the <code>notesBranch</code> by updating existing
+   * or creating new notes from the <code>notes</code> map.
+   *
+   * @param notes map of notes
+   * @param notesBranch notes branch to update
+   * @param commitAuthor author of the commit in the notes branch
+   * @param commitMessage for the commit in the notes branch
+   * @throws IOException
+   * @throws ConcurrentRefUpdateException
+   */
+  public final void commitAllNotes(NoteMap notes, String notesBranch,
+      PersonIdent commitAuthor, String commitMessage) throws IOException,
+      ConcurrentRefUpdateException {
+    this.overwrite = true;
+    commitNotes(notes, notesBranch, commitAuthor, commitMessage);
+  }
+
+  /**
+   * Create a new commit in the <code>notesBranch</code> by creating not yet
+   * existing notes from the <code>notes</code> map. The notes from the
+   * <code>notes</code> map which already exist in the note-tree of the
+   * tip of the <code>notesBranch</code> will not be updated.
+   *
+   * @param notes map of notes
+   * @param notesBranch notes branch to update
+   * @param commitAuthor author of the commit in the notes branch
+   * @param commitMessage for the commit in the notes branch
+   * @return map with those notes from the <code>notes</code> that were newly
+   *         created
+   * @throws IOException
+   * @throws ConcurrentRefUpdateException
+   */
+  public final NoteMap commitNewNotes(NoteMap notes, String notesBranch,
+      PersonIdent commitAuthor, String commitMessage) throws IOException,
+      ConcurrentRefUpdateException {
+    this.overwrite = false;
+    commitNotes(notes, notesBranch, commitAuthor, commitMessage);
+    NoteMap newlyCreated = NoteMap.newEmptyMap();
+    for (Note n : notes) {
+      if (base == null || !base.contains(n)) {
+        newlyCreated.set(n, n.getData());
+      }
+    }
+    return newlyCreated;
+  }
+
+  private void commitNotes(NoteMap notes, String notesBranch,
+      PersonIdent commitAuthor, String commitMessage) throws IOException,
+      ConcurrentRefUpdateException {
+    try {
+      revWalk = new RevWalk(db);
+      inserter = db.newObjectInserter();
+      reader = db.newObjectReader();
+      loadBase(notesBranch);
+      if (overwrite) {
+        addAllNotes(notes);
+      } else {
+        addNewNotes(notes);
+      }
+      if (base != null) {
+        oursCommit = createCommit(ours, commitAuthor, commitMessage, baseCommit);
+      } else {
+        oursCommit = createCommit(ours, commitAuthor, commitMessage);
+      }
+      updateRef(notesBranch);
+    } finally {
+      revWalk.release();
+      inserter.release();
+      reader.release();
+    }
+  }
+
+  private void addNewNotes(NoteMap notes) throws IOException {
+    for (Note n : notes) {
+      if (! ours.contains(n)) {
+        ours.set(n, n.getData());
+      }
+    }
+  }
+
+  private void addAllNotes(NoteMap notes) throws IOException {
+    for (Note n : notes) {
+      if (ours.contains(n)) {
+        // Merge the existing and the new note as if they are both new,
+        // means: base == null
+        // There is no really a common ancestry for these two note revisions
+        ObjectId noteContent = getNoteMerger().merge(null, n, ours.getNote(n),
+            reader, inserter).getData();
+        ours.set(n, noteContent);
+      } else {
+        ours.set(n, n.getData());
+      }
+    }
+  }
+
+  private NoteMerger getNoteMerger() {
+    if (noteMerger == null) {
+      noteMerger = new ReviewNoteMerger();
+    }
+    return noteMerger;
+  }
+
+  private void loadBase(String notesBranch) throws IOException {
+    Ref branch = db.getRef(notesBranch);
+    if (branch != null) {
+      baseCommit = revWalk.parseCommit(branch.getObjectId());
+      base = NoteMap.read(revWalk.getObjectReader(), baseCommit);
+    }
+    if (baseCommit != null) {
+      ours = NoteMap.read(revWalk.getObjectReader(), baseCommit);
+    } else {
+      ours = NoteMap.newEmptyMap();
+    }
+  }
+
+  private RevCommit createCommit(NoteMap map, PersonIdent author,
+      String message, RevCommit... parents) throws IOException {
+    CommitBuilder b = new CommitBuilder();
+    b.setTreeId(map.writeTree(inserter));
+    b.setAuthor(author != null ? author : gerritIdent);
+    b.setCommitter(gerritIdent);
+    if (parents.length > 0) {
+      b.setParentIds(parents);
+    }
+    b.setMessage(message);
+    ObjectId commitId = inserter.insert(b);
+    inserter.flush();
+    return revWalk.parseCommit(commitId);
+  }
+
+  private void updateRef(String notesBranch) throws IOException,
+      MissingObjectException, IncorrectObjectTypeException,
+      CorruptObjectException, ConcurrentRefUpdateException {
+    if (baseCommit != null && oursCommit.getTree().equals(baseCommit.getTree())) {
+      // If the trees are identical, there is no change in the notes.
+      // Avoid saving this commit as it has no new information.
+      return;
+    }
+
+    int remainingLockFailureCalls = MAX_LOCK_FAILURE_CALLS;
+    RefUpdate refUpdate = createRefUpdate(notesBranch, oursCommit, baseCommit);
+
+    for (;;) {
+      Result result = refUpdate.update();
+
+      if (result == Result.LOCK_FAILURE) {
+        if (--remainingLockFailureCalls > 0) {
+          try {
+            Thread.sleep(SLEEP_ON_LOCK_FAILURE_MS);
+          } catch (InterruptedException e) {
+            // ignore
+          }
+        } else {
+          throw new ConcurrentRefUpdateException("Failed to lock the ref: "
+              + notesBranch, db.getRef(notesBranch), result);
+        }
+
+      } else if (result == Result.REJECTED) {
+        RevCommit theirsCommit =
+            revWalk.parseCommit(refUpdate.getOldObjectId());
+        NoteMap theirs =
+            NoteMap.read(revWalk.getObjectReader(), theirsCommit);
+        NoteMapMerger merger =
+            new NoteMapMerger(db, getNoteMerger(), MergeStrategy.RESOLVE);
+        NoteMap merged = merger.merge(base, ours, theirs);
+        RevCommit mergeCommit =
+            createCommit(merged, gerritIdent, "Merged note commits\n",
+                theirsCommit, oursCommit);
+        refUpdate = createRefUpdate(notesBranch, mergeCommit, theirsCommit);
+        remainingLockFailureCalls = MAX_LOCK_FAILURE_CALLS;
+
+      } else if (result == Result.IO_FAILURE) {
+        throw new IOException("Couldn't update " + notesBranch + ". "
+            + result.name());
+      } else {
+        break;
+      }
+    }
+  }
+
+  private RefUpdate createRefUpdate(String notesBranch, ObjectId newObjectId,
+      ObjectId expectedOldObjectId) throws IOException {
+    RefUpdate refUpdate = db.updateRef(notesBranch);
+    refUpdate.setNewObjectId(newObjectId);
+    if (expectedOldObjectId == null) {
+      refUpdate.setExpectedOldObjectId(ObjectId.zeroId());
+    } else {
+      refUpdate.setExpectedOldObjectId(expectedOldObjectId);
+    }
+    return refUpdate;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/NotifyConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/NotifyConfig.java
new file mode 100644
index 0000000..ba2833d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/NotifyConfig.java
@@ -0,0 +1,104 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Sets;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
+import com.google.gerrit.server.mail.Address;
+
+import java.util.EnumSet;
+import java.util.Set;
+
+public class NotifyConfig implements Comparable<NotifyConfig> {
+  private String name;
+  private EnumSet<NotifyType> types = EnumSet.of(NotifyType.ALL);
+  private String filter;
+
+  private Set<GroupReference> groups = Sets.newHashSet();
+  private Set<Address> addresses = Sets.newHashSet();
+
+  public String getName() {
+    return name;
+  }
+
+  public void setName(String name) {
+    this.name = name;
+  }
+
+  public boolean isNotify(NotifyType type) {
+    return types.contains(type) || types.contains(NotifyType.ALL);
+  }
+
+  public EnumSet<NotifyType> getNotify() {
+    return types;
+  }
+
+  public void setTypes(EnumSet<NotifyType> newTypes) {
+    types = EnumSet.copyOf(newTypes);
+  }
+
+  public String getFilter() {
+    return filter;
+  }
+
+  public void setFilter(String filter) {
+    if ("*".equals(filter)) {
+      this.filter = null;
+    } else {
+      this.filter = Strings.emptyToNull(filter);
+    }
+  }
+
+  public Set<GroupReference> getGroups() {
+    return groups;
+  }
+
+  public Set<Address> getAddresses() {
+    return addresses;
+  }
+
+  public void addEmail(GroupReference group) {
+    groups.add(group);
+  }
+
+  public void addEmail(Address address) {
+    addresses.add(address);
+  }
+
+  @Override
+  public int compareTo(NotifyConfig o) {
+    return name.compareTo(o.name);
+  }
+
+  @Override
+  public int hashCode() {
+    return name.hashCode();
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (obj instanceof NotifyConfig) {
+      return compareTo((NotifyConfig) obj) == 0;
+    }
+    return false;
+  }
+
+  @Override
+  public String toString() {
+    return "NotifyConfig[" + name + " = " + addresses + " + " + groups + "]";
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/PerThreadRequestScope.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/PerThreadRequestScope.java
index 057e80d..8aea73a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/PerThreadRequestScope.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/PerThreadRequestScope.java
@@ -14,50 +14,86 @@
 
 package com.google.gerrit.server.git;
 
-import com.google.gerrit.server.RequestCleanup;
+import com.google.common.collect.Maps;
+import com.google.gerrit.server.util.RequestContext;
+import com.google.gerrit.server.util.ThreadLocalRequestContext;
 import com.google.gerrit.server.util.ThreadLocalRequestScopePropagator;
+import com.google.inject.Inject;
 import com.google.inject.Key;
 import com.google.inject.OutOfScopeException;
 import com.google.inject.Provider;
 import com.google.inject.Scope;
 
-import java.util.HashMap;
 import java.util.Map;
+import java.util.concurrent.Callable;
 
-class PerThreadRequestScope {
-  static class Propagator
-      extends ThreadLocalRequestScopePropagator<PerThreadRequestScope> {
-    Propagator() {
-      super(REQUEST, current);
+public class PerThreadRequestScope {
+  public interface Scoper {
+    <T> Callable<T> scope(Callable<T> callable);
+  }
+
+  private static class Context {
+    private final Map<Key<?>, Object> map;
+
+    private Context() {
+      map = Maps.newHashMap();
     }
 
-    @Override
-    protected PerThreadRequestScope continuingContext(
-        PerThreadRequestScope ctx) {
-      return new PerThreadRequestScope();
+    private <T> T get(Key<T> key, Provider<T> creator) {
+      @SuppressWarnings("unchecked")
+      T t = (T) map.get(key);
+      if (t == null) {
+        t = creator.get();
+        map.put(key, t);
+      }
+      return t;
     }
   }
 
-  private static final ThreadLocal<PerThreadRequestScope> current =
-      new ThreadLocal<PerThreadRequestScope>();
+  public static class Propagator extends ThreadLocalRequestScopePropagator<Context> {
+    @Inject
+    Propagator(ThreadLocalRequestContext local) {
+      super(REQUEST, current, local);
+    }
 
-  private static PerThreadRequestScope requireContext() {
-    final PerThreadRequestScope ctx = current.get();
+    @Override
+    protected Context continuingContext(Context ctx) {
+      return new Context();
+    }
+
+    public <T> Callable<T> scope(RequestContext requestContext, Callable<T> callable) {
+      final Context ctx = new Context();
+      final Callable<T> wrapped = context(requestContext, cleanup(callable));
+      return new Callable<T>() {
+        @Override
+        public T call() throws Exception {
+          Context old = current.get();
+          current.set(ctx);
+          try {
+            return wrapped.call();
+          } finally {
+            current.set(old);
+          }
+        }
+      };
+    }
+  }
+
+  private static final ThreadLocal<Context> current = new ThreadLocal<Context>();
+
+  private static Context requireContext() {
+    final Context ctx = current.get();
     if (ctx == null) {
       throw new OutOfScopeException("Not in command/request");
     }
     return ctx;
   }
 
-  static PerThreadRequestScope set(PerThreadRequestScope ctx) {
-    PerThreadRequestScope old = current.get();
-    current.set(ctx);
-    return old;
-  }
-
-  static final Scope REQUEST = new Scope() {
+  public static final Scope REQUEST = new Scope() {
+    @Override
     public <T> Provider<T> scope(final Key<T> key, final Provider<T> creator) {
       return new Provider<T>() {
+        @Override
         public T get() {
           return requireContext().get(key, creator);
         }
@@ -74,26 +110,4 @@
       return "PerThreadRequestScope.REQUEST";
     }
   };
-
-  private static final Key<RequestCleanup> RC_KEY =
-      Key.get(RequestCleanup.class);
-
-  final RequestCleanup cleanup;
-  private final Map<Key<?>, Object> map;
-
-  PerThreadRequestScope() {
-    cleanup = new RequestCleanup();
-    map = new HashMap<Key<?>, Object>();
-    map.put(RC_KEY, cleanup);
-  }
-
-  synchronized <T> T get(Key<T> key, Provider<T> creator) {
-    @SuppressWarnings("unchecked")
-    T t = (T) map.get(key);
-    if (t == null) {
-      t = creator.get();
-      map.put(key, t);
-    }
-    return t;
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
index d15095b..13e9967 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
@@ -16,22 +16,31 @@
 
 import static com.google.gerrit.common.data.Permission.isPermission;
 
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
 import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.ContributorAgreement;
 import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.common.data.PermissionRule.Action;
+import com.google.gerrit.common.data.RefConfigSection;
 import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.Project.State;
 import com.google.gerrit.reviewdb.client.Project.SubmitType;
-import com.google.gerrit.server.account.GroupCache;
-import com.google.gerrit.common.data.RefConfigSection;
+import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.config.ConfigUtil;
+import com.google.gerrit.server.mail.Address;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.util.StringUtils;
 
 import java.io.BufferedReader;
 import java.io.IOException;
@@ -39,6 +48,7 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.EnumSet;
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
@@ -56,6 +66,20 @@
   private static final String KEY_INHERIT_FROM = "inheritFrom";
   private static final String KEY_GROUP_PERMISSIONS = "exclusiveGroupPermissions";
 
+  private static final String ACCOUNTS = "accounts";
+  private static final String KEY_SAME_GROUP_VISIBILITY = "sameGroupVisibility";
+
+  private static final String CONTRIBUTOR_AGREEMENT = "contributor-agreement";
+  private static final String KEY_ACCEPTED = "accepted";
+  private static final String KEY_REQUIRE_CONTACT_INFORMATION = "requireContactInformation";
+  private static final String KEY_AUTO_VERIFY = "autoVerify";
+  private static final String KEY_AGREEMENT_URL = "agreementUrl";
+
+  private static final String NOTIFY = "notify";
+  private static final String KEY_EMAIL = "email";
+  private static final String KEY_FILTER = "filter";
+  private static final String KEY_TYPE = "type";
+
   private static final String CAPABILITY = "capability";
 
   private static final String RECEIVE = "receive";
@@ -76,8 +100,11 @@
 
   private Project.NameKey projectName;
   private Project project;
+  private AccountsSection accountsSection;
   private Map<AccountGroup.UUID, GroupReference> groupsByUUID;
   private Map<String, AccessSection> accessSections;
+  private Map<String, ContributorAgreement> contributorAgreements;
+  private Map<String, NotifyConfig> notifySections;
   private List<ValidationError> validationErrors;
   private ObjectId rulesId;
 
@@ -103,6 +130,10 @@
     return project;
   }
 
+  public AccountsSection getAccountsSection() {
+    return accountsSection;
+  }
+
   public AccessSection getAccessSection(String name) {
     return getAccessSection(name, false);
   }
@@ -136,6 +167,42 @@
     accessSections.put(section.getName(), section);
   }
 
+  public ContributorAgreement getContributorAgreement(String name) {
+    return getContributorAgreement(name, false);
+  }
+
+  public ContributorAgreement getContributorAgreement(String name, boolean create) {
+    ContributorAgreement ca = contributorAgreements.get(name);
+    if (ca == null && create) {
+      ca = new ContributorAgreement(name);
+      contributorAgreements.put(name, ca);
+    }
+    return ca;
+  }
+
+  public Collection<ContributorAgreement> getContributorAgreements() {
+    return sort(contributorAgreements.values());
+  }
+
+  public void remove(ContributorAgreement section) {
+    if (section != null) {
+      accessSections.remove(section.getName());
+    }
+  }
+
+  public void replace(ContributorAgreement section) {
+    section.setAutoVerify(resolve(section.getAutoVerify()));
+    for (PermissionRule rule : section.getAccepted()) {
+      rule.setGroup(resolve(rule.getGroup()));
+    }
+
+    contributorAgreements.put(section.getName(), section);
+  }
+
+  public Collection<NotifyConfig> getNotifyConfigs() {
+    return notifySections.values();
+  }
+
   public GroupReference resolve(AccountGroup group) {
     return resolve(GroupReference.forGroup(group));
   }
@@ -167,13 +234,13 @@
   /**
    * Check all GroupReferences use current group name, repairing stale ones.
    *
-   * @param groupCache cache to use when looking up group information by UUID.
+   * @param groupBackend cache to use when looking up group information by UUID.
    * @return true if one or more group names was stale.
    */
-  public boolean updateGroupNames(GroupCache groupCache) {
+  public boolean updateGroupNames(GroupBackend groupBackend) {
     boolean dirty = false;
     for (GroupReference ref : groupsByUUID.values()) {
-      AccountGroup g = groupCache.get(ref.getUUID());
+      GroupDescription.Basic g = groupBackend.get(ref.getUUID());
       if (g != null && !g.getName().equals(ref.getName())) {
         dirty = true;
         ref.setName(g.getName());
@@ -223,6 +290,116 @@
     p.setUseContentMerge(getBoolean(rc, SUBMIT, KEY_MERGE_CONTENT, false));
     p.setState(getEnum(rc, PROJECT, null, KEY_STATE, defaultStateValue));
 
+    loadAccountsSection(rc, groupsByName);
+    loadContributorAgreements(rc, groupsByName);
+    loadAccessSections(rc, groupsByName);
+    loadNotifySections(rc, groupsByName);
+  }
+
+  private void loadAccountsSection(
+      Config rc, Map<String, GroupReference> groupsByName) {
+    accountsSection = new AccountsSection();
+    accountsSection.setSameGroupVisibility(loadPermissionRules(
+        rc, ACCOUNTS, null, KEY_SAME_GROUP_VISIBILITY, groupsByName, false));
+  }
+
+  private void loadContributorAgreements(
+      Config rc, Map<String, GroupReference> groupsByName) {
+    contributorAgreements = new HashMap<String, ContributorAgreement>();
+    for (String name : rc.getSubsections(CONTRIBUTOR_AGREEMENT)) {
+      ContributorAgreement ca = getContributorAgreement(name, true);
+      ca.setDescription(rc.getString(CONTRIBUTOR_AGREEMENT, name, KEY_DESCRIPTION));
+      ca.setRequireContactInformation(
+          rc.getBoolean(CONTRIBUTOR_AGREEMENT, name, KEY_REQUIRE_CONTACT_INFORMATION, false));
+      ca.setAgreementUrl(rc.getString(CONTRIBUTOR_AGREEMENT, name, KEY_AGREEMENT_URL));
+      ca.setAccepted(loadPermissionRules(
+          rc, CONTRIBUTOR_AGREEMENT, name, KEY_ACCEPTED, groupsByName, false));
+
+      List<PermissionRule> rules = loadPermissionRules(
+          rc, CONTRIBUTOR_AGREEMENT, name, KEY_AUTO_VERIFY, groupsByName, false);
+      if (rules.isEmpty()) {
+        ca.setAutoVerify(null);
+      } else if (rules.size() > 1) {
+        error(new ValidationError(PROJECT_CONFIG, "Invalid rule in "
+            + CONTRIBUTOR_AGREEMENT
+            + "." + name
+            + "." + KEY_AUTO_VERIFY
+            + ": at most one group may be set"));
+      } else if (rules.get(0).getAction() != Action.ALLOW) {
+        error(new ValidationError(PROJECT_CONFIG, "Invalid rule in "
+            + CONTRIBUTOR_AGREEMENT
+            + "." + name
+            + "." + KEY_AUTO_VERIFY
+            + ": the group must be allowed"));
+      } else {
+        ca.setAutoVerify(rules.get(0).getGroup());
+      }
+    }
+  }
+
+  /**
+   * Parses the [notify] sections out of the configuration file.
+   *
+   * <pre>
+   *   [notify "reviewers"]
+   *     email = group Reviewers
+   *     type = new_changes
+   *
+   *   [notify "dev-team"]
+   *     email = dev-team@example.com
+   *     filter = branch:master
+   *
+   *   [notify "qa"]
+   *     email = qa@example.com
+   *     filter = branch:\"^(maint|stable)-.*\"
+   *     type = submitted_changes
+   * </pre>
+   */
+  private void loadNotifySections(
+      Config rc, Map<String, GroupReference> groupsByName) {
+    notifySections = Maps.newHashMap();
+    for (String sectionName : rc.getSubsections(NOTIFY)) {
+      NotifyConfig n = new NotifyConfig();
+      n.setName(sectionName);
+      n.setFilter(rc.getString(NOTIFY, sectionName, KEY_FILTER));
+
+      EnumSet<NotifyType> types = EnumSet.noneOf(NotifyType.class);
+      types.addAll(ConfigUtil.getEnumList(rc,
+          NOTIFY, sectionName, KEY_TYPE,
+          NotifyType.ALL));
+      n.setTypes(types);
+
+      for (String dst : rc.getStringList(NOTIFY, sectionName, KEY_EMAIL)) {
+        if (dst.startsWith("group ")) {
+          String groupName = dst.substring(6).trim();
+          GroupReference ref = groupsByName.get(groupName);
+          if (ref == null) {
+            ref = new GroupReference(null, groupName);
+            groupsByName.put(ref.getName(), ref);
+          }
+          if (ref.getUUID() != null) {
+            n.addEmail(ref);
+          } else {
+            error(new ValidationError(PROJECT_CONFIG,
+                "group \"" + ref.getName() + "\" not in " + GROUP_LIST));
+          }
+        } else if (dst.startsWith("user ")) {
+          error(new ValidationError(PROJECT_CONFIG, dst + " not supported"));
+        } else {
+          try {
+            n.addEmail(Address.parse(dst));
+          } catch (IllegalArgumentException err) {
+            error(new ValidationError(PROJECT_CONFIG,
+                "notify section \"" + sectionName + "\" has invalid email \"" + dst + "\""));
+          }
+        }
+      }
+      notifySections.put(sectionName, n);
+    }
+  }
+
+  private void loadAccessSections(
+      Config rc, Map<String, GroupReference> groupsByName) {
     accessSections = new HashMap<String, AccessSection>();
     for (String refName : rc.getSubsections(ACCESS)) {
       if (RefConfigSection.isValid(refName)) {
@@ -260,6 +437,15 @@
     }
   }
 
+  private List<PermissionRule> loadPermissionRules(Config rc, String section,
+      String subsection, String varName,
+      Map<String, GroupReference> groupsByName,
+      boolean useRange) {
+    Permission perm = new Permission(varName);
+    loadPermissionRules(rc, section, subsection, varName, groupsByName, perm, useRange);
+    return perm.getRules();
+  }
+
   private void loadPermissionRules(Config rc, String section,
       String subsection, String varName,
       Map<String, GroupReference> groupsByName, Permission perm,
@@ -349,6 +535,100 @@
     set(rc, PROJECT, null, KEY_STATE, p.getState(), null);
 
     Set<AccountGroup.UUID> keepGroups = new HashSet<AccountGroup.UUID>();
+    saveAccountsSection(rc, keepGroups);
+    saveContributorAgreements(rc, keepGroups);
+    saveAccessSections(rc, keepGroups);
+    saveNotifySections(rc, keepGroups);
+    groupsByUUID.keySet().retainAll(keepGroups);
+
+    saveConfig(PROJECT_CONFIG, rc);
+    saveGroupList();
+  }
+
+  private void saveAccountsSection(Config rc, Set<AccountGroup.UUID> keepGroups) {
+    if (accountsSection != null) {
+      rc.setStringList(ACCOUNTS, null, KEY_SAME_GROUP_VISIBILITY,
+          ruleToStringList(accountsSection.getSameGroupVisibility(), keepGroups));
+    }
+  }
+
+  private void saveContributorAgreements(
+      Config rc, Set<AccountGroup.UUID> keepGroups) {
+    for (ContributorAgreement ca : sort(contributorAgreements.values())) {
+      set(rc, CONTRIBUTOR_AGREEMENT, ca.getName(), KEY_DESCRIPTION, ca.getDescription());
+      set(rc, CONTRIBUTOR_AGREEMENT, ca.getName(), KEY_REQUIRE_CONTACT_INFORMATION, ca.isRequireContactInformation());
+      set(rc, CONTRIBUTOR_AGREEMENT, ca.getName(), KEY_AGREEMENT_URL, ca.getAgreementUrl());
+
+      if (ca.getAutoVerify() != null) {
+        if (ca.getAutoVerify().getUUID() != null) {
+          keepGroups.add(ca.getAutoVerify().getUUID());
+        }
+        String autoVerify = new PermissionRule(ca.getAutoVerify()).asString(false);
+        set(rc, CONTRIBUTOR_AGREEMENT, ca.getName(), KEY_AUTO_VERIFY, autoVerify);
+      } else {
+        rc.unset(CONTRIBUTOR_AGREEMENT, ca.getName(), KEY_AUTO_VERIFY);
+      }
+
+      rc.setStringList(CONTRIBUTOR_AGREEMENT, ca.getName(), KEY_ACCEPTED,
+          ruleToStringList(ca.getAccepted(), keepGroups));
+    }
+  }
+
+  private void saveNotifySections(
+      Config rc, Set<AccountGroup.UUID> keepGroups) {
+    for (NotifyConfig nc : sort(notifySections.values())) {
+      List<String> email = Lists.newArrayList();
+      for (GroupReference gr : nc.getGroups()) {
+        if (gr.getUUID() != null) {
+          keepGroups.add(gr.getUUID());
+        }
+        email.add(new PermissionRule(gr).asString(false));
+      }
+      Collections.sort(email);
+
+      List<String> addrs = Lists.newArrayList();
+      for (Address addr : nc.getAddresses()) {
+        addrs.add(addr.toString());
+      }
+      Collections.sort(addrs);
+      email.addAll(addrs);
+
+      if (email.isEmpty()) {
+        rc.unset(NOTIFY, nc.getName(), KEY_EMAIL);
+      } else {
+        rc.setStringList(NOTIFY, nc.getName(), KEY_EMAIL, email);
+      }
+
+      if (nc.getNotify().equals(EnumSet.of(NotifyType.ALL))) {
+        rc.unset(NOTIFY, nc.getName(), KEY_TYPE);
+      } else {
+        List<String> types = Lists.newArrayListWithCapacity(4);
+        for (NotifyType t : NotifyType.values()) {
+          if (nc.isNotify(t)) {
+            types.add(StringUtils.toLowerCase(t.name()));
+          }
+        }
+        rc.setStringList(NOTIFY, nc.getName(), KEY_TYPE, types);
+      }
+
+      set(rc, NOTIFY, nc.getName(), KEY_FILTER, nc.getFilter());
+    }
+  }
+
+  private List<String> ruleToStringList(
+      List<PermissionRule> list, Set<AccountGroup.UUID> keepGroups) {
+    List<String> rules = new ArrayList<String>();
+    for (PermissionRule rule : sort(list)) {
+      if (rule.getGroup().getUUID() != null) {
+        keepGroups.add(rule.getGroup().getUUID());
+      }
+      rules.add(rule.asString(false));
+    }
+    return rules;
+  }
+
+  private void saveAccessSections(
+      Config rc, Set<AccountGroup.UUID> keepGroups) {
     AccessSection capability = accessSections.get(AccessSection.GLOBAL_CAPABILITIES);
     if (capability != null) {
       Set<String> have = new HashSet<String>();
@@ -425,10 +705,6 @@
         rc.unsetSection(ACCESS, name);
       }
     }
-    groupsByUUID.keySet().retainAll(keepGroups);
-
-    saveConfig(PROJECT_CONFIG, rc);
-    saveGroupList();
   }
 
   private void saveGroupList() throws IOException {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/PushAllProjectsOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/PushAllProjectsOp.java
deleted file mode 100644
index 845d037..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/PushAllProjectsOp.java
+++ /dev/null
@@ -1,75 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.git;
-
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.util.concurrent.TimeUnit;
-
-import javax.annotation.Nullable;
-
-public class PushAllProjectsOp extends DefaultQueueOp {
-  public interface Factory {
-    PushAllProjectsOp create(String urlMatch);
-  }
-
-  private static final Logger log =
-      LoggerFactory.getLogger(PushAllProjectsOp.class);
-
-  private final ProjectCache projectCache;
-  private final ReplicationQueue replication;
-  private final String urlMatch;
-
-  @Inject
-  public PushAllProjectsOp(final WorkQueue wq, final ProjectCache projectCache,
-      final ReplicationQueue rq, @Assisted @Nullable final String urlMatch) {
-    super(wq);
-    this.projectCache = projectCache;
-    this.replication = rq;
-    this.urlMatch = urlMatch;
-  }
-
-  @Override
-  public void start(final int delay, final TimeUnit unit) {
-    if (replication.isEnabled()) {
-      super.start(delay, unit);
-    }
-  }
-
-  public void run() {
-    try {
-      for (final Project.NameKey nameKey : projectCache.all()) {
-        replication.scheduleFullSync(nameKey, urlMatch);
-      }
-    } catch (RuntimeException e) {
-      log.error("Cannot enumerate known projects", e);
-    }
-  }
-
-  @Override
-  public String toString() {
-    String s = "Replicate All Projects";
-    if (urlMatch != null) {
-      s = s + " to " + urlMatch;
-    }
-    return s;
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/PushOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/PushOp.java
deleted file mode 100644
index 0868749..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/PushOp.java
+++ /dev/null
@@ -1,433 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.git;
-
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.Project.NameKey;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.project.ProjectControl;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
-
-import com.jcraft.jsch.JSchException;
-
-import org.eclipse.jgit.errors.NoRemoteRepositoryException;
-import org.eclipse.jgit.errors.NotSupportedException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.errors.TransportException;
-import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.lib.NullProgressMonitor;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.transport.CredentialsProvider;
-import org.eclipse.jgit.transport.FetchConnection;
-import org.eclipse.jgit.transport.PushResult;
-import org.eclipse.jgit.transport.RefSpec;
-import org.eclipse.jgit.transport.RemoteConfig;
-import org.eclipse.jgit.transport.RemoteRefUpdate;
-import org.eclipse.jgit.transport.Transport;
-import org.eclipse.jgit.transport.URIish;
-import org.slf4j.Logger;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-/**
- * A push to remote operation started by {@link ReplicationQueue}.
- * <p>
- * Instance members are protected by the lock within PushQueue. Callers must
- * take that lock to ensure they are working with a current view of the object.
- */
-class PushOp implements ProjectRunnable {
-  interface Factory {
-    PushOp create(Project.NameKey d, URIish u);
-  }
-
-  private static final Logger log = PushReplication.log;
-  static final String ALL_REFS = "..all..";
-
-  private final GitRepositoryManager repoManager;
-  private final SchemaFactory<ReviewDb> schema;
-  private final PushReplication.ReplicationConfig pool;
-  private final RemoteConfig config;
-  private final CredentialsProvider credentialsProvider;
-  private final TagCache tagCache;
-
-  private final Set<String> delta = new HashSet<String>();
-  private final Project.NameKey projectName;
-  private final URIish uri;
-  private boolean pushAllRefs;
-
-  private Repository db;
-
-  /**
-   * It indicates if the current instance is in fact retrying to push.
-   */
-  private boolean retrying;
-
-  private boolean canceled;
-
-  @Inject
-  PushOp(final GitRepositoryManager grm, final SchemaFactory<ReviewDb> s,
-      final PushReplication.ReplicationConfig p, final RemoteConfig c,
-      final SecureCredentialsProvider.Factory cpFactory,
-      final TagCache tc,
-      @Assisted final Project.NameKey d, @Assisted final URIish u) {
-    repoManager = grm;
-    schema = s;
-    pool = p;
-    config = c;
-    credentialsProvider = cpFactory.create(c.getName());
-    tagCache = tc;
-    projectName = d;
-    uri = u;
-  }
-
-  public boolean isRetrying() {
-    return retrying;
-  }
-
-  public void setToRetry() {
-    retrying = true;
-  }
-
-  public void cancel() {
-    canceled = true;
-  }
-
-  public boolean wasCanceled() {
-    return canceled;
-  }
-
-  URIish getURI() {
-    return uri;
-  }
-
-  void addRef(final String ref) {
-    if (ALL_REFS.equals(ref)) {
-      delta.clear();
-      pushAllRefs = true;
-    } else if (!pushAllRefs) {
-      delta.add(ref);
-    }
-  }
-
-  public Set<String> getRefs() {
-    final Set<String> refs;
-
-    if (pushAllRefs) {
-      refs = new HashSet<String>(1);
-      refs.add(ALL_REFS);
-    } else {
-      refs = delta;
-    }
-
-    return refs;
-  }
-
-  public void addRefs(Set<String> refs) {
-    if (!pushAllRefs) {
-      for (String ref : refs) {
-        addRef(ref);
-      }
-    }
-  }
-
-  public void run() {
-    PerThreadRequestScope ctx = new PerThreadRequestScope();
-    PerThreadRequestScope old = PerThreadRequestScope.set(ctx);
-    try {
-      runPushOperation();
-    } finally {
-      PerThreadRequestScope.set(old);
-    }
-  }
-
-  private void runPushOperation() {
-    // Lock the queue, and remove ourselves, so we can't be modified once
-    // we start replication (instead a new instance, with the same URI, is
-    // created and scheduled for a future point in time.)
-    //
-    pool.notifyStarting(this);
-
-    // It should only verify if it was canceled after calling notifyStarting,
-    // since the canceled flag would be set locking the queue.
-    if (!canceled) {
-      try {
-        db = repoManager.openRepository(projectName);
-        runImpl();
-      } catch (RepositoryNotFoundException e) {
-        log.error("Cannot replicate " + projectName + "; " + e.getMessage());
-
-      } catch (NoRemoteRepositoryException e) {
-        log.error("Cannot replicate to " + uri + "; repository not found");
-
-      } catch (NotSupportedException e) {
-        log.error("Cannot replicate to " + uri, e);
-
-      } catch (TransportException e) {
-        final Throwable cause = e.getCause();
-        if (cause instanceof JSchException
-            && cause.getMessage().startsWith("UnknownHostKey:")) {
-          log.error("Cannot replicate to " + uri + ": " + cause.getMessage());
-        } else {
-          log.error("Cannot replicate to " + uri, e);
-        }
-
-        // The remote push operation should be retried.
-        pool.reschedule(this);
-      } catch (IOException e) {
-        log.error("Cannot replicate to " + uri, e);
-
-      } catch (RuntimeException e) {
-        log.error("Unexpected error during replication to " + uri, e);
-
-      } catch (Error e) {
-        log.error("Unexpected error during replication to " + uri, e);
-
-      } finally {
-        if (db != null) {
-          db.close();
-        }
-      }
-    }
-  }
-
-  @Override
-  public String toString() {
-    return "push " + uri;
-  }
-
-  private void runImpl() throws IOException {
-    final Transport tn = Transport.open(db, uri);
-    final PushResult res;
-    try {
-      res = pushVia(tn);
-    } finally {
-      try {
-        tn.close();
-      } catch (Throwable e2) {
-        log.warn("Unexpected error while closing " + uri, e2);
-      }
-    }
-
-    for (final RemoteRefUpdate u : res.getRemoteUpdates()) {
-      switch (u.getStatus()) {
-        case OK:
-        case UP_TO_DATE:
-        case NON_EXISTING:
-          break;
-
-        case NOT_ATTEMPTED:
-        case AWAITING_REPORT:
-        case REJECTED_NODELETE:
-        case REJECTED_NONFASTFORWARD:
-        case REJECTED_REMOTE_CHANGED:
-          log.error("Failed replicate of " + u.getRemoteName() + " to " + uri
-              + ": status " + u.getStatus().name());
-          break;
-
-        case REJECTED_OTHER_REASON:
-          if ("non-fast-forward".equals(u.getMessage())) {
-            log.error("Failed replicate of " + u.getRemoteName() + " to " + uri
-                + ", remote rejected non-fast-forward push."
-                + "  Check receive.denyNonFastForwards variable in config file"
-                + " of destination repository.");
-          } else {
-            log.error("Failed replicate of " + u.getRemoteName() + " to " + uri
-                + ", reason: " + u.getMessage());
-          }
-          break;
-      }
-    }
-  }
-
-  private PushResult pushVia(final Transport tn) throws IOException,
-      NotSupportedException, TransportException {
-    tn.applyConfig(config);
-    tn.setCredentialsProvider(credentialsProvider);
-
-    final List<RemoteRefUpdate> todo = generateUpdates(tn);
-    if (todo.isEmpty()) {
-      // If we have no commands selected, we have nothing to do.
-      // Calling JGit at this point would just redo the work we
-      // already did, and come up with the same answer. Instead
-      // send back an empty result.
-      //
-      return new PushResult();
-    }
-
-    return tn.push(NullProgressMonitor.INSTANCE, todo);
-  }
-
-  private List<RemoteRefUpdate> generateUpdates(final Transport tn)
-      throws IOException {
-    final ProjectControl pc;
-    try {
-      pc = pool.controlFor(projectName);
-    } catch (NoSuchProjectException e) {
-      return Collections.emptyList();
-    }
-
-    Map<String, Ref> local = db.getAllRefs();
-    if (!pc.allRefsAreVisible()) {
-      if (!pushAllRefs) {
-        // If we aren't mirroring, reduce the space we need to filter
-        // to only the references we will update during this operation.
-        //
-        Map<String, Ref> n = new HashMap<String, Ref>();
-        for (String src : delta) {
-          Ref r = local.get(src);
-          if (r != null) {
-            n.put(src, r);
-          }
-        }
-        local = n;
-      }
-
-      final ReviewDb meta;
-      try {
-        meta = schema.open();
-      } catch (OrmException e) {
-        log.error("Cannot read database to replicate to " + projectName, e);
-        return Collections.emptyList();
-      }
-      try {
-        local = new VisibleRefFilter(tagCache, db, pc, meta, true).filter(local, true);
-      } finally {
-        meta.close();
-      }
-    }
-
-    final boolean noPerms = !pool.isReplicatePermissions();
-    final List<RemoteRefUpdate> cmds = new ArrayList<RemoteRefUpdate>();
-    if (pushAllRefs) {
-      final Map<String, Ref> remote = listRemote(tn);
-
-      for (final Ref src : local.values()) {
-        if (noPerms && GitRepositoryManager.REF_CONFIG.equals(src.getName())) {
-          continue;
-        }
-
-        final RefSpec spec = matchSrc(src.getName());
-        if (spec != null) {
-          final Ref dst = remote.get(spec.getDestination());
-          if (dst == null || !src.getObjectId().equals(dst.getObjectId())) {
-            // Doesn't exist yet, or isn't the same value, request to push.
-            //
-            send(cmds, spec, src);
-          }
-        }
-      }
-
-      if (config.isMirror()) {
-        for (final Ref ref : remote.values()) {
-          if (!Constants.HEAD.equals(ref.getName())) {
-            final RefSpec spec = matchDst(ref.getName());
-            if (spec != null && !local.containsKey(spec.getSource())) {
-              // No longer on local side, request removal.
-              //
-              delete(cmds, spec);
-            }
-          }
-        }
-      }
-
-    } else {
-      for (final String src : delta) {
-        final RefSpec spec = matchSrc(src);
-        if (spec != null) {
-          // If the ref still exists locally, send it, otherwise delete it.
-          //
-          Ref srcRef = local.get(src);
-          if (srcRef != null &&
-              !(noPerms && GitRepositoryManager.REF_CONFIG.equals(src))) {
-            send(cmds, spec, srcRef);
-          } else if (config.isMirror()) {
-            delete(cmds, spec);
-          }
-        }
-      }
-    }
-
-    return cmds;
-  }
-
-  private Map<String, Ref> listRemote(final Transport tn)
-      throws NotSupportedException, TransportException {
-    final FetchConnection fc = tn.openFetch();
-    try {
-      return fc.getRefsMap();
-    } finally {
-      fc.close();
-    }
-  }
-
-  private RefSpec matchSrc(final String ref) {
-    for (final RefSpec s : config.getPushRefSpecs()) {
-      if (s.matchSource(ref)) {
-        return s.expandFromSource(ref);
-      }
-    }
-    return null;
-  }
-
-  private RefSpec matchDst(final String ref) {
-    for (final RefSpec s : config.getPushRefSpecs()) {
-      if (s.matchDestination(ref)) {
-        return s.expandFromDestination(ref);
-      }
-    }
-    return null;
-  }
-
-  private void send(final List<RemoteRefUpdate> cmds, final RefSpec spec,
-      final Ref src) throws IOException {
-    final String dst = spec.getDestination();
-    final boolean force = spec.isForceUpdate();
-    cmds.add(new RemoteRefUpdate(db, src, dst, force, null, null));
-  }
-
-  private void delete(final List<RemoteRefUpdate> cmds, final RefSpec spec)
-      throws IOException {
-    final String dst = spec.getDestination();
-    final boolean force = spec.isForceUpdate();
-    cmds.add(new RemoteRefUpdate(db, (Ref) null, dst, force, null, null));
-  }
-
-  @Override
-  public NameKey getProjectNameKey() {
-    return projectName;
-  }
-
-  @Override
-  public String getRemoteName() {
-    return config.getName();
-  }
-
-  @Override
-  public boolean hasCustomizedPrint() {
-    return true;
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/PushReplication.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/PushReplication.java
deleted file mode 100644
index 5cf5f7a..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/PushReplication.java
+++ /dev/null
@@ -1,668 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.git;
-
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.ReplicationUser;
-import com.google.gerrit.server.account.GroupMembership;
-import com.google.gerrit.server.account.ListGroupMembership;
-import com.google.gerrit.server.config.ConfigUtil;
-import com.google.gerrit.server.config.FactoryModule;
-import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.project.PerRequestProjectControlCache;
-import com.google.gerrit.server.project.ProjectControl;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.inject.AbstractModule;
-import com.google.inject.Inject;
-import com.google.inject.Injector;
-import com.google.inject.Singleton;
-import com.google.inject.servlet.RequestScoped;
-
-import com.jcraft.jsch.Session;
-
-import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.RefUpdate;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.storage.file.FileBasedConfig;
-import org.eclipse.jgit.storage.file.FileRepository;
-import org.eclipse.jgit.transport.JschConfigSessionFactory;
-import org.eclipse.jgit.transport.OpenSshConfig;
-import org.eclipse.jgit.transport.RefSpec;
-import org.eclipse.jgit.transport.RemoteConfig;
-import org.eclipse.jgit.transport.RemoteSession;
-import org.eclipse.jgit.transport.SshSessionFactory;
-import org.eclipse.jgit.transport.URIish;
-import org.eclipse.jgit.util.FS;
-import org.eclipse.jgit.util.QuotedString;
-import org.eclipse.jgit.util.io.StreamCopyThread;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.IOException;
-import java.io.OutputStream;
-import java.io.UnsupportedEncodingException;
-import java.net.URISyntaxException;
-import java.net.URLEncoder;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.concurrent.TimeUnit;
-
-/** Manages automatic replication to remote repositories. */
-@Singleton
-public class PushReplication implements ReplicationQueue {
-  static final Logger log = LoggerFactory.getLogger(PushReplication.class);
-
-  public static class Module extends AbstractModule {
-    @Override
-    protected void configure() {
-      bind(ReplicationQueue.class).to(PushReplication.class);
-    }
-  }
-
-  static {
-    // Install our own factory which always runs in batch mode, as we
-    // have no UI available for interactive prompting.
-    //
-    SshSessionFactory.setInstance(new JschConfigSessionFactory() {
-      @Override
-      protected void configure(OpenSshConfig.Host hc, Session session) {
-        // Default configuration is batch mode.
-      }
-    });
-  }
-
-  private final Injector injector;
-  private final WorkQueue workQueue;
-  private final List<ReplicationConfig> configs;
-  private final SchemaFactory<ReviewDb> database;
-  private final ReplicationUser.Factory replicationUserFactory;
-  private final GitRepositoryManager gitRepositoryManager;
-
-  @Inject
-  PushReplication(final Injector i, final WorkQueue wq, final SitePaths site,
-      final ReplicationUser.Factory ruf, final SchemaFactory<ReviewDb> db,
-      final GitRepositoryManager grm)
-      throws ConfigInvalidException, IOException {
-    injector = i;
-    workQueue = wq;
-    database = db;
-    replicationUserFactory = ruf;
-    gitRepositoryManager = grm;
-    configs = allConfigs(site);
-  }
-
-  @Override
-  public boolean isEnabled() {
-    return configs.size() > 0;
-  }
-
-  @Override
-  public void scheduleFullSync(final Project.NameKey project,
-      final String urlMatch) {
-    for (final ReplicationConfig cfg : configs) {
-      for (final URIish uri : cfg.getURIs(project, urlMatch)) {
-        cfg.schedule(project, PushOp.ALL_REFS, uri);
-      }
-    }
-  }
-
-  @Override
-  public void scheduleUpdate(final Project.NameKey project, final String ref) {
-    for (final ReplicationConfig cfg : configs) {
-      if (cfg.wouldPushRef(ref)) {
-        for (final URIish uri : cfg.getURIs(project, null)) {
-          cfg.schedule(project, ref, uri);
-        }
-      }
-    }
-  }
-
-  private static String replace(final String pat, final String key,
-      final String val) {
-    final int n = pat.indexOf("${" + key + "}");
-
-    if (n != -1) {
-      return pat.substring(0, n) + val + pat.substring(n + 3 + key.length());
-    } else {
-      return null;
-    }
-  }
-
-  private List<ReplicationConfig> allConfigs(final SitePaths site)
-      throws ConfigInvalidException, IOException {
-    final FileBasedConfig cfg =
-        new FileBasedConfig(site.replication_config, FS.DETECTED);
-
-    if (!cfg.getFile().exists()) {
-      log.warn("No " + cfg.getFile() + "; not replicating");
-      return Collections.emptyList();
-    }
-    if (cfg.getFile().length() == 0) {
-      log.info("Empty " + cfg.getFile() + "; not replicating");
-      return Collections.emptyList();
-    }
-
-    try {
-      cfg.load();
-    } catch (ConfigInvalidException e) {
-      throw new ConfigInvalidException("Config file " + cfg.getFile()
-          + " is invalid: " + e.getMessage(), e);
-    } catch (IOException e) {
-      throw new IOException("Cannot read " + cfg.getFile() + ": "
-          + e.getMessage(), e);
-    }
-
-    final List<ReplicationConfig> r = new ArrayList<ReplicationConfig>();
-    for (final RemoteConfig c : allRemotes(cfg)) {
-      if (c.getURIs().isEmpty()) {
-        continue;
-      }
-
-      for (final URIish u : c.getURIs()) {
-        if (u.getPath() == null || !u.getPath().contains("${name}")) {
-          throw new ConfigInvalidException("remote." + c.getName() + ".url"
-              + " \"" + u + "\" lacks ${name} placeholder in " + cfg.getFile());
-        }
-      }
-
-      // In case if refspec destination for push is not set then we assume it is
-      // equal to source
-      for (RefSpec ref : c.getPushRefSpecs()) {
-        if (ref.getDestination() == null) {
-          ref.setDestination(ref.getSource());
-        }
-      }
-
-
-      if (c.getPushRefSpecs().isEmpty()) {
-        RefSpec spec = new RefSpec();
-        spec = spec.setSourceDestination("refs/*", "refs/*");
-        spec = spec.setForceUpdate(true);
-        c.addPushRefSpec(spec);
-      }
-
-      r.add(new ReplicationConfig(injector, workQueue, c, cfg, database,
-          replicationUserFactory, gitRepositoryManager));
-    }
-    return Collections.unmodifiableList(r);
-  }
-
-  private List<RemoteConfig> allRemotes(final FileBasedConfig cfg)
-      throws ConfigInvalidException {
-    List<String> names = new ArrayList<String>(cfg.getSubsections("remote"));
-    Collections.sort(names);
-
-    final List<RemoteConfig> result = new ArrayList<RemoteConfig>(names.size());
-    for (final String name : names) {
-      try {
-        result.add(new RemoteConfig(cfg, name));
-      } catch (URISyntaxException e) {
-        throw new ConfigInvalidException("remote " + name
-            + " has invalid URL in " + cfg.getFile());
-      }
-    }
-    return result;
-  }
-
-  @Override
-  public void replicateNewProject(Project.NameKey projectName, String head) {
-    if (!isEnabled()) {
-      return;
-    }
-
-    for (ReplicationConfig config : configs) {
-      List<URIish> uriList = config.getURIs(projectName, "*");
-      String[] adminUrls = config.getAdminUrls();
-      boolean adminURLUsed = false;
-
-      for (String url : adminUrls) {
-        URIish adminURI = null;
-        try {
-          if (url != null && !url.isEmpty()) {
-            adminURI = new URIish(url);
-          }
-        } catch (URISyntaxException e) {
-          log.error("The URL '" + url + "' is invalid");
-        }
-
-        if (adminURI != null) {
-          final String replacedPath =
-              replace(adminURI.getPath(), "name", projectName.get());
-          if (replacedPath != null) {
-            adminURI = adminURI.setPath(replacedPath);
-            if (usingSSH(adminURI)) {
-              replicateProject(adminURI, head);
-              adminURLUsed = true;
-            } else {
-              log.error("The adminURL '" + url
-                  + "' is non-SSH which is not allowed");
-            }
-          }
-        }
-      }
-
-      if (!adminURLUsed) {
-        for (URIish uri : uriList) {
-          replicateProject(uri, head);
-        }
-      }
-    }
-  }
-
-  private void replicateProject(final URIish replicateURI, final String head) {
-    if (!replicateURI.isRemote()) {
-      replicateProjectLocally(replicateURI, head);
-    } else if (usingSSH(replicateURI)) {
-      replicateProjectOverSsh(replicateURI, head);
-    } else {
-      log.warn("Cannot create new project on remote site since neither the "
-          + "connection method is SSH nor the replication target is local: "
-          + replicateURI.toString());
-      return;
-    }
-  }
-
-  private void replicateProjectLocally(final URIish replicateURI,
-      final String head) {
-    try {
-      final Repository repo = new FileRepository(replicateURI.getPath());
-      try {
-        repo.create(true /* bare */);
-
-        final RefUpdate u = repo.updateRef(Constants.HEAD);
-        u.disableRefLog();
-        u.link(head);
-      } finally {
-        repo.close();
-      }
-    } catch (IOException e) {
-      log.error("Failed to replicate project locally: "
-          + replicateURI.getPath());
-    }
-  }
-
-  private void replicateProjectOverSsh(final URIish replicateURI,
-      final String head) {
-    SshSessionFactory sshFactory = SshSessionFactory.getInstance();
-    RemoteSession sshSession;
-    String projectPath = QuotedString.BOURNE.quote(replicateURI.getPath());
-
-    OutputStream errStream = createErrStream();
-    String cmd =
-        "mkdir -p " + projectPath + "&& cd " + projectPath
-            + "&& git init --bare" + "&& git symbolic-ref HEAD "
-            + QuotedString.BOURNE.quote(head);
-
-    try {
-      sshSession = sshFactory.getSession(replicateURI, null, FS.DETECTED, 0);
-      Process proc = sshSession.exec(cmd, 0);
-      proc.getOutputStream().close();
-      StreamCopyThread out = new StreamCopyThread(proc.getInputStream(), errStream);
-      StreamCopyThread err = new StreamCopyThread(proc.getErrorStream(), errStream);
-      out.start();
-      err.start();
-      try {
-        proc.waitFor();
-        out.halt();
-        err.halt();
-      } catch (InterruptedException interrupted) {
-        // Don't wait, drop out immediately.
-      }
-      sshSession.disconnect();
-    } catch (IOException e) {
-      log.error("Communication error when trying to replicate to: "
-          + replicateURI.toString() + "\n" + "Error reported: "
-          + e.getMessage() + "\n" + "Error in communication: "
-          + errStream.toString());
-    }
-  }
-
-  private OutputStream createErrStream() {
-    return new OutputStream() {
-      private StringBuilder all = new StringBuilder();
-      private StringBuilder sb = new StringBuilder();
-
-      @Override
-      public String toString() {
-        String r = all.toString();
-        while (r.endsWith("\n"))
-          r = r.substring(0, r.length() - 1);
-        return r;
-      }
-
-      @Override
-      public synchronized void write(final int b) {
-        if (b == '\r') {
-          return;
-        }
-
-        sb.append((char) b);
-
-        if (b == '\n') {
-          all.append(sb);
-          sb.setLength(0);
-        }
-      }
-    };
-  }
-
-  private boolean usingSSH(final URIish uri) {
-    final String scheme = uri.getScheme();
-    if (!uri.isRemote()) return false;
-    if (scheme != null && scheme.toLowerCase().contains("ssh")) return true;
-    if (scheme == null && uri.getHost() != null && uri.getPath() != null)
-      return true;
-    return false;
-  }
-
-  static class ReplicationConfig {
-    private final RemoteConfig remote;
-    private final String[] adminUrls;
-    private final int delay;
-    private final int retryDelay;
-    private final WorkQueue.Executor pool;
-    private final Map<URIish, PushOp> pending = new HashMap<URIish, PushOp>();
-    private final PushOp.Factory opFactory;
-    private final ProjectControl.Factory projectControlFactory;
-    private final GitRepositoryManager mgr;
-    private final boolean replicatePermissions;
-
-    ReplicationConfig(final Injector injector, final WorkQueue workQueue,
-        final RemoteConfig rc, final Config cfg, SchemaFactory<ReviewDb> db,
-        final ReplicationUser.Factory replicationUserFactory,
-        final GitRepositoryManager gitRepositoryManager) {
-
-      remote = rc;
-      delay = Math.max(0, getInt(rc, cfg, "replicationdelay", 15));
-      retryDelay = Math.max(0, getInt(rc, cfg, "replicationretry", 1));
-
-      final int poolSize = Math.max(0, getInt(rc, cfg, "threads", 1));
-      final String poolName = "ReplicateTo-" + rc.getName();
-      pool = workQueue.createQueue(poolSize, poolName);
-
-      String[] authGroupNames =
-          cfg.getStringList("remote", rc.getName(), "authGroup");
-      final GroupMembership authGroups;
-      if (authGroupNames.length > 0) {
-        authGroups = new ListGroupMembership(ConfigUtil.groupsFor(db, authGroupNames, //
-            log, "Group \"{0}\" not in database, removing from authGroup"));
-      } else {
-        authGroups = ReplicationUser.EVERYTHING_VISIBLE;
-      }
-
-      adminUrls = cfg.getStringList("remote", rc.getName(), "adminUrl");
-      replicatePermissions = cfg.getBoolean("remote", rc.getName(),
-              "replicatePermissions", true);
-      mgr = gitRepositoryManager;
-
-      final ReplicationUser remoteUser =
-          replicationUserFactory.create(authGroups);
-
-      projectControlFactory =
-          injector.createChildInjector(new AbstractModule() {
-            @Override
-            protected void configure() {
-              bindScope(RequestScoped.class, PerThreadRequestScope.REQUEST);
-              bind(PerRequestProjectControlCache.class).in(RequestScoped.class);
-              bind(CurrentUser.class).toInstance(remoteUser);
-            }
-          }).getInstance(ProjectControl.Factory.class);
-
-      opFactory = injector.createChildInjector(new FactoryModule() {
-        @Override
-        protected void configure() {
-          bind(PushReplication.ReplicationConfig.class).toInstance(ReplicationConfig.this);
-          bind(RemoteConfig.class).toInstance(remote);
-          factory(PushOp.Factory.class);
-        }
-      }).getInstance(PushOp.Factory.class);
-    }
-
-    private int getInt(final RemoteConfig rc, final Config cfg,
-        final String name, final int defValue) {
-      return cfg.getInt("remote", rc.getName(), name, defValue);
-    }
-
-    void schedule(final Project.NameKey project, final String ref,
-        final URIish uri) {
-      PerThreadRequestScope ctx = new PerThreadRequestScope();
-      PerThreadRequestScope old = PerThreadRequestScope.set(ctx);
-      try {
-        try {
-          if (!controlFor(project).isVisible()) {
-            return;
-          }
-        } catch (NoSuchProjectException e1) {
-          log.error("Internal error: project " + project
-              + " not found during replication");
-          return;
-        }
-      } finally {
-        PerThreadRequestScope.set(old);
-      }
-
-      if (!replicatePermissions) {
-        PushOp e;
-        synchronized (pending) {
-          e = pending.get(uri);
-        }
-        if (e == null) {
-          Repository git;
-          try {
-            git = mgr.openRepository(project);
-          } catch (RepositoryNotFoundException err) {
-            log.error("Internal error: project " + project
-                + " not found during replication", err);
-            return;
-          }
-          try {
-            Ref head = git.getRef(Constants.HEAD);
-            if (head != null
-                && head.isSymbolic()
-                && GitRepositoryManager.REF_CONFIG.equals(head.getLeaf().getName())) {
-              return;
-            }
-          } catch (IOException err) {
-            log.error("Internal error: cannot check type of project " + project
-                + " during replication", err);
-            return;
-          } finally {
-            git.close();
-          }
-        }
-      }
-
-      synchronized (pending) {
-        PushOp e = pending.get(uri);
-        if (e == null) {
-          e = opFactory.create(project, uri);
-          pool.schedule(e, delay, TimeUnit.SECONDS);
-          pending.put(uri, e);
-        }
-        e.addRef(ref);
-      }
-    }
-
-    /**
-     * It schedules again a PushOp instance.
-     * <p>
-     * It is assumed to be previously scheduled and found a
-     * transport exception. It will schedule it as a push
-     * operation to be retried after the minutes count
-     * determined by class attribute retryDelay.
-     * <p>
-     * In case the PushOp instance to be scheduled has same
-     * URI than one also pending for retry, it adds to the one
-     * pending the refs list of the parameter instance.
-     * <p>
-     * In case the PushOp instance to be scheduled has same
-     * URI than one pending, but not pending for retry, it
-     * indicates the one pending should be canceled when it
-     * starts executing, removes it from pending list, and
-     * adds its refs to the parameter instance. The parameter
-     * instance is scheduled for retry.
-     * <p>
-     * Notice all operations to indicate a PushOp should be
-     * canceled, or it is retrying, or remove/add it from/to
-     * pending Map should be protected by the lock on pending
-     * Map class instance attribute.
-     *
-     * @param pushOp The PushOp instance to be scheduled.
-     */
-    void reschedule(final PushOp pushOp) {
-      // It locks access to pending variable.
-      synchronized (pending) {
-        URIish uri = pushOp.getURI();
-        PushOp pendingPushOp = pending.get(uri);
-
-        if (pendingPushOp != null) {
-          // There is one PushOp instance already pending to same URI.
-
-          if (pendingPushOp.isRetrying()) {
-            // The one pending is one already retrying, so it should
-            // maintain it and add to it the refs of the one passed
-            // as parameter to the method.
-
-            // This scenario would happen if a PushOp has started running
-            // and then before it failed due transport exception, another
-            // one to same URI started. The first one would fail and would
-            // be rescheduled, being present in pending list. When the
-            // second one fails, it will also be rescheduled and then,
-            // here, find out replication to its URI is already pending
-            // for retry (blocking).
-            pendingPushOp.addRefs(pushOp.getRefs());
-
-          } else {
-            // The one pending is one that is NOT retrying, it was just
-            // scheduled believing no problem would happen. The one pending
-            // should be canceled, and this is done by setting its canceled
-            // flag, removing it from pending list, and adding its refs to
-            // the pushOp instance that should then, later, in this method,
-            // be scheduled for retry.
-
-            // Notice that the PushOp found pending will start running and,
-            // when notifying it is starting (with pending lock protection),
-            // it will see it was canceled and then it will do nothing with
-            // pending list and it will not execute its run implementation.
-
-            pendingPushOp.cancel();
-            pending.remove(uri);
-
-            pushOp.addRefs(pendingPushOp.getRefs());
-          }
-        }
-
-        if (pendingPushOp == null || !pendingPushOp.isRetrying()) {
-          // The PushOp method param instance should be scheduled for retry.
-          // Remember when retrying it should be used different delay.
-
-          pushOp.setToRetry();
-
-          pending.put(uri, pushOp);
-          pool.schedule(pushOp, retryDelay, TimeUnit.MINUTES);
-        }
-      }
-    }
-
-    ProjectControl controlFor(final Project.NameKey project)
-        throws NoSuchProjectException {
-      return projectControlFactory.controlFor(project);
-    }
-
-    void notifyStarting(final PushOp op) {
-      synchronized (pending) {
-        if (!op.wasCanceled()) {
-          pending.remove(op.getURI());
-        }
-      }
-    }
-
-    boolean wouldPushRef(final String ref) {
-      if (!replicatePermissions && GitRepositoryManager.REF_CONFIG.equals(ref)) {
-        return false;
-      }
-      for (final RefSpec s : remote.getPushRefSpecs()) {
-        if (s.matchSource(ref)) {
-          return true;
-        }
-      }
-      return false;
-    }
-
-    boolean isReplicatePermissions() {
-      return replicatePermissions;
-    }
-
-    List<URIish> getURIs(final Project.NameKey project, final String urlMatch) {
-      final List<URIish> r = new ArrayList<URIish>(remote.getURIs().size());
-      for (URIish uri : remote.getURIs()) {
-        if (matches(uri, urlMatch)) {
-          String name = project.get();
-          if (needsUrlEncoding(uri)) {
-            name = encode(name);
-          }
-          String replacedPath = replace(uri.getPath(), "name", name);
-          if (replacedPath != null) {
-            uri = uri.setPath(replacedPath);
-            r.add(uri);
-          }
-        }
-      }
-      return r;
-    }
-
-    static boolean needsUrlEncoding(URIish uri) {
-      return "http".equalsIgnoreCase(uri.getScheme())
-        || "https".equalsIgnoreCase(uri.getScheme())
-        || "amazon-s3".equalsIgnoreCase(uri.getScheme());
-    }
-
-    static String encode(String str) {
-      try {
-        // Some cleanup is required. The '/' character is always encoded as %2F
-        // however remote servers will expect it to be not encoded as part of the
-        // path used to the repository. Space is incorrectly encoded as '+' for this
-        // context. In the path part of a URI space should be %20, but in form data
-        // space is '+'. Our cleanup replace fixes these two issues.
-        return URLEncoder.encode(str, "UTF-8")
-          .replaceAll("%2[fF]", "/")
-          .replace("+", "%20");
-      } catch (UnsupportedEncodingException e) {
-        throw new RuntimeException(e);
-      }
-    }
-
-    String[] getAdminUrls() {
-      return this.adminUrls;
-    }
-
-    private boolean matches(URIish uri, final String urlMatch) {
-      if (urlMatch == null || urlMatch.equals("") || urlMatch.equals("*")) {
-        return true;
-      }
-      return uri.toString().contains(urlMatch);
-    }
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
index 37fbdb2..a2a809b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommits.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.git;
 
+import static org.eclipse.jgit.lib.Constants.R_HEADS;
 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;
@@ -21,15 +22,20 @@
 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.base.Predicate;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.LinkedListMultimap;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.common.util.concurrent.CheckedFuture;
 import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.common.data.ApprovalType;
-import com.google.gerrit.common.data.ApprovalTypes;
 import com.google.gerrit.common.data.Capable;
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.common.errors.NoSuchAccountException;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.ApprovalCategory;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
@@ -45,8 +51,10 @@
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountResolver;
+import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.TrackingFooters;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.MultiProgressMonitor.Task;
 import com.google.gerrit.server.mail.CreateChangeSender;
 import com.google.gerrit.server.mail.MergedSender;
@@ -61,18 +69,19 @@
 import com.google.gerrit.server.util.RequestScopePropagator;
 import com.google.gwtorm.server.AtomicUpdate;
 import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.lib.AbbreviatedObjectId;
+import org.eclipse.jgit.lib.BatchRefUpdate;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectReader;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.notes.NoteMap;
 import org.eclipse.jgit.revwalk.FooterKey;
@@ -85,6 +94,7 @@
 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;
@@ -116,6 +126,32 @@
   private static final FooterKey TESTED_BY = new FooterKey("Tested-by");
   private static final FooterKey CHANGE_ID = new FooterKey("Change-Id");
 
+  private static final String COMMAND_REJECTION_MESSAGE_FOOTER =
+      "Please read the documentation and contact an administrator\n"
+          + "if you feel the configuration is incorrect";
+
+  private enum Error {
+        CONFIG_UPDATE("You are not allowed to perform this operation.\n"
+        + "Configuration changes can only be pushed by project owners\n"
+        + "who also have 'Push' rights on " + GitRepositoryManager.REF_CONFIG),
+        UPDATE("You are not allowed to perform this operation.\n"
+        + "To push into this reference you need 'Push' rights."),
+        DELETE("You need 'Push' rights with the 'Force Push'\n"
+            + "flag set to delete references."),
+        CODE_REVIEW("You need 'Push' rights to upload code review requests.\n"
+            + "Verify that you are pushing to the right branch.");
+
+    private final String value;
+
+    Error(String value) {
+      this.value = value;
+    }
+
+    public String get() {
+      return value;
+    }
+  }
+
   interface Factory {
     ReceiveCommits create(ProjectControl projectControl, Repository repository);
   }
@@ -178,14 +214,14 @@
 
   private final IdentifiedUser currentUser;
   private final ReviewDb db;
-  private final ApprovalTypes approvalTypes;
   private final AccountResolver accountResolver;
   private final CreateChangeSender.Factory createChangeSenderFactory;
   private final MergedSender.Factory mergedSenderFactory;
   private final ReplacePatchSetSender.Factory replacePatchSetFactory;
-  private final ReplicationQueue replication;
+  private final GitReferenceUpdated replication;
   private final PatchSetInfoFactory patchSetInfoFactory;
   private final ChangeHooks hooks;
+  private final ApprovalsUtil approvalsUtil;
   private final GitRepositoryManager repoManager;
   private final ProjectCache projectCache;
   private final String canonicalWebUrl;
@@ -194,6 +230,7 @@
   private final TagCache tagCache;
   private final WorkQueue workQueue;
   private final RequestScopePropagator requestScopePropagator;
+  private final AllProjectsName allProjectsName;
 
   private final ProjectControl projectControl;
   private final Project project;
@@ -204,13 +241,12 @@
   private Branch.NameKey destBranch;
   private RefControl destBranchCtl;
 
-  private final List<Change> allNewChanges = new ArrayList<Change>();
+  private List<CreateRequest> newChanges = Collections.emptyList();
   private final Map<Change.Id, ReplaceRequest> replaceByChange =
       new HashMap<Change.Id, ReplaceRequest>();
   private final Map<RevCommit, ReplaceRequest> replaceByCommit =
       new HashMap<RevCommit, ReplaceRequest>();
 
-  private Collection<ObjectId> existingObjects;
   private Map<ObjectId, Ref> refsById;
 
   private String destTopicName;
@@ -218,21 +254,24 @@
   private final SubmoduleOp.Factory subOpFactory;
 
   private final List<Message> messages = new ArrayList<Message>();
+  private ListMultimap<Error, String> errors = LinkedListMultimap.create();
   private Task newProgress;
   private Task replaceProgress;
   private Task closeProgress;
   private Task commandProgress;
   private MessageSender messageSender;
+  private BatchRefUpdate batch;
 
   @Inject
-  ReceiveCommits(final ReviewDb db, final ApprovalTypes approvalTypes,
+  ReceiveCommits(final ReviewDb db,
       final AccountResolver accountResolver,
       final CreateChangeSender.Factory createChangeSenderFactory,
       final MergedSender.Factory mergedSenderFactory,
       final ReplacePatchSetSender.Factory replacePatchSetFactory,
-      final ReplicationQueue replication,
+      final GitReferenceUpdated replication,
       final PatchSetInfoFactory patchSetInfoFactory,
       final ChangeHooks hooks,
+      final ApprovalsUtil approvalsUtil,
       final ProjectCache projectCache,
       final GitRepositoryManager repoManager,
       final TagCache tagCache,
@@ -241,13 +280,13 @@
       final TrackingFooters trackingFooters,
       final WorkQueue workQueue,
       final RequestScopePropagator requestScopePropagator,
+      final AllProjectsName allProjectsName,
 
       @Assisted final ProjectControl projectControl,
       @Assisted final Repository repo,
       final SubmoduleOp.Factory subOpFactory) throws IOException {
     this.currentUser = (IdentifiedUser) projectControl.getCurrentUser();
     this.db = db;
-    this.approvalTypes = approvalTypes;
     this.accountResolver = accountResolver;
     this.createChangeSenderFactory = createChangeSenderFactory;
     this.mergedSenderFactory = mergedSenderFactory;
@@ -255,6 +294,7 @@
     this.replication = replication;
     this.patchSetInfoFactory = patchSetInfoFactory;
     this.hooks = hooks;
+    this.approvalsUtil = approvalsUtil;
     this.projectCache = projectCache;
     this.repoManager = repoManager;
     this.canonicalWebUrl = canonicalWebUrl;
@@ -263,6 +303,7 @@
     this.tagCache = tagCache;
     this.workQueue = workQueue;
     this.requestScopePropagator = requestScopePropagator;
+    this.allProjectsName = allProjectsName;
 
     this.projectControl = projectControl;
     this.project = projectControl.getProject();
@@ -429,15 +470,44 @@
     closeProgress = progress.beginSubTask("closed", UNKNOWN);
     commandProgress = progress.beginSubTask("refs", UNKNOWN);
 
+    batch = repo.getRefDatabase().newBatchUpdate();
+    batch.setRefLogIdent(rp.getRefLogIdent());
+    batch.setRefLogMessage("push", true);
+
     parseCommands(commands);
     if (newChange != null && newChange.getResult() == NOT_ATTEMPTED) {
-      createNewChanges();
+      newChanges = selectNewChanges();
     }
-    newProgress.end();
+    preparePatchSetsForReplace();
 
-    doReplaces();
+    if (!batch.getCommands().isEmpty()) {
+      try {
+        batch.execute(rp.getRevWalk(), commandProgress);
+      } catch (IOException err) {
+        int cnt = 0;
+        for (ReceiveCommand cmd : batch.getCommands()) {
+          if (cmd.getResult() == NOT_ATTEMPTED) {
+            cmd.setResult(REJECTED_OTHER_REASON, "internal server error");
+            cnt++;
+          }
+        }
+        log.error(String.format(
+            "Failed to store %d refs in %s", cnt, project.getName()), err);
+      }
+    }
+
+    insertChangesAndPatchSets();
+    newProgress.end();
     replaceProgress.end();
 
+    if (!errors.isEmpty()) {
+      for (Error error : errors.keySet()) {
+        rp.sendMessage(buildError(error, errors.get(error)));
+      }
+      rp.sendMessage(String.format("User: %s", displayName(currentUser)));
+      rp.sendMessage(COMMAND_REJECTION_MESSAGE_FOOTER);
+    }
+
     for (final ReceiveCommand c : commands) {
       if (c.getResult() == OK) {
         switch (c.getType()) {
@@ -475,10 +545,12 @@
           // We only schedule direct refs updates for replication.
           // Change refs are scheduled when they are created.
           //
-          replication.scheduleUpdate(project.getNameKey(), c.getRefName());
-          Branch.NameKey destBranch = new Branch.NameKey(project.getNameKey(), c.getRefName());
-          hooks.doRefUpdatedHook(destBranch, c.getOldId(), c.getNewId(), currentUser.getAccount());
-          commandProgress.update(1);
+          replication.fire(project.getNameKey(), c.getRefName());
+          hooks.doRefUpdatedHook(
+              new Branch.NameKey(project.getNameKey(), c.getRefName()),
+              c.getOldId(),
+              c.getNewId(),
+              currentUser.getAccount());
         }
       }
     }
@@ -486,22 +558,129 @@
     commandProgress.end();
     progress.end();
 
-    if (!allNewChanges.isEmpty() && canonicalWebUrl != null) {
+    Iterable<CreateRequest> created =
+        Iterables.filter(newChanges, new Predicate<CreateRequest>() {
+          @Override
+          public boolean apply(CreateRequest input) {
+            return input.created;
+          }
+        });
+    if (!Iterables.isEmpty(created) && canonicalWebUrl != null) {
       final String url = canonicalWebUrl;
       addMessage("");
       addMessage("New Changes:");
-      for (final Change c : allNewChanges) {
-        if (c.getStatus() == Change.Status.DRAFT) {
-          addMessage("  " + url + c.getChangeId() + " [DRAFT]");
+      for (CreateRequest c : created) {
+        StringBuilder m = new StringBuilder()
+            .append("  ")
+            .append(url)
+            .append(c.change.getChangeId());
+        if (c.change.getStatus() == Change.Status.DRAFT) {
+          m.append(" [DRAFT]");
         }
-        else {
-          addMessage("  " + url + c.getChangeId());
-        }
+        addMessage(m.toString());
       }
       addMessage("");
     }
   }
 
+  private void insertChangesAndPatchSets() {
+    int replaceCount = 0;
+    int okToInsert = 0;
+
+    for (ReplaceRequest replace : replaceByChange.values()) {
+      if (replace.inputCommand == newChange) {
+        replaceCount++;
+
+        if (replace.cmd != null && replace.cmd.getResult() == OK) {
+          okToInsert++;
+        }
+      } else if (replace.cmd != null && replace.cmd.getResult() == OK) {
+        try {
+          if (replace.insertPatchSet() != null) {
+            replace.inputCommand.setResult(OK);
+          }
+        } catch (IOException err) {
+          reject(replace.inputCommand, "internal server error");
+          log.error(String.format(
+              "Cannot add patch set to %d of %s",
+              replace.newPatchSet.getId(), project.getName()), err);
+        } catch (OrmException err) {
+          reject(replace.inputCommand, "internal server error");
+          log.error(String.format(
+              "Cannot add patch set to %d of %s",
+              replace.newPatchSet.getId(), project.getName()), err);
+        }
+      } else {
+        reject(replace.inputCommand, "internal server error");
+      }
+    }
+
+    if (newChange == null || newChange.getResult() != NOT_ATTEMPTED) {
+      // refs/for/ or refs/drafts/ not used, or it already failed earlier.
+      // No need to continue.
+      return;
+    }
+
+    for (CreateRequest create : newChanges) {
+      if (create.cmd.getResult() == OK) {
+        okToInsert++;
+      }
+    }
+
+    if (okToInsert != replaceCount + newChanges.size()) {
+      // One or more new references failed to create. Assume the
+      // system isn't working correctly anymore and abort.
+      reject(newChange, "internal server error");
+      log.error(String.format(
+          "Only %d of %d new change refs created in %s; aborting",
+          okToInsert, newChanges.size(), project.getName()));
+      return;
+    }
+
+    try {
+      for (ReplaceRequest replace : replaceByChange.values()) {
+        if (replace.inputCommand == newChange) {
+          replace.insertPatchSet();
+        }
+      }
+
+      for (CreateRequest create : newChanges) {
+        create.insertChange();
+      }
+      newChange.setResult(OK);
+    } catch (OrmException err) {
+      log.error("Can't insert changes for " + project.getName(), err);
+      reject(newChange, "internal server error");
+    } catch (IOException err) {
+      log.error("Can't read commits for " + project.getName(), err);
+      reject(newChange, "internal server error");
+    }
+  }
+
+  private String buildError(Error error, List<String> branches) {
+    StringBuilder sb = new StringBuilder();
+    if (branches.size() == 1) {
+      sb.append("Branch ").append(branches.get(0)).append(":\n");
+      sb.append(error.get());
+      return sb.toString();
+    }
+    sb.append("Branches");
+    String delim = " ";
+    for (String branch : branches) {
+      sb.append(delim).append(branch);
+      delim = ", ";
+    }
+    return sb.append(":\n").append(error.get()).toString();
+  }
+
+  private static String displayName(IdentifiedUser user) {
+    String displayName = user.getUserName();
+    if (displayName == null) {
+      displayName = user.getAccount().getPreferredEmail();
+    }
+    return displayName;
+  }
+
   private Account.Id toAccountId(final String nameOrEmail) throws OrmException,
       NoSuchAccountException {
     final Account a = accountResolver.findByNameOrEmail(nameOrEmail);
@@ -590,6 +769,26 @@
                     + cmd.getNewId().name() + " for " + project.getName());
                 continue;
               }
+              Project.NameKey newParent = cfg.getProject().getParent(allProjectsName);
+              Project.NameKey oldParent = project.getParent(allProjectsName);
+              if (oldParent == null) {
+                // update of the 'All-Projects' project
+                if (newParent != null) {
+                  reject(cmd, "invalid project configuration: root project cannot have parent");
+                  continue;
+                }
+              } else {
+                if (!oldParent.equals(newParent)
+                    && !currentUser.getCapabilities().canAdministrateServer()) {
+                  reject(cmd, "invalid project configuration: only Gerrit admin can set parent");
+                  continue;
+                }
+
+                if (projectCache.get(newParent) == null) {
+                  reject(cmd, "invalid project configuration: parent does not exist");
+                  continue;
+                }
+              }
             } catch (Exception e) {
               reject(cmd, "invalid project configuration");
               log.error("User " + currentUser.getUserName()
@@ -628,11 +827,9 @@
     RefControl ctl = projectControl.controlForRef(cmd.getRefName());
     if (ctl.canCreate(rp.getRevWalk(), obj)) {
       validateNewCommits(ctl, cmd);
-      if (cmd.getResult() == NOT_ATTEMPTED) {
-        cmd.execute(rp);
-      }
+      batch.addCommand(cmd);
     } else {
-      reject(cmd, "can not create new references");
+      reject(cmd);
     }
   }
 
@@ -644,11 +841,14 @@
       }
 
       validateNewCommits(ctl, cmd);
-      if (cmd.getResult() == NOT_ATTEMPTED) {
-        cmd.execute(rp);
-      }
+      batch.addCommand(cmd);
     } else {
-      reject(cmd, "can not update the reference as a fast forward");
+      if (GitRepositoryManager.REF_CONFIG.equals(ctl.getRefName())) {
+        errors.put(Error.CONFIG_UPDATE, GitRepositoryManager.REF_CONFIG);
+      } else {
+        errors.put(Error.UPDATE, ctl.getRefName());
+      }
+      reject(cmd);
     }
   }
 
@@ -674,11 +874,14 @@
   private void parseDelete(final ReceiveCommand cmd) {
     RefControl ctl = projectControl.controlForRef(cmd.getRefName());
     if (ctl.canDelete()) {
-      if (cmd.getResult() == NOT_ATTEMPTED) {
-        cmd.execute(rp);
-      }
+      batch.addCommand(cmd);
     } else {
-      reject(cmd, "can not delete references");
+      if (GitRepositoryManager.REF_CONFIG.equals(ctl.getRefName())) {
+        reject(cmd, "cannot delete project configuration");
+      } else {
+        errors.put(Error.DELETE, ctl.getRefName());
+        reject(cmd, "can not delete references");
+      }
     }
   }
 
@@ -704,9 +907,7 @@
     }
 
     if (ctl.canForceUpdate()) {
-      if (cmd.getResult() == NOT_ATTEMPTED) {
-        cmd.execute(rp);
-      }
+      batch.setAllowNonFastForwards(true).addCommand(cmd);
     } else {
       cmd.setResult(REJECTED_NONFASTFORWARD, " need '"
           + PermissionRule.FORCE_PUSH + "' privilege.");
@@ -777,7 +978,8 @@
         destBranchName.substring(0, split));
     destBranchCtl = projectControl.controlForRef(destBranch);
     if (!destBranchCtl.canUpload()) {
-      reject(cmd, "can not upload a change to this reference");
+      errors.put(Error.CODE_REVIEW, cmd.getRefName());
+      reject(cmd, "can not upload review");
       return;
     }
 
@@ -901,29 +1103,25 @@
     return true;
   }
 
-  private void createNewChanges() {
-    final List<RevCommit> toCreate = new ArrayList<RevCommit>();
+  private List<CreateRequest> selectNewChanges() {
+    final List<CreateRequest> newChanges = Lists.newArrayList();
     final RevWalk walk = rp.getRevWalk();
     walk.reset();
     walk.sort(RevSort.TOPO);
     walk.sort(RevSort.REVERSE, true);
     try {
+      Set<ObjectId> existing = Sets.newHashSet();
       walk.markStart(walk.parseCommit(newChange.getNewId()));
-      for (ObjectId id : existingObjects()) {
-        try {
-          walk.markUninteresting(walk.parseCommit(id));
-        } catch (IOException e) {
-          continue;
-        }
-      }
+      markHeadsAsUninteresting(walk, existing);
 
+      List<ChangeLookup> pending = Lists.newArrayList();
       final Set<Change.Key> newChangeIds = new HashSet<Change.Key>();
       for (;;) {
         final RevCommit c = walk.next();
         if (c == null) {
           break;
         }
-        if (replaceByCommit.containsKey(c)) {
+        if (existing.contains(c) || replaceByCommit.containsKey(c)) {
           // This commit was already scheduled to replace an existing PatchSet.
           //
           continue;
@@ -931,59 +1129,63 @@
         if (!validCommit(destBranchCtl, newChange, c)) {
           // Not a change the user can propose? Abort as early as possible.
           //
-          return;
+          return Collections.emptyList();
         }
 
+        Change.Key changeKey = new Change.Key("I" + c.name());
         final List<String> idList = c.getFooterLines(CHANGE_ID);
-        if (!idList.isEmpty()) {
-          final String idStr = idList.get(idList.size() - 1).trim();
-          if (idStr.matches("^I00*$")) {
-            // Reject this invalid line from EGit.
-            reject(newChange, "invalid Change-Id");
-            return;
-          }
+        if (idList.isEmpty()) {
+          newChanges.add(new CreateRequest(c, changeKey));
+          continue;
+        }
 
-          final Change.Key key = new Change.Key(idStr);
+        final String idStr = idList.get(idList.size() - 1).trim();
+        if (idStr.matches("^I00*$")) {
+          // Reject this invalid line from EGit.
+          reject(newChange, "invalid Change-Id");
+          return Collections.emptyList();
+        }
 
-          if (newChangeIds.contains(key)) {
-            reject(newChange, "squash commits first");
-            return;
-          }
+        changeKey = new Change.Key(idStr);
+        pending.add(new ChangeLookup(c, changeKey));
+      }
 
-          final List<Change> changes =
-              db.changes().byBranchKey(destBranch, key).toList();
-          if (changes.size() > 1) {
-            // WTF, multiple changes in this project have the same key?
-            // Since the commit is new, the user should recreate it with
-            // a different Change-Id. In practice, we should never see
-            // this error message as Change-Id should be unique.
-            //
-            reject(newChange, key.get() + " has duplicates");
-            return;
+      for (ChangeLookup p : pending) {
+        if (newChangeIds.contains(p.changeKey)) {
+          reject(newChange, "squash commits first");
+          return Collections.emptyList();
+        }
 
-          }
+        List<Change> changes = p.changes.toList();
+        if (changes.size() > 1) {
+          // WTF, multiple changes in this project have the same key?
+          // Since the commit is new, the user should recreate it with
+          // a different Change-Id. In practice, we should never see
+          // this error message as Change-Id should be unique.
+          //
+          reject(newChange, p.changeKey.get() + " has duplicates");
+          return Collections.emptyList();
+        }
 
-          if (changes.size() == 1) {
-            // Schedule as a replacement to this one matching change.
-            //
-            if (requestReplace(newChange, false, changes.get(0), c)) {
-              continue;
-            } else {
-              return;
-            }
-          }
-
-          if (changes.size() == 0) {
-            if (!isValidChangeId(idStr)) {
-              reject(newChange, "invalid Change-Id");
-              return;
-            }
-
-            newChangeIds.add(key);
+        if (changes.size() == 1) {
+          // Schedule as a replacement to this one matching change.
+          //
+          if (requestReplace(newChange, false, changes.get(0), p.commit)) {
+            continue;
+          } else {
+            return Collections.emptyList();
           }
         }
 
-        toCreate.add(c);
+        if (changes.size() == 0) {
+          if (!isValidChangeId(p.changeKey.get())) {
+            reject(newChange, "invalid Change-Id");
+            return Collections.emptyList();
+          }
+
+          newChangeIds.add(p.changeKey);
+        }
+        newChanges.add(new CreateRequest(p.commit, p.changeKey));
       }
     } catch (IOException e) {
       // Should never happen, the core receive process would have
@@ -991,159 +1193,158 @@
       //
       newChange.setResult(REJECTED_MISSING_OBJECT);
       log.error("Invalid pack upload; one or more objects weren't sent", e);
-      return;
+      return Collections.emptyList();
     } catch (OrmException e) {
       log.error("Cannot query database to locate prior changes", e);
       reject(newChange, "database error");
-      return;
+      return Collections.emptyList();
     }
 
-    if (toCreate.isEmpty() && replaceByChange.isEmpty()) {
+    if (newChanges.isEmpty() && replaceByChange.isEmpty()) {
       reject(newChange, "no new changes");
-      return;
+      return Collections.emptyList();
     }
+    for (CreateRequest create : newChanges) {
+      batch.addCommand(create.cmd);
+    }
+    return newChanges;
+  }
 
-    for (final RevCommit c : toCreate) {
-      try {
-        createChange(walk, c);
-      } catch (IOException e) {
-        log.error("Error computing patch of commit " + c.name(), e);
-        reject(newChange, "diff error");
-        return;
-      } catch (OrmException e) {
-        log.error("Error creating change for commit " + c.name(), e);
-        reject(newChange, "database error");
-        return;
+
+  private void markHeadsAsUninteresting(final RevWalk walk, Set<ObjectId> existing) {
+    for (Ref ref : repo.getAllRefs().values()) {
+      if (ref.getObjectId() == null) {
+        continue;
+      } else if (ref.getName().startsWith("refs/changes/")) {
+        existing.add(ref.getObjectId());
+      } else if (ref.getName().startsWith(R_HEADS)
+          || (destBranchCtl != null && ref.getName().equals(destBranchCtl.getRefName()))) {
+        try {
+          walk.markUninteresting(walk.parseCommit(ref.getObjectId()));
+        } catch (IOException e) {
+          log.warn(String.format("Invalid ref %s in %s",
+              ref.getName(), project.getName()), e);
+          continue;
+        }
       }
-      newProgress.update(1);
     }
-    newChange.setResult(OK);
   }
 
   private static boolean isValidChangeId(String idStr) {
     return idStr.matches("^I[0-9a-fA-F]{40}$") && !idStr.matches("^I00*$");
   }
 
-  private void createChange(final RevWalk walk, final RevCommit c)
-      throws OrmException, IOException {
-    walk.parseBody(c);
-    warnMalformedMessage(c);
+  private class ChangeLookup {
+    final RevCommit commit;
+    final Change.Key changeKey;
+    final ResultSet<Change> changes;
 
-    final Account.Id me = currentUser.getAccountId();
-    Change.Key changeKey = new Change.Key("I" + c.name());
-    final Set<Account.Id> reviewers = new HashSet<Account.Id>(reviewerId);
-    final Set<Account.Id> cc = new HashSet<Account.Id>(ccId);
-    final List<FooterLine> footerLines = c.getFooterLines();
-    for (final FooterLine footerLine : footerLines) {
-      try {
-        if (footerLine.matches(CHANGE_ID)) {
-          final String v = footerLine.getValue().trim();
-          if (isValidChangeId(v)) {
-            changeKey = new Change.Key(v);
-          }
-        } else if (isReviewer(footerLine)) {
-          reviewers.add(toAccountId(footerLine.getValue().trim()));
-        } else if (footerLine.matches(FooterKey.CC)) {
-          cc.add(toAccountId(footerLine.getValue().trim()));
-        }
-      } catch (NoSuchAccountException e) {
-        continue;
-      }
+    ChangeLookup(RevCommit c, Change.Key key) throws OrmException {
+      commit = c;
+      changeKey = key;
+      changes = db.changes().byBranchKey(destBranch, key);
     }
-    reviewers.remove(me);
-    cc.remove(me);
-    cc.removeAll(reviewers);
+  }
 
+  private class CreateRequest {
+    final RevCommit commit;
     final Change change;
     final PatchSet ps;
-    final PatchSetInfo info;
+    final ReceiveCommand cmd;
+    private final PatchSetInfo info;
+    boolean created;
 
-    change = new Change(changeKey, new Change.Id(db.nextChangeId()), me, destBranch);
-    change.setTopic(destTopicName);
-    change.nextPatchSetId();
+    CreateRequest(RevCommit c, Change.Key changeKey) throws OrmException {
+      commit = c;
 
-    db.changes().beginTransaction(change.getId());
-    try {
+      change = new Change(changeKey,
+          new Change.Id(db.nextChangeId()),
+          currentUser.getAccountId(),
+          destBranch);
+      change.setTopic(destTopicName);
+      change.nextPatchSetId();
+
       ps = new PatchSet(change.currPatchSetId());
       ps.setCreatedOn(change.getCreatedOn());
-      ps.setUploader(me);
+      ps.setUploader(change.getOwner());
       ps.setRevision(toRevId(c));
+
       if (MagicBranch.isDraft(newChange.getRefName())) {
         change.setStatus(Change.Status.DRAFT);
         ps.setDraft(true);
       }
-      insertAncestors(ps.getId(), c);
-      db.patchSets().insert(Collections.singleton(ps));
 
       info = patchSetInfoFactory.get(c, ps.getId());
       change.setCurrentPatchSet(info);
       ChangeUtil.updated(change);
-      db.changes().insert(Collections.singleton(change));
-      ChangeUtil.updateTrackingIds(db, change, trackingFooters, footerLines);
+      cmd = new ReceiveCommand(ObjectId.zeroId(), c, ps.getRefName());
+    }
 
-      final Set<Account.Id> haveApprovals = new HashSet<Account.Id>();
-      final List<ApprovalType> allTypes = approvalTypes.getApprovalTypes();
-      haveApprovals.add(me);
+    void insertChange() throws IOException, OrmException {
+      rp.getRevWalk().parseBody(commit);
+      warnMalformedMessage(commit);
 
-      if (allTypes.size() > 0) {
-        final Account.Id authorId =
-            info.getAuthor() != null ? info.getAuthor().getAccount() : null;
-        final Account.Id committerId =
-            info.getCommitter() != null ? info.getCommitter().getAccount() : null;
-        final ApprovalCategory.Id catId =
-            allTypes.get(allTypes.size() - 1).getCategory().getId();
-        if (authorId != null && haveApprovals.add(authorId)) {
-          insertDummyApproval(change, ps.getId(), authorId, catId, db);
+      final Account.Id me = currentUser.getAccountId();
+      final Set<Account.Id> reviewers = new HashSet<Account.Id>(reviewerId);
+      final Set<Account.Id> cc = new HashSet<Account.Id>(ccId);
+      final List<FooterLine> footerLines = commit.getFooterLines();
+      for (final FooterLine footerLine : footerLines) {
+        try {
+          if (ps.isDraft()) {
+            continue;
+          }
+          if (isReviewer(footerLine)) {
+            reviewers.add(toAccountId(footerLine.getValue().trim()));
+          } else if (footerLine.matches(FooterKey.CC)) {
+            cc.add(toAccountId(footerLine.getValue().trim()));
+          }
+        } catch (NoSuchAccountException e) {
+          continue;
         }
-        if (committerId != null && haveApprovals.add(committerId)) {
-          insertDummyApproval(change, ps.getId(), committerId, catId, db);
-        }
-        for (final Account.Id reviewer : reviewers) {
-          if (haveApprovals.add(reviewer)) {
-            insertDummyApproval(change, ps.getId(), reviewer, catId, db);
+      }
+      reviewers.remove(me);
+      cc.remove(me);
+      cc.removeAll(reviewers);
+
+      db.changes().beginTransaction(change.getId());
+      try {
+        insertAncestors(ps.getId(), commit);
+        db.patchSets().insert(Collections.singleton(ps));
+        db.changes().insert(Collections.singleton(change));
+        ChangeUtil.updateTrackingIds(db, change, trackingFooters, footerLines);
+        approvalsUtil.addReviewers(change, ps, info, reviewers);
+        db.commit();
+      } finally {
+        db.rollback();
+      }
+
+      created = true;
+      replication.fire(project.getNameKey(), ps.getRefName());
+      hooks.doPatchsetCreatedHook(change, ps, db);
+      newProgress.update(1);
+      workQueue.getDefaultQueue()
+          .submit(requestScopePropagator.wrap(new Runnable() {
+        @Override
+        public void run() {
+          try {
+            CreateChangeSender cm =
+                createChangeSenderFactory.create(change);
+            cm.setFrom(me);
+            cm.setPatchSet(ps, info);
+            cm.addReviewers(reviewers);
+            cm.addExtraCC(cc);
+            cm.send();
+          } catch (Exception e) {
+            log.error("Cannot send email for new change " + change.getId(), e);
           }
         }
-      }
-      db.commit();
-    } finally {
-      db.rollback();
-    }
 
-    final RefUpdate ru = repo.updateRef(ps.getRefName());
-    ru.setNewObjectId(c);
-    ru.disableRefLog();
-    if (ru.update(walk) != RefUpdate.Result.NEW) {
-      throw new IOException("Failed to create ref " + ps.getRefName() + " in "
-          + repo.getDirectory() + ": " + ru.getResult());
-    }
-    replication.scheduleUpdate(project.getNameKey(), ru.getName());
-
-    allNewChanges.add(change);
-
-    workQueue.getDefaultQueue()
-        .submit(requestScopePropagator.wrap(new Runnable() {
-      @Override
-      public void run() {
-        try {
-          final CreateChangeSender cm;
-          cm = createChangeSenderFactory.create(change);
-          cm.setFrom(me);
-          cm.setPatchSet(ps, info);
-          cm.addReviewers(reviewers);
-          cm.addExtraCC(cc);
-          cm.send();
-        } catch (Exception e) {
-          log.error("Cannot send email for new change " + change.getId(), e);
+        @Override
+        public String toString() {
+          return "send-email newchange";
         }
-      }
-
-      @Override
-      public String toString() {
-        return "send-email newchange";
-      }
-    }));
-
-    hooks.doPatchsetCreatedHook(change, ps, db);
+      }));
+    }
   }
 
   private static boolean isReviewer(final FooterLine candidateFooterLine) {
@@ -1153,349 +1354,385 @@
         || candidateFooterLine.matches(TESTED_BY);
   }
 
-  private void doReplaces() {
-    for (final ReplaceRequest request : replaceByChange.values()) {
-      try {
-        doReplace(request, false);
-        replaceProgress.update(1);
-      } catch (IOException err) {
-        log.error("Error computing replacement patch for change "
-            + request.ontoChange + ", commit " + request.newCommit.name(), err);
-        reject(request.cmd, "diff error");
-      } catch (OrmException err) {
-        log.error("Error storing replacement patch for change "
-            + request.ontoChange + ", commit " + request.newCommit.name(), err);
-        reject(request.cmd, "database error");
+  private void preparePatchSetsForReplace() {
+    try {
+      readChangesForReplace();
+      readPatchSetsForReplace();
+
+      for (ReplaceRequest req : replaceByChange.values()) {
+        if (req.inputCommand.getResult() == NOT_ATTEMPTED) {
+          req.validate(false);
+        }
       }
-      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.");
-        reject(request.cmd, "internal error");
+    } catch (OrmException err) {
+      log.error("Cannot read database before replacement", err);
+      for (ReplaceRequest req : replaceByChange.values()) {
+        if (req.inputCommand.getResult() == NOT_ATTEMPTED) {
+          req.inputCommand.setResult(REJECTED_OTHER_REASON, "internal server error");
+        }
+      }
+    } catch (IOException err) {
+      log.error("Cannot read repository before replacement", err);
+      for (ReplaceRequest req : replaceByChange.values()) {
+        if (req.inputCommand.getResult() == NOT_ATTEMPTED) {
+          req.inputCommand.setResult(REJECTED_OTHER_REASON, "internal server error");
+        }
+      }
+    }
+
+    for (ReplaceRequest req : replaceByChange.values()) {
+      if (req.inputCommand.getResult() == NOT_ATTEMPTED && req.cmd != null) {
+        batch.addCommand(req.cmd);
+      }
+    }
+
+    if (newChange != null && newChange.getResult() != NOT_ATTEMPTED) {
+      // Cancel creations tied to refs/for/ or refs/drafts/ command.
+      for (ReplaceRequest req : replaceByChange.values()) {
+        if (req.inputCommand == newChange && req.cmd != null) {
+          req.cmd.setResult(Result.REJECTED_OTHER_REASON, "aborted");
+        }
+      }
+      for (CreateRequest req : newChanges) {
+        req.cmd.setResult(Result.REJECTED_OTHER_REASON, "aborted");
       }
     }
   }
 
-  private PatchSet.Id doReplace(final ReplaceRequest request, boolean ignoreNoChanges)
-      throws IOException, OrmException {
-    final RevCommit c = request.newCommit;
-    rp.getRevWalk().parseBody(c);
-    warnMalformedMessage(c);
-
-    final Account.Id me = currentUser.getAccountId();
-    final Set<Account.Id> reviewers = new HashSet<Account.Id>(reviewerId);
-    final Set<Account.Id> cc = new HashSet<Account.Id>(ccId);
-    final List<FooterLine> footerLines = c.getFooterLines();
-    for (final FooterLine footerLine : footerLines) {
-      try {
-        if (isReviewer(footerLine)) {
-          reviewers.add(toAccountId(footerLine.getValue().trim()));
-        } else if (footerLine.matches(FooterKey.CC)) {
-          cc.add(toAccountId(footerLine.getValue().trim()));
-        }
-      } catch (NoSuchAccountException e) {
-        continue;
+  private void readChangesForReplace() throws OrmException {
+    List<CheckedFuture<Change, OrmException>> futures =
+        Lists.newArrayListWithCapacity(replaceByChange.size());
+    for (ReplaceRequest request : replaceByChange.values()) {
+      futures.add(db.changes().getAsync(request.ontoChange));
+    }
+    for (CheckedFuture<Change, OrmException> f : futures) {
+      Change c = f.checkedGet();
+      if (c != null) {
+        replaceByChange.get(c.getId()).change = c;
       }
     }
-    reviewers.remove(me);
-    cc.remove(me);
-    cc.removeAll(reviewers);
+  }
 
-    final ReplaceResult result = new ReplaceResult();
-    final Set<Account.Id> oldReviewers = new HashSet<Account.Id>();
-    final Set<Account.Id> oldCC = new HashSet<Account.Id>();
-
-    Change change = db.changes().get(request.ontoChange);
-    if (change == null) {
-      reject(request.cmd, "change " + request.ontoChange + " not found");
-      return null;
+  private void readPatchSetsForReplace() throws OrmException {
+    Map<Change.Id, ResultSet<PatchSet>> results = Maps.newHashMap();
+    for (ReplaceRequest request : replaceByChange.values()) {
+      Change.Id id = request.ontoChange;
+      results.put(id, db.patchSets().byChange(id));
     }
-    if (change.getStatus().isClosed()) {
-      reject(request.cmd, "change " + request.ontoChange + " closed");
-      return null;
+    for (ReplaceRequest req : replaceByChange.values()) {
+      req.patchSets = results.get(req.ontoChange).toList();
+    }
+  }
+
+  private class ReplaceRequest {
+    final Change.Id ontoChange;
+    final RevCommit newCommit;
+    final ReceiveCommand inputCommand;
+    final boolean checkMergedInto;
+    Change change;
+    ChangeControl changeCtl;
+    List<PatchSet> patchSets;
+    PatchSet newPatchSet;
+    ReceiveCommand cmd;
+    PatchSetInfo info;
+    ChangeMessage msg;
+    String mergedIntoRef;
+    private PatchSet.Id priorPatchSet;
+
+    ReplaceRequest(final Change.Id toChange, final RevCommit newCommit,
+        final ReceiveCommand cmd, final boolean checkMergedInto) {
+      this.ontoChange = toChange;
+      this.newCommit = newCommit;
+      this.inputCommand = cmd;
+      this.checkMergedInto = checkMergedInto;
     }
 
-    final ChangeControl changeCtl = projectControl.controlFor(change);
-    if (!changeCtl.canAddPatchSet()) {
-      reject(request.cmd, "cannot replace " + request.ontoChange);
-      return null;
-    }
-    if (!validCommit(changeCtl.getRefControl(), request.cmd, c)) {
-      return null;
-    }
-
-    final PatchSet.Id priorPatchSet = change.currentPatchSetId();
-    for (final PatchSet ps : db.patchSets().byChange(request.ontoChange)) {
-      if (ps.getRevision() == null) {
-        log.warn("Patch set " + ps.getId() + " has no revision");
-        reject(request.cmd, "change state corrupt");
-        return null;
+    boolean validate(boolean autoClose) throws IOException {
+      if (!autoClose && inputCommand.getResult() != NOT_ATTEMPTED) {
+        return false;
       }
 
-      final String revIdStr = ps.getRevision().get();
-      final ObjectId commitId;
-      try {
-        commitId = ObjectId.fromString(revIdStr);
-      } catch (IllegalArgumentException e) {
-        log.warn("Invalid revision in " + ps.getId() + ": " + revIdStr);
-        reject(request.cmd, "change state corrupt");
-        return null;
+      if (change == null || patchSets == null) {
+        reject(inputCommand, "change " + ontoChange + " not found");
+        return false;
       }
 
-      try {
-        final RevCommit prior = rp.getRevWalk().parseCommit(commitId);
+      if (change.getStatus().isClosed()) {
+        reject(inputCommand, "change " + ontoChange + " closed");
+        return false;
+      }
 
-        // Don't allow a change to directly depend upon itself. This is a
-        // very common error due to users making a new commit rather than
-        // amending when trying to address review comments.
-        //
-        if (rp.getRevWalk().isMergedInto(prior, c)) {
-          reject(request.cmd, "squash commits first");
-          return null;
+      changeCtl = projectControl.controlFor(change);
+      if (!changeCtl.canAddPatchSet()) {
+        reject(inputCommand, "cannot replace " + ontoChange);
+        return false;
+      }
+
+      rp.getRevWalk().parseBody(newCommit);
+      if (!validCommit(changeCtl.getRefControl(), inputCommand, newCommit)) {
+        return false;
+      }
+
+      priorPatchSet = change.currentPatchSetId();
+      for (final PatchSet ps : patchSets) {
+        if (ps.getRevision() == null) {
+          log.warn("Patch set " + ps.getId() + " has no revision");
+          reject(inputCommand, "change state corrupt");
+          return false;
         }
 
-        // Don't allow the same commit to appear twice on the same change
-        //
-        if (c == prior) {
-          reject(request.cmd, "commit already exists");
-          return null;
+        final String revIdStr = ps.getRevision().get();
+        final ObjectId commitId;
+        try {
+          commitId = ObjectId.fromString(revIdStr);
+        } catch (IllegalArgumentException e) {
+          log.warn("Invalid revision in " + ps.getId() + ": " + revIdStr);
+          reject(inputCommand, "change state corrupt");
+          return false;
         }
 
-        // Don't allow the same tree if the commit message is unmodified
-        // or no parents were updated (rebase), else warn that only part
-        // of the commit was modified.
-        //
-        if (priorPatchSet.equals(ps.getId()) && c.getTree() == prior.getTree()) {
-          rp.getRevWalk().parseBody(prior);
-          final boolean messageEq =
-              eq(c.getFullMessage(), prior.getFullMessage());
-          final boolean parentsEq = parentsEqual(c, prior);
-          final boolean authorEq = authorEqual(c, prior);
+        try {
+          final RevCommit prior = rp.getRevWalk().parseCommit(commitId);
 
-          if (messageEq && parentsEq && authorEq && !ignoreNoChanges) {
-            reject(request.cmd, "no changes made");
-            return null;
-          } else {
-            ObjectReader reader = rp.getRevWalk().getObjectReader();
-            StringBuilder msg = new StringBuilder();
-            msg.append("(W) ");
-            msg.append(reader.abbreviate(c).name());
-            msg.append(":");
-            msg.append(" no files changed");
-            if (!authorEq) {
-              msg.append(", author changed");
-            }
-            if (!messageEq) {
-              msg.append(", message updated");
-            }
-            if (!parentsEq) {
-              msg.append(", was rebased");
-            }
-            addMessage(msg.toString());
+          // Don't allow a change to directly depend upon itself. This is a
+          // very common error due to users making a new commit rather than
+          // amending when trying to address review comments.
+          //
+          if (rp.getRevWalk().isMergedInto(prior, newCommit)) {
+            reject(inputCommand, "squash commits first");
+            return false;
           }
-        }
-      } catch (IOException e) {
-        log.error("Change " + change.getId() + " missing " + revIdStr, e);
-        reject(request.cmd, "change state corrupt");
-        return null;
-      }
-    }
 
-    final PatchSet ps;
-    final ChangeMessage msg;
-    db.changes().beginTransaction(change.getId());
-    try {
-      change =
-        db.changes().atomicUpdate(change.getId(), new AtomicUpdate<Change>() {
-          @Override
-          public Change update(Change change) {
-            if (change.getStatus().isOpen()) {
-              change.nextPatchSetId();
-              change.setLastSha1MergeTested(null);
-              return change;
+          // Don't allow the same commit to appear twice on the same change
+          //
+          if (newCommit == prior) {
+            reject(inputCommand, "commit already exists");
+            return false;
+          }
+
+          // Don't allow the same tree if the commit message is unmodified
+          // or no parents were updated (rebase), else warn that only part
+          // of the commit was modified.
+          //
+          if (priorPatchSet.equals(ps.getId()) && newCommit.getTree() == prior.getTree()) {
+            rp.getRevWalk().parseBody(prior);
+            final boolean messageEq =
+                eq(newCommit.getFullMessage(), prior.getFullMessage());
+            final boolean parentsEq = parentsEqual(newCommit, prior);
+            final boolean authorEq = authorEqual(newCommit, prior);
+
+            if (messageEq && parentsEq && authorEq && !autoClose) {
+              reject(inputCommand, "no changes made");
+              return false;
             } else {
-              return null;
+              ObjectReader reader = rp.getRevWalk().getObjectReader();
+              StringBuilder msg = new StringBuilder();
+              msg.append("(W) ");
+              msg.append(reader.abbreviate(newCommit).name());
+              msg.append(":");
+              msg.append(" no files changed");
+              if (!authorEq) {
+                msg.append(", author changed");
+              }
+              if (!messageEq) {
+                msg.append(", message updated");
+              }
+              if (!parentsEq) {
+                msg.append(", was rebased");
+              }
+              addMessage(msg.toString());
             }
           }
-        });
-      if (change == null) {
-        reject(request.cmd, "change is closed");
-        return null;
-      }
-
-      ps = new PatchSet(change.currPatchSetId());
-      ps.setCreatedOn(new Timestamp(System.currentTimeMillis()));
-      ps.setUploader(currentUser.getAccountId());
-      ps.setRevision(toRevId(c));
-      if (MagicBranch.isDraft(request.cmd.getRefName())) {
-        ps.setDraft(true);
-      }
-      insertAncestors(ps.getId(), c);
-      db.patchSets().insert(Collections.singleton(ps));
-
-      if (request.checkMergedInto) {
-        final Ref mergedInto = findMergedInto(change.getDest().get(), c);
-        result.mergedIntoRef = mergedInto != null ? mergedInto.getName() : null;
-      }
-      final PatchSetInfo info = patchSetInfoFactory.get(c, ps.getId());
-      change.setCurrentPatchSet(info);
-      result.change = change;
-      result.patchSet = ps;
-      result.info = info;
-
-      final Account.Id authorId =
-          result.info.getAuthor() != null ? result.info.getAuthor().getAccount()
-              : null;
-      final Account.Id committerId =
-          result.info.getCommitter() != null ? result.info.getCommitter()
-              .getAccount() : null;
-
-      boolean haveAuthor = false;
-      boolean haveCommitter = false;
-      final Set<Account.Id> haveApprovals = new HashSet<Account.Id>();
-
-      oldReviewers.clear();
-      oldCC.clear();
-
-      for (PatchSetApproval a : db.patchSetApprovals().byChange(change.getId())) {
-        haveApprovals.add(a.getAccountId());
-
-        if (a.getValue() != 0) {
-          oldReviewers.add(a.getAccountId());
-        } else {
-          oldCC.add(a.getAccountId());
+        } catch (IOException e) {
+          log.error("Change " + change.getId() + " missing " + revIdStr, e);
+          reject(inputCommand, "change state corrupt");
+          return false;
         }
+      }
 
-        // ApprovalCategory.SUBMIT is still in db but not relevant in git-store
-        if (!ApprovalCategory.SUBMIT.equals(a.getCategoryId())) {
-          final ApprovalType type =
-            approvalTypes.byId(a.getCategoryId());
-          if (a.getPatchSetId().equals(priorPatchSet)
-              && type.getCategory().isCopyMinScore() && type.isMaxNegative(a)) {
-            // If there was a negative vote on the prior patch set, carry it
-            // into this patch set.
-            //
-            db.patchSetApprovals().insert(
-                Collections.singleton(new PatchSetApproval(ps.getId(), a)));
+      change.nextPatchSetId();
+      newPatchSet = new PatchSet(change.currPatchSetId());
+      newPatchSet.setCreatedOn(new Timestamp(System.currentTimeMillis()));
+      newPatchSet.setUploader(currentUser.getAccountId());
+      newPatchSet.setRevision(toRevId(newCommit));
+      if (newChange != null && MagicBranch.isDraft(newChange.getRefName())) {
+        newPatchSet.setDraft(true);
+      }
+      info = patchSetInfoFactory.get(newCommit, newPatchSet.getId());
+      cmd = new ReceiveCommand(
+          ObjectId.zeroId(),
+          newCommit,
+          newPatchSet.getRefName());
+      return true;
+    }
+
+    PatchSet.Id insertPatchSet() throws IOException, OrmException {
+      rp.getRevWalk().parseBody(newCommit);
+      warnMalformedMessage(newCommit);
+
+      final Account.Id me = currentUser.getAccountId();
+      final Set<Account.Id> reviewers = new HashSet<Account.Id>(reviewerId);
+      final Set<Account.Id> cc = new HashSet<Account.Id>(ccId);
+      final List<FooterLine> footerLines = newCommit.getFooterLines();
+      for (final FooterLine footerLine : footerLines) {
+        try {
+          if (isReviewer(footerLine)) {
+            reviewers.add(toAccountId(footerLine.getValue().trim()));
+          } else if (footerLine.matches(FooterKey.CC)) {
+            cc.add(toAccountId(footerLine.getValue().trim()));
           }
-        }
-
-        if (!haveAuthor && authorId != null && a.getAccountId().equals(authorId)) {
-          haveAuthor = true;
-        }
-        if (!haveCommitter && committerId != null
-            && a.getAccountId().equals(committerId)) {
-          haveCommitter = true;
+        } catch (NoSuchAccountException e) {
+          continue;
         }
       }
+      reviewers.remove(me);
+      cc.remove(me);
+      cc.removeAll(reviewers);
 
-      final List<ApprovalType> allTypes = approvalTypes.getApprovalTypes();
-      if (allTypes.size() > 0) {
-        final ApprovalCategory.Id catId =
-            allTypes.get(allTypes.size() - 1).getCategory().getId();
-        if (authorId != null && haveApprovals.add(authorId)) {
-          insertDummyApproval(result, authorId, catId, db);
-        }
-        if (committerId != null && haveApprovals.add(committerId)) {
-          insertDummyApproval(result, committerId, catId, db);
-        }
-        for (final Account.Id reviewer : reviewers) {
-          if (haveApprovals.add(reviewer)) {
-            insertDummyApproval(result, reviewer, catId, db);
-          }
-        }
-      }
+      final Set<Account.Id> oldReviewers = new HashSet<Account.Id>();
+      final Set<Account.Id> oldCC = new HashSet<Account.Id>();
 
-      msg =
-          new ChangeMessage(new ChangeMessage.Key(change.getId(), ChangeUtil
-              .messageUUID(db)), me, ps.getCreatedOn(), ps.getId());
-      msg.setMessage("Uploaded patch set " + ps.getPatchSetId() + ".");
-      db.changeMessages().insert(Collections.singleton(msg));
-      ChangeUtil.updateTrackingIds(db, change, trackingFooters, footerLines);
-      result.msg = msg;
-
-      if (result.mergedIntoRef == null) {
-        // Change should be new, so it can go through review again.
-        //
+      db.changes().beginTransaction(change.getId());
+      try {
         change =
-            db.changes().atomicUpdate(change.getId(), new AtomicUpdate<Change>() {
-              @Override
-              public Change update(Change change) {
-                if (change.getStatus().isOpen()) {
+          db.changes().atomicUpdate(change.getId(), new AtomicUpdate<Change>() {
+            @Override
+            public Change update(Change change) {
+              if (change.getStatus().isClosed()) {
+                return null;
+              }
+
+              change.updateNumberOfPatchSets(newPatchSet.getPatchSetId());
+              return change;
+            }
+          });
+        if (change == null) {
+          reject(inputCommand, "change is closed");
+          return null;
+        }
+
+        insertAncestors(newPatchSet.getId(), newCommit);
+        db.patchSets().insert(Collections.singleton(newPatchSet));
+
+        if (checkMergedInto) {
+          final Ref mergedInto = findMergedInto(change.getDest().get(), newCommit);
+          mergedIntoRef = mergedInto != null ? mergedInto.getName() : null;
+        }
+
+        List<PatchSetApproval> patchSetApprovals = approvalsUtil.copyVetosToLatestPatchSet(change);
+
+        final Set<Account.Id> haveApprovals = new HashSet<Account.Id>();
+        oldReviewers.clear();
+        oldCC.clear();
+
+        for (PatchSetApproval a : patchSetApprovals) {
+          haveApprovals.add(a.getAccountId());
+          if (a.getValue() != 0) {
+            oldReviewers.add(a.getAccountId());
+          } else {
+            oldCC.add(a.getAccountId());
+          }
+        }
+
+        approvalsUtil.addReviewers(change, newPatchSet, info, reviewers, haveApprovals);
+
+        msg =
+            new ChangeMessage(new ChangeMessage.Key(change.getId(), ChangeUtil
+                .messageUUID(db)), me, newPatchSet.getCreatedOn(), newPatchSet.getId());
+        msg.setMessage("Uploaded patch set " + newPatchSet.getPatchSetId() + ".");
+        db.changeMessages().insert(Collections.singleton(msg));
+        if (change.currentPatchSetId().equals(priorPatchSet)) {
+          ChangeUtil.updateTrackingIds(db, change, trackingFooters, footerLines);
+        }
+
+        if (mergedIntoRef == null) {
+          // Change should be new, so it can go through review again.
+          //
+          change =
+              db.changes().atomicUpdate(change.getId(), new AtomicUpdate<Change>() {
+                @Override
+                public Change update(Change change) {
+                  if (change.getStatus().isClosed()) {
+                    return null;
+                  }
+
+                  if (!change.currentPatchSetId().equals(priorPatchSet)) {
+                    return change;
+                  }
+
                   if (destTopicName != null) {
                     change.setTopic(destTopicName);
                   }
-                  if (change.getStatus() == Change.Status.DRAFT && ps.isDraft()) {
+                  if (change.getStatus() == Change.Status.DRAFT && newPatchSet.isDraft()) {
                     // Leave in draft status.
                   } else {
                     change.setStatus(Change.Status.NEW);
                   }
-                  change.setCurrentPatchSet(result.info);
+                  change.setLastSha1MergeTested(null);
+                  change.setCurrentPatchSet(info);
                   ChangeUtil.updated(change);
                   return change;
-                } else {
-                  return null;
                 }
-              }
-            });
-        if (change == null) {
-          db.patchSets().delete(Collections.singleton(ps));
-          db.changeMessages().delete(Collections.singleton(msg));
-          reject(request.cmd, "change is closed");
-          return null;
+              });
+          if (change == null) {
+            db.patchSets().delete(Collections.singleton(newPatchSet));
+            db.changeMessages().delete(Collections.singleton(msg));
+            reject(inputCommand, "change is closed");
+            return null;
+          }
         }
+
+        db.commit();
+      } finally {
+        db.rollback();
       }
 
-      db.commit();
-    } finally {
-      db.rollback();
-    }
+      if (mergedIntoRef != null) {
+        // Change was already submitted to a branch, close it.
+        //
+        markChangeMergedByPush(db, this);
+      }
 
-    if (result.mergedIntoRef != null) {
-      // Change was already submitted to a branch, close it.
-      //
-      markChangeMergedByPush(db, result);
-    }
-
-    final RefUpdate ru = repo.updateRef(ps.getRefName());
-    ru.setNewObjectId(c);
-    ru.disableRefLog();
-    if (ru.update(rp.getRevWalk()) != RefUpdate.Result.NEW) {
-      throw new IOException("Failed to create ref " + ps.getRefName() + " in "
-          + repo.getDirectory() + ": " + ru.getResult());
-    }
-    replication.scheduleUpdate(project.getNameKey(), ru.getName());
-    hooks.doPatchsetCreatedHook(result.change, ps, db);
-    request.cmd.setResult(OK);
-
-    workQueue.getDefaultQueue()
-        .submit(requestScopePropagator.wrap(new Runnable() {
-      @Override
-      public void run() {
-        try {
-          final ReplacePatchSetSender cm;
-          cm = replacePatchSetFactory.create(result.change);
-          cm.setFrom(me);
-          cm.setPatchSet(ps, result.info);
-          cm.setChangeMessage(result.msg);
-          cm.addReviewers(reviewers);
-          cm.addExtraCC(cc);
-          cm.addReviewers(oldReviewers);
-          cm.addExtraCC(oldCC);
-          cm.send();
-        } catch (Exception e) {
-          log.error("Cannot send email for new patch set " + ps.getId(), e);
+      if (cmd.getResult() == NOT_ATTEMPTED) {
+        cmd.execute(rp);
+      }
+      replication.fire(project.getNameKey(), newPatchSet.getRefName());
+      hooks.doPatchsetCreatedHook(change, newPatchSet, db);
+      replaceProgress.update(1);
+      if (mergedIntoRef != null) {
+        hooks.doChangeMergedHook(
+            change, currentUser.getAccount(), newPatchSet, db);
+      }
+      workQueue.getDefaultQueue()
+          .submit(requestScopePropagator.wrap(new Runnable() {
+        @Override
+        public void run() {
+          try {
+            ReplacePatchSetSender cm =
+                replacePatchSetFactory.create(change);
+            cm.setFrom(me);
+            cm.setPatchSet(newPatchSet, info);
+            cm.setChangeMessage(msg);
+            cm.addReviewers(reviewers);
+            cm.addExtraCC(cc);
+            cm.addReviewers(oldReviewers);
+            cm.addExtraCC(oldCC);
+            cm.send();
+          } catch (Exception e) {
+            log.error("Cannot send email for new patch set " + newPatchSet.getId(), e);
+          }
+          if (mergedIntoRef != null) {
+            sendMergedEmail(ReplaceRequest.this);
+          }
         }
-      }
 
-      @Override
-      public String toString() {
-        return "send-email newpatchset";
-      }
-    }));
-
-    sendMergedEmail(result);
-    return result != null ? result.info.getKey() : null;
+        @Override
+        public String toString() {
+          return "send-email newpatchset";
+        }
+      }));
+      return newPatchSet.getId();
+    }
   }
 
   static boolean parentsEqual(RevCommit a, RevCommit b) {
@@ -1534,23 +1771,6 @@
     }
   }
 
-  private void insertDummyApproval(final ReplaceResult result,
-      final Account.Id forAccount, final ApprovalCategory.Id catId,
-      final ReviewDb db) throws OrmException {
-    insertDummyApproval(result.change, result.patchSet.getId(), forAccount,
-        catId, db);
-  }
-
-  private void insertDummyApproval(final Change change, final PatchSet.Id psId,
-      final Account.Id forAccount, final ApprovalCategory.Id catId,
-      final ReviewDb db) throws OrmException {
-    final PatchSetApproval ca =
-        new PatchSetApproval(new PatchSetApproval.Key(psId, forAccount, catId),
-            (short) 0);
-    ca.cache(change);
-    db.patchSetApprovals().insert(Collections.singleton(ca));
-  }
-
   private Ref findMergedInto(final String first, final RevCommit commit) {
     try {
       final Map<String, Ref> all = repo.getAllRefs();
@@ -1578,46 +1798,32 @@
     return rw.isMergedInto(commit, rw.parseCommit(ref.getObjectId()));
   }
 
-  private static class ReplaceRequest {
-    final Change.Id ontoChange;
-    final RevCommit newCommit;
-    final ReceiveCommand cmd;
-    final boolean checkMergedInto;
-
-    ReplaceRequest(final Change.Id toChange, final RevCommit newCommit,
-        final ReceiveCommand cmd, final boolean checkMergedInto) {
-      this.ontoChange = toChange;
-      this.newCommit = newCommit;
-      this.cmd = cmd;
-      this.checkMergedInto = checkMergedInto;
-    }
-  }
-
-  private static class ReplaceResult {
-    Change change;
-    PatchSet patchSet;
-    PatchSetInfo info;
-    ChangeMessage msg;
-    String mergedIntoRef;
-  }
-
   private void validateNewCommits(RefControl ctl, ReceiveCommand cmd) {
+    if (ctl.canForgeAuthor()
+        && ctl.canForgeCommitter()
+        && ctl.canForgeGerritServerIdentity()
+        && ctl.canUploadMerges()
+        && !project.isUseSignedOffBy()
+        && Iterables.isEmpty(rejectCommits)
+        && !GitRepositoryManager.REF_CONFIG.equals(ctl.getRefName())
+        && !(MagicBranch.isMagicBranch(cmd.getRefName())
+            || NEW_PATCHSET.matcher(cmd.getRefName()).matches())) {
+      return;
+    }
+
     final RevWalk walk = rp.getRevWalk();
     walk.reset();
     walk.sort(RevSort.NONE);
     try {
+      Set<ObjectId> existing = Sets.newHashSet();
       walk.markStart(walk.parseCommit(cmd.getNewId()));
-      for (ObjectId id : existingObjects()) {
-        try {
-          walk.markUninteresting(walk.parseCommit(id));
-        } catch (IOException e) {
-          continue;
-        }
-      }
+      markHeadsAsUninteresting(walk, existing);
 
       RevCommit c;
       while ((c = walk.next()) != null) {
-        if (!validCommit(ctl, cmd, c)) {
+        if (existing.contains(c)) {
+          continue;
+        } else if (!validCommit(ctl, cmd, c)) {
           break;
         }
       }
@@ -1627,17 +1833,6 @@
     }
   }
 
-  private Collection<ObjectId> existingObjects() {
-    if (existingObjects == null) {
-      Map<String, Ref> refs = repo.getAllRefs();
-      existingObjects = new ArrayList<ObjectId>(refs.size());
-      for (Ref r : refs.values()) {
-        existingObjects.add(r.getObjectId());
-      }
-    }
-    return existingObjects;
-  }
-
   private boolean validCommit(final RefControl ctl, final ReceiveCommand cmd,
       final RevCommit c) throws MissingObjectException, IOException {
     rp.getRevWalk().parseBody(c);
@@ -1702,7 +1897,7 @@
     }
 
     final List<String> idList = c.getFooterLines(CHANGE_ID);
-    if ((MagicBranch.isMagicBranch(cmd.getRefName()) || NEW_PATCHSET.matcher(cmd.getRefName()).matches())) {
+    if (MagicBranch.isMagicBranch(cmd.getRefName()) || NEW_PATCHSET.matcher(cmd.getRefName()).matches()) {
       if (idList.isEmpty()) {
         if (project.isRequireChangeID()) {
           String errMsg = "missing Change-Id in commit message";
@@ -1874,24 +2069,31 @@
         final Ref ref = byCommit.get(c.copy());
         if (ref != null) {
           rw.parseBody(c);
-          closeChange(cmd, PatchSet.Id.fromRef(ref.getName()), c);
+          Change.Key closedChange =
+              closeChange(cmd, PatchSet.Id.fromRef(ref.getName()), c);
           closeProgress.update(1);
+          if (closedChange != null) {
+            byKey.remove(closedChange);
+          }
         }
 
         rw.parseBody(c);
         for (final String changeId : c.getFooterLines(CHANGE_ID)) {
           final Change.Id onto = byKey.get(new Change.Key(changeId.trim()));
           if (onto != null) {
-            toClose.add(new ReplaceRequest(onto, c, cmd, false));
+            final ReplaceRequest req = new ReplaceRequest(onto, c, cmd, false);
+            req.change = db.changes().get(onto);
+            req.patchSets = db.patchSets().byChange(onto).toList();
+            toClose.add(req);
             break;
           }
         }
       }
 
       for (final ReplaceRequest req : toClose) {
-        final PatchSet.Id psi = doReplace(req, true);
+        final PatchSet.Id psi = req.validate(true) ? req.insertPatchSet() : null;
         if (psi != null) {
-          closeChange(req.cmd, psi, req.newCommit);
+          closeChange(req.inputCommand, psi, req.newCommit);
           closeProgress.update(1);
         }
       }
@@ -1916,7 +2118,7 @@
     }
   }
 
-  private void closeChange(final ReceiveCommand cmd, final PatchSet.Id psi,
+  private Change.Key closeChange(final ReceiveCommand cmd, final PatchSet.Id psi,
       final RevCommit commit) throws OrmException {
     final String refName = cmd.getRefName();
     final Change.Id cid = psi.getParentKey();
@@ -1925,7 +2127,7 @@
     final PatchSet ps = db.patchSets().get(psi);
     if (change == null || ps == null) {
       log.warn(project.getName() + " " + psi + " is missing");
-      return;
+      return null;
     }
 
     if (change.getStatus() == Change.Status.MERGED ||
@@ -1934,17 +2136,19 @@
       // might just be moving from an experimental branch into
       // a more stable branch.
       //
-      return;
+      return null;
     }
 
-    final ReplaceResult result = new ReplaceResult();
+    ReplaceRequest result = new ReplaceRequest(cid, commit, cmd, false);
     result.change = change;
-    result.patchSet = ps;
+    result.newPatchSet = ps;
     result.info = patchSetInfoFactory.get(commit, psi);
     result.mergedIntoRef = refName;
-
     markChangeMergedByPush(db, result);
+    hooks.doChangeMergedHook(
+        change, currentUser.getAccount(), result.newPatchSet, db);
     sendMergedEmail(result);
+    return change.getKey();
   }
 
   private Map<ObjectId, Ref> changeRefsById() throws IOException {
@@ -1969,7 +2173,7 @@
   }
 
   private void markChangeMergedByPush(final ReviewDb db,
-      final ReplaceResult result) throws OrmException {
+      final ReplaceRequest result) throws OrmException {
     final Change change = result.change;
     final String mergedIntoRef = result.mergedIntoRef;
 
@@ -1977,7 +2181,7 @@
     change.setStatus(Change.Status.MERGED);
     ChangeUtil.updated(change);
 
-    ApprovalsUtil.syncChangeStatus(db, change);
+    approvalsUtil.syncChangeStatus(change);
 
     final StringBuilder msgBuf = new StringBuilder();
     msgBuf.append("Change has been successfully pushed");
@@ -2011,36 +2215,27 @@
     });
   }
 
-  private void sendMergedEmail(final ReplaceResult result) {
-    if (result != null && result.mergedIntoRef != null) {
-      workQueue.getDefaultQueue()
-          .submit(requestScopePropagator.wrap(new Runnable() {
-        @Override
-        public void run() {
-          try {
-            final MergedSender cm = mergedSenderFactory.create(result.change);
-            cm.setFrom(currentUser.getAccountId());
-            cm.setPatchSet(result.patchSet, result.info);
-            cm.send();
-          } catch (Exception e) {
-            final PatchSet.Id psi = result.patchSet.getId();
-            log.error("Cannot send email for submitted patch set " + psi, e);
-          }
+  private void sendMergedEmail(final ReplaceRequest result) {
+    workQueue.getDefaultQueue()
+        .submit(requestScopePropagator.wrap(new Runnable() {
+      @Override
+      public void run() {
+        try {
+          final MergedSender cm = mergedSenderFactory.create(result.change);
+          cm.setFrom(currentUser.getAccountId());
+          cm.setPatchSet(result.newPatchSet, result.info);
+          cm.send();
+        } catch (Exception e) {
+          final PatchSet.Id psi = result.newPatchSet.getId();
+          log.error("Cannot send email for submitted patch set " + psi, e);
         }
-
-        @Override
-        public String toString() {
-          return "send-email merged";
-        }
-      }));
-
-      try {
-        hooks.doChangeMergedHook(result.change, currentUser.getAccount(),
-            result.patchSet, db);
-      } catch (OrmException err) {
-        log.error("Cannot open change: " + result.change.getChangeId(), err);
       }
-    }
+
+      @Override
+      public String toString() {
+        return "send-email merged";
+      }
+    }));
   }
 
   private void insertAncestors(PatchSet.Id id, RevCommit src)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsAdvertiseRefsHook.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsAdvertiseRefsHook.java
index e87fe2b..ec8c080 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsAdvertiseRefsHook.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsAdvertiseRefsHook.java
@@ -16,7 +16,7 @@
 
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.transport.AdvertiseRefsHook;
-import org.eclipse.jgit.transport.ReceivePack;
+import org.eclipse.jgit.transport.BaseReceivePack;
 import org.eclipse.jgit.transport.UploadPack;
 
 import java.util.HashMap;
@@ -31,7 +31,7 @@
   }
 
   @Override
-  public void advertiseRefs(ReceivePack rp) {
+  public void advertiseRefs(BaseReceivePack rp) {
     Map<String, Ref> oldRefs = rp.getAdvertisedRefs();
     if (oldRefs == null) {
       oldRefs = rp.getRepository().getAllRefs();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/RenameGroupOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/RenameGroupOp.java
index f5e8fa8..7490006 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/RenameGroupOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/RenameGroupOp.java
@@ -123,11 +123,13 @@
       ref.setName(newName);
       md.getCommitBuilder().setAuthor(author);
       md.setMessage("Rename group " + oldName + " to " + newName + "\n");
-      if (config.commit(md)) {
+      try {
+        config.commit(md);
         projectCache.evict(config.getProject());
         success = true;
-
-      } else {
+      } catch (IOException e) {
+        log.error("Could not commit rename of group " + oldName + " to "
+            + newName + " in " + md.getProjectName().get(), e);
         try {
           Thread.sleep(25 /* milliseconds */);
         } catch (InterruptedException wakeUp) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplicationQueue.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplicationQueue.java
deleted file mode 100644
index f7bb9f8..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplicationQueue.java
+++ /dev/null
@@ -1,59 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.git;
-
-import com.google.gerrit.reviewdb.client.Project;
-
-/** Manages replication to other nodes. */
-public interface ReplicationQueue {
-  /** Is replication to one or more other destinations configured? */
-  boolean isEnabled();
-
-  /**
-   * Schedule a full replication for a single project.
-   * <p>
-   * All remote URLs are checked to verify the are current with regards to the
-   * local project state. If not, they are updated by pushing new refs, updating
-   * existing ones which don't match, and deleting stale refs which have been
-   * removed from the local repository.
-   *
-   * @param project identity of the project to replicate.
-   * @param urlMatch substring that must appear in a URI to support replication.
-   */
-  void scheduleFullSync(Project.NameKey project, String urlMatch);
-
-  /**
-   * Schedule update of a single ref.
-   * <p>
-   * This method automatically tries to batch together multiple requests in the
-   * same project, to take advantage of Git's native ability to update multiple
-   * refs during a single push operation.
-   *
-   * @param project identity of the project to replicate.
-   * @param ref unique name of the ref; must start with {@code refs/}.
-   */
-  void scheduleUpdate(Project.NameKey project, String ref);
-
-  /**
-   * Create new empty project at the remote sites.
-   * <p>
-   * When a new project has been created locally call this method to make sure
-   * that the project will be created at the remote sites as well.
-   *
-   * @param project of the project to be created.
-   * @param head name HEAD should point at (must be {@code refs/heads/...}).
-   */
-  void replicateNewProject(Project.NameKey project, String head);
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/RepositoryCaseMismatchException.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/RepositoryCaseMismatchException.java
index 64ba6e8..98ddf80 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/RepositoryCaseMismatchException.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/RepositoryCaseMismatchException.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.server.git;
 
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.client.Project.NameKey;
 
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/SecureCredentialsProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/SecureCredentialsProvider.java
deleted file mode 100644
index d51936a..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/SecureCredentialsProvider.java
+++ /dev/null
@@ -1,92 +0,0 @@
-// Copyright (C) 2011 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.git;
-
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
-
-import org.eclipse.jgit.errors.UnsupportedCredentialItem;
-import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.transport.CredentialItem;
-import org.eclipse.jgit.transport.CredentialsProvider;
-import org.eclipse.jgit.transport.URIish;
-
-/** Looks up a remote's password in secure.config. */
-public class SecureCredentialsProvider extends CredentialsProvider {
-  public interface Factory {
-    SecureCredentialsProvider create(String remoteName);
-  }
-
-  private final String cfgUser;
-  private final String cfgPass;
-
-  @Inject
-  SecureCredentialsProvider(@GerritServerConfig Config cfg,
-      @Assisted String remoteName) {
-    cfgUser = cfg.getString("remote", remoteName, "username");
-    cfgPass = cfg.getString("remote", remoteName, "password");
-  }
-
-  @Override
-  public boolean isInteractive() {
-    return false;
-  }
-
-  @Override
-  public boolean supports(CredentialItem... items) {
-    for (CredentialItem i : items) {
-      if (i instanceof CredentialItem.Username) {
-        continue;
-      } else if (i instanceof CredentialItem.Password) {
-        continue;
-      } else {
-        return false;
-      }
-    }
-    return true;
-  }
-
-  @Override
-  public boolean get(URIish uri, CredentialItem... items)
-      throws UnsupportedCredentialItem {
-    String username = uri.getUser();
-    if (username == null) {
-      username = cfgUser;
-    }
-    if (username == null) {
-      return false;
-    }
-
-    String password = uri.getPass();
-    if (password == null) {
-      password = cfgPass;
-    }
-    if (password == null) {
-      return false;
-    }
-
-    for (CredentialItem i : items) {
-      if (i instanceof CredentialItem.Username) {
-        ((CredentialItem.Username) i).setValue(username);
-      } else if (i instanceof CredentialItem.Password) {
-        ((CredentialItem.Password) i).setValue(password.toCharArray());
-      } else {
-        throw new UnsupportedCredentialItem(uri, i.getPromptText());
-      }
-    }
-    return true;
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
index 706ba7d..ccb91a3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
@@ -21,6 +21,7 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.util.SubmoduleSectionParser;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.SchemaFactory;
@@ -84,7 +85,7 @@
   private final Map<Change.Id, CodeReviewCommit> commits;
   private final PersonIdent myIdent;
   private final GitRepositoryManager repoManager;
-  private final ReplicationQueue replication;
+  private final GitReferenceUpdated replication;
   private final SchemaFactory<ReviewDb> schemaFactory;
   private final Set<Branch.NameKey> updatedSubscribers;
 
@@ -96,7 +97,7 @@
       @Assisted Project destProject, @Assisted List<Change> submitted,
       @Assisted final Map<Change.Id, CodeReviewCommit> commits,
       @GerritPersonIdent final PersonIdent myIdent,
-      GitRepositoryManager repoManager, ReplicationQueue replication) {
+      GitRepositoryManager repoManager, GitReferenceUpdated replication) {
     this.destBranch = destBranch;
     this.mergeTip = mergeTip;
     this.rw = rw;
@@ -118,7 +119,7 @@
       schema = schemaFactory.open();
 
       updateSubmoduleSubscriptions();
-      updateSuperProjects(destBranch, mergeTip.getId().toObjectId(), null);
+      updateSuperProjects(destBranch, rw, mergeTip.getId().toObjectId(), null);
     } catch (OrmException e) {
       throw new SubmoduleException("Cannot open database", e);
     } finally {
@@ -192,7 +193,7 @@
     }
   }
 
-  private void updateSuperProjects(final Branch.NameKey updatedBranch,
+  private void updateSuperProjects(final Branch.NameKey updatedBranch, RevWalk myRw,
       final ObjectId mergedCommit, final String msg) throws SubmoduleException {
     try {
       final List<SubmoduleSubscription> subscribers =
@@ -201,6 +202,9 @@
       if (!subscribers.isEmpty()) {
         String msgbuf = msg;
         if (msgbuf == null) {
+          // Initialize the message buffer
+          msgbuf = "";
+
           // The first updatedBranch on a cascade event of automatic
           // updates of repos is added to updatedSubscribers set so
           // if we face a situation having
@@ -236,7 +240,7 @@
             paths.put(updatedBranch, s.getPath());
 
             try {
-              updateGitlinks(s.getSuperProject(), modules, paths, msgbuf);
+              updateGitlinks(s.getSuperProject(), myRw, modules, paths, msgbuf);
             } catch (SubmoduleException e) {
               throw e;
             }
@@ -248,7 +252,7 @@
     }
   }
 
-  private void updateGitlinks(final Branch.NameKey subscriber,
+  private void updateGitlinks(final Branch.NameKey subscriber, RevWalk myRw,
       final Map<Branch.NameKey, ObjectId> modules,
       final Map<Branch.NameKey, String> paths, final String msg)
       throws SubmoduleException {
@@ -257,12 +261,13 @@
     final StringBuilder msgbuf = new StringBuilder();
     msgbuf.append("Updated " + subscriber.getParentKey().get());
     Repository pdb = null;
+    RevWalk recRw = null;
 
     try {
       boolean sameAuthorForAll = true;
 
       for (final Map.Entry<Branch.NameKey, ObjectId> me : modules.entrySet()) {
-        RevCommit c = rw.parseCommit(me.getValue());
+        RevCommit c = myRw.parseCommit(me.getValue());
 
         msgbuf.append("\nProject: ");
         msgbuf.append(me.getKey().getParentKey().get());
@@ -331,7 +336,7 @@
       switch (rfu.update()) {
         case NEW:
         case FAST_FORWARD:
-          replication.scheduleUpdate(subscriber.getParentKey(), rfu.getName());
+          replication.fire(subscriber.getParentKey(), rfu.getName());
           // TODO since this is performed "in the background" no mail will be
           // sent to inform users about the updated branch
           break;
@@ -340,12 +345,17 @@
           throw new IOException(rfu.getResult().name());
       }
 
+      recRw = new RevWalk(pdb);
+
       // Recursive call: update subscribers of the subscriber
-      updateSuperProjects(subscriber, commitId, msgbuf.toString());
+      updateSuperProjects(subscriber, recRw, commitId, msgbuf.toString());
     } catch (IOException e) {
       logAndThrowSubmoduleException("Cannot update gitlinks for "
           + subscriber.get(), e);
     } finally {
+      if (recRw != null) {
+        recRw.release();
+      }
       if (pdb != null) {
         pdb.close();
       }
@@ -355,14 +365,17 @@
   private static DirCache readTree(final Repository pdb, final Ref branch)
       throws MissingObjectException, IncorrectObjectTypeException, IOException {
     final RevWalk rw = new RevWalk(pdb);
-
-    final DirCache dc = DirCache.newInCore();
-    final DirCacheBuilder b = dc.builder();
-    b.addTree(new byte[0], // no prefix path
-        DirCacheEntry.STAGE_0, // standard stage
-        pdb.newObjectReader(), rw.parseTree(branch.getObjectId()));
-    b.finish();
-    return dc;
+    try {
+      final DirCache dc = DirCache.newInCore();
+      final DirCacheBuilder b = dc.builder();
+      b.addTree(new byte[0], // no prefix path
+          DirCacheEntry.STAGE_0, // standard stage
+          pdb.newObjectReader(), rw.parseTree(branch.getObjectId()));
+      b.finish();
+      return dc;
+    } finally {
+      rw.release();
+    }
   }
 
   private static void logAndThrowSubmoduleException(final String errorMsg,
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/VersionedMetaData.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java
index c34cc54..e9c5536 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.git;
 
+import com.google.common.base.Objects;
+
 import org.eclipse.jgit.dircache.DirCache;
 import org.eclipse.jgit.dircache.DirCacheBuilder;
 import org.eclipse.jgit.dircache.DirCacheEditor;
@@ -23,7 +25,7 @@
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.MissingObjectException;
-import org.eclipse.jgit.errors.UnmergedPathException;
+import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Constants;
@@ -144,80 +146,179 @@
    * Update this metadata branch, recording a new commit on its reference.
    *
    * @param update helper information to define the update that will occur.
-   * @return true if the update was successful, false if it failed because of a
-   *         concurrent update to the same reference.
+   * @return the commit that was created
    * @throws IOException if there is a storage problem and the update cannot be
-   *         executed as requested.
+   *         executed as requested or if it failed because of a concurrent
+   *         update to the same reference
    */
-  public boolean commit(MetaDataUpdate update) throws IOException {
-    final Repository db = update.getRepository();
-    final CommitBuilder commit = update.getCommitBuilder();
-
-    reader = db.newObjectReader();
-    inserter = db.newObjectInserter();
+  public RevCommit commit(MetaDataUpdate update) throws IOException {
+    BatchMetaDataUpdate batch = openUpdate(update);
     try {
-      final RevWalk rw = new RevWalk(reader);
-      final RevTree src = revision != null ? rw.parseTree(revision) : null;
-      final ObjectId res = writeTree(src, commit);
-
-      if (res.equals(src)) {
-        // If there are no changes to the content, don't create the commit.
-        return true;
-      }
-
-      commit.setTreeId(res);
-      if (revision != null) {
-        commit.setParentId(revision);
-      }
-
-      RefUpdate ru = db.updateRef(getRefName());
-      if (revision != null) {
-        ru.setExpectedOldObjectId(revision);
-      } else {
-        ru.setExpectedOldObjectId(ObjectId.zeroId());
-      }
-      ru.setNewObjectId(inserter.insert(commit));
-      ru.disableRefLog();
-      inserter.flush();
-
-      switch (ru.update(rw)) {
-        case NEW:
-        case FAST_FORWARD:
-          revision = rw.parseCommit(ru.getNewObjectId());
-          update.replicate(ru.getName());
-          return true;
-
-        case LOCK_FAILURE:
-          return false;
-
-        default:
-          throw new IOException("Cannot update " + ru.getName() + " in "
-              + db.getDirectory() + ": " + ru.getResult());
-      }
-    } catch (ConfigInvalidException e) {
-      throw new IOException("Cannot update " + getRefName() + " in "
-          + db.getDirectory() + ": " + e.getMessage(), e);
+      batch.write(update.getCommitBuilder());
+      return batch.commit();
     } finally {
-      inserter.release();
-      inserter = null;
-
-      reader.release();
-      reader = null;
+      batch.close();
     }
   }
 
-  private ObjectId writeTree(RevTree srcTree, CommitBuilder commit)
-      throws IOException, MissingObjectException, IncorrectObjectTypeException,
-      UnmergedPathException, ConfigInvalidException {
+  /**
+   * Creates a new commit and a new ref based on this commit.
+   *
+   * @param update helper information to define the update that will occur.
+   * @param refName name of the ref that should be created
+   * @return the commit that was created
+   * @throws IOException if there is a storage problem and the update cannot be
+   *         executed as requested or if it failed because of a concurrent
+   *         update to the same reference
+   */
+  public RevCommit commitToNewRef(MetaDataUpdate update, String refName) throws IOException {
+    BatchMetaDataUpdate batch = openUpdate(update);
     try {
-      newTree = readTree(srcTree);
-      onSave(commit);
-      return newTree.writeTree(inserter);
+      batch.write(update.getCommitBuilder());
+      return batch.createRef(refName);
     } finally {
-      newTree = null;
+      batch.close();
     }
   }
 
+  public interface BatchMetaDataUpdate {
+    void write(CommitBuilder commit) throws IOException;
+    void write(VersionedMetaData config, CommitBuilder commit) throws IOException;
+    RevCommit createRef(String refName) throws IOException;
+    RevCommit commit() throws IOException;
+    RevCommit commitAt(ObjectId revision) throws IOException;
+    void close();
+  }
+
+  public BatchMetaDataUpdate openUpdate(final MetaDataUpdate update) throws IOException {
+    final Repository db = update.getRepository();
+
+    reader = db.newObjectReader();
+    inserter = db.newObjectInserter();
+    final RevWalk rw = new RevWalk(reader);
+    final RevTree tree = revision != null ? rw.parseTree(revision) : null;
+    newTree = readTree(tree);
+    return new BatchMetaDataUpdate() {
+      AnyObjectId src = revision;
+      AnyObjectId srcTree = tree;
+
+      @Override
+      public void write(CommitBuilder commit) throws IOException {
+        write(VersionedMetaData.this, commit);
+      }
+
+      private void doSave(VersionedMetaData config, CommitBuilder commit) throws IOException {
+        DirCache nt = config.newTree;
+        ObjectReader r = config.reader;
+        ObjectInserter i = config.inserter;
+        try {
+          config.newTree = newTree;
+          config.reader = reader;
+          config.inserter = inserter;
+          config.onSave(commit);
+        } catch (ConfigInvalidException e) {
+          throw new IOException("Cannot update " + getRefName() + " in "
+              + db.getDirectory() + ": " + e.getMessage(), e);
+        } finally {
+          config.newTree = nt;
+          config.reader = r;
+          config.inserter = i;
+        }
+      }
+
+      @Override
+      public void write(VersionedMetaData config, CommitBuilder commit) throws IOException {
+        doSave(config, commit);
+
+        final ObjectId res = newTree.writeTree(inserter);
+        if (res.equals(srcTree)) {
+          // If there are no changes to the content, don't create the commit.
+          return;
+        }
+
+        commit.setTreeId(res);
+        if (src != null) {
+          commit.addParentId(src);
+        }
+
+        src = inserter.insert(commit);
+        srcTree = res;
+      }
+
+      @Override
+      public RevCommit createRef(String refName) throws IOException {
+        if (Objects.equal(src, revision)) {
+          return revision;
+        }
+
+        RefUpdate ru = db.updateRef(refName);
+        ru.setExpectedOldObjectId(ObjectId.zeroId());
+        ru.setNewObjectId(src);
+        ru.disableRefLog();
+        inserter.flush();
+        RefUpdate.Result result = ru.update();
+        switch (result) {
+          case NEW:
+            revision = rw.parseCommit(ru.getNewObjectId());
+            update.replicate(ru.getName());
+            return revision;
+          default:
+            throw new IOException("Cannot update " + ru.getName() + " in "
+                + db.getDirectory() + ": " + ru.getResult());
+        }
+      }
+
+      @Override
+      public RevCommit commit() throws IOException {
+        return commitAt(revision);
+      }
+
+      @Override
+      public RevCommit commitAt(ObjectId expected) throws IOException {
+        if (Objects.equal(src, expected)) {
+          return revision;
+        }
+
+        RefUpdate ru = db.updateRef(getRefName());
+        if (expected != null) {
+          ru.setExpectedOldObjectId(expected);
+        } else {
+          ru.setExpectedOldObjectId(ObjectId.zeroId());
+        }
+        ru.setNewObjectId(src);
+        ru.disableRefLog();
+        inserter.flush();
+
+        switch (ru.update(rw)) {
+          case NEW:
+          case FAST_FORWARD:
+            revision = rw.parseCommit(ru.getNewObjectId());
+            update.replicate(ru.getName());
+            return revision;
+
+          default:
+            throw new IOException("Cannot update " + ru.getName() + " in "
+                + db.getDirectory() + ": " + ru.getResult());
+        }
+      }
+
+      @Override
+      public void close() {
+        newTree = null;
+
+        if (inserter != null) {
+          inserter.release();
+          inserter = null;
+        }
+
+        if (reader != null) {
+          reader.release();
+          reader = null;
+        }
+      }
+    };
+  }
+
   private DirCache readTree(RevTree tree) throws IOException,
       MissingObjectException, IncorrectObjectTypeException {
     DirCache dc = DirCache.newInCore();
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..8d27c0e 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,12 +62,21 @@
   }
 
   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>();
 
     for (Ref ref : refs.values()) {
-      if (PatchSet.isRef(ref.getName())) {
+      if (ref.getName().startsWith(GitRepositoryManager.REFS_CACHE_AUTOMERGE)) {
+        continue;
+      } else if (PatchSet.isRef(ref.getName())) {
         // Reference to a patch set is visible if the change is visible.
         //
         if (visibleChanges.contains(Change.Id.fromRef(ref.getName()))) {
@@ -92,8 +103,10 @@
     // to identify what tags we can actually reach, and what we cannot.
     //
     if (!deferredTags.isEmpty() && (!result.isEmpty() || filterTagsSeperately)) {
-      TagMatcher tags = tagCache.get(projectName).
-          matcher(db, filterTagsSeperately ? filter(db.getAllRefs()).values() : result.values());
+      TagMatcher tags = tagCache.get(projectName).matcher(
+          tagCache,
+          db,
+          filterTagsSeperately ? filter(refs).values() : result.values());
       for (Ref tag : deferredTags) {
         if (tags.isReachable(tag)) {
           result.put(tag.getName(), tag);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/WorkQueue.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/WorkQueue.java
index 987ab7c..bb11e62 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/WorkQueue.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/WorkQueue.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.git;
 
-import com.google.gerrit.lifecycle.LifecycleListener;
+import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.reviewdb.client.Project.NameKey;
 import com.google.gerrit.server.util.IdGenerator;
@@ -172,6 +172,10 @@
           );
     }
 
+    public void unregisterWorkQueue() {
+      queues.remove(this);
+    }
+
     @Override
     protected <V> RunnableScheduledFuture<V> decorateTask(
         final Runnable runnable, RunnableScheduledFuture<V> r) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ioutil/ColumnFormatter.java b/gerrit-server/src/main/java/com/google/gerrit/server/ioutil/ColumnFormatter.java
new file mode 100644
index 0000000..a73f1cb
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ioutil/ColumnFormatter.java
@@ -0,0 +1,82 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.ioutil;
+
+import com.google.gerrit.server.StringUtil;
+
+import java.io.PrintWriter;
+
+/**
+ * Simple output formatter for column-oriented data, writing its output to
+ * a {@link java.io.PrintWriter} object. Handles escaping of the column
+ * data so that the resulting output is unambiguous and reasonably safe and
+ * machine parsable.
+ */
+public class ColumnFormatter {
+  private char columnSeparator;
+  private boolean firstColumn;
+  private final PrintWriter out;
+
+  /**
+   * @param out The writer to which output should be sent.
+   * @param columnSeparator A character that should serve as the separator
+   *        token between columns of output. As only non-printable characters
+   *        in the column text are ever escaped, the column separator must be
+   *        a non-printable character if the output needs to be unambiguously
+   *        parsed.
+   */
+  public ColumnFormatter(final PrintWriter out, final char columnSeparator) {
+    this.out = out;
+    this.columnSeparator = columnSeparator;
+    this.firstColumn = true;
+  }
+
+  /**
+   * Adds a text string as a new column in the current line of output,
+   * taking care of escaping as necessary.
+   *
+   * @param content the string to add.
+   */
+  public void addColumn(final String content) {
+    if (!firstColumn) {
+      out.print(columnSeparator);
+    }
+    out.print(StringUtil.escapeString(content));
+    firstColumn = false;
+  }
+
+  /**
+   * Finishes the output by flushing the current line and takes care of any
+   * other cleanup action.
+   */
+  public void finish() {
+    nextLine();
+    out.flush();
+  }
+
+  /**
+   * Flushes the current line of output and makes the formatter ready to
+   * start receiving new column data for a new line (or end-of-file).
+   * If the current line is empty nothing is done, i.e. consecutive calls
+   * to this method without intervening calls to {@link #addColumn} will
+   * be squashed.
+   */
+  public void nextLine() {
+    if (!firstColumn) {
+      out.print('\n');
+      firstColumn = true;
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/AbandonedSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/AbandonedSender.java
index 7fabcfe1..0eb3dfe 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/AbandonedSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/AbandonedSender.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.mail;
 
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
 import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
@@ -38,7 +39,7 @@
 
     ccAllApprovals();
     bccStarredBy();
-    bccWatchesNotifyAllComments();
+    bccWatches(NotifyType.ALL_COMMENTS);
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/Address.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/Address.java
index 624e626..4e9ed2b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/Address.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/Address.java
@@ -55,6 +55,19 @@
   }
 
   @Override
+  public int hashCode() {
+    return email.hashCode();
+  }
+
+  @Override
+  public boolean equals(Object other) {
+    if (other instanceof Address) {
+      return email.equals(((Address) other).email);
+    }
+    return false;
+  }
+
+  @Override
   public String toString() {
     try {
       return toHeaderString();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java
index ff0bb578..6c33949 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java
@@ -14,39 +14,53 @@
 
 package com.google.gerrit.server.mail;
 
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.gerrit.common.data.GroupDescriptions;
+import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupInclude;
+import com.google.gerrit.reviewdb.client.AccountGroupMember;
 import com.google.gerrit.reviewdb.client.AccountProjectWatch;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.client.PatchSetInfo;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.StarredChange;
-import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.git.NotifyConfig;
 import com.google.gerrit.server.patch.PatchList;
 import com.google.gerrit.server.patch.PatchListEntry;
+import com.google.gerrit.server.patch.PatchListNotAvailableException;
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.query.Predicate;
 import com.google.gerrit.server.query.QueryParseException;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeQueryBuilder;
+import com.google.gerrit.server.query.change.SingleGroupUser;
 import com.google.gwtorm.server.OrmException;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import java.text.MessageFormat;
-import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Date;
 import java.util.HashSet;
-import java.util.List;
+import java.util.Queue;
 import java.util.Set;
 import java.util.TreeSet;
 
 /** Sends an email to one or more interested parties. */
 public abstract class ChangeEmail extends OutgoingEmail {
+  private static final Logger log = LoggerFactory.getLogger(ChangeEmail.class);
+
   protected final Change change;
   protected PatchSet patchSet;
   protected PatchSetInfo patchSetInfo;
@@ -151,7 +165,6 @@
 
   private void setListIdHeader() throws EmailException {
     // Set a reasonable list id so that filters can be used to sort messages
-    setVHeader("Mailing-List", "list $email.listId");
     setVHeader("List-Id", "<$email.listId.replace('@', '.')>");
     if (getSettingsUrl() != null) {
       setVHeader("List-Unsubscribe", "<$email.settingsUrl>");
@@ -224,42 +237,46 @@
 
   /** Create the change message and the affected file list. */
   public String getChangeDetail() {
-    StringBuilder detail = new StringBuilder();
+    try {
+      StringBuilder detail = new StringBuilder();
 
-    if (patchSetInfo != null) {
-      detail.append(patchSetInfo.getMessage().trim() + "\n");
-    } else {
-      detail.append(change.getSubject().trim() + "\n");
-    }
-
-    if (patchSet != null) {
-      detail.append("---\n");
-      PatchList patchList = getPatchList();
-      for (PatchListEntry p : patchList.getPatches()) {
-        if (Patch.COMMIT_MSG.equals(p.getNewName())) {
-          continue;
-        }
-        detail.append(p.getChangeType().getCode() + " " + p.getNewName() + "\n");
+      if (patchSetInfo != null) {
+        detail.append(patchSetInfo.getMessage().trim() + "\n");
+      } else {
+        detail.append(change.getSubject().trim() + "\n");
       }
-      detail.append(MessageFormat.format("" //
-          + "{0,choice,0#0 files|1#1 file|1<{0} files} changed, " //
-          + "{1,choice,0#0 insertions|1#1 insertion|1<{1} insertions}(+), " //
-          + "{2,choice,0#0 deletions|1#1 deletion|1<{2} deletions}(-)" //
-          + "\n", patchList.getPatches().size() - 1, //
-          patchList.getInsertions(), //
-          patchList.getDeletions()));
-      detail.append("\n");
+
+      if (patchSet != null) {
+        detail.append("---\n");
+        PatchList patchList = getPatchList();
+        for (PatchListEntry p : patchList.getPatches()) {
+          if (Patch.COMMIT_MSG.equals(p.getNewName())) {
+            continue;
+          }
+          detail.append(p.getChangeType().getCode() + " " + p.getNewName() + "\n");
+        }
+        detail.append(MessageFormat.format("" //
+            + "{0,choice,0#0 files|1#1 file|1<{0} files} changed, " //
+            + "{1,choice,0#0 insertions|1#1 insertion|1<{1} insertions}(+), " //
+            + "{2,choice,0#0 deletions|1#1 deletion|1<{2} deletions}(-)" //
+            + "\n", patchList.getPatches().size() - 1, //
+            patchList.getInsertions(), //
+            patchList.getDeletions()));
+        detail.append("\n");
+      }
+      return detail.toString();
+    } catch (Exception err) {
+      log.warn("Cannot format change detail", err);
+      return "";
     }
-    return detail.toString();
   }
 
-
   /** 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. */
@@ -295,53 +312,149 @@
       // Just don't BCC everyone. Better to send a partial message to those
       // we already have queued up then to fail deliver entirely to people
       // who have a lower interest in the change.
+      log.warn("Cannot BCC users that starred updated change", err);
     }
   }
 
-  /** BCC any user who has set "notify all comments" on this project. */
-  protected void bccWatchesNotifyAllComments() {
+  /** BCC users and groups that want notification of events. */
+  protected void bccWatches(NotifyType type) {
     try {
-      // BCC anyone else who has interest in this project's changes
-      //
-      for (final AccountProjectWatch w : getWatches()) {
-        if (w.isNotify(NotifyType.ALL_COMMENTS)) {
-          add(RecipientType.BCC, w.getAccountId());
-        }
+      Watchers matching = getWatches(type);
+      for (Account.Id user : matching.accounts) {
+        add(RecipientType.BCC, user);
+      }
+      for (Address addr : matching.emails) {
+        add(RecipientType.BCC, addr);
       }
     } catch (OrmException err) {
       // Just don't CC everyone. Better to send a partial message to those
       // we already have queued up then to fail deliver entirely to people
       // who have a lower interest in the change.
+      log.warn("Cannot BCC watchers for " + type, err);
     }
   }
 
   /** Returns all watches that are relevant */
-  protected final List<AccountProjectWatch> getWatches() throws OrmException {
+  protected final Watchers getWatches(NotifyType type) throws OrmException {
+    Watchers matching = new Watchers();
     if (changeData == null) {
-      return Collections.emptyList();
+      return matching;
     }
 
-    List<AccountProjectWatch> matching = new ArrayList<AccountProjectWatch>();
     Set<Account.Id> projectWatchers = new HashSet<Account.Id>();
 
     for (AccountProjectWatch w : args.db.get().accountProjectWatches()
         .byProject(change.getProject())) {
       projectWatchers.add(w.getAccountId());
-      add(matching, w);
-    }
-
-    for (AccountProjectWatch w : args.db.get().accountProjectWatches()
-        .byProject(args.allProjectsName)) {
-      if (!projectWatchers.contains(w.getAccountId())) {
+      if (w.isNotify(type)) {
         add(matching, w);
       }
     }
 
-    return Collections.unmodifiableList(matching);
+    for (AccountProjectWatch w : args.db.get().accountProjectWatches()
+        .byProject(args.allProjectsName)) {
+      if (!projectWatchers.contains(w.getAccountId()) && w.isNotify(type)) {
+        add(matching, w);
+      }
+    }
+
+    ProjectState state = projectState;
+    while (state != null) {
+      for (NotifyConfig nc : state.getConfig().getNotifyConfigs()) {
+        if (nc.isNotify(type)) {
+          try {
+            add(matching, nc, state.getProject().getNameKey());
+          } catch (QueryParseException e) {
+            log.warn(String.format(
+                "Project %s has invalid notify %s filter \"%s\": %s",
+                state.getProject().getName(), nc.getName(),
+                nc.getFilter(), e.getMessage()));
+          }
+        }
+      }
+      state = state.getParentState();
+    }
+
+    return matching;
+  }
+
+  protected static class Watchers {
+    protected final Set<Account.Id> accounts = Sets.newHashSet();
+    protected final Set<Address> emails = Sets.newHashSet();
   }
 
   @SuppressWarnings("unchecked")
-  private void add(List<AccountProjectWatch> matching, AccountProjectWatch w)
+  private void add(Watchers matching, NotifyConfig nc, Project.NameKey project)
+      throws OrmException, QueryParseException {
+    for (GroupReference ref : nc.getGroups()) {
+      AccountGroup group =
+          GroupDescriptions.toAccountGroup(args.groupBackend.get(ref.getUUID()));
+      if (group == null) {
+        log.warn(String.format(
+            "Project %s has invalid group %s in notify section %s",
+            project.get(), ref.getName(), nc.getName()));
+        continue;
+      }
+
+      if (group.getType() != AccountGroup.Type.INTERNAL) {
+        log.warn(String.format(
+            "Project %s cannot use group %s of type %s in notify section %s",
+            project.get(), ref.getName(), group.getType(), nc.getName()));
+        continue;
+      }
+
+      ChangeQueryBuilder qb = args.queryBuilder.create(new SingleGroupUser(
+          args.capabilityControlFactory,
+          ref.getUUID()));
+      qb.setAllowFile(true);
+      Predicate<ChangeData> p = qb.is_visible();
+      if (nc.getFilter() != null) {
+        p = Predicate.and(qb.parse(nc.getFilter()), p);
+        p = args.queryRewriter.get().rewrite(p);
+      }
+      if (p.match(changeData)) {
+        recursivelyAddAllAccounts(matching, group);
+      }
+    }
+
+    if (!nc.getAddresses().isEmpty()) {
+      if (nc.getFilter() != null) {
+        ChangeQueryBuilder qb = args.queryBuilder.create(args.anonymousUser);
+        qb.setAllowFile(true);
+        Predicate<ChangeData> p = qb.parse(nc.getFilter());
+        p = args.queryRewriter.get().rewrite(p);
+        if (p.match(changeData)) {
+          matching.emails.addAll(nc.getAddresses());
+        }
+      } else {
+        matching.emails.addAll(nc.getAddresses());
+      }
+    }
+  }
+
+  private void recursivelyAddAllAccounts(Watchers matching, AccountGroup group)
+      throws OrmException {
+    Set<AccountGroup.Id> seen = Sets.newHashSet();
+    Queue<AccountGroup.Id> scan = Lists.newLinkedList();
+    scan.add(group.getId());
+    seen.add(group.getId());
+    while (!scan.isEmpty()) {
+      AccountGroup.Id next = scan.remove();
+      for (AccountGroupMember m : args.db.get().accountGroupMembers()
+          .byGroup(next)) {
+        matching.accounts.add(m.getAccountId());
+      }
+      for (AccountGroupInclude m : args.db.get().accountGroupIncludes()
+          .byGroup(next)) {
+        if (seen.add(m.getIncludeId())) {
+          scan.add(m.getIncludeId());
+        }
+      }
+    }
+  }
+
+  @SuppressWarnings("unchecked")
+  private void add(Watchers matching, AccountProjectWatch w)
       throws OrmException {
     IdentifiedUser user =
         args.identifiedUserFactory.create(args.db, w.getAccountId());
@@ -353,13 +466,13 @@
         p = Predicate.and(qb.parse(w.getFilter()), p);
         p = args.queryRewriter.get().rewrite(p);
         if (p.match(changeData)) {
-          matching.add(w);
+          matching.accounts.add(w.getAccountId());
         }
       } catch (QueryParseException e) {
         // Ignore broken filter expressions.
       }
     } else if (p.match(changeData)) {
-      matching.add(w);
+      matching.accounts.add(w.getAccountId());
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java
index f054ee8..e7cc1ff 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java
@@ -17,13 +17,14 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
 import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.patch.PatchFile;
 import com.google.gerrit.server.patch.PatchList;
+import com.google.gerrit.server.patch.PatchListNotAvailableException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Repository;
 
 import java.io.IOException;
@@ -68,7 +69,7 @@
 
     ccAllApprovals();
     bccStarredBy();
-    bccWatchesNotifyAllComments();
+    bccWatches(NotifyType.ALL_COMMENTS);
   }
 
   @Override
@@ -77,11 +78,22 @@
   }
 
   public String getInlineComments() {
+    return getInlineComments(1);
+  }
+
+  public String getInlineComments(int lines) {
     StringBuilder  cmts = new StringBuilder();
 
     final Repository repo = getRepository();
     try {
-      final PatchList patchList = repo != null ? getPatchList() : null;
+      PatchList patchList = null;
+      if (repo != null) {
+        try {
+          patchList = getPatchList();
+        } catch (PatchListNotAvailableException e) {
+          patchList = null;
+        }
+      }
 
       Patch.Key currentFileKey = null;
       PatchFile currentFileData = null;
@@ -113,19 +125,29 @@
           }
         }
 
-        cmts.append("Line " + lineNbr);
         if (currentFileData != null) {
+          int maxLines;
           try {
-            final String lineStr = currentFileData.getLine(side, lineNbr);
-            cmts.append(": ");
-            cmts.append(lineStr);
-          } catch (Throwable cce) {
-            // Don't quote the line if we can't safely convert it.
+            maxLines = currentFileData.getLineCount(side);
+          } catch (Throwable e) {
+            maxLines = lineNbr;
+          }
+
+          final int startLine = Math.max(1, lineNbr - lines + 1);
+          final int stopLine = Math.min(maxLines, lineNbr + lines);
+
+          for (int line = startLine; line <= lineNbr; ++line) {
+            appendFileLine(cmts, currentFileData, side, line);
+          }
+
+          cmts.append(c.getMessage().trim());
+          cmts.append("\n");
+
+          for (int line = lineNbr + 1; line < stopLine; ++line) {
+            appendFileLine(cmts, currentFileData, side, line);
           }
         }
-        cmts.append("\n");
 
-        cmts.append(c.getMessage().trim());
         cmts.append("\n\n");
       }
     } finally {
@@ -136,10 +158,22 @@
     return cmts.toString();
   }
 
+  private void appendFileLine(StringBuilder cmts, PatchFile fileData, short side, int line) {
+    cmts.append("Line " + line);
+    try {
+      final String lineStr = fileData.getLine(side, line);
+      cmts.append(": ");
+      cmts.append(lineStr);
+    } catch (Throwable e) {
+      // Don't quote the line if we can't safely convert it.
+    }
+    cmts.append("\n");
+  }
+
   private Repository getRepository() {
     try {
       return args.server.openRepository(projectState.getProject().getNameKey());
-    } catch (RepositoryNotFoundException e) {
+    } catch (IOException e) {
       return null;
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java
index c6f716d..9b82c71 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CreateChangeSender.java
@@ -15,74 +15,65 @@
 package com.google.gerrit.server.mail;
 
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupMember;
-import com.google.gerrit.reviewdb.client.AccountProjectWatch;
-import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
-import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.ssh.SshInfo;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
-import java.util.HashSet;
-import java.util.Set;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 /** Notify interested parties of a brand new change. */
 public class CreateChangeSender extends NewChangeSender {
+  private static final Logger log =
+      LoggerFactory.getLogger(CreateChangeSender.class);
+
   public static interface Factory {
     public CreateChangeSender create(Change change);
   }
 
-  private final GroupCache groupCache;
-
   @Inject
   public CreateChangeSender(EmailArguments ea,
       @AnonymousCowardName String anonymousCowardName, SshInfo sshInfo,
-      GroupCache groupCache, @Assisted Change c) {
+      @Assisted Change c) {
     super(ea, anonymousCowardName, sshInfo, c);
-    this.groupCache = groupCache;
   }
 
   @Override
   protected void init() throws EmailException {
     super.init();
 
-    bccWatchers();
-  }
-
-  private void bccWatchers() {
     try {
+      // BCC anyone who has interest in this project's changes
       // Try to mark interested owners with a TO and not a BCC line.
       //
-      final Set<Account.Id> owners = new HashSet<Account.Id>();
-      for (AccountGroup.UUID uuid : getProjectOwners()) {
-        AccountGroup group = groupCache.get(uuid);
-        if (group != null) {
-          for (AccountGroupMember m : args.db.get().accountGroupMembers()
-              .byGroup(group.getId())) {
-            owners.add(m.getAccountId());
-          }
+      Watchers matching = getWatches(NotifyType.NEW_CHANGES);
+      for (Account.Id user : matching.accounts) {
+        if (isOwnerOfProjectOrBranch(user)) {
+          add(RecipientType.TO, user);
+        } else {
+          add(RecipientType.BCC, user);
         }
       }
-
-      // BCC anyone who has interest in this project's changes
-      //
-      for (final AccountProjectWatch w : getWatches()) {
-        if (w.isNotify(NotifyType.NEW_CHANGES)) {
-          if (owners.contains(w.getAccountId())) {
-            add(RecipientType.TO, w.getAccountId());
-          } else {
-            add(RecipientType.BCC, w.getAccountId());
-          }
-        }
+      for (Address addr : matching.emails) {
+        add(RecipientType.BCC, addr);
       }
     } catch (OrmException err) {
       // Just don't CC everyone. Better to send a partial message to those
       // we already have queued up then to fail deliver entirely to people
       // who have a lower interest in the change.
+      log.warn("Cannot BCC watchers for new change", err);
     }
   }
+
+  private boolean isOwnerOfProjectOrBranch(Account.Id user) {
+    return projectState != null
+        && change != null
+        && projectState.controlFor(args.identifiedUserFactory.create(user))
+          .controlForRef(change.getDest())
+          .isOwner();
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java
index 68f78d0..e6eda82 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java
@@ -15,10 +15,12 @@
 package com.google.gerrit.server.mail;
 
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.IdentifiedUser.GenericFactory;
 import com.google.gerrit.server.account.AccountCache;
-import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.account.CapabilityControl;
+import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -37,13 +39,15 @@
 class EmailArguments {
   final GitRepositoryManager server;
   final ProjectCache projectCache;
-  final GroupCache groupCache;
+  final GroupBackend groupBackend;
   final AccountCache accountCache;
   final PatchListCache patchListCache;
   final FromAddressGenerator fromAddressGenerator;
   final EmailSender emailSender;
   final PatchSetInfoFactory patchSetInfoFactory;
   final IdentifiedUser.GenericFactory identifiedUserFactory;
+  final CapabilityControl.Factory capabilityControlFactory;
+  final AnonymousUser anonymousUser;
   final Provider<String> urlProvider;
   final AllProjectsName allProjectsName;
 
@@ -51,32 +55,39 @@
   final Provider<ChangeQueryRewriter> queryRewriter;
   final Provider<ReviewDb> db;
   final RuntimeInstance velocityRuntime;
+  final EmailSettings settings;
 
   @Inject
   EmailArguments(GitRepositoryManager server, ProjectCache projectCache,
-      GroupCache groupCache, AccountCache accountCache,
+      GroupBackend groupBackend, AccountCache accountCache,
       PatchListCache patchListCache, FromAddressGenerator fromAddressGenerator,
       EmailSender emailSender, PatchSetInfoFactory patchSetInfoFactory,
       GenericFactory identifiedUserFactory,
+      CapabilityControl.Factory capabilityControlFactory,
+      AnonymousUser anonymousUser,
       @CanonicalWebUrl @Nullable Provider<String> urlProvider,
       AllProjectsName allProjectsName,
       ChangeQueryBuilder.Factory queryBuilder,
       Provider<ChangeQueryRewriter> queryRewriter, Provider<ReviewDb> db,
-      RuntimeInstance velocityRuntime) {
+      RuntimeInstance velocityRuntime,
+      EmailSettings settings) {
     this.server = server;
     this.projectCache = projectCache;
-    this.groupCache = groupCache;
+    this.groupBackend = groupBackend;
     this.accountCache = accountCache;
     this.patchListCache = patchListCache;
     this.fromAddressGenerator = fromAddressGenerator;
     this.emailSender = emailSender;
     this.patchSetInfoFactory = patchSetInfoFactory;
     this.identifiedUserFactory = identifiedUserFactory;
+    this.capabilityControlFactory = capabilityControlFactory;
+    this.anonymousUser = anonymousUser;
     this.urlProvider = urlProvider;
     this.allProjectsName = allProjectsName;
     this.queryBuilder = queryBuilder;
     this.queryRewriter = queryRewriter;
     this.db = db;
     this.velocityRuntime = velocityRuntime;
+    this.settings = settings;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailSettings.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailSettings.java
new file mode 100644
index 0000000..7e44877
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailSettings.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.mail;
+
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.lib.Config;
+
+@Singleton
+class EmailSettings {
+  final boolean includeDiff;
+  final int maximumDiffSize;
+
+  @Inject
+  EmailSettings(@GerritServerConfig Config cfg) {
+    includeDiff = cfg.getBoolean("sendemail", "includeDiff", false);
+    maximumDiffSize = cfg.getInt("sendemail", "maximumDiffSize", 256 << 10);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailTokenVerifier.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailTokenVerifier.java
index 4307854..8501426 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailTokenVerifier.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailTokenVerifier.java
@@ -40,6 +40,8 @@
 
   /** Exception thrown when a token does not parse correctly. */
   public static class InvalidTokenException extends Exception {
+    private static final long serialVersionUID = 1L;
+
     public InvalidTokenException() {
       super("Invalid token");
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/FromAddressGeneratorProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/FromAddressGeneratorProvider.java
index bb50374..86cebdc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/FromAddressGeneratorProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/FromAddressGeneratorProvider.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.mail;
 
+import com.google.common.base.Charsets;
 import com.google.gerrit.common.data.ParameterizedString;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.GerritPersonIdent;
@@ -24,9 +25,13 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
+import org.apache.commons.codec.binary.Base64;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.PersonIdent;
 
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+
 /** Creates a {@link FromAddressGenerator} from the {@link GerritServerConfig} */
 @Singleton
 public class FromAddressGeneratorProvider implements
@@ -121,7 +126,7 @@
   }
 
   static final class PatternGen implements FromAddressGenerator {
-    private final String senderEmail;
+    private final ParameterizedString senderEmailPattern;
     private final Address serverAddress;
     private final AccountCache accountCache;
     private final String anonymousCowardName;
@@ -130,7 +135,7 @@
     PatternGen(final Address serverAddress, final AccountCache accountCache,
         final String anonymousCowardName,
         final ParameterizedString namePattern, final String senderEmail) {
-      this.senderEmail = senderEmail;
+      this.senderEmailPattern = new ParameterizedString(senderEmail);
       this.serverAddress = serverAddress;
       this.accountCache = accountCache;
       this.anonymousCowardName = anonymousCowardName;
@@ -158,7 +163,25 @@
         senderName = serverAddress.name;
       }
 
+      String senderEmail;
+      if (senderEmailPattern.getParameterNames().isEmpty()) {
+        senderEmail = senderEmailPattern.getRawPattern();
+      } else {
+        senderEmail = senderEmailPattern
+            .replace("userHash", hashOf(senderName))
+            .toString();
+      }
       return new Address(senderName, senderEmail);
     }
   }
+
+  private static String hashOf(String data) {
+    try {
+      MessageDigest hash = MessageDigest.getInstance("MD5");
+      byte[] bytes = hash.digest(data.getBytes(Charsets.UTF_8));
+      return Base64.encodeBase64URLSafeString(bytes);
+    } catch (NoSuchAlgorithmException e) {
+      throw new RuntimeException("No MD5 available", e);
+    }
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java
index 3590b8a..70b2d7f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergedSender.java
@@ -17,7 +17,6 @@
 import com.google.gerrit.common.data.ApprovalType;
 import com.google.gerrit.common.data.ApprovalTypes;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountProjectWatch;
 import com.google.gerrit.reviewdb.client.ApprovalCategory;
 import com.google.gerrit.reviewdb.client.ApprovalCategoryValue;
 import com.google.gerrit.reviewdb.client.Change;
@@ -53,8 +52,8 @@
 
     ccAllApprovals();
     bccStarredBy();
-    bccWatchesNotifyAllComments();
-    bccWatchesNotifySubmittedChanges();
+    bccWatches(NotifyType.ALL_COMMENTS);
+    bccWatches(NotifyType.SUBMITTED_CHANGES);
   }
 
   @Override
@@ -140,20 +139,4 @@
     }
     m.put(ca.getCategoryId(), ca);
   }
-
-  private void bccWatchesNotifySubmittedChanges() {
-    try {
-      // BCC anyone else who has interest in this project's changes
-      //
-      for (final AccountProjectWatch w : getWatches()) {
-        if (w.isNotify(NotifyType.SUBMITTED_CHANGES)) {
-          add(RecipientType.BCC, w.getAccountId());
-        }
-      }
-    } catch (OrmException err) {
-      // Just don't CC everyone. Better to send a partial message to those
-      // we already have queued up then to fail deliver entirely to people
-      // who have a lower interest in the change.
-    }
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/NewChangeSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/NewChangeSender.java
index 2e459d4..82c1405 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/NewChangeSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/NewChangeSender.java
@@ -16,10 +16,21 @@
 
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.patch.PatchList;
+import com.google.gerrit.server.patch.PatchListNotAvailableException;
 import com.google.gerrit.server.ssh.SshInfo;
 
 import com.jcraft.jsch.HostKey;
 
+import org.eclipse.jgit.diff.DiffFormatter;
+import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.util.RawParseUtils;
+import org.eclipse.jgit.util.TemporaryBuffer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashSet;
@@ -28,6 +39,9 @@
 
 /** Sends an email alerting a user to a new change for them to review. */
 public abstract class NewChangeSender extends ChangeEmail {
+  private static final Logger log =
+      LoggerFactory.getLogger(NewChangeSender.class);
+
   private final SshInfo sshInfo;
   private final Set<Account.Id> reviewers = new HashSet<Account.Id>();
   private final Set<Account.Id> extraCC = new HashSet<Account.Id>();
@@ -85,4 +99,50 @@
     }
     return host;
   }
+
+  public boolean getIncludeDiff() {
+    return args.settings.includeDiff;
+  }
+
+  /** Show patch set as unified difference.  */
+  public String getUnifiedDiff() {
+    PatchList patchList;
+    try {
+      patchList = getPatchList();
+      if (patchList.getOldId() == null) {
+        // Octopus merges are not well supported for diff output by Gerrit.
+        // Currently these always have a null oldId in the PatchList.
+        return "";
+      }
+    } catch (PatchListNotAvailableException e) {
+      log.error("Cannot format patch", e);
+      return "";
+    }
+
+    TemporaryBuffer.Heap buf =
+        new TemporaryBuffer.Heap(args.settings.maximumDiffSize);
+    DiffFormatter fmt = new DiffFormatter(buf);
+    Repository git;
+    try {
+      git = args.server.openRepository(change.getProject());
+    } catch (IOException e) {
+      log.error("Cannot open repository to format patch", e);
+      return "";
+    }
+    try {
+      fmt.setRepository(git);
+      fmt.setDetectRenames(true);
+      fmt.format(patchList.getOldId(), patchList.getNewId());
+      return RawParseUtils.decode(buf.toByteArray());
+    } catch (IOException e) {
+      if (JGitText.get().inMemoryBufferLimitExceeded.equals(e.getMessage())) {
+        return "";
+      }
+      log.error("Cannot format patch", e);
+      return "";
+    } finally {
+      fmt.release();
+      git.close();
+    }
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
index de8628f..caa441d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.mail;
 
+import com.google.common.collect.Sets;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.UserIdentity;
 import com.google.gerrit.server.account.AccountState;
@@ -34,14 +35,13 @@
 import java.io.StringWriter;
 import java.net.MalformedURLException;
 import java.net.URL;
-import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Date;
 import java.util.HashSet;
 import java.util.Iterator;
 import java.util.LinkedHashMap;
-import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 /** Sends an email to one or more interested parties. */
 public abstract class OutgoingEmail {
@@ -53,7 +53,7 @@
   protected String messageClass;
   private final HashSet<Account.Id> rcptTo = new HashSet<Account.Id>();
   private final Map<String, EmailHeader> headers;
-  private final List<Address> smtpRcptTo = new ArrayList<Address>();
+  private final Set<Address> smtpRcptTo = Sets.newHashSet();
   private Address smtpFromAddress;
   private StringBuilder body;
   protected VelocityContext velocityContext;
@@ -282,7 +282,7 @@
       return false;
     }
 
-    if (rcptTo.size() == 1 && rcptTo.contains(fromId)) {
+    if (smtpRcptTo.size() == 1 && rcptTo.size() == 1 && rcptTo.contains(fromId)) {
       // If the only recipient is also the sender, don't bother.
       //
       return false;
@@ -324,14 +324,15 @@
   protected void add(final RecipientType rt, final Address addr) {
     if (addr != null && addr.email != null && addr.email.length() > 0) {
       if (args.emailSender.canEmail(addr.email)) {
-        smtpRcptTo.add(addr);
-        switch (rt) {
-          case TO:
-            ((EmailHeader.AddressList) headers.get(HDR_TO)).add(addr);
-            break;
-          case CC:
-            ((EmailHeader.AddressList) headers.get(HDR_CC)).add(addr);
-            break;
+        if (smtpRcptTo.add(addr)) {
+          switch (rt) {
+            case TO:
+              ((EmailHeader.AddressList) headers.get(HDR_TO)).add(addr);
+              break;
+            case CC:
+              ((EmailHeader.AddressList) headers.get(HDR_CC)).add(addr);
+              break;
+          }
         }
       } else {
         log.warn("Not emailing " + addr.email + " (prohibited by allowrcpt)");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RestoredSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RestoredSender.java
index c9afdde..946c29f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RestoredSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RestoredSender.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.mail;
 
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
 import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
@@ -38,7 +39,7 @@
 
     ccAllApprovals();
     bccStarredBy();
-    bccWatchesNotifyAllComments();
+    bccWatches(NotifyType.ALL_COMMENTS);
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RevertedSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RevertedSender.java
index 964bfed..033bd56 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RevertedSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RevertedSender.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.mail;
 
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
 import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
@@ -37,7 +38,7 @@
 
     ccAllApprovals();
     bccStarredBy();
-    bccWatchesNotifyAllComments();
+    bccWatches(NotifyType.ALL_COMMENTS);
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/SmtpEmailSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/SmtpEmailSender.java
index f681710..ce45ffe 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/SmtpEmailSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/SmtpEmailSender.java
@@ -182,7 +182,12 @@
 
         Writer w = client.sendMessageData();
         if (w == null) {
-          throw new EmailException("Server " + smtpHost + " rejected body");
+          /* Include rejected recipient error messages here to not lose that
+           * information. That piece of the puzzle is vital if zero recipients
+           * are accepted and the server consequently rejects the DATA command.
+           */
+          throw new EmailException(rejected + "Server " + smtpHost
+              + " rejected DATA command: " + client.getReplyString());
         }
         w = new BufferedWriter(w);
 
@@ -201,7 +206,8 @@
         w.close();
 
         if (!client.completePendingCommand()) {
-          throw new EmailException("Server " + smtpHost + " rejected body");
+          throw new EmailException("Server " + smtpHost
+              + " rejected message body: " + client.getReplyString());
         }
 
         client.logout();
@@ -237,7 +243,8 @@
       }
       if (!client.login()) {
         String e = client.getReplyString();
-        throw new EmailException("SMTP server rejected login: " + e);
+        throw new EmailException(
+            "SMTP server rejected HELO/EHLO greeting: " + e);
       }
 
       if (smtpEncryption == Encryption.TLS) {
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..62ed5e4 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
@@ -29,8 +29,9 @@
 import java.util.List;
 
 public class IntraLineDiffKey implements Serializable {
-  static final long serialVersionUID = 3L;
+  static final long serialVersionUID = 4L;
 
+  private transient boolean ignoreWhitespace;
   private transient ObjectId aId;
   private transient ObjectId bId;
 
@@ -45,7 +46,8 @@
   private transient String path;
 
   public IntraLineDiffKey(ObjectId aId, Text aText, ObjectId bId, Text bText,
-      List<Edit> edits, Project.NameKey projectKey, ObjectId commit, String path) {
+      List<Edit> edits, Project.NameKey projectKey, ObjectId commit, String path,
+      boolean ignoreWhitespace) {
     this.aId = aId;
     this.bId = bId;
 
@@ -56,6 +58,8 @@
     this.projectKey = projectKey;
     this.commit = commit;
     this.path = path;
+
+    this.ignoreWhitespace = ignoreWhitespace;
   }
 
   Text getTextA() {
@@ -70,11 +74,11 @@
     return edits;
   }
 
-  ObjectId getBlobA() {
+  public ObjectId getBlobA() {
     return aId;
   }
 
-  ObjectId getBlobB() {
+  public ObjectId getBlobB() {
     return bId;
   }
 
@@ -96,6 +100,7 @@
 
     h = h * 31 + aId.hashCode();
     h = h * 31 + bId.hashCode();
+    h = h * 31 + (ignoreWhitespace ? 1 : 0);
 
     return h;
   }
@@ -105,7 +110,8 @@
     if (o instanceof IntraLineDiffKey) {
       final IntraLineDiffKey k = (IntraLineDiffKey) o;
       return aId.equals(k.aId) //
-          && bId.equals(k.bId);
+          && bId.equals(k.bId) //
+          && ignoreWhitespace == k.ignoreWhitespace;
     }
     return false;
   }
@@ -114,6 +120,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());
@@ -124,10 +133,12 @@
   private void writeObject(final ObjectOutputStream out) throws IOException {
     writeNotNull(out, aId);
     writeNotNull(out, bId);
+    out.writeBoolean(ignoreWhitespace);
   }
 
   private void readObject(final ObjectInputStream in) throws IOException {
     aId = readNotNull(in);
     bId = readNotNull(in);
+    ignoreWhitespace = in.readBoolean();
   }
 }
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/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..bb231a5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java
@@ -15,9 +15,9 @@
 
 package com.google.gerrit.server.patch;
 
-import com.google.gerrit.reviewdb.client.Patch;
+import com.google.common.cache.CacheLoader;
 import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
-import com.google.gerrit.server.cache.EntryCreator;
+import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.Inject;
 
@@ -54,6 +54,8 @@
 import org.eclipse.jgit.treewalk.filter.TreeFilter;
 import org.eclipse.jgit.util.TemporaryBuffer;
 import org.eclipse.jgit.util.io.DisabledOutputStream;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
 import java.io.InputStream;
@@ -62,7 +64,9 @@
 import java.util.List;
 import java.util.Map;
 
-class PatchListLoader extends EntryCreator<PatchListKey, PatchList> {
+class PatchListLoader extends CacheLoader<PatchListKey, PatchList> {
+  static final Logger log = LoggerFactory.getLogger(PatchListLoader.class);
+
   private final GitRepositoryManager repoManager;
 
   @Inject
@@ -71,7 +75,7 @@
   }
 
   @Override
-  public PatchList createEntry(final PatchListKey key) throws Exception {
+  public PatchList load(final PatchListKey key) throws Exception {
     final Repository repo = repoManager.openRepository(key.projectKey);
     try {
       return readPatchList(key, repo);
@@ -251,16 +255,34 @@
 
     ObjectId treeId;
     ResolveMerger m = (ResolveMerger) MergeStrategy.RESOLVE.newMerger(repo, true);
-    ObjectInserter ins = m.getObjectInserter();
+    final ObjectInserter ins = repo.newObjectInserter();
     try {
       DirCache dc = DirCache.newInCore();
       m.setDirCache(dc);
+      m.setObjectInserter(new ObjectInserter.Filter() {
+        @Override
+        protected ObjectInserter delegate() {
+          return ins;
+        }
+
+        @Override
+        public void flush() {
+        }
+
+        @Override
+        public void release() {
+        }
+      });
 
       boolean couldMerge = false;
       try {
         couldMerge = m.merge(b.getParents());
       } catch (IOException e) {
-        //
+        // It is not safe to continue further down in this method as throwing
+        // an exception most likely means that the merge tree was not created
+        // and m.getMergeResults() is empty. This would mean that all paths are
+        // unmerged and Gerrit UI would show all paths in the patch list.
+        return null;
       }
 
       if (couldMerge) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListNotAvailableException.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListNotAvailableException.java
new file mode 100644
index 0000000..2ccc9f1
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListNotAvailableException.java
@@ -0,0 +1,27 @@
+// 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;
+
+public class PatchListNotAvailableException extends Exception {
+  private static final long serialVersionUID = 1L;
+
+  public PatchListNotAvailableException(String message) {
+    super(message);
+  }
+
+  public PatchListNotAvailableException(Throwable cause) {
+    super(cause);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListWeigher.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListWeigher.java
new file mode 100644
index 0000000..d715246
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListWeigher.java
@@ -0,0 +1,30 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.patch;
+
+import com.google.common.cache.Weigher;
+
+/** Approximates memory usage for PatchList in bytes of memory used. */
+public class PatchListWeigher implements Weigher<PatchListKey, PatchList> {
+  @Override
+  public int weigh(PatchListKey key, PatchList value) {
+    int size = 16 + 4*8 + 2*36 // Size of PatchListKey, 64 bit JVM
+        + 16 + 3*8 + 3*4 + 20; // Size of PatchList, 64 bit JVM
+    for (PatchListEntry e : value.getPatches()) {
+      size += e.weigh();
+    }
+    return size;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java
index f59dcc3..8165bf2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchSetInfoFactory.java
@@ -18,7 +18,6 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetInfo;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.reviewdb.client.UserIdentity;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -68,31 +67,39 @@
   }
 
   public PatchSetInfo get(ReviewDb db, PatchSet.Id patchSetId)
-    throws PatchSetInfoNotAvailableException {
-    Repository repo = null;
+      throws PatchSetInfoNotAvailableException {
     try {
       final PatchSet patchSet = db.patchSets().get(patchSetId);
       final Change change = db.changes().get(patchSet.getId().getParentKey());
-      final Project.NameKey projectKey = change.getProject();
-      repo = repoManager.openRepository(projectKey);
+      return get(change, patchSet);
+    } catch (OrmException e) {
+      throw new PatchSetInfoNotAvailableException(e);
+    }
+  }
+
+  public PatchSetInfo get(Change change, PatchSet patchSet)
+      throws PatchSetInfoNotAvailableException {
+    Repository repo;
+    try {
+      repo = repoManager.openRepository(change.getProject());
+    } catch (IOException e) {
+      throw new PatchSetInfoNotAvailableException(e);
+    }
+    try {
       final RevWalk rw = new RevWalk(repo);
       try {
         final RevCommit src =
             rw.parseCommit(ObjectId.fromString(patchSet.getRevision().get()));
-        PatchSetInfo info = get(src, patchSetId);
+        PatchSetInfo info = get(src, patchSet.getId());
         info.setParents(toParentInfos(src.getParents(), rw));
         return info;
       } finally {
         rw.release();
       }
-    } catch (OrmException e) {
-      throw new PatchSetInfoNotAvailableException(e);
     } catch (IOException e) {
       throw new PatchSetInfoNotAvailableException(e);
     } finally {
-      if (repo != null) {
-        repo.close();
-      }
+      repo.close();
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/AutoRegisterModules.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/AutoRegisterModules.java
new file mode 100644
index 0000000..e8af060
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/AutoRegisterModules.java
@@ -0,0 +1,393 @@
+// 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 static com.google.gerrit.server.plugins.PluginGuiceEnvironment.is;
+
+import com.google.common.collect.LinkedListMultimap;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Sets;
+import com.google.gerrit.extensions.annotations.Export;
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.extensions.annotations.Listen;
+import com.google.inject.AbstractModule;
+import com.google.inject.Module;
+import com.google.inject.Scopes;
+import com.google.inject.TypeLiteral;
+import com.google.inject.internal.UniqueAnnotations;
+
+import org.eclipse.jgit.util.IO;
+import org.objectweb.asm.AnnotationVisitor;
+import org.objectweb.asm.Attribute;
+import org.objectweb.asm.ClassReader;
+import org.objectweb.asm.ClassVisitor;
+import org.objectweb.asm.FieldVisitor;
+import org.objectweb.asm.MethodVisitor;
+import org.objectweb.asm.Opcodes;
+import org.objectweb.asm.Type;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.annotation.Annotation;
+import java.lang.reflect.ParameterizedType;
+import java.util.Enumeration;
+import java.util.Map;
+import java.util.Set;
+import java.util.jar.JarEntry;
+import java.util.jar.JarFile;
+
+class AutoRegisterModules {
+  private static final int SKIP_ALL = ClassReader.SKIP_CODE
+      | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES;
+  private final String pluginName;
+  private final PluginGuiceEnvironment env;
+  private final JarFile jarFile;
+  private final ClassLoader classLoader;
+  private final ModuleGenerator sshGen;
+  private final ModuleGenerator httpGen;
+
+  private Set<Class<?>> sysSingletons;
+  private Multimap<TypeLiteral<?>, Class<?>> sysListen;
+
+  Module sysModule;
+  Module sshModule;
+  Module httpModule;
+
+  AutoRegisterModules(String pluginName,
+      PluginGuiceEnvironment env,
+      JarFile jarFile,
+      ClassLoader classLoader) {
+    this.pluginName = pluginName;
+    this.env = env;
+    this.jarFile = jarFile;
+    this.classLoader = classLoader;
+    this.sshGen = env.hasSshModule() ? env.newSshModuleGenerator() : null;
+    this.httpGen = env.hasHttpModule() ? env.newHttpModuleGenerator() : null;
+  }
+
+  AutoRegisterModules discover() throws InvalidPluginException {
+    sysSingletons = Sets.newHashSet();
+    sysListen = LinkedListMultimap.create();
+
+    if (sshGen != null) {
+      sshGen.setPluginName(pluginName);
+    }
+    if (httpGen != null) {
+      httpGen.setPluginName(pluginName);
+    }
+
+    scan();
+
+    if (!sysSingletons.isEmpty() || !sysListen.isEmpty()) {
+      sysModule = makeSystemModule();
+    }
+    if (sshGen != null) {
+      sshModule = sshGen.create();
+    }
+    if (httpGen != null) {
+      httpModule = httpGen.create();
+    }
+    return this;
+  }
+
+  private Module makeSystemModule() {
+    return new AbstractModule() {
+      @Override
+      protected void configure() {
+        for (Class<?> clazz : sysSingletons) {
+          bind(clazz).in(Scopes.SINGLETON);
+        }
+        for (Map.Entry<TypeLiteral<?>, Class<?>> e : sysListen.entries()) {
+          @SuppressWarnings("unchecked")
+          TypeLiteral<Object> type = (TypeLiteral<Object>) e.getKey();
+
+          @SuppressWarnings("unchecked")
+          Class<Object> impl = (Class<Object>) e.getValue();
+
+          Annotation n = impl.getAnnotation(Export.class);
+          if (n == null) {
+            n = impl.getAnnotation(javax.inject.Named.class);
+          }
+          if (n == null) {
+            n = impl.getAnnotation(com.google.inject.name.Named.class);
+          }
+          if (n == null) {
+            n = UniqueAnnotations.create();
+          }
+          bind(type).annotatedWith(n).to(impl);
+        }
+      }
+    };
+  }
+
+  private void scan() throws InvalidPluginException {
+    Enumeration<JarEntry> e = jarFile.entries();
+    while (e.hasMoreElements()) {
+      JarEntry entry = e.nextElement();
+      if (skip(entry)) {
+        continue;
+      }
+
+      ClassData def = new ClassData();
+      try {
+        new ClassReader(read(entry)).accept(def, SKIP_ALL);
+      } catch (IOException err) {
+        throw new InvalidPluginException("Cannot auto-register", err);
+      } catch (RuntimeException err) {
+        PluginLoader.log.warn(String.format(
+            "Plugin %s has invaild class file %s inside of %s",
+            pluginName, entry.getName(), jarFile.getName()), err);
+        continue;
+      }
+
+      if (def.exportedAsName != null) {
+        if (def.isConcrete()) {
+          export(def);
+        } else {
+          PluginLoader.log.warn(String.format(
+              "Plugin %s tries to @Export(\"%s\") abstract class %s",
+              pluginName, def.exportedAsName, def.className));
+        }
+      } else if (def.listen) {
+        if (def.isConcrete()) {
+          listen(def);
+        } else {
+          PluginLoader.log.warn(String.format(
+              "Plugin %s tries to @Listen abstract class %s",
+              pluginName, def.className));
+        }
+      }
+    }
+  }
+
+  private void export(ClassData def) throws InvalidPluginException {
+    Class<?> clazz;
+    try {
+      clazz = Class.forName(def.className, false, classLoader);
+    } catch (ClassNotFoundException err) {
+      throw new InvalidPluginException(String.format(
+          "Cannot load %s with @Export(\"%s\")",
+          def.className, def.exportedAsName), err);
+    }
+
+    Export export = clazz.getAnnotation(Export.class);
+    if (export == null) {
+      PluginLoader.log.warn(String.format(
+          "In plugin %s asm incorrectly parsed %s with @Export(\"%s\")",
+          pluginName, clazz.getName(), def.exportedAsName));
+      return;
+    }
+
+    if (is("org.apache.sshd.server.Command", clazz)) {
+      if (sshGen != null) {
+        sshGen.export(export, clazz);
+      }
+    } else if (is("javax.servlet.http.HttpServlet", clazz)) {
+      if (httpGen != null) {
+        httpGen.export(export, clazz);
+        listen(clazz, clazz);
+      }
+    } else {
+      int cnt = sysListen.size();
+      listen(clazz, clazz);
+      if (cnt == sysListen.size()) {
+        // If no bindings were recorded, the extension isn't recognized.
+        throw new InvalidPluginException(String.format(
+            "Class %s with @Export(\"%s\") not supported",
+            clazz.getName(), export.value()));
+      }
+    }
+  }
+
+  private void listen(ClassData def) throws InvalidPluginException {
+    Class<?> clazz;
+    try {
+      clazz = Class.forName(def.className, false, classLoader);
+    } catch (ClassNotFoundException err) {
+      throw new InvalidPluginException(String.format(
+          "Cannot load %s with @Listen",
+          def.className), err);
+    }
+
+    Listen listen = clazz.getAnnotation(Listen.class);
+    if (listen != null) {
+      listen(clazz, clazz);
+    } else {
+      PluginLoader.log.warn(String.format(
+          "In plugin %s asm incorrectly parsed %s with @Listen",
+          pluginName, clazz.getName()));
+    }
+  }
+
+  private void listen(java.lang.reflect.Type type, Class<?> clazz)
+      throws InvalidPluginException {
+    while (type != null) {
+      Class<?> rawType;
+      if (type instanceof ParameterizedType) {
+        rawType = (Class<?>) ((ParameterizedType) type).getRawType();
+      } else if (type instanceof Class) {
+        rawType = (Class<?>) type;
+      } else {
+        return;
+      }
+
+      if (rawType.getAnnotation(ExtensionPoint.class) != null) {
+        TypeLiteral<?> tl = TypeLiteral.get(type);
+        if (env.hasDynamicSet(tl)) {
+          sysSingletons.add(clazz);
+          sysListen.put(tl, clazz);
+        } else if (env.hasDynamicMap(tl)) {
+          if (clazz.getAnnotation(Export.class) == null) {
+            throw new InvalidPluginException(String.format(
+                "Class %s requires @Export(\"name\") annotation for %s",
+                clazz.getName(), rawType.getName()));
+          }
+          sysSingletons.add(clazz);
+          sysListen.put(tl, clazz);
+        } else {
+          throw new InvalidPluginException(String.format(
+              "Cannot register %s, server does not accept %s",
+              clazz.getName(), rawType.getName()));
+        }
+        return;
+      }
+
+      java.lang.reflect.Type[] interfaces = rawType.getGenericInterfaces();
+      if (interfaces != null) {
+        for (java.lang.reflect.Type i : interfaces) {
+          listen(i, clazz);
+        }
+      }
+
+      type = rawType.getGenericSuperclass();
+    }
+  }
+
+  private static boolean skip(JarEntry entry) {
+    if (!entry.getName().endsWith(".class")) {
+      return true; // Avoid non-class resources.
+    }
+    if (entry.getSize() <= 0) {
+      return true; // Directories have 0 size.
+    }
+    if (entry.getSize() >= 1024 * 1024) {
+      return true; // Do not scan huge class files.
+    }
+    return false;
+  }
+
+  private byte[] read(JarEntry entry) throws IOException {
+    byte[] data = new byte[(int) entry.getSize()];
+    InputStream in = jarFile.getInputStream(entry);
+    try {
+      IO.readFully(in, data, 0, data.length);
+    } finally {
+      in.close();
+    }
+    return data;
+  }
+
+  private static class ClassData implements ClassVisitor {
+    private static final String EXPORT = Type.getType(Export.class).getDescriptor();
+    private static final String LISTEN = Type.getType(Listen.class).getDescriptor();
+
+    String className;
+    int access;
+    String exportedAsName;
+    boolean listen;
+
+    boolean isConcrete() {
+      return (access & Opcodes.ACC_ABSTRACT) == 0
+          && (access & Opcodes.ACC_INTERFACE) == 0;
+    }
+
+    @Override
+    public void visit(int version, int access, String name, String signature,
+        String superName, String[] interfaces) {
+      this.className = Type.getObjectType(name).getClassName();
+      this.access = access;
+    }
+
+    @Override
+    public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
+      if (visible && EXPORT.equals(desc)) {
+        return new AbstractAnnotationVisitor() {
+          @Override
+          public void visit(String name, Object value) {
+            exportedAsName = (String) value;
+          }
+        };
+      }
+      if (visible && LISTEN.equals(desc)) {
+        listen = true;
+        return null;
+      }
+      return null;
+    }
+
+    @Override
+    public void visitSource(String arg0, String arg1) {
+    }
+
+    @Override
+    public void visitOuterClass(String arg0, String arg1, String arg2) {
+    }
+
+    @Override
+    public MethodVisitor visitMethod(int arg0, String arg1, String arg2,
+        String arg3, String[] arg4) {
+      return null;
+    }
+
+    @Override
+    public void visitInnerClass(String arg0, String arg1, String arg2, int arg3) {
+    }
+
+    @Override
+    public FieldVisitor visitField(int arg0, String arg1, String arg2,
+        String arg3, Object arg4) {
+      return null;
+    }
+
+    @Override
+    public void visitEnd() {
+    }
+
+    @Override
+    public void visitAttribute(Attribute arg0) {
+    }
+  }
+
+  private static abstract class AbstractAnnotationVisitor implements
+      AnnotationVisitor {
+    @Override
+    public AnnotationVisitor visitAnnotation(String arg0, String arg1) {
+      return null;
+    }
+
+    @Override
+    public AnnotationVisitor visitArray(String arg0) {
+      return null;
+    }
+
+    @Override
+    public void visitEnum(String arg0, String arg1, String arg2) {
+    }
+
+    @Override
+    public void visitEnd() {
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CleanupHandle.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CleanupHandle.java
new file mode 100644
index 0000000..7018a3b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CleanupHandle.java
@@ -0,0 +1,47 @@
+// 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 java.io.File;
+import java.io.IOException;
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.WeakReference;
+import java.util.jar.JarFile;
+
+class CleanupHandle extends WeakReference<ClassLoader> {
+  private final File tmpFile;
+  private final JarFile jarFile;
+
+  CleanupHandle(File tmpFile,
+      JarFile jarFile,
+      ClassLoader ref,
+      ReferenceQueue<ClassLoader> queue) {
+    super(ref, queue);
+    this.tmpFile = tmpFile;
+    this.jarFile = jarFile;
+  }
+
+  void cleanup() {
+    try {
+      jarFile.close();
+    } catch (IOException err) {
+    }
+    if (!tmpFile.delete() && tmpFile.exists()) {
+      PluginLoader.log.warn("Cannot delete " + tmpFile.getAbsolutePath());
+    } else {
+      PluginLoader.log.info("Cleaned plugin " + tmpFile.getName());
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CopyConfigModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CopyConfigModule.java
new file mode 100644
index 0000000..f34826d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CopyConfigModule.java
@@ -0,0 +1,102 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.plugins;
+
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.SitePath;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.config.TrackingFooters;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import com.google.inject.Provides;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.lib.Config;
+
+import java.io.File;
+
+/**
+ * Copies critical objects from the {@code dbInjector} into a plugin.
+ * <p>
+ * Most explicit bindings are copied automatically from the cfgInjector and
+ * sysInjector to be made available to a plugin's private world. This module is
+ * necessary to get things bound in the dbInjector that are not otherwise easily
+ * available, but that a plugin author might expect to exist.
+ */
+@Singleton
+class CopyConfigModule extends AbstractModule {
+  @Inject
+  @SitePath
+  private File sitePath;
+
+  @Provides
+  @SitePath
+  File getSitePath() {
+    return sitePath;
+  }
+
+  @Inject
+  private SitePaths sitePaths;
+
+  @Provides
+  SitePaths getSitePaths() {
+    return sitePaths;
+  }
+
+  @Inject
+  private TrackingFooters trackingFooters;
+
+  @Provides
+  TrackingFooters getTrackingFooters() {
+    return trackingFooters;
+  }
+
+  @Inject
+  @GerritServerConfig
+  private Config gerritServerConfig;
+
+  @Provides
+  @GerritServerConfig
+  Config getGerritServerConfig() {
+    return gerritServerConfig;
+  }
+
+  @Inject
+  private SchemaFactory<ReviewDb> schemaFactory;
+
+  @Provides
+  SchemaFactory<ReviewDb> getSchemaFactory() {
+    return schemaFactory;
+  }
+
+  @Inject
+  private GitRepositoryManager gitRepositoryManager;
+
+  @Provides
+  GitRepositoryManager getGitRepositoryManager() {
+    return gitRepositoryManager;
+  }
+
+  @Inject
+  CopyConfigModule() {
+  }
+
+  @Override
+  protected void configure() {
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/InvalidPluginException.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/InvalidPluginException.java
new file mode 100644
index 0000000..31be10c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/InvalidPluginException.java
@@ -0,0 +1,27 @@
+// 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;
+
+public class InvalidPluginException extends Exception {
+  private static final long serialVersionUID = 1L;
+
+  public InvalidPluginException(String message) {
+    super(message);
+  }
+
+  public InvalidPluginException(String message, Throwable why) {
+    super(message, why);
+  }
+}
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..3f7bc97
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ListPlugins.java
@@ -0,0 +1,116 @@
+// 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 the installed plugins. */
+public class ListPlugins {
+  private final PluginLoader pluginLoader;
+
+  @Option(name = "--format", metaVar = "FMT", usage = "Output display format")
+  private OutputFormat format = OutputFormat.TEXT;
+
+  @Option(name = "--all", aliases = {"-a"}, usage = "List all plugins, including disabled plugins")
+  private boolean all;
+
+  @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(all));
+    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 %-8s\n", "Name", "Version", "Status");
+      stdout
+          .print("-------------------------------------------------------------------------------\n");
+    }
+
+    for (Plugin p : plugins) {
+      PluginInfo info = new PluginInfo();
+      info.version = p.getVersion();
+      info.disabled = p.isDisabled() ? true : null;
+
+      if (format.isJson()) {
+        output.put(p.getName(), info);
+      } else {
+        stdout.format("%-30s %-10s %-8s\n", p.getName(),
+            Strings.nullToEmpty(info.version),
+            p.isDisabled() ? "DISABLED" : "ENABLED");
+      }
+    }
+
+    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;
+    // disabled is only read via reflection when building the json output.  We
+    // do not want to show a compiler error that it isn't used.
+    @SuppressWarnings("unused")
+    Boolean disabled;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ModuleGenerator.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ModuleGenerator.java
new file mode 100644
index 0000000..92e3b1d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ModuleGenerator.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.plugins;
+
+import com.google.gerrit.extensions.annotations.Export;
+import com.google.inject.Module;
+
+public interface ModuleGenerator {
+  void setPluginName(String name);
+
+  void export(Export export, Class<?> type) throws InvalidPluginException;
+
+  Module create() throws InvalidPluginException;
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java
new file mode 100644
index 0000000..91ffbba
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java
@@ -0,0 +1,325 @@
+// 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.gerrit.extensions.annotations.PluginData;
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.extensions.registration.RegistrationHandle;
+import com.google.gerrit.extensions.registration.ReloadableRegistrationHandle;
+import com.google.gerrit.extensions.systemstatus.ServerInformation;
+import com.google.gerrit.lifecycle.LifecycleManager;
+import com.google.inject.AbstractModule;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.Module;
+import com.google.inject.Provider;
+import com.google.inject.ProvisionException;
+
+import org.eclipse.jgit.storage.file.FileSnapshot;
+
+import java.io.File;
+import java.util.Collections;
+import java.util.List;
+import java.util.jar.Attributes;
+import java.util.jar.JarFile;
+import java.util.jar.Manifest;
+
+import javax.annotation.Nullable;
+
+public class Plugin {
+  public static enum ApiType {
+    EXTENSION, PLUGIN;
+  }
+
+  /** Unique key that changes whenever a plugin reloads. */
+  public static final class CacheKey {
+    private final String name;
+
+    CacheKey(String name) {
+      this.name = name;
+    }
+
+    @Override
+    public String toString() {
+      int id = System.identityHashCode(this);
+      return String.format("Plugin[%s@%x]", name, id);
+    }
+  }
+
+  static {
+    // Guice logs warnings about multiple injectors being created.
+    // Silence this in case HTTP plugins are used.
+    java.util.logging.Logger.getLogger("com.google.inject.servlet.GuiceFilter")
+        .setLevel(java.util.logging.Level.OFF);
+  }
+
+  static ApiType getApiType(Manifest manifest) throws InvalidPluginException {
+    Attributes main = manifest.getMainAttributes();
+    String v = main.getValue("Gerrit-ApiType");
+    if (Strings.isNullOrEmpty(v)
+        || ApiType.EXTENSION.name().equalsIgnoreCase(v)) {
+      return ApiType.EXTENSION;
+    } else if (ApiType.PLUGIN.name().equalsIgnoreCase(v)) {
+      return ApiType.PLUGIN;
+    } else {
+      throw new InvalidPluginException("Invalid Gerrit-ApiType: " + v);
+    }
+  }
+
+  private final CacheKey cacheKey;
+  private final String name;
+  private final File srcJar;
+  private final FileSnapshot snapshot;
+  private final JarFile jarFile;
+  private final Manifest manifest;
+  private final File dataDir;
+  private final ApiType apiType;
+  private final ClassLoader classLoader;
+  private final boolean disabled;
+  private Class<? extends Module> sysModule;
+  private Class<? extends Module> sshModule;
+  private Class<? extends Module> httpModule;
+
+  private Injector sysInjector;
+  private Injector sshInjector;
+  private Injector httpInjector;
+  private LifecycleManager manager;
+  private List<ReloadableRegistrationHandle<?>> reloadableHandles;
+
+  public Plugin(String name,
+      File srcJar,
+      FileSnapshot snapshot,
+      JarFile jarFile,
+      Manifest manifest,
+      File dataDir,
+      ApiType apiType,
+      ClassLoader classLoader,
+      @Nullable Class<? extends Module> sysModule,
+      @Nullable Class<? extends Module> sshModule,
+      @Nullable Class<? extends Module> httpModule) {
+    this.cacheKey = new CacheKey(name);
+    this.name = name;
+    this.srcJar = srcJar;
+    this.snapshot = snapshot;
+    this.jarFile = jarFile;
+    this.manifest = manifest;
+    this.dataDir = dataDir;
+    this.apiType = apiType;
+    this.classLoader = classLoader;
+    this.disabled = srcJar.getName().endsWith(".disabled");
+    this.sysModule = sysModule;
+    this.sshModule = sshModule;
+    this.httpModule = httpModule;
+  }
+
+  File getSrcJar() {
+    return srcJar;
+  }
+
+  public CacheKey getCacheKey() {
+    return cacheKey;
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  @Nullable
+  public String getVersion() {
+    Attributes main = manifest.getMainAttributes();
+    return main.getValue(Attributes.Name.IMPLEMENTATION_VERSION);
+  }
+
+  public ApiType getApiType() {
+    return apiType;
+  }
+
+  boolean canReload() {
+    Attributes main = manifest.getMainAttributes();
+    String v = main.getValue("Gerrit-ReloadMode");
+    if (Strings.isNullOrEmpty(v) || "reload".equalsIgnoreCase(v)) {
+      return true;
+    } else if ("restart".equalsIgnoreCase(v)) {
+      return false;
+    } else {
+      PluginLoader.log.warn(String.format(
+          "Plugin %s has invalid Gerrit-ReloadMode %s; assuming restart",
+          name, v));
+      return false;
+    }
+  }
+
+  boolean isModified(File jar) {
+    return snapshot.lastModified() != jar.lastModified();
+  }
+
+  public boolean isDisabled() {
+    return disabled;
+  }
+
+  public void start(PluginGuiceEnvironment env) throws Exception {
+    Injector root = newRootInjector(env);
+    manager = new LifecycleManager();
+
+    AutoRegisterModules auto = null;
+    if (sysModule == null && sshModule == null && httpModule == null) {
+      auto = new AutoRegisterModules(name, env, jarFile, classLoader);
+      auto.discover();
+    }
+
+    if (sysModule != null) {
+      sysInjector = root.createChildInjector(root.getInstance(sysModule));
+      manager.add(sysInjector);
+    } else if (auto != null && auto.sysModule != null) {
+      sysInjector = root.createChildInjector(auto.sysModule);
+      manager.add(sysInjector);
+    } else {
+      sysInjector = root;
+    }
+
+    if (env.hasSshModule()) {
+      List<Module> modules = Lists.newLinkedList();
+      if (apiType == ApiType.PLUGIN) {
+        modules.add(env.getSshModule());
+      }
+      if (sshModule != null) {
+        modules.add(sysInjector.getInstance(sshModule));
+        sshInjector = sysInjector.createChildInjector(modules);
+        manager.add(sshInjector);
+      } else if (auto != null && auto.sshModule != null) {
+        modules.add(auto.sshModule);
+        sshInjector = sysInjector.createChildInjector(modules);
+        manager.add(sshInjector);
+      }
+    }
+
+    if (env.hasHttpModule()) {
+      List<Module> modules = Lists.newLinkedList();
+      if (apiType == ApiType.PLUGIN) {
+        modules.add(env.getHttpModule());
+      }
+      if (httpModule != null) {
+        modules.add(sysInjector.getInstance(httpModule));
+        httpInjector = sysInjector.createChildInjector(modules);
+        manager.add(httpInjector);
+      } else if (auto != null && auto.httpModule != null) {
+        modules.add(auto.httpModule);
+        httpInjector = sysInjector.createChildInjector(modules);
+        manager.add(httpInjector);
+      }
+    }
+
+    manager.start();
+  }
+
+  private Injector newRootInjector(final PluginGuiceEnvironment env) {
+    List<Module> modules = Lists.newArrayListWithCapacity(4);
+    modules.add(env.getSysModule());
+    if (apiType == ApiType.PLUGIN) {
+      modules.add(env.getSysModule());
+    } else {
+      modules.add(new AbstractModule() {
+        @Override
+        protected void configure() {
+          bind(ServerInformation.class).toInstance(env.getServerInformation());
+        }
+      });
+    }
+    modules.add(new AbstractModule() {
+      @Override
+      protected void configure() {
+        bind(String.class)
+          .annotatedWith(PluginName.class)
+          .toInstance(name);
+
+        bind(File.class)
+          .annotatedWith(PluginData.class)
+          .toProvider(new Provider<File>() {
+            private volatile boolean ready;
+
+            @Override
+            public File get() {
+              if (!ready) {
+                synchronized (dataDir) {
+                  if (!dataDir.exists() && !dataDir.mkdirs()) {
+                    throw new ProvisionException(String.format(
+                        "Cannot create %s for plugin %s",
+                        dataDir.getAbsolutePath(), name));
+                  }
+                  ready = true;
+                }
+              }
+              return dataDir;
+            }
+          });
+      }
+    });
+    return Guice.createInjector(modules);
+  }
+
+  public void stop() {
+    if (manager != null) {
+      manager.stop();
+      manager = null;
+      sysInjector = null;
+      sshInjector = null;
+      httpInjector = null;
+    }
+  }
+
+  public JarFile getJarFile() {
+    return jarFile;
+  }
+
+  public Injector getSysInjector() {
+    return sysInjector;
+  }
+
+  @Nullable
+  public Injector getSshInjector() {
+    return sshInjector;
+  }
+
+  @Nullable
+  public Injector getHttpInjector() {
+    return httpInjector;
+  }
+
+  public void add(RegistrationHandle handle) {
+    if (manager != null) {
+      if (handle instanceof ReloadableRegistrationHandle) {
+        if (reloadableHandles == null) {
+          reloadableHandles = Lists.newArrayList();
+        }
+        reloadableHandles.add((ReloadableRegistrationHandle<?>) handle);
+      }
+      manager.add(handle);
+    }
+  }
+
+  List<ReloadableRegistrationHandle<?>> getReloadableHandles() {
+    if (reloadableHandles != null) {
+      return reloadableHandles;
+    }
+    return Collections.emptyList();
+  }
+
+  @Override
+  public String toString() {
+    return "Plugin [" + name + "]";
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginCleanerTask.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginCleanerTask.java
new file mode 100644
index 0000000..7081d70
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginCleanerTask.java
@@ -0,0 +1,100 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.plugins;
+
+import com.google.gerrit.server.git.WorkQueue;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+
+@Singleton
+class PluginCleanerTask implements Runnable {
+  private final WorkQueue workQueue;
+  private final PluginLoader loader;
+  private volatile int pending;
+  private Future<?> self;
+  private int attempts;
+  private long start;
+
+  @Inject
+  PluginCleanerTask(WorkQueue workQueue, PluginLoader loader) {
+    this.workQueue = workQueue;
+    this.loader = loader;
+  }
+
+  @Override
+  public void run() {
+    try {
+      for (int t = 0; t < 2 * (attempts + 1); t++) {
+        System.gc();
+        Thread.sleep(50);
+      }
+    } catch (InterruptedException e) {
+    }
+
+    int left = loader.processPendingCleanups();
+    synchronized (this) {
+      pending = left;
+      self = null;
+
+      if (0 < left) {
+        long waiting = System.currentTimeMillis() - start;
+        PluginLoader.log.warn(String.format(
+            "%d plugins still waiting to be reclaimed after %d minutes",
+            pending,
+            TimeUnit.MILLISECONDS.toMinutes(waiting)));
+        attempts = Math.min(attempts + 1, 15);
+        ensureScheduled();
+      } else {
+        attempts = 0;
+      }
+    }
+  }
+
+  @Override
+  public String toString() {
+    int p = pending;
+    if (0 < p) {
+      return String.format("Plugin Cleaner (waiting for %d plugins)", p);
+    }
+    return "Plugin Cleaner";
+  }
+
+  synchronized void clean(int expect) {
+    if (self == null && pending == 0) {
+      start = System.currentTimeMillis();
+    }
+    pending = expect;
+    ensureScheduled();
+  }
+
+  private void ensureScheduled() {
+    if (self == null && 0 < pending) {
+      if (attempts == 1) {
+        self = workQueue.getDefaultQueue().schedule(
+            this,
+            30,
+            TimeUnit.SECONDS);
+      } else {
+        self = workQueue.getDefaultQueue().schedule(
+            this,
+            attempts + 1,
+            TimeUnit.MINUTES);
+      }
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java
new file mode 100644
index 0000000..18460ff
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java
@@ -0,0 +1,494 @@
+// 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 static com.google.gerrit.extensions.registration.PrivateInternals_DynamicTypes.dynamicMapsOf;
+import static com.google.gerrit.extensions.registration.PrivateInternals_DynamicTypes.dynamicSetsOf;
+
+import com.google.common.collect.LinkedListMultimap;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.registration.PrivateInternals_DynamicMapImpl;
+import com.google.gerrit.extensions.registration.PrivateInternals_DynamicTypes;
+import com.google.gerrit.extensions.registration.RegistrationHandle;
+import com.google.gerrit.extensions.registration.ReloadableRegistrationHandle;
+import com.google.gerrit.extensions.systemstatus.ServerInformation;
+import com.google.inject.AbstractModule;
+import com.google.inject.Binding;
+import com.google.inject.Guice;
+import com.google.inject.Injector;
+import com.google.inject.Key;
+import com.google.inject.Module;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import com.google.inject.TypeLiteral;
+import com.google.inject.internal.UniqueAnnotations;
+
+import java.lang.annotation.Annotation;
+import java.lang.reflect.ParameterizedType;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArrayList;
+
+import javax.annotation.Nullable;
+import javax.inject.Inject;
+
+/**
+ * Tracks Guice bindings that should be exposed to loaded plugins.
+ * <p>
+ * This is an internal implementation detail of how the main server is able to
+ * export its explicit Guice bindings to tightly coupled plugins, giving them
+ * access to singletons and request scoped resources just like any core code.
+ */
+@Singleton
+public class PluginGuiceEnvironment {
+  private final Injector sysInjector;
+  private final ServerInformation srvInfo;
+  private final CopyConfigModule copyConfigModule;
+  private final Set<Key<?>> copyConfigKeys;
+  private final List<StartPluginListener> onStart;
+  private final List<ReloadPluginListener> onReload;
+
+  private Module sysModule;
+  private Module sshModule;
+  private Module httpModule;
+
+  private Provider<ModuleGenerator> sshGen;
+  private Provider<ModuleGenerator> httpGen;
+
+  private Map<TypeLiteral<?>, DynamicSet<?>> sysSets;
+  private Map<TypeLiteral<?>, DynamicSet<?>> sshSets;
+  private Map<TypeLiteral<?>, DynamicSet<?>> httpSets;
+
+  private Map<TypeLiteral<?>, DynamicMap<?>> sysMaps;
+  private Map<TypeLiteral<?>, DynamicMap<?>> sshMaps;
+  private Map<TypeLiteral<?>, DynamicMap<?>> httpMaps;
+
+  @Inject
+  PluginGuiceEnvironment(
+      Injector sysInjector,
+      ServerInformation srvInfo,
+      CopyConfigModule ccm) {
+    this.sysInjector = sysInjector;
+    this.srvInfo = srvInfo;
+    this.copyConfigModule = ccm;
+    this.copyConfigKeys = Guice.createInjector(ccm).getAllBindings().keySet();
+
+    onStart = new CopyOnWriteArrayList<StartPluginListener>();
+    onStart.addAll(listeners(sysInjector, StartPluginListener.class));
+
+    onReload = new CopyOnWriteArrayList<ReloadPluginListener>();
+    onReload.addAll(listeners(sysInjector, ReloadPluginListener.class));
+
+    sysSets = dynamicSetsOf(sysInjector);
+    sysMaps = dynamicMapsOf(sysInjector);
+  }
+
+  ServerInformation getServerInformation() {
+    return srvInfo;
+  }
+
+  boolean hasDynamicSet(TypeLiteral<?> type) {
+    return sysSets.containsKey(type)
+        || (sshSets != null && sshSets.containsKey(type))
+        || (httpSets != null && httpSets.containsKey(type));
+  }
+
+  boolean hasDynamicMap(TypeLiteral<?> type) {
+    return sysMaps.containsKey(type)
+        || (sshMaps != null && sshMaps.containsKey(type))
+        || (httpMaps != null && httpMaps.containsKey(type));
+  }
+
+  Module getSysModule() {
+    return sysModule;
+  }
+
+  public void setCfgInjector(Injector cfgInjector) {
+    final Module cm = copy(cfgInjector);
+    final Module sm = copy(sysInjector);
+    sysModule = new AbstractModule() {
+      @Override
+      protected void configure() {
+        install(copyConfigModule);
+        install(cm);
+        install(sm);
+      }
+    };
+  }
+
+  public void setSshInjector(Injector injector) {
+    sshModule = copy(injector);
+    sshGen = injector.getProvider(ModuleGenerator.class);
+    sshSets = dynamicSetsOf(injector);
+    sshMaps = dynamicMapsOf(injector);
+    onStart.addAll(listeners(injector, StartPluginListener.class));
+    onReload.addAll(listeners(injector, ReloadPluginListener.class));
+  }
+
+  boolean hasSshModule() {
+    return sshModule != null;
+  }
+
+  Module getSshModule() {
+    return sshModule;
+  }
+
+  ModuleGenerator newSshModuleGenerator() {
+    return sshGen.get();
+  }
+
+  public void setHttpInjector(Injector injector) {
+    httpModule = copy(injector);
+    httpGen = injector.getProvider(ModuleGenerator.class);
+    httpSets = dynamicSetsOf(injector);
+    httpMaps = dynamicMapsOf(injector);
+    onStart.addAll(listeners(injector, StartPluginListener.class));
+    onReload.addAll(listeners(injector, ReloadPluginListener.class));
+  }
+
+  boolean hasHttpModule() {
+    return httpModule != null;
+  }
+
+  Module getHttpModule() {
+    return httpModule;
+  }
+
+  ModuleGenerator newHttpModuleGenerator() {
+    return httpGen.get();
+  }
+
+  void onStartPlugin(Plugin plugin) {
+    for (StartPluginListener l : onStart) {
+      l.onStartPlugin(plugin);
+    }
+
+    attachSet(sysSets, plugin.getSysInjector(), plugin);
+    attachSet(sshSets, plugin.getSshInjector(), plugin);
+    attachSet(httpSets, plugin.getHttpInjector(), plugin);
+
+    attachMap(sysMaps, plugin.getSysInjector(), plugin);
+    attachMap(sshMaps, plugin.getSshInjector(), plugin);
+    attachMap(httpMaps, plugin.getHttpInjector(), plugin);
+  }
+
+  private void attachSet(Map<TypeLiteral<?>, DynamicSet<?>> sets,
+      @Nullable Injector src,
+      Plugin plugin) {
+    for (RegistrationHandle h : PrivateInternals_DynamicTypes
+        .attachSets(src, sets)) {
+      plugin.add(h);
+    }
+  }
+
+  private void attachMap(Map<TypeLiteral<?>, DynamicMap<?>> maps,
+      @Nullable Injector src,
+      Plugin plugin) {
+    for (RegistrationHandle h : PrivateInternals_DynamicTypes
+        .attachMaps(src, plugin.getName(), maps)) {
+      plugin.add(h);
+    }
+  }
+
+  void onReloadPlugin(Plugin oldPlugin, Plugin newPlugin) {
+    for (ReloadPluginListener l : onReload) {
+      l.onReloadPlugin(oldPlugin, newPlugin);
+    }
+
+    // Index all old registrations by the raw type. These may be replaced
+    // during the reattach calls below. Any that are not replaced will be
+    // removed when the old plugin does its stop routine.
+    ListMultimap<TypeLiteral<?>, ReloadableRegistrationHandle<?>> old =
+        LinkedListMultimap.create();
+    for (ReloadableRegistrationHandle<?> h : oldPlugin.getReloadableHandles()) {
+      old.put(h.getKey().getTypeLiteral(), h);
+    }
+
+    reattachMap(old, sysMaps, newPlugin.getSysInjector(), newPlugin);
+    reattachMap(old, sshMaps, newPlugin.getSshInjector(), newPlugin);
+    reattachMap(old, httpMaps, newPlugin.getHttpInjector(), newPlugin);
+
+    reattachSet(old, sysSets, newPlugin.getSysInjector(), newPlugin);
+    reattachSet(old, sshSets, newPlugin.getSshInjector(), newPlugin);
+    reattachSet(old, httpSets, newPlugin.getHttpInjector(), newPlugin);
+  }
+
+  private void reattachMap(
+      ListMultimap<TypeLiteral<?>, ReloadableRegistrationHandle<?>> oldHandles,
+      Map<TypeLiteral<?>, DynamicMap<?>> maps,
+      @Nullable Injector src,
+      Plugin newPlugin) {
+    if (src == null || maps == null || maps.isEmpty()) {
+      return;
+    }
+
+    for (Map.Entry<TypeLiteral<?>, DynamicMap<?>> e : maps.entrySet()) {
+      @SuppressWarnings("unchecked")
+      TypeLiteral<Object> type = (TypeLiteral<Object>) e.getKey();
+
+      @SuppressWarnings("unchecked")
+      PrivateInternals_DynamicMapImpl<Object> map =
+          (PrivateInternals_DynamicMapImpl<Object>) e.getValue();
+
+      Map<Annotation, ReloadableRegistrationHandle<?>> am = Maps.newHashMap();
+      for (ReloadableRegistrationHandle<?> h : oldHandles.get(type)) {
+        Annotation a = h.getKey().getAnnotation();
+        if (a != null && !UNIQUE_ANNOTATION.isInstance(a)) {
+          am.put(a, h);
+        }
+      }
+
+      for (Binding<?> binding : bindings(src, e.getKey())) {
+        @SuppressWarnings("unchecked")
+        Binding<Object> b = (Binding<Object>) binding;
+        Key<Object> key = b.getKey();
+        if (key.getAnnotation() == null) {
+          continue;
+        }
+
+        @SuppressWarnings("unchecked")
+        ReloadableRegistrationHandle<Object> h =
+            (ReloadableRegistrationHandle<Object>) am.remove(key.getAnnotation());
+        if (h != null) {
+          replace(newPlugin, h, b);
+          oldHandles.remove(type, h);
+        } else {
+          newPlugin.add(map.put(
+              newPlugin.getName(),
+              b.getKey(),
+              b.getProvider()));
+        }
+      }
+    }
+  }
+
+  /** Type used to declare unique annotations. Guice hides this, so extract it. */
+  private static final Class<?> UNIQUE_ANNOTATION =
+      UniqueAnnotations.create().getClass();
+
+  private void reattachSet(
+      ListMultimap<TypeLiteral<?>, ReloadableRegistrationHandle<?>> oldHandles,
+      Map<TypeLiteral<?>, DynamicSet<?>> sets,
+      @Nullable Injector src,
+      Plugin newPlugin) {
+    if (src == null || sets == null || sets.isEmpty()) {
+      return;
+    }
+
+    for (Map.Entry<TypeLiteral<?>, DynamicSet<?>> e : sets.entrySet()) {
+      @SuppressWarnings("unchecked")
+      TypeLiteral<Object> type = (TypeLiteral<Object>) e.getKey();
+
+      @SuppressWarnings("unchecked")
+      DynamicSet<Object> set = (DynamicSet<Object>) e.getValue();
+
+      // Index all old handles that match this DynamicSet<T> keyed by
+      // annotations. Ignore the unique annotations, thereby favoring
+      // the @Named annotations or some other non-unique naming.
+      Map<Annotation, ReloadableRegistrationHandle<?>> am = Maps.newHashMap();
+      List<ReloadableRegistrationHandle<?>> old = oldHandles.get(type);
+      Iterator<ReloadableRegistrationHandle<?>> oi = old.iterator();
+      while (oi.hasNext()) {
+        ReloadableRegistrationHandle<?> h = oi.next();
+        Annotation a = h.getKey().getAnnotation();
+        if (a != null && !UNIQUE_ANNOTATION.isInstance(a)) {
+          am.put(a, h);
+          oi.remove();
+        }
+      }
+
+      // Replace old handles with new bindings, favoring cases where there
+      // is an exact match on an @Named annotation. If there is no match
+      // pick any handle and replace it. We generally expect only one
+      // handle of each DynamicSet type when using unique annotations, but
+      // possibly multiple ones if @Named was used. Plugin authors that want
+      // atomic replacement across reloads should use @Named annotations with
+      // stable names that do not change across plugin versions to ensure the
+      // handles are swapped correctly.
+      oi = old.iterator();
+      for (Binding<?> binding : bindings(src, type)) {
+        @SuppressWarnings("unchecked")
+        Binding<Object> b = (Binding<Object>) binding;
+        Key<Object> key = b.getKey();
+        if (key.getAnnotation() == null) {
+          continue;
+        }
+
+        @SuppressWarnings("unchecked")
+        ReloadableRegistrationHandle<Object> h1 =
+            (ReloadableRegistrationHandle<Object>) am.remove(key.getAnnotation());
+        if (h1 != null) {
+          replace(newPlugin, h1, b);
+        } else if (oi.hasNext()) {
+          @SuppressWarnings("unchecked")
+          ReloadableRegistrationHandle<Object> h2 =
+            (ReloadableRegistrationHandle<Object>) oi.next();
+          oi.remove();
+          replace(newPlugin, h2, b);
+        } else {
+          newPlugin.add(set.add(b.getKey(), b.getProvider()));
+        }
+      }
+    }
+  }
+
+  private static <T> void replace(Plugin newPlugin,
+      ReloadableRegistrationHandle<T> h, Binding<T> b) {
+    RegistrationHandle n = h.replace(b.getKey(), b.getProvider());
+    if (n != null){
+      newPlugin.add(n);
+    }
+  }
+
+  static <T> List<T> listeners(Injector src, Class<T> type) {
+    List<Binding<T>> bindings = bindings(src, TypeLiteral.get(type));
+    int cnt = bindings != null ? bindings.size() : 0;
+    List<T> found = Lists.newArrayListWithCapacity(cnt);
+    if (bindings != null) {
+      for (Binding<T> b : bindings) {
+        found.add(b.getProvider().get());
+      }
+    }
+    return found;
+  }
+
+  private static <T> List<Binding<T>> bindings(Injector src, TypeLiteral<T> type) {
+    return src.findBindingsByType(type);
+  }
+
+  private Module copy(Injector src) {
+    Set<TypeLiteral<?>> dynamicTypes = Sets.newHashSet();
+    for (Map.Entry<Key<?>, Binding<?>> e : src.getBindings().entrySet()) {
+      TypeLiteral<?> type = e.getKey().getTypeLiteral();
+      if (type.getRawType() == DynamicSet.class
+          || type.getRawType() == DynamicMap.class) {
+        ParameterizedType t = (ParameterizedType) type.getType();
+        dynamicTypes.add(TypeLiteral.get(t.getActualTypeArguments()[0]));
+      }
+    }
+
+    final Map<Key<?>, Binding<?>> bindings = Maps.newLinkedHashMap();
+    for (Map.Entry<Key<?>, Binding<?>> e : src.getBindings().entrySet()) {
+      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());
+      }
+    }
+    bindings.remove(Key.get(Injector.class));
+    bindings.remove(Key.get(java.util.logging.Logger.class));
+
+    return new AbstractModule() {
+      @SuppressWarnings("unchecked")
+      @Override
+      protected void configure() {
+        for (Map.Entry<Key<?>, Binding<?>> e : bindings.entrySet()) {
+          Key<Object> k = (Key<Object>) e.getKey();
+          Binding<Object> b = (Binding<Object>) e.getValue();
+          bind(k).toProvider(b.getProvider());
+        }
+      }
+    };
+  }
+
+  private boolean shouldCopy(Key<?> key) {
+    if (copyConfigKeys.contains(key)) {
+      return false;
+    }
+    Class<?> type = key.getTypeLiteral().getRawType();
+    if (LifecycleListener.class.isAssignableFrom(type)) {
+      return false;
+    }
+    if (StartPluginListener.class.isAssignableFrom(type)) {
+      return false;
+    }
+
+    if (type.getName().startsWith("com.google.inject.")) {
+      return false;
+    }
+
+    if (is("org.apache.sshd.server.Command", type)) {
+      return false;
+    }
+
+    if (is("javax.servlet.Filter", type)) {
+      return false;
+    }
+    if (is("javax.servlet.ServletContext", type)) {
+      return false;
+    }
+    if (is("javax.servlet.ServletRequest", type)) {
+      return false;
+    }
+    if (is("javax.servlet.ServletResponse", type)) {
+      return false;
+    }
+    if (is("javax.servlet.http.HttpServlet", type)) {
+      return false;
+    }
+    if (is("javax.servlet.http.HttpServletRequest", type)) {
+      return false;
+    }
+    if (is("javax.servlet.http.HttpServletResponse", type)) {
+      return false;
+    }
+    if (is("javax.servlet.http.HttpSession", type)) {
+      return false;
+    }
+    if (Map.class.isAssignableFrom(type)
+        && key.getAnnotationType() != null
+        && "com.google.inject.servlet.RequestParameters"
+            .equals(key.getAnnotationType().getName())) {
+      return false;
+    }
+    if (type.getName().startsWith("com.google.gerrit.httpd.GitOverHttpServlet$")) {
+      return false;
+    }
+    return true;
+  }
+
+  static boolean is(String name, Class<?> type) {
+    while (type != null) {
+      if (name.equals(type.getName())) {
+        return true;
+      }
+
+      Class<?>[] interfaces = type.getInterfaces();
+      if (interfaces != null) {
+        for (Class<?> i : interfaces) {
+          if (is(name, i)) {
+            return true;
+          }
+        }
+      }
+
+      type = type.getSuperclass();
+    }
+    return false;
+  }
+}
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/plugins/PluginInstallException.java
similarity index 65%
copy from gerrit-server/src/main/java/com/google/gerrit/server/cache/EvictionPolicy.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginInstallException.java
index cff4f11..77fa702 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/EvictionPolicy.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginInstallException.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.
@@ -12,13 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.cache;
+package com.google.gerrit.server.plugins;
 
-/** How entries should be evicted from the cache. */
-public enum EvictionPolicy {
-  /** Least recently used is evicted first. */
-  LRU,
+public class PluginInstallException extends Exception {
+  private static final long serialVersionUID = 1L;
 
-  /** Least frequently used is evicted first. */
-  LFU;
+  public PluginInstallException(Throwable why) {
+    super(why.getMessage(), why);
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
new file mode 100644
index 0000000..d1c2aa4
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
@@ -0,0 +1,512 @@
+// 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.Joiner;
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.extensions.systemstatus.ServerInformation;
+import com.google.gerrit.server.config.ConfigUtil;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.inject.Inject;
+import com.google.inject.Module;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.storage.file.FileSnapshot;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FileFilter;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.InputStream;
+import java.lang.ref.ReferenceQueue;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.text.SimpleDateFormat;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.ConcurrentMap;
+import java.util.concurrent.TimeUnit;
+import java.util.jar.Attributes;
+import java.util.jar.JarFile;
+import java.util.jar.Manifest;
+
+@Singleton
+public class PluginLoader implements LifecycleListener {
+  static final Logger log = LoggerFactory.getLogger(PluginLoader.class);
+
+  private final File pluginsDir;
+  private final File dataDir;
+  private final File tmpDir;
+  private final PluginGuiceEnvironment env;
+  private final ServerInformationImpl srvInfoImpl;
+  private final ConcurrentMap<String, Plugin> running;
+  private final ConcurrentMap<String, Plugin> disabled;
+  private final Map<String, FileSnapshot> broken;
+  private final ReferenceQueue<ClassLoader> cleanupQueue;
+  private final ConcurrentMap<CleanupHandle, Boolean> cleanupHandles;
+  private final Provider<PluginCleanerTask> cleaner;
+  private final PluginScannerThread scanner;
+
+  @Inject
+  public PluginLoader(SitePaths sitePaths,
+      PluginGuiceEnvironment pe,
+      ServerInformationImpl sii,
+      Provider<PluginCleanerTask> pct,
+      @GerritServerConfig Config cfg) {
+    pluginsDir = sitePaths.plugins_dir;
+    dataDir = sitePaths.data_dir;
+    tmpDir = sitePaths.tmp_dir;
+    env = pe;
+    srvInfoImpl = sii;
+    running = Maps.newConcurrentMap();
+    disabled = Maps.newConcurrentMap();
+    broken = Maps.newHashMap();
+    cleanupQueue = new ReferenceQueue<ClassLoader>();
+    cleanupHandles = Maps.newConcurrentMap();
+    cleaner = pct;
+
+    long checkFrequency = ConfigUtil.getTimeUnit(cfg,
+        "plugins", null, "checkFrequency",
+        TimeUnit.MINUTES.toMillis(1), TimeUnit.MILLISECONDS);
+    if (checkFrequency > 0) {
+      scanner = new PluginScannerThread(this, checkFrequency);
+    } else {
+      scanner = null;
+    }
+  }
+
+  public Iterable<Plugin> getPlugins(boolean all) {
+    if (!all) {
+      return running.values();
+    } else {
+      ArrayList<Plugin> plugins = new ArrayList<Plugin>(running.values());
+      plugins.addAll(disabled.values());
+      return plugins;
+    }
+  }
+
+  public void installPluginFromStream(String name, InputStream in)
+      throws IOException, PluginInstallException {
+    if (!name.endsWith(".jar")) {
+      name += ".jar";
+    }
+
+    File jar = new File(pluginsDir, name);
+    name = nameOf(jar);
+
+    File old = new File(pluginsDir, ".last_" + name + ".zip");
+    File tmp = asTemp(in, ".next_" + name, ".zip", pluginsDir);
+    synchronized (this) {
+      Plugin active = running.get(name);
+      if (active != null) {
+        log.info(String.format("Replacing plugin %s", name));
+        old.delete();
+        jar.renameTo(old);
+      }
+
+      new File(pluginsDir, name + ".jar.disabled").delete();
+      tmp.renameTo(jar);
+      try {
+        runPlugin(name, jar, active);
+        if (active == null) {
+          log.info(String.format("Installed plugin %s", name));
+        }
+      } catch (PluginInstallException e) {
+        jar.delete();
+        throw e;
+      }
+
+      cleanInBackground();
+    }
+  }
+
+  public static File storeInTemp(String pluginName, InputStream in,
+      SitePaths sitePaths) throws IOException {
+    return asTemp(in, tempNameFor(pluginName), ".jar", sitePaths.tmp_dir);
+  }
+
+  private static File asTemp(InputStream in,
+      String prefix, String suffix,
+      File dir) throws IOException {
+    File tmp = File.createTempFile(prefix, suffix, dir);
+    boolean keep = false;
+    try {
+      FileOutputStream out = new FileOutputStream(tmp);
+      try {
+        byte[] data = new byte[8192];
+        int n;
+        while ((n = in.read(data)) > 0) {
+          out.write(data, 0, n);
+        }
+        keep = true;
+        return tmp;
+      } finally {
+        out.close();
+      }
+    } finally {
+      if (!keep) {
+        tmp.delete();
+      }
+    }
+  }
+
+  public void disablePlugins(Set<String> names) {
+    synchronized (this) {
+      for (String name : names) {
+        Plugin active = running.get(name);
+        if (active == null) {
+          continue;
+        }
+
+        log.info(String.format("Disabling plugin %s", name));
+        File off = new File(pluginsDir, active.getName() + ".jar.disabled");
+        active.getSrcJar().renameTo(off);
+
+        active.stop();
+        running.remove(name);
+        try {
+          FileSnapshot snapshot = FileSnapshot.save(off);
+          Plugin offPlugin = loadPlugin(name, off, snapshot);
+          disabled.put(name, offPlugin);
+        } catch (Throwable e) {
+          // This shouldn't happen, as the plugin was loaded earlier.
+          log.warn(String.format("Cannot load disabled plugin %s", name),
+              e.getCause());
+        }
+      }
+      cleanInBackground();
+    }
+  }
+
+  public void enablePlugins(Set<String> names) throws PluginInstallException {
+    synchronized (this) {
+      for (String name : names) {
+        Plugin off = disabled.get(name);
+        if (off == null) {
+          continue;
+        }
+
+        log.info(String.format("Enabling plugin %s", name));
+        File on = new File(pluginsDir, off.getName() + ".jar");
+        off.getSrcJar().renameTo(on);
+
+        disabled.remove(name);
+        runPlugin(name, on, null);
+      }
+      cleanInBackground();
+    }
+  }
+
+  @Override
+  public synchronized void start() {
+    log.info("Loading plugins from " + pluginsDir.getAbsolutePath());
+    srvInfoImpl.state = ServerInformation.State.STARTUP;
+    rescan();
+    srvInfoImpl.state = ServerInformation.State.RUNNING;
+    if (scanner != null) {
+      scanner.start();
+    }
+  }
+
+  @Override
+  public void stop() {
+    if (scanner != null) {
+      scanner.end();
+    }
+    srvInfoImpl.state = ServerInformation.State.SHUTDOWN;
+    synchronized (this) {
+      for (Plugin p : running.values()) {
+        p.stop();
+      }
+      running.clear();
+      disabled.clear();
+      broken.clear();
+      if (cleanupHandles.size() > running.size()) {
+        System.gc();
+        processPendingCleanups();
+      }
+    }
+  }
+
+  public void reload(List<String> names)
+      throws InvalidPluginException, PluginInstallException {
+    synchronized (this) {
+      List<Plugin> reload = Lists.newArrayListWithCapacity(names.size());
+      List<String> bad = Lists.newArrayListWithExpectedSize(4);
+      for (String name : names) {
+        Plugin active = running.get(name);
+        if (active != null) {
+          reload.add(active);
+        } else {
+          bad.add(name);
+        }
+      }
+      if (!bad.isEmpty()) {
+        throw new InvalidPluginException(String.format(
+            "Plugin(s) \"%s\" not running",
+            Joiner.on("\", \"").join(bad)));
+      }
+
+      for (Plugin active : reload) {
+        String name = active.getName();
+        try {
+          log.info(String.format("Reloading plugin %s", name));
+          runPlugin(name, active.getSrcJar(), active);
+        } catch (PluginInstallException e) {
+          log.warn(String.format("Cannot reload plugin %s", name), e.getCause());
+          throw e;
+        }
+      }
+
+      cleanInBackground();
+    }
+  }
+
+  public synchronized void rescan() {
+    List<File> jars = scanJarsInPluginsDirectory();
+    stopRemovedPlugins(jars);
+    dropRemovedDisabledPlugins(jars);
+
+    for (File jar : jars) {
+      String name = nameOf(jar);
+      FileSnapshot brokenTime = broken.get(name);
+      if (brokenTime != null && !brokenTime.isModified(jar)) {
+        continue;
+      }
+
+      Plugin active = running.get(name);
+      if (active != null && !active.isModified(jar)) {
+        continue;
+      }
+
+      if (active != null) {
+        log.info(String.format("Reloading plugin %s", name));
+      }
+
+      try {
+        Plugin loadedPlugin = runPlugin(name, jar, active);
+        if (active == null && !loadedPlugin.isDisabled()) {
+          log.info(String.format("Loaded plugin %s", name));
+        }
+      } catch (PluginInstallException e) {
+        log.warn(String.format("Cannot load plugin %s", name), e.getCause());
+      }
+    }
+
+    cleanInBackground();
+  }
+
+  private Plugin runPlugin(String name, File jar, Plugin oldPlugin)
+      throws PluginInstallException {
+    FileSnapshot snapshot = FileSnapshot.save(jar);
+    try {
+      Plugin newPlugin = loadPlugin(name, jar, snapshot);
+      boolean reload = oldPlugin != null
+          && oldPlugin.canReload()
+          && newPlugin.canReload();
+      if (!reload && oldPlugin != null) {
+        oldPlugin.stop();
+        running.remove(name);
+      }
+      if (!newPlugin.isDisabled()) {
+        newPlugin.start(env);
+      }
+      if (reload) {
+        env.onReloadPlugin(oldPlugin, newPlugin);
+        oldPlugin.stop();
+      } else if (!newPlugin.isDisabled()) {
+        env.onStartPlugin(newPlugin);
+      }
+      if (!newPlugin.isDisabled()) {
+        running.put(name, newPlugin);
+      } else {
+        disabled.put(name, newPlugin);
+      }
+      broken.remove(name);
+      return newPlugin;
+    } catch (Throwable err) {
+      broken.put(name, snapshot);
+      throw new PluginInstallException(err);
+    }
+  }
+
+  private void stopRemovedPlugins(List<File> jars) {
+    Set<String> unload = Sets.newHashSet(running.keySet());
+    for (File jar : jars) {
+      if (!jar.getName().endsWith(".disabled")) {
+        unload.remove(nameOf(jar));
+      }
+    }
+    for (String name : unload){
+      log.info(String.format("Unloading plugin %s", name));
+      running.remove(name).stop();
+    }
+  }
+
+  private void dropRemovedDisabledPlugins(List<File> jars) {
+    Set<String> unload = Sets.newHashSet(disabled.keySet());
+    for (File jar : jars) {
+      if (jar.getName().endsWith(".disabled")) {
+        unload.remove(nameOf(jar));
+      }
+    }
+    for (String name : unload) {
+      disabled.remove(name);
+    }
+  }
+
+  synchronized int processPendingCleanups() {
+    CleanupHandle h;
+    while ((h = (CleanupHandle) cleanupQueue.poll()) != null) {
+      h.cleanup();
+      cleanupHandles.remove(h);
+    }
+    return Math.max(0, cleanupHandles.size() - running.size());
+  }
+
+  private void cleanInBackground() {
+    int cnt = Math.max(0, cleanupHandles.size() - running.size());
+    if (0 < cnt) {
+      cleaner.get().clean(cnt);
+    }
+  }
+
+  private static String nameOf(File jar) {
+    String name = jar.getName();
+    if (name.endsWith(".disabled")) {
+      name = name.substring(0, name.lastIndexOf('.'));
+    }
+    int ext = name.lastIndexOf('.');
+    return 0 < ext ? name.substring(0, ext) : name;
+  }
+
+  private Plugin loadPlugin(String name, File srcJar, FileSnapshot snapshot)
+      throws IOException, ClassNotFoundException, InvalidPluginException {
+    File tmp;
+    FileInputStream in = new FileInputStream(srcJar);
+    try {
+      tmp = asTemp(in, tempNameFor(name), ".jar", tmpDir);
+    } finally {
+      in.close();
+    }
+
+    JarFile jarFile = new JarFile(tmp);
+    boolean keep = false;
+    try {
+      Manifest manifest = jarFile.getManifest();
+      Plugin.ApiType type = Plugin.getApiType(manifest);
+      Attributes main = manifest.getMainAttributes();
+      String sysName = main.getValue("Gerrit-Module");
+      String sshName = main.getValue("Gerrit-SshModule");
+      String httpName = main.getValue("Gerrit-HttpModule");
+
+      if (!Strings.isNullOrEmpty(sshName) && type != Plugin.ApiType.PLUGIN) {
+        throw new InvalidPluginException(String.format(
+            "Using Gerrit-SshModule requires Gerrit-ApiType: %s",
+            Plugin.ApiType.PLUGIN));
+      }
+
+      URL[] urls = {tmp.toURI().toURL()};
+      ClassLoader parentLoader = parentFor(type);
+      ClassLoader pluginLoader = new URLClassLoader(urls, parentLoader);
+      cleanupHandles.put(
+          new CleanupHandle(tmp, jarFile, pluginLoader, cleanupQueue),
+          Boolean.TRUE);
+
+      Class<? extends Module> sysModule = load(sysName, pluginLoader);
+      Class<? extends Module> sshModule = load(sshName, pluginLoader);
+      Class<? extends Module> httpModule = load(httpName, pluginLoader);
+      keep = true;
+      return new Plugin(name,
+          srcJar, snapshot,
+          jarFile, manifest,
+          new File(dataDir, name), type, pluginLoader,
+          sysModule, sshModule, httpModule);
+    } finally {
+      if (!keep) {
+        jarFile.close();
+      }
+    }
+  }
+
+  private static ClassLoader parentFor(Plugin.ApiType type)
+      throws InvalidPluginException {
+    switch (type) {
+      case EXTENSION:
+        return PluginName.class.getClassLoader();
+      case PLUGIN:
+        return PluginLoader.class.getClassLoader();
+      default:
+        throw new InvalidPluginException("Unsupported ApiType " + type);
+    }
+  }
+
+  private static String tempNameFor(String name) {
+    SimpleDateFormat fmt = new SimpleDateFormat("yyMMdd_HHmm");
+    return "plugin_" + name + "_" + fmt.format(new Date()) + "_";
+  }
+
+  private Class<? extends Module> load(String name, ClassLoader pluginLoader)
+      throws ClassNotFoundException {
+    if (Strings.isNullOrEmpty(name)) {
+      return null;
+    }
+
+    @SuppressWarnings("unchecked")
+    Class<? extends Module> clazz =
+        (Class<? extends Module>) Class.forName(name, false, pluginLoader);
+    if (!Module.class.isAssignableFrom(clazz)) {
+      throw new ClassCastException(String.format(
+          "Class %s does not implement %s",
+          name, Module.class.getName()));
+    }
+    return clazz;
+  }
+
+  private List<File> scanJarsInPluginsDirectory() {
+    if (pluginsDir == null || !pluginsDir.exists()) {
+      return Collections.emptyList();
+    }
+    File[] matches = pluginsDir.listFiles(new FileFilter() {
+      @Override
+      public boolean accept(File pathname) {
+        String n = pathname.getName();
+        return (n.endsWith(".jar") || n.endsWith(".jar.disabled"))
+            && pathname.isFile();
+      }
+    });
+    if (matches == null) {
+      log.error("Cannot list " + pluginsDir.getAbsolutePath());
+      return Collections.emptyList();
+    }
+    return Arrays.asList(matches);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginModule.java
new file mode 100644
index 0000000..ab7dc3c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginModule.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.plugins;
+
+import com.google.gerrit.extensions.systemstatus.ServerInformation;
+import com.google.gerrit.lifecycle.LifecycleModule;
+
+public class PluginModule extends LifecycleModule {
+  @Override
+  protected void configure() {
+    bind(ServerInformationImpl.class);
+    bind(ServerInformation.class).to(ServerInformationImpl.class);
+
+    bind(PluginCleanerTask.class);
+    bind(PluginGuiceEnvironment.class);
+    bind(PluginLoader.class);
+
+    bind(CopyConfigModule.class);
+    listener().to(PluginLoader.class);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginScannerThread.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginScannerThread.java
new file mode 100644
index 0000000..a484c5d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginScannerThread.java
@@ -0,0 +1,52 @@
+// 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 java.util.concurrent.CountDownLatch;
+import java.util.concurrent.TimeUnit;
+
+class PluginScannerThread extends Thread {
+  private final CountDownLatch done = new CountDownLatch(1);
+  private final PluginLoader loader;
+  private final long checkFrequencyMillis;
+
+  PluginScannerThread(PluginLoader loader, long checkFrequencyMillis) {
+    this.loader = loader;
+    this.checkFrequencyMillis = checkFrequencyMillis;
+    setDaemon(true);
+    setName("PluginScanner");
+  }
+
+  @Override
+  public void run() {
+    for (;;) {
+      try {
+        if (done.await(checkFrequencyMillis, TimeUnit.MILLISECONDS)) {
+          return;
+        }
+      } catch (InterruptedException e) {
+      }
+      loader.rescan();
+    }
+  }
+
+  void end() {
+    done.countDown();
+    try {
+      join();
+    } catch (InterruptedException e) {
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CachePool.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ReloadPluginListener.java
similarity index 67%
copy from gerrit-server/src/main/java/com/google/gerrit/server/cache/CachePool.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/plugins/ReloadPluginListener.java
index 3370b08..72a499e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CachePool.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ReloadPluginListener.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,9 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.cache;
+package com.google.gerrit.server.plugins;
 
-public interface CachePool {
-  public <K, V> ProxyCache<K, V> register(CacheProvider<K, V> provider);
+/** Broadcasts event indicating a plugin was reloaded. */
+public interface ReloadPluginListener {
+  public void onReloadPlugin(Plugin oldPlugin, Plugin newPlugin);
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerInformationImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerInformationImpl.java
new file mode 100644
index 0000000..c4a8900
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ServerInformationImpl.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.plugins;
+
+import com.google.gerrit.extensions.systemstatus.ServerInformation;
+import com.google.inject.Singleton;
+
+@Singleton
+class ServerInformationImpl implements ServerInformation {
+  volatile State state = State.STARTUP;
+
+  @Override
+  public State getState() {
+    return state;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CachePool.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/StartPluginListener.java
similarity index 69%
copy from gerrit-server/src/main/java/com/google/gerrit/server/cache/CachePool.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/plugins/StartPluginListener.java
index 3370b08..aaad370 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CachePool.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/StartPluginListener.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,9 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.cache;
+package com.google.gerrit.server.plugins;
 
-public interface CachePool {
-  public <K, V> ProxyCache<K, V> register(CacheProvider<K, V> provider);
+/** Broadcasts event indicating a plugin was loaded. */
+public interface StartPluginListener {
+  public void onStartPlugin(Plugin plugin);
 }
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 d17762e..db4b021 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
@@ -26,10 +26,11 @@
 import com.google.gerrit.rules.StoredValues;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.util.Providers;
 
 import com.googlecode.prolog_cafe.compiler.CompileException;
 import com.googlecode.prolog_cafe.lang.IntegerTerm;
@@ -49,6 +50,8 @@
 import java.util.List;
 import java.util.Set;
 
+import javax.annotation.Nullable;
+
 
 /** Access control management for a user accessing a single change. */
 public class ChangeControl {
@@ -161,7 +164,7 @@
 
   /** Can this user see this change? */
   public boolean isVisible(ReviewDb db) throws OrmException {
-    if (change.getStatus() == Change.Status.DRAFT && !isDraftVisible(db)) {
+    if (change.getStatus() == Change.Status.DRAFT && !isDraftVisible(db, null)) {
       return false;
     }
     return isRefVisible();
@@ -174,7 +177,7 @@
 
   /** Can this user see the given patchset? */
   public boolean isPatchVisible(PatchSet ps, ReviewDb db) throws OrmException {
-    if (ps.isDraft() && !isDraftVisible(db)) {
+    if (ps.isDraft() && !isDraftVisible(db, null)) {
       return false;
     }
     return isVisible(db);
@@ -186,6 +189,7 @@
         || getRefControl().isOwner() // branch owner can abandon
         || getProjectControl().isOwner() // project owner can abandon
         || getCurrentUser().getCapabilities().canAdministrateServer() // site administers are god
+        || getRefControl().canAbandon() // user can abandon a specific ref
     ;
   }
 
@@ -207,7 +211,8 @@
 
   /** Can this user restore this change? */
   public boolean canRestore() {
-    return canAbandon(); // Anyone who can abandon the change can restore it back
+    return canAbandon() // Anyone who can abandon the change can restore it back
+        && getRefControl().canUpload(); // as long as you can upload too
   }
 
   /** All value ranges of any allowed label permission. */
@@ -236,10 +241,20 @@
 
   /** Is this user a reviewer for the change? */
   public boolean isReviewer(ReviewDb db) throws OrmException {
+    return isReviewer(db, null);
+  }
+
+  /** Is this user a reviewer for the change? */
+  public boolean isReviewer(ReviewDb db, @Nullable ChangeData cd)
+      throws OrmException {
     if (getCurrentUser() instanceof IdentifiedUser) {
       final IdentifiedUser user = (IdentifiedUser) getCurrentUser();
-      ResultSet<PatchSetApproval> results =
-        db.patchSetApprovals().byChange(change.getId());
+      Iterable<PatchSetApproval> results;
+      if (cd != null) {
+        results = cd.currentApprovals(Providers.of(db));
+      } else {
+        results = db.patchSetApprovals().byChange(change.getId());
+      }
       for (PatchSetApproval approval : results) {
         if (user.getAccountId().equals(approval.getAccountId())) {
           return true;
@@ -279,34 +294,43 @@
     return false;
   }
 
-  public List<SubmitRecord> canSubmit(ReviewDb db, PatchSet.Id patchSetId) {
-    if (change.getStatus().isClosed()) {
+  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, false);
+  }
+
+  public List<SubmitRecord> canSubmit(ReviewDb db, PatchSet patchSet,
+      @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);
     }
 
-    if (!patchSetId.equals(change.currentPatchSetId())) {
-      return ruleError("Patch set " + patchSetId + " is not current");
+    if (!patchSet.getId().equals(change.currentPatchSetId())) {
+      return ruleError("Patch set " + patchSet.getPatchSetId() + " is not current");
     }
 
     try {
-      if (change.getStatus() == Change.Status.DRAFT){
-        if (!isVisible(db)) {
-          return ruleError("Patch set " + patchSetId + " not found");
+      if (change.getStatus() == Change.Status.DRAFT) {
+        if (!isDraftVisible(db, cd)) {
+          return ruleError("Patch set " + patchSet.getPatchSetId() + " not found");
         } else {
           return ruleError("Cannot submit draft changes");
         }
       }
-      if (isDraftPatchSet(patchSetId, db)) {
-        if (!isVisible(db)) {
-          return ruleError("Patch set " + patchSetId + " not found");
+      if (patchSet.isDraft()) {
+        if (!isDraftVisible(db, cd)) {
+          return ruleError("Patch set " + patchSet.getPatchSetId() + " not found");
         } else {
           return ruleError("Cannot submit draft patch sets");
         }
       }
     } catch (OrmException err) {
-      return logRuleError("Cannot read patch set " + patchSetId, err);
+      return logRuleError("Cannot read patch set " + patchSet.getId(), err);
     }
 
     List<Term> results = new ArrayList<Term>();
@@ -324,7 +348,8 @@
     try {
       env.set(StoredValues.REVIEW_DB, db);
       env.set(StoredValues.CHANGE, change);
-      env.set(StoredValues.PATCH_SET_ID, patchSetId);
+      env.set(StoredValues.CHANGE_DATA, cd);
+      env.set(StoredValues.PATCH_SET, patchSet);
       env.set(StoredValues.CHANGE_CONTROL, this);
 
       submitRule = env.once(
@@ -335,6 +360,10 @@
             + getProject().getName());
       }
 
+      if (fastEvalLabels) {
+        env.once("gerrit", "assume_range_from_label");
+      }
+
       try {
         for (Term[] template : env.all(
             "gerrit", "can_submit",
@@ -373,6 +402,10 @@
             parentEnv.once("gerrit", "locate_submit_filter", new VariableTerm());
         if (filterRule != null) {
           try {
+            if (fastEvalLabels) {
+              env.once("gerrit", "assume_range_from_label");
+            }
+
             Term resultsTerm = toListTerm(results);
             results.clear();
             Term[] template = parentEnv.once(
@@ -409,12 +442,18 @@
       return ruleError("Project submit rule has no solution");
     }
 
-    // Convert the results from Prolog Cafe's format to Gerrit's common format.
-    // can_submit/1 terminates when an ok(P) record is found. Therefore walk
-    // the results backwards, using only that ok(P) record if it exists. This
-    // skips partial results that occur early in the output. Later after the loop
-    // the out collection is reversed to restore it to the original ordering.
-    //
+    return resultsToSubmitRecord(submitRule, results);
+  }
+
+  /**
+   * Convert the results from Prolog Cafe's format to Gerrit's common format.
+   *
+   * can_submit/1 terminates when an ok(P) record is found. Therefore walk
+   * the results backwards, using only that ok(P) record if it exists. This
+   * skips partial results that occur early in the output. Later after the loop
+   * the out collection is reversed to restore it to the original ordering.
+   */
+  public List<SubmitRecord> resultsToSubmitRecord(Term submitRule, List<Term> results) {
     List<SubmitRecord> out = new ArrayList<SubmitRecord>(results.size());
     for (int resultIdx = results.size() - 1; 0 <= resultIdx; resultIdx--) {
       Term submitRecord = results.get(resultIdx);
@@ -468,6 +507,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;
 
@@ -516,16 +558,9 @@
     }
   }
 
-  private boolean isDraftVisible(ReviewDb db) throws OrmException {
-    return isOwner() || isReviewer(db);
-  }
-
-  private boolean isDraftPatchSet(PatchSet.Id id, ReviewDb db) throws OrmException {
-    PatchSet ps = db.patchSets().get(id);
-    if (ps == null) {
-      throw new OrmException("Patch set " + id + " not found");
-    }
-    return ps.isDraft();
+  private boolean isDraftVisible(ReviewDb db, ChangeData cd)
+      throws OrmException {
+    return isOwner() || isReviewer(db, cd);
   }
 
   private static boolean isUser(Term who) {
@@ -535,7 +570,7 @@
         && who.arg(0).isInteger();
   }
 
-  private static Term toListTerm(List<Term> terms) {
+  public static Term toListTerm(List<Term> terms) {
     Term list = Prolog.Nil;
     for (int i = terms.size() - 1; i >= 0; i--) {
       list = new ListTerm(terms.get(i), list);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
index 65bdc1e..3dbd7b7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
@@ -15,21 +15,24 @@
 package com.google.gerrit.server.project;
 
 import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.common.errors.ProjectCreationFailedException;
+import com.google.gerrit.extensions.events.NewProjectCreatedListener;
+import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.config.ProjectOwnerGroups;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
-import com.google.gerrit.server.git.ReplicationQueue;
 import com.google.gerrit.server.git.RepositoryCaseMismatchException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
@@ -49,6 +52,8 @@
 
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
 import java.util.Set;
 
 
@@ -64,28 +69,32 @@
   private final Set<AccountGroup.UUID> projectOwnerGroups;
   private final IdentifiedUser currentUser;
   private final GitRepositoryManager repoManager;
-  private final ReplicationQueue replication;
+  private final GitReferenceUpdated referenceUpdated;
+  private final DynamicSet<NewProjectCreatedListener> createdListener;
   private final PersonIdent serverIdent;
   private CreateProjectArgs createProjectArgs;
   private ProjectCache projectCache;
-  private GroupCache groupCache;
+  private GroupBackend groupBackend;
   private MetaDataUpdate.User metaDataUpdateFactory;
 
   @Inject
   CreateProject(@ProjectOwnerGroups Set<AccountGroup.UUID> pOwnerGroups,
       IdentifiedUser identifiedUser, GitRepositoryManager gitRepoManager,
-      ReplicationQueue replicateq, ReviewDb db,
-      @GerritPersonIdent PersonIdent personIdent, final GroupCache groupCache,
-      final MetaDataUpdate.User metaDataUpdateFactory,
+      GitReferenceUpdated referenceUpdated,
+      DynamicSet<NewProjectCreatedListener> createdListener,
+      ReviewDb db,
+      @GerritPersonIdent PersonIdent personIdent, GroupBackend groupBackend,
+      MetaDataUpdate.User metaDataUpdateFactory,
       @Assisted CreateProjectArgs createPArgs, ProjectCache pCache) {
     this.projectOwnerGroups = pOwnerGroups;
     this.currentUser = identifiedUser;
     this.repoManager = gitRepoManager;
-    this.replication = replicateq;
+    this.referenceUpdated = referenceUpdated;
+    this.createdListener = createdListener;
     this.serverIdent = personIdent;
     this.createProjectArgs = createPArgs;
     this.projectCache = pCache;
-    this.groupCache = groupCache;
+    this.groupBackend = groupBackend;
     this.metaDataUpdateFactory = metaDataUpdateFactory;
   }
 
@@ -95,10 +104,23 @@
     try {
       final String head =
           createProjectArgs.permissionsOnly ? GitRepositoryManager.REF_CONFIG
-              : createProjectArgs.branch;
+              : createProjectArgs.branch.get(0);
       final Repository repo = repoManager.createRepository(nameKey);
       try {
-        replication.replicateNewProject(nameKey, head);
+        NewProjectCreatedListener.Event event = new NewProjectCreatedListener.Event() {
+          @Override
+          public String getProjectName() {
+            return nameKey.get();
+          }
+
+          @Override
+          public String getHeadName() {
+            return head;
+          }
+        };
+        for (NewProjectCreatedListener l : createdListener) {
+          l.onNewProjectCreated(event);
+        }
 
         final RefUpdate u = repo.updateRef(Constants.HEAD);
         u.disableRefLog();
@@ -108,7 +130,7 @@
 
         if (!createProjectArgs.permissionsOnly
             && createProjectArgs.createEmptyCommit) {
-          createEmptyCommit(repo, nameKey, createProjectArgs.branch);
+          createEmptyCommits(repo, nameKey, createProjectArgs.branch);
         }
       } finally {
         repo.close();
@@ -130,10 +152,10 @@
         } finally {
           repo.close();
         }
-      } catch (RepositoryNotFoundException doesNotExist) {
+      } catch (IOException ioErr) {
         final String msg = "Cannot create " + nameKey;
         log.error(msg, err);
-        throw new ProjectCreationFailedException(msg, err);
+        throw new ProjectCreationFailedException(msg, ioErr);
       }
     } catch (Exception e) {
       final String msg = "Cannot create " + nameKey;
@@ -166,9 +188,9 @@
         final AccessSection all =
             config.getAccessSection(AccessSection.ALL, true);
         for (AccountGroup.UUID ownerId : createProjectArgs.ownerIds) {
-          AccountGroup accountGroup = groupCache.get(ownerId);
-          if (accountGroup != null) {
-            GroupReference group = config.resolve(accountGroup);
+          GroupDescription.Basic g = groupBackend.get(ownerId);
+          if (g != null) {
+            GroupReference group = config.resolve(GroupReference.forGroup(g));
             all.getPermission(Permission.OWNER, true).add(
                 new PermissionRule(group));
           }
@@ -176,17 +198,14 @@
       }
 
       md.setMessage("Created project\n");
-      if (!config.commit(md)) {
-        throw new IOException("Cannot create "
-            + createProjectArgs.getProjectName());
-      }
+      config.commit(md);
     } finally {
       md.close();
     }
     projectCache.onCreateProject(createProjectArgs.getProject());
     repoManager.setProjectDescription(createProjectArgs.getProject(),
         createProjectArgs.projectDescription);
-    replication.scheduleUpdate(createProjectArgs.getProject(),
+    referenceUpdated.fire(createProjectArgs.getProject(),
         GitRepositoryManager.REF_CONFIG);
   }
 
@@ -216,20 +235,32 @@
           new ArrayList<AccountGroup.UUID>(projectOwnerGroups);
     }
 
-    while (createProjectArgs.branch.startsWith("/")) {
-      createProjectArgs.branch = createProjectArgs.branch.substring(1);
+    List<String> transformedBranches = new ArrayList<String>();
+    if (createProjectArgs.branch == null ||
+        createProjectArgs.branch.isEmpty()) {
+      createProjectArgs.branch = Collections.singletonList(Constants.MASTER);
     }
-    if (!createProjectArgs.branch.startsWith(Constants.R_HEADS)) {
-      createProjectArgs.branch = Constants.R_HEADS + createProjectArgs.branch;
+    for (String branch : createProjectArgs.branch) {
+      while (branch.startsWith("/")) {
+        branch = branch.substring(1);
+      }
+      if (!branch.startsWith(Constants.R_HEADS)) {
+        branch = Constants.R_HEADS + branch;
+      }
+      if (!Repository.isValidRefName(branch)) {
+        throw new ProjectCreationFailedException(String.format(
+            "Branch \"%s\" is not a valid name.", branch));
+      }
+      if (!transformedBranches.contains(branch)) {
+        transformedBranches.add(branch);
+      }
     }
-    if (!Repository.isValidRefName(createProjectArgs.branch)) {
-      throw new ProjectCreationFailedException(String.format(
-          "Branch \"%s\" is not a valid name.", createProjectArgs.branch));
-    }
+    createProjectArgs.branch = transformedBranches;
   }
 
-  private void createEmptyCommit(final Repository repo,
-      final Project.NameKey project, final String ref) throws IOException {
+  private void createEmptyCommits(final Repository repo,
+      final Project.NameKey project, final List<String> refs)
+      throws IOException {
     ObjectInserter oi = repo.newObjectInserter();
     try {
       CommitBuilder cb = new CommitBuilder();
@@ -241,15 +272,18 @@
       ObjectId id = oi.insert(cb);
       oi.flush();
 
-      RefUpdate ru = repo.updateRef(Constants.HEAD);
-      ru.setNewObjectId(id);
-      final Result result = ru.update();
-      switch (result) {
-        case NEW:
-          replication.scheduleUpdate(project, ref);
-          break;
-        default: {
-          throw new IOException(result.name());
+      for (String ref : refs) {
+        RefUpdate ru = repo.updateRef(ref);
+        ru.setNewObjectId(id);
+        final Result result = ru.update();
+        switch (result) {
+          case NEW:
+            referenceUpdated.fire(project, ref);
+            break;
+          default: {
+            throw new IOException(String.format(
+              "Failed to create ref \"%s\": %s", ref, result.name()));
+          }
         }
       }
     } catch (IOException e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProjectArgs.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProjectArgs.java
index 98adf85..2dee4f4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProjectArgs.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProjectArgs.java
@@ -30,7 +30,7 @@
   public boolean contributorAgreements;
   public boolean signedOffBy;
   public boolean permissionsOnly;
-  public String branch;
+  public List<String> branch;
   public boolean contentMerge;
   public boolean changeIdRequired;
   public boolean createEmptyCommit;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java
index 95a8b77..e5a11ca 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
@@ -14,10 +14,15 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.common.collect.Maps;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.Project.NameKey;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.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;
 import com.google.inject.Inject;
 
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
@@ -35,7 +40,10 @@
 import java.io.PrintWriter;
 import java.io.UnsupportedEncodingException;
 import java.util.Arrays;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
+import java.util.Set;
 import java.util.SortedSet;
 import java.util.TreeMap;
 import java.util.TreeSet;
@@ -51,6 +59,12 @@
         return !PERMISSIONS.matches(git);
       }
     },
+    PARENT_CANDIDATES {
+      @Override
+      boolean matches(Repository git) {
+        return true;
+      }
+    },
     PERMISSIONS {
       @Override
       boolean matches(Repository git) throws IOException {
@@ -75,6 +89,9 @@
   private final GitRepositoryManager repoManager;
   private final ProjectNode.Factory projectNodeFactory;
 
+  @Option(name = "--format", metaVar = "FMT", usage = "Output display format")
+  private OutputFormat format = OutputFormat.TEXT;
+
   @Option(name = "--show-branch", aliases = {"-b"}, multiValued = true,
       usage = "displays the sha of each project in the specified branch")
   private List<String> showBranch;
@@ -93,6 +110,11 @@
   @Option(name = "--all", usage = "display all projects that are accessible by the calling user")
   private boolean all;
 
+  @Option(name = "--limit", aliases = {"-n"}, metaVar = "CNT", usage = "maximum number of projects to list")
+  private int limit;
+
+  private String matchPrefix;
+
   @Inject
   protected ListProjects(CurrentUser currentUser, ProjectCache projectCache,
       GitRepositoryManager repoManager,
@@ -115,6 +137,20 @@
     return showDescription;
   }
 
+  public OutputFormat getFormat() {
+    return format;
+  }
+
+  public ListProjects setFormat(OutputFormat fmt) {
+    this.format = fmt;
+    return this;
+  }
+
+  public ListProjects setMatchPrefix(String prefix) {
+    this.matchPrefix = prefix;
+    return this;
+  }
+
   public void display(OutputStream out) {
     final PrintWriter stdout;
     try {
@@ -124,88 +160,153 @@
       throw new RuntimeException("JVM lacks UTF-8 encoding", e);
     }
 
+    int found = 0;
+    Map<String, ProjectInfo> output = Maps.newTreeMap();
+    Map<String, String> hiddenNames = Maps.newHashMap();
+    Set<String> rejected = new HashSet<String>();
+
     final TreeMap<Project.NameKey, ProjectNode> treeMap =
         new TreeMap<Project.NameKey, ProjectNode>();
     try {
-      for (final Project.NameKey projectName : projectCache.all()) {
+      for (final Project.NameKey projectName : scan()) {
         final ProjectState e = projectCache.get(projectName);
         if (e == null) {
           // If we can't get it from the cache, pretend its not present.
           //
           continue;
         }
-
-        final ProjectControl pctl = e.controlFor(currentUser);
-        final boolean isVisible = pctl.isVisible() || (all && pctl.isOwner());
-        if (showTree) {
-          treeMap.put(projectName,
-              projectNodeFactory.create(pctl.getProject(), isVisible));
-          continue;
-        }
-
-        if (!isVisible) {
-          // Require the project itself to be visible to the user.
-          //
-          continue;
-        }
-
-        try {
-          if (showBranch != null) {
-            Repository git = repoManager.openRepository(projectName);
-            try {
-              if (!type.matches(git)) {
-                continue;
-              }
-
-              List<Ref> refs = getBranchRefs(projectName, pctl);
-              if (!hasValidRef(refs)) {
-               continue;
-              }
-
-              for (Ref ref : refs) {
-                if (ref == null) {
-                  // Print stub (forty '-' symbols)
-                  stdout.print("----------------------------------------");
-                } else {
-                  stdout.print(ref.getObjectId().name());
-                }
-                stdout.print(' ');
-              }
-            } finally {
-              git.close();
+        ProjectInfo info = new ProjectInfo();
+        if (type == FilterType.PARENT_CANDIDATES) {
+          ProjectState parentState = e.getParentState();
+          if (parentState != null
+              && !output.keySet().contains(parentState.getProject().getName())
+              && !rejected.contains(parentState.getProject().getName())) {
+            ProjectControl parentCtrl = parentState.controlFor(currentUser);
+            if (parentCtrl.isVisible() || parentCtrl.isOwner()) {
+              info.name = parentState.getProject().getName();
+              info.description = parentState.getProject().getDescription();
+            } else {
+              rejected.add(parentState.getProject().getName());
+              continue;
             }
-
-          } else if (type != FilterType.ALL) {
-            Repository git = repoManager.openRepository(projectName);
-            try {
-              if (!type.matches(git)) {
-                continue;
-              }
-            } finally {
-              git.close();
-            }
+          } else {
+            continue;
           }
 
-        } catch (RepositoryNotFoundException err) {
-          // If the Git repository is gone, the project doesn't actually exist anymore.
-          continue;
-        } catch (IOException err) {
-          log.warn("Unexpected error reading " + projectName, err);
+        } else {
+          final ProjectControl pctl = e.controlFor(currentUser);
+          final boolean isVisible = pctl.isVisible() || (all && pctl.isOwner());
+          if (showTree && !format.isJson()) {
+            treeMap.put(projectName,
+                projectNodeFactory.create(pctl.getProject(), isVisible));
+            continue;
+          }
+
+          if (!isVisible && !(showTree && pctl.isOwner())) {
+            // Require the project itself to be visible to the user.
+            //
+            continue;
+          }
+
+          info.name = projectName.get();
+          if (showTree && format.isJson()) {
+            ProjectState parent = e.getParentState();
+            if (parent != null) {
+              ProjectControl parentCtrl = parent.controlFor(currentUser);
+              if (parentCtrl.isVisible() || parentCtrl.isOwner()) {
+                info.parent = parent.getProject().getName();
+              } else {
+                info.parent = hiddenNames.get(parent.getProject().getName());
+                if (info.parent == null) {
+                  info.parent = "?-" + (hiddenNames.size() + 1);
+                  hiddenNames.put(parent.getProject().getName(), info.parent);
+                }
+              }
+            }
+          }
+          if (showDescription && !e.getProject().getDescription().isEmpty()) {
+            info.description = e.getProject().getDescription();
+          }
+
+          try {
+            if (showBranch != null) {
+              Repository git = repoManager.openRepository(projectName);
+              try {
+                if (!type.matches(git)) {
+                  continue;
+                }
+
+                List<Ref> refs = getBranchRefs(projectName, pctl);
+                if (!hasValidRef(refs)) {
+                  continue;
+                }
+
+                for (int i = 0; i < showBranch.size(); i++) {
+                  Ref ref = refs.get(i);
+                  if (ref != null && ref.getObjectId() != null) {
+                    if (info.branches == null) {
+                      info.branches = Maps.newLinkedHashMap();
+                    }
+                    info.branches.put(showBranch.get(i), ref.getObjectId().name());
+                  }
+                }
+              } finally {
+                git.close();
+              }
+            } else if (!showTree && type != FilterType.ALL) {
+              Repository git = repoManager.openRepository(projectName);
+              try {
+                if (!type.matches(git)) {
+                  continue;
+                }
+              } finally {
+                git.close();
+              }
+            }
+
+          } catch (RepositoryNotFoundException err) {
+            // If the Git repository is gone, the project doesn't actually exist anymore.
+            continue;
+          } catch (IOException err) {
+            log.warn("Unexpected error reading " + projectName, err);
+            continue;
+          }
+        }
+
+        if (limit > 0 && ++found > limit) {
+          break;
+        }
+
+        if (format.isJson()) {
+          output.put(info.name, info);
           continue;
         }
 
-        stdout.print(projectName.get());
+        if (showBranch != null) {
+          for (String name : showBranch) {
+            String ref = info.branches != null ? info.branches.get(name) : null;
+            if (ref == null) {
+              // Print stub (forty '-' symbols)
+              ref = "----------------------------------------";
+            }
+            stdout.print(ref);
+            stdout.print(' ');
+          }
+        }
+        stdout.print(info.name);
 
-        String desc;
-        if (showDescription && !(desc = e.getProject().getDescription()).isEmpty()) {
+        if (info.description != null) {
           // We still want to list every project as one-liners, hence escaping \n.
-          stdout.print(" - " + desc.replace("\n", "\\n"));
+          stdout.print(" - " + StringUtil.escapeString(info.description));
         }
-
-        stdout.print("\n");
+        stdout.print('\n');
       }
 
-      if (showTree && treeMap.size() > 0) {
+      if (format.isJson()) {
+        format.newGson().toJson(
+            output, new TypeToken<Map<String, ProjectInfo>>() {}.getType(), stdout);
+        stdout.print('\n');
+      } else if (showTree && treeMap.size() > 0) {
         printProjectTree(stdout, treeMap);
       }
     } finally {
@@ -213,6 +314,14 @@
     }
   }
 
+  private Iterable<NameKey> scan() {
+    if (matchPrefix != null) {
+      return projectCache.byName(matchPrefix);
+    } else {
+      return projectCache.all();
+    }
+  }
+
   private void printProjectTree(final PrintWriter stdout,
       final TreeMap<Project.NameKey, ProjectNode> treeMap) {
     final SortedSet<ProjectNode> sortedNodes = new TreeSet<ProjectNode>();
@@ -270,4 +379,11 @@
     }
     return false;
   }
+
+  private static class ProjectInfo {
+    transient String name;
+    String parent;
+    String description;
+    Map<String, String> branches;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCache.java
index 70f4013..7a111311 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCache.java
@@ -32,6 +32,12 @@
   /** Invalidate the cached information about the given project. */
   public void evict(Project p);
 
+  /**
+   * Remove information about the given project from the cache. It will no
+   * longer be returned from {@link #all()}.
+   */
+  void remove(Project p);
+
   /** @return sorted iteration of projects. */
   public abstract Iterable<Project.NameKey> all();
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java
index 51d8fb2..a7fdb4e 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;
@@ -29,18 +30,23 @@
 
 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 +54,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 +70,16 @@
   }
 
   private final AllProjectsName allProjectsName;
-  private final Cache<Project.NameKey, ProjectState> byName;
-  private final Cache<ListKey,SortedSet<Project.NameKey>> list;
+  private final LoadingCache<String, ProjectState> byName;
+  private final LoadingCache<ListKey, SortedSet<Project.NameKey>> list;
   private final Lock listLock;
   private final ProjectCacheClock clock;
 
   @Inject
   ProjectCacheImpl(
       final AllProjectsName allProjectsName,
-      @Named(CACHE_NAME) final Cache<Project.NameKey, ProjectState> byName,
-      @Named(CACHE_LIST) final Cache<ListKey, SortedSet<Project.NameKey>> list,
+      @Named(CACHE_NAME) LoadingCache<String, ProjectState> byName,
+      @Named(CACHE_LIST) LoadingCache<ListKey, SortedSet<Project.NameKey>> list,
       ProjectCacheClock clock) {
     this.allProjectsName = allProjectsName;
     this.byName = byName;
@@ -99,29 +106,55 @@
    * @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) {
+      if (!(e.getCause() instanceof RepositoryNotFoundException)) {
+        log.warn(String.format("Cannot read project %s", projectName.get()), e);
+      }
+      return null;
+    }
   }
 
   /** Invalidate the cached information about the given project. */
   public void evict(final Project p) {
     if (p != null) {
-      byName.remove(p.getNameKey());
+      byName.invalidate(p.getNameKey().get());
     }
   }
 
   @Override
+  public void remove(final Project p) {
+    listLock.lock();
+    try {
+      SortedSet<Project.NameKey> n = Sets.newTreeSet(list.get(ListKey.ALL));
+      n.remove(p.getNameKey());
+      list.put(ListKey.ALL, Collections.unmodifiableSortedSet(n));
+    } catch (ExecutionException e) {
+      log.warn("Cannot list avaliable projects", e);
+    } finally {
+      listLock.unlock();
+    }
+    evict(p);
+  }
+
+  @Override
   public void onCreateProject(Project.NameKey newProjectName) {
     listLock.lock();
     try {
-      SortedSet<Project.NameKey> n = list.get(ListKey.ALL);
-      n = new TreeSet<Project.NameKey>(n);
+      SortedSet<Project.NameKey> n = Sets.newTreeSet(list.get(ListKey.ALL));
       n.add(newProjectName);
       list.put(ListKey.ALL, Collections.unmodifiableSortedSet(n));
+    } catch (ExecutionException e) {
+      log.warn("Cannot list avaliable projects", e);
     } finally {
       listLock.unlock();
     }
@@ -129,18 +162,28 @@
 
   @Override
   public Iterable<Project.NameKey> all() {
-    return list.get(ListKey.ALL);
+    try {
+      return list.get(ListKey.ALL);
+    } catch (ExecutionException e) {
+      log.warn("Cannot list available projects", e);
+      return Collections.emptyList();
+    }
   }
 
   @Override
   public Iterable<Project.NameKey> byName(final String pfx) {
+    final Iterable<Project.NameKey> src;
+    try {
+      src = list.get(ListKey.ALL).tailSet(new Project.NameKey(pfx));
+    } catch (ExecutionException e) {
+      return Collections.emptyList();
+    }
     return new Iterable<Project.NameKey>() {
       @Override
       public Iterator<Project.NameKey> iterator() {
         return new Iterator<Project.NameKey>() {
+          private Iterator<Project.NameKey> itr = src.iterator();
           private Project.NameKey next;
-          private Iterator<Project.NameKey> itr =
-              list.get(ListKey.ALL).tailSet(new Project.NameKey(pfx)).iterator();
 
           @Override
           public boolean hasNext() {
@@ -182,7 +225,7 @@
     };
   }
 
-  static class Loader extends EntryCreator<Project.NameKey, ProjectState> {
+  static class Loader extends CacheLoader<String, ProjectState> {
     private final ProjectState.Factory projectStateFactory;
     private final GitRepositoryManager mgr;
 
@@ -193,19 +236,15 @@
     }
 
     @Override
-    public ProjectState createEntry(Project.NameKey key) throws Exception {
+    public ProjectState load(String projectName) throws Exception {
+      Project.NameKey key = new Project.NameKey(projectName);
+      Repository git = mgr.openRepository(key);
       try {
-        Repository git = mgr.openRepository(key);
-        try {
-          final ProjectConfig cfg = new ProjectConfig(key);
-          cfg.load(git);
-          return projectStateFactory.create(cfg);
-        } finally {
-          git.close();
-        }
-
-      } catch (RepositoryNotFoundException notFound) {
-        return null;
+        ProjectConfig cfg = new ProjectConfig(key);
+        cfg.load(git);
+        return projectStateFactory.create(cfg);
+      } finally {
+        git.close();
       }
     }
   }
@@ -217,7 +256,7 @@
     }
   }
 
-  static class Lister extends EntryCreator<ListKey, SortedSet<Project.NameKey>> {
+  static class Lister extends CacheLoader<ListKey, SortedSet<Project.NameKey>> {
     private final GitRepositoryManager mgr;
 
     @Inject
@@ -226,7 +265,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 29a6432..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
@@ -14,37 +14,30 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.common.collect.Lists;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.Capable;
+import com.google.gerrit.common.data.ContributorAgreement;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRule;
-import com.google.gerrit.reviewdb.client.AbstractAgreement;
-import com.google.gerrit.reviewdb.client.AccountAgreement;
+import com.google.gerrit.common.data.PermissionRule.Action;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupAgreement;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.ContributorAgreement;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.ReplicationUser;
-import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.InternalUser;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.GitReceivePackGroups;
 import com.google.gerrit.server.config.GitUploadPackGroups;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
+import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -56,9 +49,6 @@
 
 /** Access control management for a user accessing a project's data. */
 public class ProjectControl {
-  private static final Logger log =
-      LoggerFactory.getLogger(ProjectControl.class);
-
   public static final int VISIBLE = 1 << 0;
   public static final int OWNER = 1 << 1;
 
@@ -124,28 +114,26 @@
   private final Set<AccountGroup.UUID> receiveGroups;
 
   private final String canonicalWebUrl;
-  private final SchemaFactory<ReviewDb> schema;
   private final CurrentUser user;
   private final ProjectState state;
-  private final GroupCache groupCache;
   private final PermissionCollection.Factory permissionFilter;
+  private final Collection<ContributorAgreement> contributorAgreements;
 
   private List<SectionMatcher> allSections;
   private Map<String, RefControl> refControls;
   private Boolean declaredOwner;
 
+
   @Inject
   ProjectControl(@GitUploadPackGroups Set<AccountGroup.UUID> uploadGroups,
       @GitReceivePackGroups Set<AccountGroup.UUID> receiveGroups,
-      final SchemaFactory<ReviewDb> schema, final GroupCache groupCache,
-      final PermissionCollection.Factory permissionFilter,
+      final ProjectCache pc, final PermissionCollection.Factory permissionFilter,
       @CanonicalWebUrl @Nullable final String canonicalWebUrl,
       @Assisted CurrentUser who, @Assisted ProjectState ps) {
     this.uploadGroups = uploadGroups;
     this.receiveGroups = receiveGroups;
-    this.schema = schema;
-    this.groupCache = groupCache;
     this.permissionFilter = permissionFilter;
+    this.contributorAgreements = pc.getAllProjects().getConfig().getContributorAgreements();
     this.canonicalWebUrl = canonicalWebUrl;
     user = who;
     state = ps;
@@ -198,7 +186,7 @@
 
   /** Can this user see this project exists? */
   public boolean isVisible() {
-    return (visibleForReplication()
+    return (user instanceof InternalUser
         || canPerformOnAnyRef(Permission.READ)) && !isHidden();
   }
 
@@ -209,14 +197,12 @@
 
   /** Can this user see all the refs in this projects? */
   public boolean allRefsAreVisible() {
-    return visibleForReplication()
-        || canPerformOnAllRefs(Permission.READ);
+    return allRefsAreVisibleExcept(Collections.<String> emptySet());
   }
 
-  /** Is this project completely visible for replication? */
-  boolean visibleForReplication() {
-    return user instanceof ReplicationUser
-        && ((ReplicationUser) user).isEverythingVisible();
+  public boolean allRefsAreVisibleExcept(Set<String> except) {
+    return user instanceof InternalUser
+        || canPerformOnAllRefs(Permission.READ, except);
   }
 
   /** Is this user a project owner? Ownership does not imply {@link #isVisible()} */
@@ -247,12 +233,7 @@
     }
     Project project = state.getProject();
     if (project.isUseContributorAgreements()) {
-      try {
-        return verifyActiveContributorAgreement();
-      } catch (OrmException e) {
-        log.error("Cannot query database for agreements", e);
-        return new Capable("Cannot verify contribution agreement");
-      }
+      return verifyActiveContributorAgreement();
     }
     return Capable.OK;
   }
@@ -270,119 +251,60 @@
     return all;
   }
 
-  private Capable verifyActiveContributorAgreement() throws OrmException {
+  private Capable verifyActiveContributorAgreement() {
     if (! (user instanceof IdentifiedUser)) {
       return new Capable("Must be logged in to verify Contributor Agreement");
     }
     final IdentifiedUser iUser = (IdentifiedUser) user;
-    final ReviewDb db = schema.open();
 
-    AbstractAgreement bestAgreement = null;
-    ContributorAgreement bestCla = null;
-    try {
+    boolean hasContactInfo = !missing(iUser.getAccount().getFullName())
+        && !missing(iUser.getAccount().getPreferredEmail())
+        && iUser.getAccount().isContactFiled();
 
-      OUTER: for (AccountGroup.UUID groupUUID : iUser.getEffectiveGroups().getKnownGroups()) {
-        AccountGroup group = groupCache.get(groupUUID);
-        if (group == null) {
-          continue;
-        }
-
-        final List<AccountGroupAgreement> temp =
-            db.accountGroupAgreements().byGroup(group.getId()).toList();
-
-        Collections.reverse(temp);
-
-        for (final AccountGroupAgreement a : temp) {
-          final ContributorAgreement cla =
-              db.contributorAgreements().get(a.getAgreementId());
-          if (cla == null) {
-            continue;
-          }
-
-          bestAgreement = a;
-          bestCla = cla;
-          break OUTER;
-        }
+    List<AccountGroup.UUID> okGroupIds = Lists.newArrayList();
+    List<AccountGroup.UUID> missingInfoGroupIds = Lists.newArrayList();
+    for (ContributorAgreement ca : contributorAgreements) {
+      List<AccountGroup.UUID> groupIds;
+      if (hasContactInfo || !ca.isRequireContactInformation()) {
+        groupIds = okGroupIds;
+      } else {
+        groupIds = missingInfoGroupIds;
       }
 
-      if (bestAgreement == null) {
-        final List<AccountAgreement> temp =
-            db.accountAgreements().byAccount(iUser.getAccountId()).toList();
+      for (PermissionRule rule : ca.getAccepted()) {
+        if ((rule.getAction() == Action.ALLOW) && (rule.getGroup() != null)
+            && (rule.getGroup().getUUID() != null)) {
+          groupIds.add(new AccountGroup.UUID(rule.getGroup().getUUID().get()));
+        }
+      }
+    }
 
-        Collections.reverse(temp);
+    if (iUser.getEffectiveGroups().containsAnyOf(okGroupIds)) {
+      return Capable.OK;
+    }
 
-        for (final AccountAgreement a : temp) {
-          final ContributorAgreement cla =
-              db.contributorAgreements().get(a.getAgreementId());
-          if (cla == null) {
-            continue;
-          }
-
-          bestAgreement = a;
-          bestCla = cla;
+    if (iUser.getEffectiveGroups().containsAnyOf(missingInfoGroupIds)) {
+      final StringBuilder msg = new StringBuilder();
+      for (ContributorAgreement ca : contributorAgreements) {
+        if (ca.isRequireContactInformation()) {
+          msg.append(ca.getName());
           break;
         }
       }
-    } finally {
-      db.close();
-    }
-
-
-    if (bestCla != null && !bestCla.isActive()) {
-      final StringBuilder msg = new StringBuilder();
-      msg.append(bestCla.getShortName());
-      msg.append(" contributor agreement is expired.\n");
+      msg.append(" contributor agreement requires");
+      msg.append(" current contact information.\n");
       if (canonicalWebUrl != null) {
-        msg.append("\nPlease complete a new agreement");
+        msg.append("\nPlease review your contact information");
         msg.append(":\n\n  ");
         msg.append(canonicalWebUrl);
         msg.append("#");
-        msg.append(PageLinks.SETTINGS_AGREEMENTS);
+        msg.append(PageLinks.SETTINGS_CONTACT);
         msg.append("\n");
       }
       msg.append("\n");
       return new Capable(msg.toString());
     }
 
-    if (bestCla != null && bestCla.isRequireContactInformation()) {
-      boolean fail = false;
-      fail |= missing(iUser.getAccount().getFullName());
-      fail |= missing(iUser.getAccount().getPreferredEmail());
-      fail |= !iUser.getAccount().isContactFiled();
-
-      if (fail) {
-        final StringBuilder msg = new StringBuilder();
-        msg.append(bestCla.getShortName());
-        msg.append(" contributor agreement requires");
-        msg.append(" current contact information.\n");
-        if (canonicalWebUrl != null) {
-          msg.append("\nPlease review your contact information");
-          msg.append(":\n\n  ");
-          msg.append(canonicalWebUrl);
-          msg.append("#");
-          msg.append(PageLinks.SETTINGS_CONTACT);
-          msg.append("\n");
-        }
-        msg.append("\n");
-        return new Capable(msg.toString());
-      }
-    }
-
-    if (bestAgreement != null) {
-      switch (bestAgreement.getStatus()) {
-        case VERIFIED:
-          return Capable.OK;
-        case REJECTED:
-          return new Capable(bestCla.getShortName()
-              + " contributor agreement was rejected."
-              + "\n       (rejected on " + bestAgreement.getReviewedOn()
-              + ")\n");
-        case NEW:
-          return new Capable(bestCla.getShortName()
-              + " contributor agreement is still pending review.\n");
-      }
-    }
-
     final StringBuilder msg = new StringBuilder();
     msg.append(" A Contributor Agreement must be completed before uploading");
     if (canonicalWebUrl != null) {
@@ -430,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)) {
@@ -441,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/ProjectState.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
index 6eac998..e06c948 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.project;
 
-import com.google.gerrit.common.CollectionsUtil;
+import com.google.common.collect.Lists;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.Permission;
@@ -96,20 +96,24 @@
       ? new CapabilityCollection(config.getAccessSection(AccessSection.GLOBAL_CAPABILITIES))
       : null;
 
-    HashSet<AccountGroup.UUID> groups = new HashSet<AccountGroup.UUID>();
-    AccessSection all = config.getAccessSection(AccessSection.ALL);
-    if (all != null) {
-      Permission owner = all.getPermission(Permission.OWNER);
-      if (owner != null) {
-        for (PermissionRule rule : owner.getRules()) {
-          GroupReference ref = rule.getGroup();
-          if (ref.getUUID() != null) {
-            groups.add(ref.getUUID());
+    if (isAllProjects && !Permission.canBeOnAllProjects(AccessSection.ALL, Permission.OWNER)) {
+      localOwners = Collections.emptySet();
+    } else {
+      HashSet<AccountGroup.UUID> groups = new HashSet<AccountGroup.UUID>();
+      AccessSection all = config.getAccessSection(AccessSection.ALL);
+      if (all != null) {
+        Permission owner = all.getPermission(Permission.OWNER);
+        if (owner != null) {
+          for (PermissionRule rule : owner.getRules()) {
+            GroupReference ref = rule.getGroup();
+            if (ref.getUUID() != null) {
+              groups.add(ref.getUUID());
+            }
           }
         }
       }
+      localOwners = Collections.unmodifiableSet(groups);
     }
-    localOwners = Collections.unmodifiableSet(groups);
   }
 
   boolean needsRefresh(long generation) {
@@ -176,6 +180,18 @@
       Collection<AccessSection> fromConfig = config.getAccessSections();
       sm = new ArrayList<SectionMatcher>(fromConfig.size());
       for (AccessSection section : fromConfig) {
+        if (isAllProjects) {
+          List<Permission> copy =
+              Lists.newArrayListWithCapacity(section.getPermissions().size());
+          for (Permission p : section.getPermissions()) {
+            if (Permission.canBeOnAllProjects(section.getName(), p.getName())) {
+              copy.add(p);
+            }
+          }
+          section = new AccessSection(section.getName());
+          section.setPermissions(copy);
+        }
+
         SectionMatcher matcher = SectionMatcher.wrap(section);
         if (matcher != null) {
           sm.add(matcher);
@@ -275,4 +291,8 @@
     }
     return projectCache.get(getProject().getParent(allProjectsName));
   }
+
+  public boolean isAllProjects() {
+    return isAllProjects;
+  }
 }
\ No newline at end of file
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
index db370e0..a6182d1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.InternalUser;
 import com.google.gerrit.server.git.GitRepositoryManager;
 
 import dk.brics.automaton.RegExp;
@@ -101,7 +102,7 @@
 
   /** Can this user see this reference exists? */
   public boolean isVisible() {
-    return (projectControl.visibleForReplication() || canPerform(Permission.READ))
+    return (getCurrentUser() instanceof InternalUser || canPerform(Permission.READ))
         && canRead();
   }
 
@@ -154,7 +155,14 @@
       // rules. Allowing this to be done by a non-project-owner opens
       // a security hole enabling editing of access rules, and thus
       // granting of powers beyond pushing to the configuration.
-      return false;
+
+      // On the AllProjects project the owner access right cannot be assigned,
+      // this why for the AllProjects project we allow administrators to push
+      // configuration changes if they have push without being project owner.
+      if (!(projectControl.getProjectState().isAllProjects() &&
+          getCurrentUser().getCapabilities().canAdministrateServer())) {
+        return false;
+      }
     }
     return canPerform(Permission.PUSH)
         && canWrite();
@@ -309,6 +317,11 @@
     return canPerform(Permission.FORGE_SERVER);
   }
 
+  /** @return true if this user can abandon a change for this ref */
+  public boolean canAbandon() {
+    return canPerform(Permission.ABANDON);
+  }
+
   /** All value ranges of any allowed label permission. */
   public List<PermissionRange> getLabelRanges() {
     List<PermissionRange> r = new ArrayList<PermissionRange>();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionSortCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionSortCache.java
index 047997e..db879de 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 org.slf4j.Logger;
@@ -44,9 +43,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);
       }
     };
@@ -66,7 +63,7 @@
     }
 
     EntryKey key = new EntryKey(ref, sections);
-    EntryVal val = cache.get(key);
+    EntryVal val = cache.getIfPresent(key);
     if (val != null) {
       int[] srcIdx = val.order;
       if (srcIdx != null) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/Predicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/Predicate.java
index ce47d2d..b0f12b5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/Predicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/Predicate.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.query;
 
+import com.google.common.collect.Iterables;
 import com.google.gwtorm.server.OrmException;
 
 import java.util.Collection;
@@ -42,25 +43,43 @@
  * @type <T> type of object the predicate can evaluate in memory.
  */
 public abstract class Predicate<T> {
+  /** A predicate that matches any input, always, with no cost. */
+  @SuppressWarnings("unchecked")
+  public static <T> Predicate<T> any() {
+    return (Predicate<T>) Any.INSTANCE;
+  }
+
   /** Combine the passed predicates into a single AND node. */
   public static <T> Predicate<T> and(final Predicate<T>... that) {
+    if (that.length == 1) {
+      return that[0];
+    }
     return new AndPredicate<T>(that);
   }
 
   /** Combine the passed predicates into a single AND node. */
   public static <T> Predicate<T> and(
       final Collection<? extends Predicate<T>> that) {
+    if (that.size() == 1) {
+      return Iterables.getOnlyElement(that);
+    }
     return new AndPredicate<T>(that);
   }
 
   /** Combine the passed predicates into a single OR node. */
   public static <T> Predicate<T> or(final Predicate<T>... that) {
+    if (that.length == 1) {
+      return that[0];
+    }
     return new OrPredicate<T>(that);
   }
 
   /** Combine the passed predicates into a single OR node. */
   public static <T> Predicate<T> or(
       final Collection<? extends Predicate<T>> that) {
+    if (that.size() == 1) {
+      return Iterables.getOnlyElement(that);
+    }
     return new OrPredicate<T>(that);
   }
 
@@ -107,4 +126,36 @@
 
   @Override
   public abstract boolean equals(Object other);
+
+  private static class Any<T> extends Predicate<T> {
+    private static final Any<Object> INSTANCE = new Any<Object>();
+
+    private Any() {
+    }
+
+    @Override
+    public Predicate<T> copy(Collection<? extends Predicate<T>> children) {
+      return this;
+    }
+
+    @Override
+    public boolean match(T object) {
+      return true;
+    }
+
+    @Override
+    public int getCost() {
+      return 0;
+    }
+
+    @Override
+    public int hashCode() {
+      return System.identityHashCode(this);
+    }
+
+    @Override
+    public boolean equals(Object other) {
+      return other == this;
+    }
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
index 7a85b6f..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
@@ -14,11 +14,14 @@
 
 package com.google.gerrit.server.query.change;
 
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
 import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSet.Id;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.TrackingId;
@@ -28,7 +31,10 @@
 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;
 import com.google.inject.Provider;
 
 import org.eclipse.jgit.lib.ObjectId;
@@ -46,9 +52,61 @@
 import java.util.Map;
 
 public class ChangeData {
+  public static void ensureChangeLoaded(
+      Provider<ReviewDb> db, List<ChangeData> changes) throws OrmException {
+    Map<Change.Id, ChangeData> missing = Maps.newHashMap();
+    for (ChangeData cd : changes) {
+      if (cd.change == null) {
+        missing.put(cd.getId(), cd);
+      }
+    }
+    if (!missing.isEmpty()) {
+      for (Change change : db.get().changes().get(missing.keySet())) {
+        missing.get(change.getId()).change = change;
+      }
+    }
+  }
+
+  public static void ensureCurrentPatchSetLoaded(
+      Provider<ReviewDb> db, List<ChangeData> changes) throws OrmException {
+    Map<PatchSet.Id, ChangeData> missing = Maps.newHashMap();
+    for (ChangeData cd : changes) {
+      if (cd.currentPatchSet == null && cd.patches == null) {
+        missing.put(cd.change(db).currentPatchSetId(), cd);
+      }
+    }
+    if (!missing.isEmpty()) {
+      for (PatchSet ps : db.get().patchSets().get(missing.keySet())) {
+        ChangeData cd = missing.get(ps.getId());
+        cd.currentPatchSet = ps;
+        cd.patches = Lists.newArrayList(ps);
+      }
+    }
+  }
+
+  public static void ensureCurrentApprovalsLoaded(
+      Provider<ReviewDb> db, List<ChangeData> changes) throws OrmException {
+    List<ResultSet<PatchSetApproval>> pending = Lists.newArrayList();
+    for (ChangeData cd : changes) {
+      if (cd.currentApprovals == null && cd.approvals == null) {
+        pending.add(db.get().patchSetApprovals()
+            .byPatchSet(cd.change(db).currentPatchSetId()));
+      }
+    }
+    if (!pending.isEmpty()) {
+      int idx = 0;
+      for (ChangeData cd : changes) {
+        if (cd.currentApprovals == null && cd.approvals == null) {
+          cd.currentApprovals = pending.get(idx++).toList();
+        }
+      }
+    }
+  }
+
   private final Change.Id legacyId;
   private Change change;
   private String commitMessage;
+  private PatchSet currentPatchSet;
   private Collection<PatchSet> patches;
   private Collection<PatchSetApproval> approvals;
   private Map<PatchSet.Id,Collection<PatchSetApproval>> approvalsMap;
@@ -57,6 +115,7 @@
   private Collection<PatchLineComment> comments;
   private Collection<TrackingId> trackingIds;
   private CurrentUser visibleTo;
+  private ChangeControl changeControl;
   private List<ChangeMessage> messages;
 
   public ChangeData(final Change.Id id) {
@@ -84,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())) {
@@ -125,8 +191,13 @@
     return visibleTo == user;
   }
 
-  void cacheVisibleTo(CurrentUser user) {
-    visibleTo = user;
+  ChangeControl changeControl() {
+    return changeControl;
+  }
+
+  void cacheVisibleTo(ChangeControl ctl) {
+    visibleTo = ctl.getCurrentUser();
+    changeControl = ctl;
   }
 
   public Change change(Provider<ReviewDb> db) throws OrmException {
@@ -137,16 +208,19 @@
   }
 
   public PatchSet currentPatchSet(Provider<ReviewDb> db) throws OrmException {
-    Change c = change(db);
-    if (c == null) {
-      return null;
-    }
-    for (PatchSet p : patches(db)) {
-      if (p.getId().equals(c.currentPatchSetId())) {
-        return p;
+    if (currentPatchSet == null) {
+      Change c = change(db);
+      if (c == null) {
+        return null;
+      }
+      for (PatchSet p : patches(db)) {
+        if (p.getId().equals(c.currentPatchSetId())) {
+          currentPatchSet = p;
+          return p;
+        }
       }
     }
-    return null;
+    return currentPatchSet;
   }
 
   public Collection<PatchSetApproval> currentApprovals(Provider<ReviewDb> db)
@@ -155,24 +229,21 @@
       Change c = change(db);
       if (c == null) {
         currentApprovals = Collections.emptyList();
+      } else if (approvals != null) {
+        Map<Id, Collection<PatchSetApproval>> map = approvalsMap(db);
+        currentApprovals = map.get(c.currentPatchSetId());
+        if (currentApprovals == null) {
+          currentApprovals = Collections.emptyList();
+          map.put(c.currentPatchSetId(), currentApprovals);
+        }
       } else {
-        currentApprovals = approvalsFor(db, c.currentPatchSetId());
+        currentApprovals = db.get().patchSetApprovals()
+            .byPatchSet(c.currentPatchSetId()).toList();
       }
     }
     return currentApprovals;
   }
 
-  public Collection<PatchSetApproval> approvalsFor(Provider<ReviewDb> db,
-      PatchSet.Id psId) throws OrmException {
-    List<PatchSetApproval> r = new ArrayList<PatchSetApproval>();
-    for (PatchSetApproval p : approvals(db)) {
-      if (p.getPatchSetId().equals(psId)) {
-        r.add(p);
-      }
-    }
-    return r;
-  }
-
   public String commitMessage(GitRepositoryManager repoManager,
       Provider<ReviewDb> db) throws IOException, OrmException {
     if (commitMessage == null) {
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 4d8e806..e80ad67 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
@@ -14,7 +14,9 @@
 
 package com.google.gerrit.server.query.change;
 
+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;
@@ -25,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;
@@ -44,6 +47,7 @@
 
 import java.util.ArrayList;
 import java.util.Collection;
+import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
@@ -67,6 +71,9 @@
   private static final Pattern PAT_LABEL =
       Pattern.compile("^[a-zA-Z][a-zA-Z0-9]*((=|>=|<=)[+-]?|[+-])\\d+$");
 
+  // NOTE: As new search operations are added, please keep the
+  // SearchSuggestOracle up to date.
+
   public static final String FIELD_AGE = "age";
   public static final String FIELD_BRANCH = "branch";
   public static final String FIELD_CHANGE = "change";
@@ -103,7 +110,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;
@@ -117,7 +124,8 @@
         CapabilityControl.Factory capabilityControlFactory,
         ChangeControl.Factory changeControlFactory,
         ChangeControl.GenericFactory changeControlGenericFactory,
-        AccountResolver accountResolver, GroupCache groupCache,
+        AccountResolver accountResolver,
+        GroupBackend groupBackend,
         ApprovalTypes approvalTypes,
         AllProjectsName allProjectsName,
         PatchListCache patchListCache,
@@ -130,7 +138,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;
@@ -206,10 +214,7 @@
     }
 
     if ("draft".equalsIgnoreCase(value)) {
-      if (currentUser instanceof IdentifiedUser) {
-        return new HasDraftByPredicate(args.dbProvider,
-            ((IdentifiedUser) currentUser).getAccountId());
-      }
+      return new HasDraftByPredicate(args.dbProvider, self());
     }
 
     throw new IllegalArgumentException();
@@ -233,6 +238,14 @@
       return new IsReviewedPredicate(args.dbProvider);
     }
 
+    if ("owner".equalsIgnoreCase(value)) {
+      return new OwnerPredicate(args.dbProvider, self());
+    }
+
+    if ("reviewer".equalsIgnoreCase(value)) {
+      return new ReviewerPredicate(args.dbProvider, self());
+    }
+
     try {
       return status(value);
     } catch (IllegalArgumentException e) {
@@ -303,58 +316,68 @@
   @Operator
   public Predicate<ChangeData> starredby(String who)
       throws QueryParseException, OrmException {
-    Account account = args.accountResolver.find(who);
-    if (account == null) {
-      throw error("User " + who + " not found");
+    if ("self".equals(who)) {
+      return new IsStarredByPredicate(args.dbProvider, currentUser);
     }
-    return new IsStarredByPredicate(args.dbProvider, //
-        args.userFactory.create(args.dbProvider, account.getId()));
+    Set<Account.Id> m = parseAccount(who);
+    List<IsStarredByPredicate> p = Lists.newArrayListWithCapacity(m.size());
+    for (Account.Id id : m) {
+      p.add(new IsStarredByPredicate(args.dbProvider,
+          args.userFactory.create(args.dbProvider, id)));
+    }
+    return Predicate.or(p);
   }
 
   @Operator
   public Predicate<ChangeData> watchedby(String who)
       throws QueryParseException, OrmException {
-    Account account = args.accountResolver.find(who);
-    if (account == null) {
-      throw error("User " + who + " not found");
+    Set<Account.Id> m = parseAccount(who);
+    List<IsWatchedByPredicate> p = Lists.newArrayListWithCapacity(m.size());
+    for (Account.Id id : m) {
+      if (currentUser instanceof IdentifiedUser
+          && id.equals(((IdentifiedUser) currentUser).getAccountId())) {
+        p.add(new IsWatchedByPredicate(args, currentUser));
+      } else {
+        p.add(new IsWatchedByPredicate(args,
+            args.userFactory.create(args.dbProvider, id)));
+      }
     }
-    return new IsWatchedByPredicate(args, args.userFactory.create(
-        args.dbProvider, account.getId()));
+    return Predicate.or(p);
   }
 
   @Operator
   public Predicate<ChangeData> draftby(String who) throws QueryParseException,
       OrmException {
-    Account account = args.accountResolver.find(who);
-    if (account == null) {
-      throw error("User " + who + " not found");
+    Set<Account.Id> m = parseAccount(who);
+    List<HasDraftByPredicate> p = Lists.newArrayListWithCapacity(m.size());
+    for (Account.Id id : m) {
+      p.add(new HasDraftByPredicate(args.dbProvider, id));
     }
-    return new HasDraftByPredicate(args.dbProvider, account.getId());
+    return Predicate.or(p);
   }
 
   @Operator
   public Predicate<ChangeData> visibleto(String who)
       throws QueryParseException, OrmException {
-    Account account = args.accountResolver.find(who);
-    if (account != null) {
-      return visibleto(args.userFactory
-          .create(args.dbProvider, account.getId()));
+    if ("self".equals(who)) {
+      return is_visible();
+    }
+    Set<Account.Id> m = args.accountResolver.findAll(who);
+    if (!m.isEmpty()) {
+      List<Predicate<ChangeData>> p = Lists.newArrayListWithCapacity(m.size());
+      for (Account.Id id : m) {
+        return visibleto(args.userFactory.create(args.dbProvider, id));
+      }
+      return Predicate.or(p);
     }
 
     // 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));
     }
@@ -375,57 +398,43 @@
   @Operator
   public Predicate<ChangeData> owner(String who) throws QueryParseException,
       OrmException {
-    Set<Account.Id> m = args.accountResolver.findAll(who);
-    if (m.isEmpty()) {
-      throw error("User " + who + " not found");
-    } else if (m.size() == 1) {
-      Account.Id id = m.iterator().next();
-      return new OwnerPredicate(args.dbProvider, id);
-    } else {
-      List<OwnerPredicate> p = new ArrayList<OwnerPredicate>(m.size());
-      for (Account.Id id : m) {
-        p.add(new OwnerPredicate(args.dbProvider, id));
-      }
-      return Predicate.or(p);
+    Set<Account.Id> m = parseAccount(who);
+    List<OwnerPredicate> p = Lists.newArrayListWithCapacity(m.size());
+    for (Account.Id id : m) {
+      p.add(new OwnerPredicate(args.dbProvider, id));
     }
+    return Predicate.or(p);
   }
 
   @Operator
-  public Predicate<ChangeData> ownerin(String group) throws QueryParseException,
-      OrmException {
-    AccountGroup g = args.groupCache.get(new AccountGroup.NameKey(group));
+  public Predicate<ChangeData> ownerin(String group)
+      throws QueryParseException {
+    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
   public Predicate<ChangeData> reviewer(String who)
       throws QueryParseException, OrmException {
-    Set<Account.Id> m = args.accountResolver.findAll(who);
-    if (m.isEmpty()) {
-      throw error("User " + who + " not found");
-    } else if (m.size() == 1) {
-      Account.Id id = m.iterator().next();
-      return new ReviewerPredicate(args.dbProvider, id);
-    } else {
-      List<ReviewerPredicate> p = new ArrayList<ReviewerPredicate>(m.size());
-      for (Account.Id id : m) {
-        p.add(new ReviewerPredicate(args.dbProvider, id));
-      }
-      return Predicate.or(p);
+    Set<Account.Id> m = parseAccount(who);
+    List<ReviewerPredicate> p = Lists.newArrayListWithCapacity(m.size());
+    for (Account.Id id : m) {
+      p.add(new ReviewerPredicate(args.dbProvider, id));
     }
+    return Predicate.or(p);
   }
 
   @Operator
   public Predicate<ChangeData> reviewerin(String group)
-      throws QueryParseException, OrmException {
-    AccountGroup g = args.groupCache.get(new AccountGroup.NameKey(group));
+      throws QueryParseException {
+    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
@@ -532,4 +541,23 @@
       throw error("Unsupported query:" + query);
     }
   }
+
+  private Set<Account.Id> parseAccount(String who)
+      throws QueryParseException, OrmException {
+    if ("self".equals(who)) {
+      return Collections.singleton(self());
+    }
+    Set<Account.Id> matches = args.accountResolver.findAll(who);
+    if (matches.isEmpty()) {
+      throw error("User " + who + " not found");
+    }
+    return matches;
+  }
+
+  private Account.Id self() {
+    if (currentUser instanceof IdentifiedUser) {
+      return ((IdentifiedUser) currentUser).getAccountId();
+    }
+    throw new IllegalArgumentException();
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsVisibleToPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsVisibleToPredicate.java
index 413e6c4..b73465a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsVisibleToPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsVisibleToPredicate.java
@@ -55,15 +55,18 @@
     }
     try {
       Change c = cd.change(db);
-      if (c != null && changeControl.controlFor(c, user).isVisible(db.get())) {
-        cd.cacheVisibleTo(user);
-        return true;
-      } else {
+      if (c == null) {
         return false;
       }
+
+      ChangeControl cc = changeControl.controlFor(c, user);
+      if (cc.isVisible(db.get())) {
+        cd.cacheVisibleTo(cc);
+        return true;
+      }
     } catch (NoSuchChangeException e) {
-      return false;
     }
+    return false;
   }
 
   @Override
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
new file mode 100644
index 0000000..f0ed66a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ListChanges.java
@@ -0,0 +1,672 @@
+// 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.query.change;
+
+import static com.google.gerrit.common.changes.ListChangesOption.ALL_COMMITS;
+import static com.google.gerrit.common.changes.ListChangesOption.ALL_FILES;
+import static com.google.gerrit.common.changes.ListChangesOption.ALL_REVISIONS;
+import static com.google.gerrit.common.changes.ListChangesOption.CURRENT_COMMIT;
+import static com.google.gerrit.common.changes.ListChangesOption.CURRENT_FILES;
+import static com.google.gerrit.common.changes.ListChangesOption.CURRENT_REVISION;
+import static com.google.gerrit.common.changes.ListChangesOption.LABELS;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.gerrit.common.changes.ListChangesOption;
+import com.google.gerrit.common.data.ApprovalType;
+import com.google.gerrit.common.data.ApprovalTypes;
+import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.PatchSetInfo;
+import com.google.gerrit.reviewdb.client.PatchSetInfo.ParentInfo;
+import com.google.gerrit.reviewdb.client.UserIdentity;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.AnonymousUser;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.OutputFormat;
+import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.events.AccountAttribute;
+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.patch.PatchSetInfoFactory;
+import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.query.QueryParseException;
+import com.google.gerrit.server.ssh.SshInfo;
+import com.google.gson.reflect.TypeToken;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import com.jcraft.jsch.HostKey;
+
+import org.eclipse.jgit.lib.Config;
+import org.kohsuke.args4j.Option;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.io.Writer;
+import java.sql.Timestamp;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Map;
+
+public class ListChanges {
+  private static final Logger log = LoggerFactory.getLogger(ListChanges.class);
+
+  @Singleton
+  static class Urls {
+    final String git;
+    final String http;
+
+    @Inject
+    Urls(@GerritServerConfig Config cfg) {
+      this.git = ensureSlash(cfg.getString("gerrit", null, "canonicalGitUrl"));
+      this.http = ensureSlash(cfg.getString("gerrit", null, "gitHttpUrl"));
+    }
+
+    private static String ensureSlash(String in) {
+      if (in != null && !in.endsWith("/")) {
+        return in + "/";
+      }
+      return in;
+    }
+  }
+
+  private final QueryProcessor imp;
+  private final Provider<ReviewDb> db;
+  private final ApprovalTypes approvalTypes;
+  private final CurrentUser user;
+  private final AnonymousUser anonymous;
+  private final ChangeControl.Factory changeControlFactory;
+  private final PatchSetInfoFactory patchSetInfoFactory;
+  private final PatchListCache patchListCache;
+  private final SshInfo sshInfo;
+  private final Provider<String> urlProvider;
+  private final Urls urls;
+  private boolean reverse;
+  private Map<Account.Id, AccountAttribute> accounts;
+  private Map<Change.Id, ChangeControl> controls;
+  private EnumSet<ListChangesOption> options;
+
+  @Option(name = "--format", metaVar = "FMT", usage = "Output display format")
+  private OutputFormat format = OutputFormat.TEXT;
+
+  @Option(name = "--query", aliases = {"-q"}, metaVar = "QUERY", multiValued = true, usage = "Query string")
+  private List<String> queries;
+
+  @Option(name = "--limit", aliases = {"-n"}, metaVar = "CNT", usage = "Maximum number of results to return")
+  public void setLimit(int limit) {
+    imp.setLimit(limit);
+  }
+
+  @Option(name = "-o", multiValued = true, usage = "Output options per change")
+  public void addOption(ListChangesOption o) {
+    options.add(o);
+  }
+
+  @Option(name = "-O", usage = "Output option flags, in hex")
+  void setOptionFlagsHex(String hex) {
+    options.addAll(ListChangesOption.fromBits(Integer.parseInt(hex, 16)));
+  }
+
+  @Option(name = "-P", metaVar = "SORTKEY", usage = "Previous changes before SORTKEY")
+  public void setSortKeyAfter(String key) {
+    // Querying for the prior page of changes requires sortkey_after predicate.
+    // Changes are shown most recent->least recent. The previous page of
+    // results contains changes that were updated after the given key.
+    imp.setSortkeyAfter(key);
+    reverse = true;
+  }
+
+  @Option(name = "-N", metaVar = "SORTKEY", usage = "Next changes after SORTKEY")
+  public void setSortKeyBefore(String key) {
+    // Querying for the next page of changes requires sortkey_before predicate.
+    // Changes are shown most recent->least recent. The next page contains
+    // changes that were updated before the given key.
+    imp.setSortkeyBefore(key);
+  }
+
+  @Inject
+  ListChanges(QueryProcessor qp,
+      Provider<ReviewDb> db,
+      ApprovalTypes at,
+      CurrentUser u,
+      AnonymousUser au,
+      ChangeControl.Factory cf,
+      PatchSetInfoFactory psi,
+      PatchListCache plc,
+      SshInfo sshInfo,
+      @CanonicalWebUrl Provider<String> curl,
+      Urls urls) {
+    this.imp = qp;
+    this.db = db;
+    this.approvalTypes = at;
+    this.user = u;
+    this.anonymous = au;
+    this.changeControlFactory = cf;
+    this.patchSetInfoFactory = psi;
+    this.patchListCache = plc;
+    this.sshInfo = sshInfo;
+    this.urlProvider = curl;
+    this.urls = urls;
+
+    accounts = Maps.newHashMap();
+    controls = Maps.newHashMap();
+    options = EnumSet.noneOf(ListChangesOption.class);
+  }
+
+  public OutputFormat getFormat() {
+    return format;
+  }
+
+  public ListChanges setFormat(OutputFormat fmt) {
+    this.format = fmt;
+    return this;
+  }
+
+  public ListChanges addQuery(String query) {
+    if (queries == null) {
+      queries = Lists.newArrayList();
+    }
+    queries.add(query);
+    return this;
+  }
+
+  public void query(Writer out)
+      throws OrmException, QueryParseException, IOException {
+    if (imp.isDisabled()) {
+      throw new QueryParseException("query disabled");
+    }
+    if (queries == null || queries.isEmpty()) {
+      queries = Collections.singletonList("status:open");
+    } else if (queries.size() > 10) {
+      // Hard-code a default maximum number of queries to prevent
+      // users from submitting too much to the server in a single call.
+      throw new QueryParseException("limit of 10 queries");
+    }
+
+    List<List<ChangeInfo>> res = Lists.newArrayListWithCapacity(queries.size());
+    for (String query : queries) {
+      List<ChangeData> changes = imp.queryChanges(query);
+      boolean moreChanges = imp.getLimit() > 0 && changes.size() > imp.getLimit();
+      if (moreChanges) {
+        if (reverse) {
+          changes = changes.subList(1, changes.size());
+        } else {
+          changes = changes.subList(0, imp.getLimit());
+        }
+      }
+      ChangeData.ensureChangeLoaded(db, changes);
+      ChangeData.ensureCurrentPatchSetLoaded(db, changes);
+      ChangeData.ensureCurrentApprovalsLoaded(db, changes);
+
+      List<ChangeInfo> info = Lists.newArrayListWithCapacity(changes.size());
+      for (ChangeData cd : changes) {
+        info.add(toChangeInfo(cd));
+      }
+      if (moreChanges && !info.isEmpty()) {
+        if (reverse) {
+          info.get(0)._moreChanges = true;
+        } else {
+          info.get(info.size() - 1)._moreChanges = true;
+        }
+      }
+      res.add(info);
+    }
+
+    if (!accounts.isEmpty()) {
+      for (Account account : db.get().accounts().get(accounts.keySet())) {
+        AccountAttribute a = accounts.get(account.getId());
+        a.name = Strings.emptyToNull(account.getFullName());
+      }
+    }
+
+    if (format.isJson()) {
+      format.newGson().toJson(
+          res.size() == 1 ? res.get(0) : res,
+          new TypeToken<List<ChangeInfo>>() {}.getType(),
+          out);
+      out.write('\n');
+    } else {
+      boolean firstQuery = true;
+      for (List<ChangeInfo> info : res) {
+        if (firstQuery) {
+          firstQuery = false;
+        } else {
+          out.write('\n');
+        }
+        for (ChangeInfo c : info) {
+          String id = new Change.Key(c.id).abbreviate();
+          String subject = c.subject;
+          if (subject.length() + id.length() > 80) {
+            subject = subject.substring(0, 80 - id.length());
+          }
+          out.write(id);
+          out.write(' ');
+          out.write(subject.replace('\n', ' '));
+          out.write('\n');
+        }
+      }
+    }
+  }
+
+  private ChangeInfo toChangeInfo(ChangeData cd) throws OrmException {
+    ChangeInfo out = new ChangeInfo();
+    Change in = cd.change(db);
+    out.project = in.getProject().get();
+    out.branch = in.getDest().getShortName();
+    out.topic = in.getTopic();
+    out.id = in.getKey().get();
+    out.subject = in.getSubject();
+    out.status = in.getStatus();
+    out.owner = asAccountAttribute(in.getOwner());
+    out.created = in.getCreatedOn();
+    out.updated = in.getLastUpdatedOn();
+    out._number = in.getId().get();
+    out._sortkey = in.getSortKey();
+    out.starred = user.getStarredChanges().contains(in.getId()) ? true : null;
+    out.reviewed = in.getStatus().isOpen() && isChangeReviewed(cd) ? true : null;
+    out.labels = options.contains(LABELS) ? labelsFor(cd) : null;
+
+    if (options.contains(ALL_REVISIONS) || options.contains(CURRENT_REVISION)) {
+      out.revisions = revisions(cd);
+      for (String commit : out.revisions.keySet()) {
+        if (out.revisions.get(commit).isCurrent) {
+          out.current_revision = commit;
+          break;
+        }
+      }
+    }
+
+    return out;
+  }
+
+  private AccountAttribute asAccountAttribute(Account.Id user) {
+    if (user == null) {
+      return null;
+    }
+    AccountAttribute a = accounts.get(user);
+    if (a == null) {
+      a = new AccountAttribute();
+      accounts.put(user, a);
+    }
+    return a;
+  }
+
+  private ChangeControl control(ChangeData cd) throws OrmException {
+    ChangeControl ctrl = cd.changeControl();
+    if (ctrl != null && ctrl.getCurrentUser() == user) {
+      return ctrl;
+    }
+
+    ctrl = controls.get(cd.getId());
+    if (ctrl != null) {
+      return ctrl;
+    }
+
+    try {
+      ctrl = changeControlFactory.controlFor(cd.change(db));
+    } catch (NoSuchChangeException e) {
+      return null;
+    }
+    controls.put(cd.getId(), ctrl);
+    return ctrl;
+  }
+
+  private Map<String, LabelInfo> labelsFor(ChangeData cd) throws OrmException {
+    ChangeControl ctl = control(cd);
+    if (ctl == null) {
+      return Collections.emptyMap();
+    }
+
+    PatchSet ps = cd.currentPatchSet(db);
+    if (ps == null) {
+      return Collections.emptyMap();
+    }
+
+    Map<String, LabelInfo> labels = Maps.newLinkedHashMap();
+    for (SubmitRecord rec : ctl.canSubmit(db.get(), ps, cd, true, false)) {
+      if (rec.labels == null) {
+        continue;
+      }
+      for (SubmitRecord.Label r : rec.labels) {
+        LabelInfo p = labels.get(r.label);
+        if (p == null || p._status.compareTo(r.status) < 0) {
+          LabelInfo n = new LabelInfo();
+          n._status = r.status;
+          switch (r.status) {
+            case OK:
+              n.approved = asAccountAttribute(r.appliedBy);
+              break;
+            case REJECT:
+              n.rejected = asAccountAttribute(r.appliedBy);
+              break;
+          }
+          n.optional = n._status == SubmitRecord.Label.Status.MAY ? true : null;
+          labels.put(r.label, n);
+        }
+      }
+    }
+
+    Collection<PatchSetApproval> approvals = null;
+    for (Map.Entry<String, LabelInfo> e : labels.entrySet()) {
+      if (e.getValue().approved != null || e.getValue().rejected != null) {
+        continue;
+      }
+
+      ApprovalType type = approvalTypes.byLabel(e.getKey());
+      if (type == null || type.getMin() == null || type.getMax() == null) {
+        // Unknown or misconfigured type can't have intermediate scores.
+        continue;
+      }
+
+      short min = type.getMin().getValue();
+      short max = type.getMax().getValue();
+      if (-1 <= min && max <= 1) {
+        // Types with a range of -1..+1 can't have intermediate scores.
+        continue;
+      }
+
+      if (approvals == null) {
+        approvals = cd.currentApprovals(db);
+      }
+      for (PatchSetApproval psa : approvals) {
+        short val = psa.getValue();
+        if (val != 0 && min < val && val < max
+            && psa.getCategoryId().equals(type.getCategory().getId())) {
+          if (0 < val) {
+            e.getValue().recommended = asAccountAttribute(psa.getAccountId());
+            e.getValue().value = val != 1 ? val : null;
+          } else {
+            e.getValue().disliked = asAccountAttribute(psa.getAccountId());
+            e.getValue().value = val != -1 ? val : null;
+          }
+        }
+      }
+    }
+    return labels;
+  }
+
+  private boolean isChangeReviewed(ChangeData cd) throws OrmException {
+    if (user instanceof IdentifiedUser) {
+      PatchSet currentPatchSet = cd.currentPatchSet(db);
+      if (currentPatchSet == null) {
+        return false;
+      }
+
+      List<ChangeMessage> messages =
+          db.get().changeMessages().byPatchSet(currentPatchSet.getId()).toList();
+
+      if (messages.isEmpty()) {
+        return false;
+      }
+
+      // Sort messages to let the most recent ones at the beginning.
+      Collections.sort(messages, new Comparator<ChangeMessage>() {
+        @Override
+        public int compare(ChangeMessage a, ChangeMessage b) {
+          return b.getWrittenOn().compareTo(a.getWrittenOn());
+        }
+      });
+
+      Account.Id currentUserId = ((IdentifiedUser) user).getAccountId();
+      Account.Id changeOwnerId = cd.change(db).getOwner();
+      for (ChangeMessage cm : messages) {
+        if (currentUserId.equals(cm.getAuthor())) {
+          return true;
+        } else if (changeOwnerId.equals(cm.getAuthor())) {
+          return false;
+        }
+      }
+    }
+    return false;
+  }
+
+  private Map<String, RevisionInfo> revisions(ChangeData cd) throws OrmException {
+    ChangeControl ctl = control(cd);
+    if (ctl == null) {
+      return Collections.emptyMap();
+    }
+
+    Collection<PatchSet> src;
+    if (options.contains(ALL_REVISIONS)) {
+      src = cd.patches(db);
+    } else {
+      src = Collections.singletonList(cd.currentPatchSet(db));
+    }
+    Map<String, RevisionInfo> res = Maps.newLinkedHashMap();
+    for (PatchSet in : src) {
+      if (ctl.isPatchVisible(in, db.get())) {
+        res.put(in.getRevision().get(), toRevisionInfo(cd, in));
+      }
+    }
+    return res;
+  }
+
+  private RevisionInfo toRevisionInfo(ChangeData cd, PatchSet in)
+      throws OrmException {
+    RevisionInfo out = new RevisionInfo();
+    out.isCurrent = in.getId().equals(cd.change(db).currentPatchSetId());
+    out._number = in.getId().get();
+    out.draft = in.isDraft() ? true : null;
+    out.fetch = makeFetchMap(cd, in);
+
+    if (options.contains(ALL_COMMITS)
+        || (out.isCurrent && options.contains(CURRENT_COMMIT))) {
+      try {
+        PatchSetInfo info = patchSetInfoFactory.get(db.get(), in.getId());
+        out.commit = new CommitInfo();
+        out.commit.parents = Lists.newArrayListWithCapacity(info.getParents().size());
+        out.commit.author = toGitPerson(info.getAuthor());
+        out.commit.committer = toGitPerson(info.getCommitter());
+        out.commit.subject = info.getSubject();
+        out.commit.message = info.getMessage();
+
+        for (ParentInfo parent : info.getParents()) {
+          CommitInfo i = new CommitInfo();
+          i.commit = parent.id.get();
+          i.subject = parent.shortMessage;
+          out.commit.parents.add(i);
+        }
+      } catch (PatchSetInfoNotAvailableException e) {
+        log.warn("Cannot load PatchSetInfo " + in.getId(), e);
+      }
+    }
+
+    if (options.contains(ALL_FILES)
+        || (out.isCurrent && options.contains(CURRENT_FILES))) {
+      PatchList list;
+      try {
+        list = patchListCache.get(cd.change(db), in);
+      } catch (PatchListNotAvailableException e) {
+        log.warn("Cannot load PatchList " + in.getId(), e);
+        list = null;
+      }
+      if (list != null) {
+        out.files = Maps.newTreeMap();
+        for (PatchListEntry e : list.getPatches()) {
+          if (Patch.COMMIT_MSG.equals(e.getNewName())) {
+            continue;
+          }
+
+          FileInfo d = new FileInfo();
+          d.status = e.getChangeType() != Patch.ChangeType.MODIFIED
+              ? e.getChangeType().getCode()
+              : null;
+          d.oldPath = e.getOldName();
+          if (e.getPatchType() == Patch.PatchType.BINARY) {
+            d.binary = true;
+          } else {
+            d.linesInserted = e.getInsertions() > 0 ? e.getInsertions() : null;
+            d.linesDeleted = e.getDeletions() > 0 ? e.getDeletions() : null;
+          }
+
+          FileInfo o = out.files.put(e.getNewName(), d);
+          if (o != null) {
+            // This should only happen on a delete-add break created by JGit
+            // when the file was rewritten and too little content survived. Write
+            // a single record with data from both sides.
+            d.status = Patch.ChangeType.REWRITE.getCode();
+            if (o.binary != null && o.binary) {
+              d.binary = true;
+            }
+            if (o.linesInserted != null) {
+              d.linesInserted = o.linesInserted;
+            }
+            if (o.linesDeleted != null) {
+              d.linesDeleted = o.linesDeleted;
+            }
+          }
+        }
+      }
+    }
+    return out;
+  }
+
+  private Map<String, FetchInfo> makeFetchMap(ChangeData cd, PatchSet in)
+      throws OrmException {
+    Map<String, FetchInfo> r = Maps.newLinkedHashMap();
+    String refName = in.getRefName();
+    ChangeControl ctl = control(cd);
+    if (ctl != null && ctl.forUser(anonymous).isPatchVisible(in, db.get())) {
+      if (urls.git != null) {
+        r.put("git", new FetchInfo(urls.git
+            + cd.change(db).getProject().get(), refName));
+      }
+    }
+    if (urls.http != null) {
+      r.put("http", new FetchInfo(urls.http
+          + cd.change(db).getProject().get(), refName));
+    } else {
+      String http = urlProvider.get();
+      if (!Strings.isNullOrEmpty(http)) {
+        r.put("http", new FetchInfo(http
+            + cd.change(db).getProject().get(), refName));
+      }
+    }
+    if (!sshInfo.getHostKeys().isEmpty()) {
+      HostKey host = sshInfo.getHostKeys().get(0);
+      r.put("ssh", new FetchInfo(String.format(
+          "ssh://%s/%s",
+          host.getHost(), cd.change(db).getProject().get()),
+          refName));
+    }
+
+    return r;
+  }
+
+  private static GitPerson toGitPerson(UserIdentity committer) {
+    GitPerson p = new GitPerson();
+    p.name = committer.getName();
+    p.email = committer.getEmail();
+    p.date = committer.getDate();
+    p.tz = committer.getTimeZone();
+    return p;
+  }
+
+  static class ChangeInfo {
+    String project;
+    String branch;
+    String topic;
+    String id;
+    String subject;
+    Change.Status status;
+    Timestamp created;
+    Timestamp updated;
+    Boolean starred;
+    Boolean reviewed;
+
+    String _sortkey;
+    int _number;
+
+    AccountAttribute owner;
+    Map<String, LabelInfo> labels;
+    String current_revision;
+    Map<String, RevisionInfo> revisions;
+
+    Boolean _moreChanges;
+  }
+
+  static class RevisionInfo {
+    private transient boolean isCurrent;
+    Boolean draft;
+    int _number;
+    Map<String, FetchInfo> fetch;
+    CommitInfo commit;
+    Map<String, FileInfo> files;
+  }
+
+  static class FetchInfo {
+    String url;
+    String ref;
+
+    FetchInfo(String url, String ref) {
+      this.url = url;
+      this.ref = ref;
+    }
+  }
+
+  static class GitPerson {
+    String name;
+    String email;
+    Timestamp date;
+    int tz;
+  }
+
+  static class CommitInfo {
+    String commit;
+    List<CommitInfo> parents;
+    GitPerson author;
+    GitPerson committer;
+    String subject;
+    String message;
+  }
+
+  static class FileInfo {
+    Character status;
+    Boolean binary;
+    String oldPath;
+    Integer linesInserted;
+    Integer linesDeleted;
+  }
+
+  static class LabelInfo {
+    transient SubmitRecord.Label.Status _status;
+    AccountAttribute approved;
+    AccountAttribute rejected;
+
+    AccountAttribute recommended;
+    AccountAttribute disliked;
+    Short value;
+    Boolean optional;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java
index a2fa7fe..f44282a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.query.change;
 
 import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.server.ReviewDb;
@@ -55,6 +56,30 @@
   private static final Logger log =
       LoggerFactory.getLogger(QueryProcessor.class);
 
+  private final Comparator<ChangeData> cmpAfter =
+      new Comparator<ChangeData>() {
+        @Override
+        public int compare(ChangeData a, ChangeData b) {
+          try {
+            return a.change(db).getSortKey().compareTo(b.change(db).getSortKey());
+          } catch (OrmException e) {
+            return 0;
+          }
+        }
+      };
+
+  private final Comparator<ChangeData> cmpBefore =
+      new Comparator<ChangeData>() {
+        @Override
+        public int compare(ChangeData a, ChangeData b) {
+          try {
+            return b.change(db).getSortKey().compareTo(a.change(db).getSortKey());
+          } catch (OrmException e) {
+            return 0;
+          }
+        }
+      };
+
   public static enum OutputFormat {
     TEXT, JSON;
   }
@@ -71,6 +96,9 @@
   private final int maxLimit;
 
   private OutputFormat outputFormat = OutputFormat.TEXT;
+  private int limit;
+  private String sortkeyAfter;
+  private String sortkeyBefore;
   private boolean includePatchSets;
   private boolean includeCurrentPatchSet;
   private boolean includeApprovals;
@@ -78,6 +106,7 @@
   private boolean includeFiles;
   private boolean includeCommitMessage;
   private boolean includeDependencies;
+  private boolean includeSubmitRecords;
 
   private OutputStream outputStream = DisabledOutputStream.INSTANCE;
   private PrintWriter out;
@@ -97,6 +126,22 @@
       .getMax();
   }
 
+  int getLimit() {
+    return limit;
+  }
+
+  void setLimit(int n) {
+    limit = n;
+  }
+
+  void setSortkeyAfter(String sortkey) {
+    sortkeyAfter = sortkey;
+  }
+
+  void setSortkeyBefore(String sortkey) {
+    sortkeyBefore = sortkey;
+  }
+
   public void setIncludePatchSets(boolean on) {
     includePatchSets = on;
   }
@@ -141,11 +186,23 @@
     includeCommitMessage = on;
   }
 
+  public void setIncludeSubmitRecords(boolean on) {
+    includeSubmitRecords = on;
+  }
+
   public void setOutput(OutputStream out, OutputFormat fmt) {
     this.outputStream = out;
     this.outputFormat = fmt;
   }
 
+  /**
+   * Query for changes that match the query string.
+   * <p>
+   * If a limit was specified using {@link #setLimit(int)} this method may
+   * return up to {@code limit + 1} results, allowing the caller to determine if
+   * there are more than {@code limit} matches and suggest to its own caller
+   * that the query could be retried with {@link #setSortkeyBefore(String)}.
+   */
   public List<ChangeData> queryChanges(final String queryString)
       throws OrmException, QueryParseException {
     final Predicate<ChangeData> visibleToMe = queryBuilder.is_visible();
@@ -175,19 +232,14 @@
       }
     }
 
-    Collections.sort(results, new Comparator<ChangeData>() {
-      @Override
-      public int compare(ChangeData a, ChangeData b) {
-        return b.getChange().getSortKey().compareTo(
-            a.getChange().getSortKey());
-      }
-    });
-
+    Collections.sort(results, sortkeyAfter != null ? cmpAfter : cmpBefore);
     int limit = limit(s);
     if (limit < results.size()) {
       results = results.subList(0, limit);
     }
-
+    if (sortkeyAfter != null) {
+      Collections.reverse(results);
+    }
     return results;
   }
 
@@ -196,7 +248,7 @@
         new BufferedWriter( //
             new OutputStreamWriter(outputStream, "UTF-8")));
     try {
-      if (maxLimit <= 0) {
+      if (isDisabled()) {
         ErrorMessage m = new ErrorMessage();
         m.message = "query disabled";
         show(m);
@@ -213,6 +265,15 @@
           eventFactory.extend(c, d.getChange());
           eventFactory.addTrackingIds(c, d.trackingIds(db));
 
+          if (includeSubmitRecords) {
+            PatchSet.Id psId = d.getChange().currentPatchSetId();
+            PatchSet patchSet = db.get().patchSets().get(psId);
+            Change.Id changeId = psId.getParentKey();
+            List<SubmitRecord> submitResult = d.changeControl().canSubmit( //
+                db.get(), patchSet, null, false, true);
+            eventFactory.addSubmitRecords(c, submitResult);
+          }
+
           if (includeCommitMessage) {
             eventFactory.addCommitMessage(c, d.commitMessage(repoManager, db));
           }
@@ -233,7 +294,7 @@
             if (current != null) {
               c.currentPatchSet = eventFactory.asPatchSetAttribute(current);
               eventFactory.addApprovals(c.currentPatchSet, //
-                  d.approvalsFor(db, current.getId()));
+                  d.currentApprovals(db));
 
               if (includeFiles) {
                 eventFactory.addPatchSetFileNames(c.currentPatchSet,
@@ -283,8 +344,13 @@
     }
   }
 
+  boolean isDisabled() {
+    return maxLimit <= 0;
+  }
+
   private int limit(Predicate<ChangeData> s) {
-    return queryBuilder.hasLimit(s) ? queryBuilder.getLimit(s) : maxLimit;
+    int n = queryBuilder.hasLimit(s) ? queryBuilder.getLimit(s) : maxLimit;
+    return limit > 0 ? Math.min(n, limit) + 1 : n;
   }
 
   @SuppressWarnings("unchecked")
@@ -293,9 +359,17 @@
 
     Predicate<ChangeData> q = queryBuilder.parse(queryString);
     if (!queryBuilder.hasSortKey(q)) {
-      q = Predicate.and(q, queryBuilder.sortkey_before("z"));
+      if (sortkeyBefore != null) {
+        q = Predicate.and(q, queryBuilder.sortkey_before(sortkeyBefore));
+      } else if (sortkeyAfter != null) {
+        q = Predicate.and(q, queryBuilder.sortkey_after(sortkeyAfter));
+      } else {
+        q = Predicate.and(q, queryBuilder.sortkey_before("z"));
+      }
     }
-    q = Predicate.and(q, queryBuilder.limit(maxLimit), visibleToMe);
+    q = Predicate.and(q,
+        queryBuilder.limit(limit > 0 ? Math.min(limit, maxLimit) + 1 : maxLimit),
+        visibleToMe);
 
     Predicate<ChangeData> s = queryRewriter.rewrite(q);
     if (!(s instanceof ChangeDataSource)) {
@@ -303,7 +377,7 @@
     }
 
     if (!(s instanceof ChangeDataSource)) {
-      throw new QueryParseException("cannot execute query: " + s);
+      throw new QueryParseException("invalid query: " + s);
     }
 
     return s;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SingleGroupUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SingleGroupUser.java
index cdce217..270b2e7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SingleGroupUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SingleGroupUser.java
@@ -27,15 +27,15 @@
 import java.util.Collections;
 import java.util.Set;
 
-final class SingleGroupUser extends CurrentUser {
+public final class SingleGroupUser extends CurrentUser {
   private final GroupMembership groups;
 
-  SingleGroupUser(CapabilityControl.Factory capabilityControlFactory,
+  public SingleGroupUser(CapabilityControl.Factory capabilityControlFactory,
       AccountGroup.UUID groupId) {
     this(capabilityControlFactory, Collections.singleton(groupId));
   }
 
-  SingleGroupUser(CapabilityControl.Factory capabilityControlFactory,
+  public SingleGroupUser(CapabilityControl.Factory capabilityControlFactory,
       Set<AccountGroup.UUID> groups) {
     super(capabilityControlFactory, AccessPath.UNKNOWN);
     this.groups = new ListGroupMembership(groups);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceProvider.java
index 2a98e96..cc48019 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceProvider.java
@@ -18,7 +18,7 @@
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static java.util.concurrent.TimeUnit.SECONDS;
 
-import com.google.gerrit.lifecycle.LifecycleListener;
+import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java
index d8809da..fd379b2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaCreator.java
@@ -32,9 +32,9 @@
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.SitePath;
 import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MetaDataUpdate;
-import com.google.gerrit.server.git.NoReplication;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gwtorm.jdbc.JdbcExecutor;
 import com.google.gwtorm.jdbc.JdbcSchema;
@@ -161,7 +161,7 @@
     anonymous =
         newGroup(c, "Anonymous Users", AccountGroup.ANONYMOUS_USERS);
     anonymous.setDescription("Any user, signed-in or not");
-    anonymous.setOwnerGroupId(admin.getId());
+    anonymous.setOwnerGroupUUID(admin.getGroupUUID());
     anonymous.setType(AccountGroup.Type.SYSTEM);
     c.accountGroups().insert(Collections.singleton(anonymous));
     c.accountGroupNames().insert(
@@ -170,7 +170,7 @@
     registered =
         newGroup(c, "Registered Users", AccountGroup.REGISTERED_USERS);
     registered.setDescription("Any signed-in user");
-    registered.setOwnerGroupId(admin.getId());
+    registered.setOwnerGroupUUID(admin.getGroupUUID());
     registered.setType(AccountGroup.Type.SYSTEM);
     c.accountGroups().insert(Collections.singleton(registered));
     c.accountGroupNames().insert(
@@ -178,7 +178,7 @@
 
     final AccountGroup batchUsers = newGroup(c, "Non-Interactive Users", null);
     batchUsers.setDescription("Users who perform batch actions on Gerrit");
-    batchUsers.setOwnerGroupId(admin.getId());
+    batchUsers.setOwnerGroupUUID(admin.getGroupUUID());
     batchUsers.setType(AccountGroup.Type.INTERNAL);
     c.accountGroups().insert(Collections.singleton(batchUsers));
     c.accountGroupNames().insert(
@@ -186,7 +186,7 @@
 
     owners = newGroup(c, "Project Owners", AccountGroup.PROJECT_OWNERS);
     owners.setDescription("Any owner of the project");
-    owners.setOwnerGroupId(admin.getId());
+    owners.setOwnerGroupUUID(admin.getGroupUUID());
     owners.setType(AccountGroup.Type.SYSTEM);
     c.accountGroups().insert(Collections.singleton(owners));
     c.accountGroupNames().insert(
@@ -219,7 +219,8 @@
       }
     }
     try {
-      MetaDataUpdate md = new MetaDataUpdate(new NoReplication(), allProjectsName, git);
+      MetaDataUpdate md =
+          new MetaDataUpdate(GitReferenceUpdated.DISABLED, allProjectsName, git);
       md.getCommitBuilder().setAuthor(serverUser);
       md.getCommitBuilder().setCommitter(serverUser);
 
@@ -251,13 +252,12 @@
       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)) {
-        throw new IOException("Cannot create " + allProjectsName.get());
-      }
+      config.commit(md);
     } finally {
       git.close();
     }
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 f789300..f127a21 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_64> C = Schema_64.class;
+  public static final Class<Schema_73> C = Schema_73.class;
 
   public static class Module extends AbstractModule {
     @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersionCheck.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersionCheck.java
index 75d8a39..133b856 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersionCheck.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersionCheck.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.schema;
 
-import com.google.gerrit.lifecycle.LifecycleListener;
+import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.reviewdb.client.CurrentSchemaVersion;
 import com.google.gerrit.reviewdb.server.ReviewDb;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_53.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_53.java
index d49b34e..8207c31 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_53.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_53.java
@@ -37,9 +37,9 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.account.GroupUUID;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MetaDataUpdate;
-import com.google.gerrit.server.git.NoReplication;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gwtorm.jdbc.JdbcSchema;
 import com.google.gwtorm.server.OrmException;
@@ -158,13 +158,15 @@
         // inheritable permissions. For example 'All-Projects'.
         try {
           git = mgr.createRepository(nameKey);
-        } catch (RepositoryNotFoundException err) {
+        } catch (IOException err) {
           throw new OrmException("Cannot create repository " + name, err);
         }
+      } catch (IOException e) {
+        throw new OrmException(e);
       }
       try {
         MetaDataUpdate md =
-            new MetaDataUpdate(new NoReplication(), nameKey, git);
+            new MetaDataUpdate(GitReferenceUpdated.DISABLED, nameKey, git);
         md.getCommitBuilder().setAuthor(serverUser);
         md.getCommitBuilder().setCommitter(serverUser);
 
@@ -182,9 +184,7 @@
         }
 
         md.setMessage("Import project configuration from SQL\n");
-        if (!config.commit(md)) {
-          throw new OrmException("Cannot export project " + name);
-        }
+        config.commit(md);
       } catch (ConfigInvalidException err) {
         throw new OrmException("Cannot read project " + name, err);
       } catch (IOException err) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_57.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_57.java
index 2247fdb..3a288e2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_57.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_57.java
@@ -26,9 +26,9 @@
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.config.AllProjectsNameProvider;
 import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.LocalDiskRepositoryManager;
 import com.google.gerrit.server.git.MetaDataUpdate;
-import com.google.gerrit.server.git.NoReplication;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -81,7 +81,8 @@
     try {
       Repository git = mgr.openRepository(allProjects);
       try {
-        MetaDataUpdate md = new MetaDataUpdate(new NoReplication(), allProjects, git);
+        MetaDataUpdate md =
+            new MetaDataUpdate(GitReferenceUpdated.DISABLED, allProjects, git);
         md.getCommitBuilder().setAuthor(serverUser);
         md.getCommitBuilder().setCommitter(serverUser);
 
@@ -134,9 +135,7 @@
         }
 
         md.setMessage("Upgrade to Gerrit Code Review schema 57\n");
-        if (!config.commit(md)) {
-          throw new OrmException("Cannot update " + allProjects);
-        }
+        config.commit(md);
       } finally {
         git.close();
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_64.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_64.java
index 26890a3..e665bdc 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_64.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_64.java
@@ -23,9 +23,9 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MetaDataUpdate;
-import com.google.gerrit.server.git.NoReplication;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gwtorm.jdbc.JdbcSchema;
 import com.google.gwtorm.server.OrmException;
@@ -33,7 +33,6 @@
 import com.google.inject.Provider;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Repository;
 
@@ -88,12 +87,12 @@
     Repository git;
     try {
       git = mgr.openRepository(allProjects);
-    } catch (RepositoryNotFoundException e) {
+    } catch (IOException e) {
       throw new OrmException(e);
     }
     try {
       MetaDataUpdate md =
-          new MetaDataUpdate(new NoReplication(), allProjects, git);
+          new MetaDataUpdate(GitReferenceUpdated.DISABLED, allProjects, git);
       md.getCommitBuilder().setAuthor(serverUser);
       md.getCommitBuilder().setCommitter(serverUser);
 
@@ -107,9 +106,7 @@
       }
 
       md.setMessage("Upgrade to Gerrit Code Review schema 64\n");
-      if (!config.commit(md)) {
-        throw new OrmException("Cannot update " + allProjects);
-      }
+      config.commit(md);
     } catch (IOException e) {
       throw new OrmException(e);
     } catch (ConfigInvalidException e) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_65.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_65.java
new file mode 100644
index 0000000..1cdf25c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_65.java
@@ -0,0 +1,461 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.schema;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.primitives.Longs;
+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.GroupReference;
+import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.common.data.PermissionRule.Action;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupMember;
+import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
+import com.google.gerrit.reviewdb.client.AccountGroupName;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.account.GroupUUID;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.AnonymousCowardName;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.git.VersionedMetaData.BatchMetaDataUpdate;
+import com.google.gwtorm.jdbc.JdbcSchema;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.util.SystemReader;
+
+import java.io.IOException;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.sql.Timestamp;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.List;
+import java.util.Map;
+import java.util.TimeZone;
+
+public class Schema_65 extends SchemaVersion {
+  private final AllProjectsName allProjects;
+  private final GitRepositoryManager mgr;
+  private final PersonIdent serverUser;
+  private final @AnonymousCowardName String anonymousCowardName;
+
+  @Inject
+  Schema_65(Provider<Schema_64> prior,
+      AllProjectsName allProjects,
+      GitRepositoryManager mgr,
+      @GerritPersonIdent PersonIdent serverUser,
+      @AnonymousCowardName String anonymousCowardName) {
+    super(prior);
+    this.allProjects = allProjects;
+    this.mgr = mgr;
+    this.serverUser = serverUser;
+    this.anonymousCowardName = anonymousCowardName;
+  }
+
+  @Override
+  protected void migrateData(ReviewDb db, UpdateUI ui)
+      throws OrmException, SQLException {
+    Repository git;
+    try {
+      git = mgr.openRepository(allProjects);
+    } catch (IOException e) {
+      throw new OrmException(e);
+    }
+    try {
+      MetaDataUpdate md =
+          new MetaDataUpdate(GitReferenceUpdated.DISABLED, allProjects, git);
+      ProjectConfig config = ProjectConfig.read(md);
+      Map<Integer, ContributorAgreement> agreements = getAgreementToAdd(db, config);
+      if (agreements.isEmpty()) {
+        return;
+      }
+      ui.message("Moved contributor agreements to project.config");
+
+      // Create the auto verify groups.
+      List<AccountGroup.UUID> adminGroupUUIDs = getAdministrateServerGroups(db, config);
+      for (ContributorAgreement agreement : agreements.values()) {
+        if (agreement.getAutoVerify() != null) {
+          getOrCreateGroupForIndividuals(db, config, adminGroupUUIDs, agreement);
+        }
+      }
+
+      // Scan AccountAgreement
+      long minTime = addAccountAgreements(db, config, adminGroupUUIDs, agreements);
+
+      ProjectConfig base = ProjectConfig.read(md, null);
+      for (ContributorAgreement agreement : agreements.values()) {
+        base.replace(agreement);
+      }
+      base.getAccountsSection().setSameGroupVisibility(
+          config.getAccountsSection().getSameGroupVisibility());
+
+      BatchMetaDataUpdate batch = base.openUpdate(md);
+      try {
+        // Scan AccountGroupAgreement
+        List<AccountGroupAgreement> groupAgreements =
+            getAccountGroupAgreements(db, agreements);
+
+        // Find the earliest change
+        for (AccountGroupAgreement aga : groupAgreements) {
+          minTime = Math.min(minTime, aga.getTime());
+        }
+        minTime -= 60 * 1000; // 1 Minute
+
+        CommitBuilder commit = new CommitBuilder();
+        commit.setAuthor(new PersonIdent(serverUser, new Date(minTime)));
+        commit.setCommitter(new PersonIdent(serverUser, new Date(minTime)));
+        commit.setMessage("Add the ContributorAgreements for upgrade to Gerrit Code Review schema 65\n");
+        batch.write(commit);
+
+        for (AccountGroupAgreement aga : groupAgreements) {
+          AccountGroup group = db.accountGroups().get(aga.groupId);
+          if (group == null) {
+            continue;
+          }
+
+          ContributorAgreement agreement = agreements.get(aga.claId);
+          agreement.getAccepted().add(new PermissionRule(config.resolve(group)));
+          base.replace(agreement);
+
+          PersonIdent ident = null;
+          if (aga.reviewedBy != null) {
+            Account ua = db.accounts().get(aga.reviewedBy);
+            if (ua != null) {
+              String name = ua.getFullName();
+              String email = ua.getPreferredEmail();
+
+              if (email == null || email.isEmpty()) {
+                // No preferred email is configured. Use a generic identity so we
+                // don't leak an address the user may have given us, but doesn't
+                // necessarily want to publish through Git records.
+                //
+                String user = ua.getUserName();
+                if (user == null || user.isEmpty()) {
+                  user = "account-" + ua.getId().toString();
+                }
+
+                String host = SystemReader.getInstance().getHostname();
+                email = user + "@" + host;
+              }
+
+              if (name == null || name.isEmpty()) {
+                final int at = email.indexOf('@');
+                if (0 < at) {
+                  name = email.substring(0, at);
+                } else {
+                  name = anonymousCowardName;
+                }
+              }
+
+              ident = new PersonIdent(name, email, new Date(aga.getTime()), TimeZone.getDefault());
+            }
+          }
+          if (ident == null) {
+            ident = new PersonIdent(serverUser, new Date(aga.getTime()));
+          }
+
+          // Build the commits such that it keeps track of the date added and
+          // who added it.
+          commit = new CommitBuilder();
+          commit.setAuthor(ident);
+          commit.setCommitter(new PersonIdent(serverUser, new Date(aga.getTime())));
+
+          String msg = String.format("Accept %s contributor agreement for %s\n",
+              agreement.getName(), group.getName());
+          if (!Strings.isNullOrEmpty(aga.reviewComments)) {
+            msg += "\n" + aga.reviewComments + "\n";
+          }
+          commit.setMessage(msg);
+          batch.write(commit);
+        }
+
+        // Merge the agreements with the other data in project.config.
+        commit = new CommitBuilder();
+        commit.setAuthor(serverUser);
+        commit.setCommitter(serverUser);
+        commit.setMessage("Upgrade to Gerrit Code Review schema 65\n");
+        commit.addParentId(config.getRevision());
+        batch.write(config, commit);
+
+        // Save the the final metadata.
+        batch.commitAt(config.getRevision());
+      } finally {
+        batch.close();
+      }
+    } catch (IOException e) {
+      throw new OrmException(e);
+    } catch (ConfigInvalidException e) {
+      throw new OrmException(e);
+    } finally {
+      git.close();
+    }
+  }
+
+  private Map<Integer, ContributorAgreement> getAgreementToAdd(
+      ReviewDb db, ProjectConfig config) throws SQLException {
+    Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
+    try {
+      ResultSet rs = stmt.executeQuery(
+          "SELECT short_name, id, require_contact_information," +
+          "       short_description, agreement_url, auto_verify " +
+          "FROM contributor_agreements WHERE active = 'Y'");
+      try {
+        Map<Integer, ContributorAgreement> agreements = Maps.newHashMap();
+        while (rs.next()) {
+          String name = rs.getString(1);
+          if (config.getContributorAgreement(name) != null) {
+            continue; // already exists
+          }
+          ContributorAgreement a = config.getContributorAgreement(name, true);
+          agreements.put(rs.getInt(2), a);
+
+          a.setRequireContactInformation("Y".equals(rs.getString(3)));
+          a.setDescription(rs.getString(4));
+          a.setAgreementUrl(rs.getString(5));
+          if ("Y".equals(rs.getString(6))) {
+            a.setAutoVerify(new GroupReference(null, null));
+          }
+        }
+        return agreements;
+      } finally {
+        rs.close();
+      }
+    } finally {
+      stmt.close();
+    }
+  }
+
+  private AccountGroup createGroup(ReviewDb db, String groupName,
+      AccountGroup.UUID adminGroupUUID, String description)
+          throws OrmException {
+    final AccountGroup.Id groupId =
+        new AccountGroup.Id(db.nextAccountGroupId());
+    final AccountGroup.NameKey nameKey = new AccountGroup.NameKey(groupName);
+    final AccountGroup.UUID uuid = GroupUUID.make(groupName, serverUser);
+    final AccountGroup group = new AccountGroup(nameKey, groupId, uuid);
+    group.setOwnerGroupUUID(adminGroupUUID);
+    group.setDescription(description);
+    final AccountGroupName gn = new AccountGroupName(group);
+    // first insert the group name to validate that the group name hasn't
+    // already been used to create another group
+    db.accountGroupNames().insert(Collections.singleton(gn));
+    db.accountGroups().insert(Collections.singleton(group));
+    return group;
+  }
+
+  private List<AccountGroup.UUID> getAdministrateServerGroups(
+      ReviewDb db, ProjectConfig cfg) {
+    List<PermissionRule> rules = cfg.getAccessSection(AccessSection.GLOBAL_CAPABILITIES)
+       .getPermission(GlobalCapability.ADMINISTRATE_SERVER)
+       .getRules();
+
+    List<AccountGroup.UUID> groups =
+        Lists.newArrayListWithExpectedSize(rules.size());
+    for (PermissionRule rule : rules) {
+      if (rule.getAction() == Action.ALLOW) {
+        groups.add(rule.getGroup().getUUID());
+      }
+    }
+    if (groups.isEmpty()) {
+      throw new IllegalStateException("no administrator group found");
+    }
+
+    return groups;
+  }
+
+  private GroupReference getOrCreateGroupForIndividuals(ReviewDb db,
+      ProjectConfig config, List<AccountGroup.UUID> adminGroupUUIDs,
+      ContributorAgreement agreement)
+          throws OrmException {
+    if (!agreement.getAccepted().isEmpty()) {
+      return agreement.getAccepted().get(0).getGroup();
+    }
+
+    String name = "CLA Accepted - " + agreement.getName();
+    AccountGroupName agn =
+        db.accountGroupNames().get(new AccountGroup.NameKey(name));
+    AccountGroup ag;
+    if (agn != null) {
+      ag = db.accountGroups().get(agn.getId());
+      if (ag == null) {
+        throw new IllegalStateException(
+            "account group name exists but account group does not: " + name);
+      }
+
+      if (!adminGroupUUIDs.contains(ag.getOwnerGroupUUID())) {
+        throw new IllegalStateException(
+            "individual group exists with non admin owner group: " + name);
+      }
+    } else {
+      ag = createGroup(db, name, adminGroupUUIDs.get(0),
+          String.format("Users who have accepted the %s CLA", agreement.getName()));
+    }
+    GroupReference group = config.resolve(ag);
+    agreement.setAccepted(Lists.newArrayList(new PermissionRule(group)));
+    if (agreement.getAutoVerify() != null) {
+      agreement.setAutoVerify(group);
+    }
+
+    // Don't allow accounts in the same individual CLA group to see each
+    // other in same group visibility mode.
+    List<PermissionRule> sameGroupVisibility =
+        config.getAccountsSection().getSameGroupVisibility();
+    PermissionRule rule = new PermissionRule(group);
+    rule.setDeny();
+    if (!sameGroupVisibility.contains(rule)) {
+      sameGroupVisibility.add(rule);
+    }
+    return group;
+  }
+
+  private long addAccountAgreements(ReviewDb db, ProjectConfig config,
+      List<AccountGroup.UUID> adminGroupUUIDs,
+      Map<Integer, ContributorAgreement> agreements)
+          throws SQLException, OrmException {
+    Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
+    try {
+      ResultSet rs = stmt.executeQuery(
+          "SELECT account_id, cla_id, accepted_on, reviewed_by," +
+          "       reviewed_on, review_comments " +
+          "FROM account_agreements WHERE status = 'V'");
+      try {
+        long minTime = System.currentTimeMillis();
+        while (rs.next()) {
+          Account.Id accountId = new Account.Id(rs.getInt(1));
+          Account.Id reviewerId = new Account.Id(rs.getInt(4));
+          if (rs.wasNull()) {
+            reviewerId = accountId;
+          }
+
+          int claId = rs.getInt(2);
+          ContributorAgreement agreement = agreements.get(claId);
+          if (agreement == null) {
+            continue;  // Agreement is invalid
+          }
+
+          Timestamp acceptedOn = rs.getTimestamp(3);
+          minTime = Math.min(minTime, acceptedOn.getTime());
+
+          // Enter Agreement
+          GroupReference individualGroup =
+              getOrCreateGroupForIndividuals(db, config, adminGroupUUIDs, agreement);
+          AccountGroup.Id groupId = db.accountGroups()
+              .byUUID(individualGroup.getUUID())
+              .toList()
+              .get(0)
+              .getId();
+
+          final AccountGroupMember.Key key =
+              new AccountGroupMember.Key(accountId, groupId);
+          AccountGroupMember m = db.accountGroupMembers().get(key);
+          if (m == null) {
+            m = new AccountGroupMember(key);
+            db.accountGroupMembersAudit().insert(
+                Collections.singleton(
+                    new AccountGroupMemberAudit(m, reviewerId, acceptedOn)));
+            db.accountGroupMembers().insert(Collections.singleton(m));
+          }
+        }
+        return minTime;
+      } finally {
+        rs.close();
+      }
+    } finally {
+      stmt.close();
+    }
+  }
+
+  private static class AccountGroupAgreement {
+    private AccountGroup.Id groupId;
+    private int claId;
+    private Timestamp acceptedOn;
+    private Account.Id reviewedBy;
+    private Timestamp reviewedOn;
+    private String reviewComments;
+
+    private long getTime() {
+      return (reviewedOn == null) ? acceptedOn.getTime() : reviewedOn.getTime();
+    }
+  }
+
+  private List<AccountGroupAgreement> getAccountGroupAgreements(
+      ReviewDb db, Map<Integer, ContributorAgreement> agreements)
+          throws SQLException {
+
+    Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
+    try {
+      ResultSet rs = stmt.executeQuery(
+          "SELECT group_id, cla_id, accepted_on, reviewed_by, reviewed_on, " +
+          "       review_comments " +
+          "FROM account_group_agreements");
+      try {
+        List<AccountGroupAgreement> groupAgreements = Lists.newArrayList();
+        while (rs.next()) {
+          AccountGroupAgreement a = new AccountGroupAgreement();
+          a.groupId = new AccountGroup.Id(rs.getInt(1));
+          a.claId = rs.getInt(2);
+          if (!agreements.containsKey(a.claId)) {
+            continue; // Agreement is invalid
+          }
+          a.acceptedOn = rs.getTimestamp(3);
+          a.reviewedBy = new Account.Id(rs.getInt(4));
+          if (rs.wasNull()) {
+            a.reviewedBy = null;
+          }
+
+          a.reviewedOn = rs.getTimestamp(5);
+          if (rs.wasNull()) {
+            a.reviewedOn = null;
+          }
+
+          a.reviewComments = rs.getString(6);
+          if (rs.wasNull()) {
+            a.reviewComments = null;
+          }
+          groupAgreements.add(a);
+        }
+        Collections.sort(groupAgreements, new Comparator<AccountGroupAgreement>() {
+          @Override
+          public int compare(
+              AccountGroupAgreement a1, AccountGroupAgreement a2) {
+            return Longs.compare(a1.getTime(), a2.getTime());
+          }
+        });
+        return groupAgreements;
+      } finally {
+        rs.close();
+      }
+    } finally {
+      stmt.close();
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_66.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_66.java
new file mode 100644
index 0000000..94f5d2c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_66.java
@@ -0,0 +1,46 @@
+// 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.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.sql.SQLException;
+import java.sql.Statement;
+
+public class Schema_66 extends SchemaVersion {
+
+  @Inject
+  Schema_66(Provider<Schema_65> prior) {
+    super(prior);
+  }
+
+  @Override
+  protected void migrateData(ReviewDb db, UpdateUI ui)
+      throws OrmException, SQLException {
+    final Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
+    try {
+      stmt.executeUpdate("UPDATE accounts SET reverse_patch_set_order = 'Y' "+
+                         "WHERE display_patch_sets_in_reverse_order = 'Y'");
+      stmt.executeUpdate("UPDATE accounts SET show_username_in_review_category = 'Y' " +
+                         "WHERE display_person_name_in_review_category = 'Y'");
+    } finally {
+      stmt.close();
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_67.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_67.java
new file mode 100644
index 0000000..bec2f3f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_67.java
@@ -0,0 +1,93 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.schema;
+
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.jdbc.JdbcSchema;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.List;
+import java.util.Map;
+import java.util.Map.Entry;
+import java.util.Set;
+
+public class Schema_67 extends SchemaVersion {
+
+  @Inject
+  Schema_67(Provider<Schema_66> prior) {
+    super(prior);
+  }
+
+  @Override
+  protected void migrateData(ReviewDb db, UpdateUI ui)
+      throws OrmException, SQLException {
+    ui.message("Update ownerGroupId to ownerGroupUUID");
+
+    // Scan all AccountGroup, and find the ones that need the owner_group_id
+    // migrated to owner_group_uuid.
+    Map<AccountGroup.Id, AccountGroup.Id> idMap = Maps.newHashMap();
+    Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
+    try {
+      ResultSet rs = stmt.executeQuery(
+          "SELECT group_id, owner_group_id FROM account_groups"
+          + " WHERE owner_group_uuid is NULL or owner_group_uuid =''");
+      try {
+        while (rs.next()) {
+          AccountGroup.Id groupId = new AccountGroup.Id(rs.getInt(1));
+          AccountGroup.Id ownerId = new AccountGroup.Id(rs.getInt(2));
+          idMap.put(groupId, ownerId);
+        }
+      } finally {
+        rs.close();
+      }
+    } finally {
+      stmt.close();
+    }
+
+    // Lookup up all groups by ID.
+    Set<AccountGroup.Id> all =
+        Sets.newHashSet(Iterables.concat(idMap.keySet(), idMap.values()));
+    Map<AccountGroup.Id, AccountGroup> groups = Maps.newHashMap();
+    com.google.gwtorm.server.ResultSet<AccountGroup> rs =
+        db.accountGroups().get(all);
+    try {
+      for (AccountGroup group : rs) {
+        groups.put(group.getId(), group);
+      }
+    } finally {
+      rs.close();
+    }
+
+    // Update the ownerGroupUUID.
+    List<AccountGroup> toUpdate = Lists.newArrayListWithCapacity(idMap.size());
+    for (Entry<AccountGroup.Id, AccountGroup.Id> entry : idMap.entrySet()) {
+      AccountGroup group = groups.get(entry.getKey());
+      AccountGroup owner = groups.get(entry.getValue());
+      group.setOwnerGroupUUID(owner.getGroupUUID());
+      toUpdate.add(group);
+    }
+
+    db.accountGroups().update(toUpdate);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_68.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_68.java
new file mode 100644
index 0000000..4dc2b6e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_68.java
@@ -0,0 +1,60 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.schema;
+
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.jdbc.JdbcSchema;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.sql.SQLException;
+import java.sql.Statement;
+
+public class Schema_68 extends SchemaVersion {
+  @Inject
+  Schema_68(Provider<Schema_67> prior) {
+    super(prior);
+  }
+
+  @Override
+  protected void migrateData(final ReviewDb db, final UpdateUI ui)
+      throws SQLException {
+    final Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
+    try {
+      stmt.execute("CREATE INDEX submodule_subscription_access_bySubscription"
+          + " ON submodule_subscriptions (submodule_project_name, submodule_branch_name)");
+    } catch (SQLException e) {
+      // the index creation might have failed because the index exists already,
+      // in this case the exception can be safely ignored,
+      // but there are also other possible reasons for an exception here that
+      // should not be ignored,
+      // -> ask the user whether to ignore this exception or not
+      ui.message("warning: Cannot create index for submodule subscriptions");
+      ui.message(e.getMessage());
+
+      if (ui.isBatch()) {
+        ui.message("you may ignore this warning when running in interactive mode");
+        throw e;
+      } else {
+        final boolean answer = ui.yesno(false, "Ignore warning and proceed with schema upgrade");
+        if (!answer) {
+          throw e;
+        }
+      }
+    } finally {
+      stmt.close();
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_69.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_69.java
new file mode 100644
index 0000000..fa56966
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_69.java
@@ -0,0 +1,228 @@
+// 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");
+        config.commit(md);
+      } 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/Schema_70.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_70.java
new file mode 100644
index 0000000..03b33a0
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_70.java
@@ -0,0 +1,61 @@
+// 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.gwtorm.schema.sql.DialectPostgreSQL;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.sql.SQLException;
+import java.sql.Statement;
+
+public class Schema_70 extends SchemaVersion {
+  @Inject
+  protected Schema_70(Provider<Schema_69> prior) {
+    super(prior);
+  }
+
+  @Override
+  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException,
+      SQLException {
+    Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
+    try {
+      stmt.executeUpdate("UPDATE tracking_ids SET tracking_key = tracking_id");
+      execute(stmt, "DROP INDEX tracking_ids_byTrkId");
+      if (((JdbcSchema) db).getDialect() instanceof DialectPostgreSQL) {
+        execute(stmt, "ALTER TABLE tracking_ids DROP CONSTRAINT tracking_ids_pkey");
+      } else {
+        execute(stmt, "ALTER TABLE tracking_ids DROP PRIMARY KEY");
+      }
+      stmt.execute("ALTER TABLE tracking_ids"
+          + " ADD PRIMARY KEY (change_id, tracking_key, tracking_system)");
+      stmt.execute("CREATE INDEX tracking_ids_byTrkKey"
+          + " ON tracking_ids (tracking_key)");
+    } finally {
+      stmt.close();
+    }
+  }
+
+  private static final void execute(Statement stmt, String command) {
+    try {
+      stmt.execute(command);
+    } catch (SQLException e) {
+      // ignore
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_71.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_71.java
new file mode 100644
index 0000000..8d5b943
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_71.java
@@ -0,0 +1,43 @@
+// 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_71 extends SchemaVersion {
+  @Inject
+  Schema_71(Provider<Schema_70> prior) {
+    super(prior);
+  }
+
+  @Override
+  protected void migrateData(final ReviewDb db, final UpdateUI ui)
+      throws SQLException {
+    final Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
+    try {
+      stmt.executeUpdate("UPDATE account_diff_preferences SET show_line_endings='Y'");
+    }
+    finally {
+      stmt.close();
+    }
+  }
+}
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/schema/Schema_72.java
similarity index 65%
rename from gerrit-server/src/main/java/com/google/gerrit/server/cache/EvictionPolicy.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_72.java
index cff4f11..748837b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/EvictionPolicy.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_72.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.
@@ -12,13 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.cache;
+package com.google.gerrit.server.schema;
 
-/** How entries should be evicted from the cache. */
-public enum EvictionPolicy {
-  /** Least recently used is evicted first. */
-  LRU,
+import com.google.inject.Inject;
+import com.google.inject.Provider;
 
-  /** Least frequently used is evicted first. */
-  LFU;
+public class Schema_72 extends SchemaVersion {
+  @Inject
+  Schema_72(Provider<Schema_71> prior) {
+    super(prior);
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_73.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_73.java
new file mode 100644
index 0000000..2732a3d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_73.java
@@ -0,0 +1,43 @@
+// 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_73 extends SchemaVersion {
+  @Inject
+  Schema_73(Provider<Schema_72> prior) {
+    super(prior);
+  }
+
+  @Override
+  protected void migrateData(final ReviewDb db, final UpdateUI ui)
+      throws SQLException {
+    final Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
+    try {
+      stmt.executeUpdate("CREATE INDEX change_messages_byPatchset ON change_messages (patchset_change_id, patchset_patch_set_id )");
+    }
+    finally {
+      stmt.close();
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/UpdateUI.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/UpdateUI.java
index 64b3afa..eff5575 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/UpdateUI.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/UpdateUI.java
@@ -24,6 +24,8 @@
 
   boolean yesno(boolean def, String msg);
 
+  boolean isBatch();
+
   void pruneSchema(StatementExecutor e, List<String> pruneList)
       throws OrmException;
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/FallbackRequestContext.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/FallbackRequestContext.java
new file mode 100644
index 0000000..686a108
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/FallbackRequestContext.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.util;
+
+import com.google.gerrit.server.AnonymousUser;
+import com.google.gerrit.server.CurrentUser;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+/**
+ * The default RequestContext to use when not in a request scope e.g.
+ * ThreadLocalRequestContext is not set.
+ */
+@Singleton
+public class FallbackRequestContext implements RequestContext {
+
+  private final AnonymousUser user;
+
+  @Inject
+  FallbackRequestContext(AnonymousUser user) {
+    this.user = user;
+  }
+
+  @Override
+  public CurrentUser getCurrentUser() {
+    return user;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/GuiceRequestScopePropagator.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/GuiceRequestScopePropagator.java
index 9befc7d..fa07176 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/GuiceRequestScopePropagator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/GuiceRequestScopePropagator.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.server.util;
 
 import com.google.common.collect.Maps;
-import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.RemotePeer;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.inject.Inject;
@@ -38,17 +37,15 @@
 
   private final String url;
   private final SocketAddress peer;
-  private final CurrentUser user;
 
   @Inject
   GuiceRequestScopePropagator(
       @CanonicalWebUrl @Nullable Provider<String> urlProvider,
       @RemotePeer Provider<SocketAddress> remotePeerProvider,
-      Provider<CurrentUser> currentUserProvider) {
-    super(ServletScopes.REQUEST);
+      ThreadLocalRequestContext local) {
+    super(ServletScopes.REQUEST, local);
     this.url = urlProvider != null ? urlProvider.get() : null;
     this.peer = remotePeerProvider.get();
-    this.user = currentUserProvider.get();
   }
 
   /**
@@ -69,9 +66,6 @@
         Providers.of(peer));
     seedMap.put(Key.get(SocketAddress.class, RemotePeer.class), peer);
 
-    seedMap.put(Key.get(typeOfProvider(CurrentUser.class)), Providers.of(user));
-    seedMap.put(Key.get(CurrentUser.class), user);
-
     return ServletScopes.continueRequest(callable, seedMap);
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/UnnamedCacheBinding.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestContext.java
similarity index 63%
copy from gerrit-server/src/main/java/com/google/gerrit/server/cache/UnnamedCacheBinding.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/util/RequestContext.java
index 43039e1..ca8573f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/UnnamedCacheBinding.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestContext.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.
@@ -12,11 +12,15 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.cache;
+package com.google.gerrit.server.util;
 
+import com.google.gerrit.server.CurrentUser;
 
-/** 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);
+/**
+ * The RequestContext is an interface exposing the fields that are needed
+ * by the GerritGlobalModule scope.
+ */
+public interface RequestContext {
+
+  CurrentUser getCurrentUser();
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestScopePropagator.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestScopePropagator.java
index 3661aa2..84c61e9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestScopePropagator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestScopePropagator.java
@@ -42,9 +42,12 @@
 public abstract class RequestScopePropagator {
 
   private final Scope scope;
+  private final ThreadLocalRequestContext local;
 
-  protected RequestScopePropagator(Scope scope) {
+  protected RequestScopePropagator(Scope scope,
+      ThreadLocalRequestContext local) {
     this.scope = scope;
+    this.local = local;
   }
 
   /**
@@ -70,26 +73,8 @@
    * @return a new Callable which will execute in the current request scope.
    */
   public final <T> Callable<T> wrap(final Callable<T> callable) {
-    final Callable<T> wrapped = wrapImpl(new Callable<T>() {
-      @Override
-      public T call() throws Exception {
-        RequestCleanup cleanup = scope.scope(
-            Key.get(RequestCleanup.class),
-            new Provider<RequestCleanup>() {
-              @Override
-              public RequestCleanup get() {
-                return new RequestCleanup();
-              }
-            }).get();
-
-        try {
-          return callable.call();
-        } finally {
-          cleanup.run();
-        }
-      }
-    });
-
+    final Callable<T> wrapped =
+        wrapImpl(context(local.getContext(), cleanup(callable)));
     return new Callable<T>() {
       @Override
       public T call() throws Exception {
@@ -178,4 +163,41 @@
    * @see #wrap(Callable)
    */
   protected abstract <T> Callable<T> wrapImpl(final Callable<T> callable);
+
+  protected <T> Callable<T> context(final RequestContext context,
+      final Callable<T> callable) {
+    return new Callable<T>() {
+      @Override
+      public T call() throws Exception {
+        RequestContext old = local.setContext(context);
+        try {
+          return callable.call();
+        } finally {
+          local.setContext(old);
+        }
+      }
+    };
+  }
+
+  protected <T> Callable<T> cleanup(final Callable<T> callable) {
+    return new Callable<T>() {
+      @Override
+      public T call() throws Exception {
+        RequestCleanup cleanup = scope.scope(
+            Key.get(RequestCleanup.class),
+            new Provider<RequestCleanup>() {
+              @Override
+              public RequestCleanup get() {
+                return new RequestCleanup();
+              }
+            }).get();
+
+        try {
+          return callable.call();
+        } finally {
+          cleanup.run();
+        }
+      }
+    };
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestContext.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestContext.java
new file mode 100644
index 0000000..b411512
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestContext.java
@@ -0,0 +1,87 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.util;
+
+import com.google.common.base.Objects;
+import com.google.gerrit.common.errors.NotSignedInException;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import com.google.inject.Module;
+import com.google.inject.Provides;
+import com.google.inject.ProvisionException;
+import com.google.inject.name.Named;
+import com.google.inject.name.Names;
+
+import javax.annotation.Nullable;
+
+/**
+ * ThreadLocalRequestContext manages the current RequestContext using a
+ * ThreadLocal. When the context is set, the fields exposed by the context
+ * are considered in scope. Otherwise, the FallbackRequestContext is used.
+ */
+public class ThreadLocalRequestContext {
+  private static final String FALLBACK = "FALLBACK";
+
+  public static Module module() {
+    return new AbstractModule() {
+      @Override
+      protected void configure() {
+        bind(ThreadLocalRequestContext.class);
+        bind(RequestContext.class).annotatedWith(Names.named(FALLBACK))
+            .to(FallbackRequestContext.class);
+      }
+
+      @Provides
+      RequestContext provideRequestContext(
+          @Named(FALLBACK) RequestContext fallback) {
+        return Objects.firstNonNull(local.get(), fallback);
+      }
+
+      @Provides
+      CurrentUser provideCurrentUser(RequestContext ctx) {
+        return ctx.getCurrentUser();
+      }
+
+      @Provides
+      IdentifiedUser provideCurrentUser(CurrentUser user) {
+        if (user instanceof IdentifiedUser) {
+          return (IdentifiedUser) user;
+        }
+        throw new ProvisionException(NotSignedInException.MESSAGE,
+            new NotSignedInException());
+      }
+    };
+  }
+
+  private static final ThreadLocal<RequestContext> local =
+      new ThreadLocal<RequestContext>();
+
+  @Inject
+  ThreadLocalRequestContext() {
+  }
+
+  public RequestContext setContext(@Nullable RequestContext ctx) {
+    RequestContext old = getContext();
+    local.set(ctx);
+    return old;
+  }
+
+  @Nullable
+  public RequestContext getContext() {
+    return local.get();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestScopePropagator.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestScopePropagator.java
index 581ccc1..7728d6f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestScopePropagator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestScopePropagator.java
@@ -31,8 +31,8 @@
   private final ThreadLocal<C> threadLocal;
 
   protected ThreadLocalRequestScopePropagator(Scope scope,
-      ThreadLocal<C> threadLocal) {
-    super(scope);
+      ThreadLocal<C> threadLocal, ThreadLocalRequestContext local) {
+    super(scope, local);
     this.threadLocal = threadLocal;
   }
 
@@ -45,19 +45,16 @@
     return new Callable<T>() {
       @Override
       public T call() throws Exception {
-        if (threadLocal.get() != null) {
-          // This is consistent with the Guice ServletScopes.continueRequest()
-          // behavior.
-          throw new IllegalStateException("Cannot continue request, "
-              + "thread already has request in progress. A new thread must "
-              + "be used to propagate the request scope context.");
-        }
-
+        C old = threadLocal.get();
         threadLocal.set(ctx);
         try {
           return callable.call();
         } finally {
-          threadLocal.remove();
+          if (old != null) {
+            threadLocal.set(old);
+          } else {
+            threadLocal.remove();
+          }
         }
       }
     };
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/workflow/FunctionState.java b/gerrit-server/src/main/java/com/google/gerrit/server/workflow/FunctionState.java
index d08bd1f..74c97f3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/workflow/FunctionState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/workflow/FunctionState.java
@@ -26,7 +26,6 @@
 import com.google.gerrit.reviewdb.client.ApprovalCategory.Id;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
@@ -57,7 +56,7 @@
 
   @Inject
   FunctionState(final ApprovalTypes approvalTypes,
-      final IdentifiedUser.GenericFactory userFactory, final GroupCache egc,
+      final IdentifiedUser.GenericFactory userFactory,
       @Assisted final ChangeControl c, @Assisted final PatchSet.Id psId,
       @Assisted final Collection<PatchSetApproval> all) {
     this.approvalTypes = approvalTypes;
diff --git a/gerrit-server/src/main/java/gerrit/AbstractCommitUserIdentityPredicate.java b/gerrit-server/src/main/java/gerrit/AbstractCommitUserIdentityPredicate.java
index ac74147..606e883 100644
--- a/gerrit-server/src/main/java/gerrit/AbstractCommitUserIdentityPredicate.java
+++ b/gerrit-server/src/main/java/gerrit/AbstractCommitUserIdentityPredicate.java
@@ -27,7 +27,6 @@
 import com.googlecode.prolog_cafe.lang.Term;
 
 abstract class AbstractCommitUserIdentityPredicate extends Predicate.P3 {
-  private static final long serialVersionUID = 1L;
   private static final SymbolTerm user = SymbolTerm.intern("user", 1);
   private static final SymbolTerm anonymous = SymbolTerm.intern("anonymous");
 
diff --git a/gerrit-server/src/main/java/gerrit/PRED__load_commit_labels_1.java b/gerrit-server/src/main/java/gerrit/PRED__load_commit_labels_1.java
index c760426..a26a492 100644
--- a/gerrit-server/src/main/java/gerrit/PRED__load_commit_labels_1.java
+++ b/gerrit-server/src/main/java/gerrit/PRED__load_commit_labels_1.java
@@ -9,7 +9,9 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.rules.PrologEnvironment;
 import com.google.gerrit.rules.StoredValues;
+import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gwtorm.server.OrmException;
+import com.google.inject.util.Providers;
 
 import com.googlecode.prolog_cafe.lang.IntegerTerm;
 import com.googlecode.prolog_cafe.lang.JavaException;
@@ -24,8 +26,6 @@
 
 /** Exports list of {@code commit_label( label('Code-Review', 2), user(12345789) )}. */
 class PRED__load_commit_labels_1 extends Predicate.P1 {
-  private static final long serialVersionUID = 1L;
-
   private static final SymbolTerm sym_commit_label = SymbolTerm.intern("commit_label", 2);
   private static final SymbolTerm sym_label = SymbolTerm.intern("label", 2);
   private static final SymbolTerm sym_user = SymbolTerm.intern("user", 1);
@@ -44,10 +44,18 @@
     try {
       PrologEnvironment env = (PrologEnvironment) engine.control;
       ReviewDb db = StoredValues.REVIEW_DB.get(engine);
-      PatchSet.Id patchSetId = StoredValues.PATCH_SET_ID.get(engine);
+      PatchSet patchSet = StoredValues.PATCH_SET.get(engine);
+      ChangeData cd = StoredValues.CHANGE_DATA.getOrNull(engine);
       ApprovalTypes types = env.getInjector().getInstance(ApprovalTypes.class);
 
-      for (PatchSetApproval a : db.patchSetApprovals().byPatchSet(patchSetId)) {
+      Iterable<PatchSetApproval> approvals;
+      if (cd != null) {
+        approvals = cd.currentApprovals(Providers.of(db));
+      } else {
+        approvals = db.patchSetApprovals().byPatchSet(patchSet.getId());
+      }
+
+      for (PatchSetApproval a : approvals) {
         if (a.getValue() == 0) {
           continue;
         }
diff --git a/gerrit-server/src/main/java/gerrit/PRED__user_label_range_4.java b/gerrit-server/src/main/java/gerrit/PRED__user_label_range_4.java
index 68f9bf6..a955307 100644
--- a/gerrit-server/src/main/java/gerrit/PRED__user_label_range_4.java
+++ b/gerrit-server/src/main/java/gerrit/PRED__user_label_range_4.java
@@ -38,8 +38,6 @@
  * </pre>
  */
 class PRED__user_label_range_4 extends Predicate.P4 {
-  private static final long serialVersionUID = 1L;
-
   PRED__user_label_range_4(Term a1, Term a2, Term a3, Term a4, Operation n) {
     arg1 = a1;
     arg2 = a2;
diff --git a/gerrit-server/src/main/java/gerrit/PRED_change_branch_1.java b/gerrit-server/src/main/java/gerrit/PRED_change_branch_1.java
index 7cc9f35..51396ed 100644
--- a/gerrit-server/src/main/java/gerrit/PRED_change_branch_1.java
+++ b/gerrit-server/src/main/java/gerrit/PRED_change_branch_1.java
@@ -26,8 +26,6 @@
 import com.googlecode.prolog_cafe.lang.Term;
 
 public class PRED_change_branch_1 extends Predicate.P1 {
-  private static final long serialVersionUID = 1L;
-
   public PRED_change_branch_1(Term a1, Operation n) {
     arg1 = a1;
     cont = n;
diff --git a/gerrit-server/src/main/java/gerrit/PRED_change_owner_1.java b/gerrit-server/src/main/java/gerrit/PRED_change_owner_1.java
index 09be902..b127fff 100644
--- a/gerrit-server/src/main/java/gerrit/PRED_change_owner_1.java
+++ b/gerrit-server/src/main/java/gerrit/PRED_change_owner_1.java
@@ -28,7 +28,6 @@
 import com.googlecode.prolog_cafe.lang.Term;
 
 public class PRED_change_owner_1 extends Predicate.P1 {
-  private static final long serialVersionUID = 1L;
   private static final SymbolTerm user = SymbolTerm.intern("user", 1);
 
   public PRED_change_owner_1(Term a1, Operation n) {
diff --git a/gerrit-server/src/main/java/gerrit/PRED_change_project_1.java b/gerrit-server/src/main/java/gerrit/PRED_change_project_1.java
index d1cd20d..fb9f865 100644
--- a/gerrit-server/src/main/java/gerrit/PRED_change_project_1.java
+++ b/gerrit-server/src/main/java/gerrit/PRED_change_project_1.java
@@ -26,8 +26,6 @@
 import com.googlecode.prolog_cafe.lang.Term;
 
 public class PRED_change_project_1 extends Predicate.P1 {
-  private static final long serialVersionUID = 1L;
-
   public PRED_change_project_1(Term a1, Operation n) {
     arg1 = a1;
     cont = n;
diff --git a/gerrit-server/src/main/java/gerrit/PRED_change_topic_1.java b/gerrit-server/src/main/java/gerrit/PRED_change_topic_1.java
index d200f7e..34885f9 100644
--- a/gerrit-server/src/main/java/gerrit/PRED_change_topic_1.java
+++ b/gerrit-server/src/main/java/gerrit/PRED_change_topic_1.java
@@ -25,8 +25,6 @@
 import com.googlecode.prolog_cafe.lang.Term;
 
 public class PRED_change_topic_1 extends Predicate.P1 {
-  private static final long serialVersionUID = 1L;
-
   public PRED_change_topic_1(Term a1, Operation n) {
     arg1 = a1;
     cont = n;
diff --git a/gerrit-server/src/main/java/gerrit/PRED_commit_author_3.java b/gerrit-server/src/main/java/gerrit/PRED_commit_author_3.java
index a0817a1..3700909 100644
--- a/gerrit-server/src/main/java/gerrit/PRED_commit_author_3.java
+++ b/gerrit-server/src/main/java/gerrit/PRED_commit_author_3.java
@@ -24,8 +24,6 @@
 import com.googlecode.prolog_cafe.lang.Term;
 
 public class PRED_commit_author_3 extends AbstractCommitUserIdentityPredicate {
-  private static final long serialVersionUID = 1L;
-
   public PRED_commit_author_3(Term a1, Term a2, Term a3, Operation n) {
     super(a1, a2, a3, n);
   }
diff --git a/gerrit-server/src/main/java/gerrit/PRED_commit_committer_3.java b/gerrit-server/src/main/java/gerrit/PRED_commit_committer_3.java
index 78e6cf7..64823df 100644
--- a/gerrit-server/src/main/java/gerrit/PRED_commit_committer_3.java
+++ b/gerrit-server/src/main/java/gerrit/PRED_commit_committer_3.java
@@ -24,8 +24,6 @@
 import com.googlecode.prolog_cafe.lang.Term;
 
 public class PRED_commit_committer_3 extends AbstractCommitUserIdentityPredicate {
-  private static final long serialVersionUID = 1L;
-
   public PRED_commit_committer_3(Term a1, Term a2, Term a3, Operation n) {
     super(a1, a2, a3, n);
   }
diff --git a/gerrit-server/src/main/java/gerrit/PRED_commit_delta_4.java b/gerrit-server/src/main/java/gerrit/PRED_commit_delta_4.java
index c2c2d1c..9ce7098 100644
--- a/gerrit-server/src/main/java/gerrit/PRED_commit_delta_4.java
+++ b/gerrit-server/src/main/java/gerrit/PRED_commit_delta_4.java
@@ -43,7 +43,6 @@
  * </pre>
  */
 public class PRED_commit_delta_4 extends Predicate.P4 {
-  private static final long serialVersionUID = 1L;
   private static final SymbolTerm add = SymbolTerm.intern("add");
   private static final SymbolTerm modify = SymbolTerm.intern("modify");
   private static final SymbolTerm delete = SymbolTerm.intern("delete");
@@ -110,8 +109,8 @@
           continue;
         }
 
-        if (regex.matcher(newName).matches() ||
-            (oldName != null && regex.matcher(oldName).matches())) {
+        if (regex.matcher(newName).find() ||
+            (oldName != null && regex.matcher(oldName).find())) {
           SymbolTerm changeSym = getTypeSymbol(changeType);
           SymbolTerm newSym = SymbolTerm.create(newName);
           SymbolTerm oldSym = Prolog.Nil;
diff --git a/gerrit-server/src/main/java/gerrit/PRED_commit_edits_2.java b/gerrit-server/src/main/java/gerrit/PRED_commit_edits_2.java
index f0accf0..2c7949c 100644
--- a/gerrit-server/src/main/java/gerrit/PRED_commit_edits_2.java
+++ b/gerrit-server/src/main/java/gerrit/PRED_commit_edits_2.java
@@ -54,8 +54,6 @@
  * </pre>
  */
 public class PRED_commit_edits_2 extends Predicate.P2 {
-  private static final long serialVersionUID = 1L;
-
   public PRED_commit_edits_2(Term a1, Term a2, Operation n) {
     arg1 = a1;
     arg2 = a2;
diff --git a/gerrit-server/src/main/java/gerrit/PRED_commit_message_1.java b/gerrit-server/src/main/java/gerrit/PRED_commit_message_1.java
index 0ed7443..e2eb6b1 100644
--- a/gerrit-server/src/main/java/gerrit/PRED_commit_message_1.java
+++ b/gerrit-server/src/main/java/gerrit/PRED_commit_message_1.java
@@ -32,8 +32,6 @@
  * </pre>
  */
 public class PRED_commit_message_1 extends Predicate.P1 {
-  private static final long serialVersionUID = 1L;
-
   public PRED_commit_message_1(Term a1, Operation n) {
     arg1 = a1;
     cont = n;
diff --git a/gerrit-server/src/main/java/gerrit/PRED_current_user_1.java b/gerrit-server/src/main/java/gerrit/PRED_current_user_1.java
index 23cedce..09a46f7 100644
--- a/gerrit-server/src/main/java/gerrit/PRED_current_user_1.java
+++ b/gerrit-server/src/main/java/gerrit/PRED_current_user_1.java
@@ -20,7 +20,6 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PeerDaemonUser;
-import com.google.gerrit.server.ReplicationUser;
 import com.google.gerrit.server.project.ChangeControl;
 
 import com.googlecode.prolog_cafe.lang.EvaluationException;
@@ -34,7 +33,6 @@
 import com.googlecode.prolog_cafe.lang.Term;
 
 public class PRED_current_user_1 extends Predicate.P1 {
-  private static final long serialVersionUID = 1L;
   private static final SymbolTerm user = SymbolTerm.intern("user", 1);
   private static final SymbolTerm anonymous = SymbolTerm.intern("anonymous");
   private static final SymbolTerm peerDaemon = SymbolTerm.intern("peer_daemon");
@@ -61,8 +59,6 @@
       resultTerm = anonymous;
     } else if (curUser instanceof PeerDaemonUser) {
       resultTerm = peerDaemon;
-    } else if (curUser instanceof ReplicationUser) {
-      resultTerm = replication;
     } else {
       throw new EvaluationException("Unknown user type");
     }
diff --git a/gerrit-server/src/main/java/gerrit/PRED_current_user_2.java b/gerrit-server/src/main/java/gerrit/PRED_current_user_2.java
index 0a15608..3f4b656 100644
--- a/gerrit-server/src/main/java/gerrit/PRED_current_user_2.java
+++ b/gerrit-server/src/main/java/gerrit/PRED_current_user_2.java
@@ -20,15 +20,12 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.rules.PrologEnvironment;
 import com.google.gerrit.rules.StoredValues;
-import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.inject.Provider;
+import com.google.inject.util.Providers;
 
-import com.googlecode.prolog_cafe.lang.HashtableOfTerm;
 import com.googlecode.prolog_cafe.lang.IllegalTypeException;
 import com.googlecode.prolog_cafe.lang.IntegerTerm;
-import com.googlecode.prolog_cafe.lang.InternalException;
 import com.googlecode.prolog_cafe.lang.JavaObjectTerm;
 import com.googlecode.prolog_cafe.lang.Operation;
 import com.googlecode.prolog_cafe.lang.PInstantiationException;
@@ -39,6 +36,8 @@
 import com.googlecode.prolog_cafe.lang.SymbolTerm;
 import com.googlecode.prolog_cafe.lang.Term;
 
+import java.util.Map;
+
 /**
  * Loads a CurrentUser object for a user identity.
  * <p>
@@ -50,10 +49,8 @@
  * </pre>
  */
 class PRED_current_user_2 extends Predicate.P2 {
-  private static final long serialVersionUID = 1L;
   private static final SymbolTerm user = intern("user", 1);
   private static final SymbolTerm anonymous = intern("anonymous");
-  private static final SymbolTerm current_user = intern("current_user");
 
   PRED_current_user_2(Term a1, Term a2, Operation n) {
     arg1 = a1;
@@ -71,24 +68,14 @@
       throw new PInstantiationException(this, 1);
     }
 
-    HashtableOfTerm userHash = userHash(engine);
-    Term userTerm = userHash.get(a1);
-    if (userTerm != null && userTerm.isJavaObject()) {
-      if (!(((JavaObjectTerm) userTerm).object() instanceof CurrentUser)) {
-        userTerm = createUser(engine, a1, userHash);
-      }
-    } else {
-      userTerm = createUser(engine, a1, userHash);
-    }
-
-    if (!a2.unify(userTerm, engine.trail)) {
+    if (!a2.unify(createUser(engine, a1), engine.trail)) {
       return engine.fail();
     }
 
     return cont;
   }
 
-  public Term createUser(Prolog engine, Term key, HashtableOfTerm userHash) {
+  public Term createUser(Prolog engine, Term key) {
     if (!key.isStructure()
         || key.arity() != 1
         || !((StructureTerm) key).functor().equals(user)) {
@@ -98,54 +85,30 @@
     Term idTerm = key.arg(0);
     CurrentUser user;
     if (idTerm.isInteger()) {
+      Map<Account.Id, IdentifiedUser> cache = StoredValues.USERS.get(engine);
       Account.Id accountId = new Account.Id(((IntegerTerm) idTerm).intValue());
-
-      final ReviewDb db = StoredValues.REVIEW_DB.getOrNull(engine);
-      IdentifiedUser.GenericFactory userFactory = userFactory(engine);
-      if (db != null) {
-        user = userFactory.create(new Provider<ReviewDb>() {
-          public ReviewDb get() {
-            return db;
-          }
-        }, accountId);
-      } else {
-        user = userFactory.create(accountId);
+      user = cache.get(accountId);
+      if (user == null) {
+        ReviewDb db = StoredValues.REVIEW_DB.getOrNull(engine);
+        IdentifiedUser.GenericFactory userFactory = userFactory(engine);
+        IdentifiedUser who;
+        if (db != null) {
+          who = userFactory.create(Providers.of(db), accountId);
+        } else {
+          who = userFactory.create(accountId);
+        }
+        cache.put(accountId, who);
+        user = who;
       }
 
-
     } else if (idTerm.equals(anonymous)) {
-      user = anonymousUser(engine);
+      user = StoredValues.ANONYMOUS_USER.get(engine);
 
     } else {
       throw new IllegalTypeException(this, 1, "user(int)", key);
     }
 
-    Term userTerm = new JavaObjectTerm(user);
-    userHash.put(key, userTerm);
-    return userTerm;
-  }
-
-  private static HashtableOfTerm userHash(Prolog engine) {
-    Term userHash = engine.getHashManager().get(current_user);
-    if (userHash == null) {
-      HashtableOfTerm users = new HashtableOfTerm();
-      engine.getHashManager().put(current_user, new JavaObjectTerm(userHash));
-      return users;
-    }
-
-    if (userHash.isJavaObject()) {
-      Object obj = ((JavaObjectTerm) userHash).object();
-      if (obj instanceof HashtableOfTerm) {
-        return (HashtableOfTerm) obj;
-      }
-    }
-
-    throw new InternalException(current_user + " is not HashtableOfTerm");
-  }
-
-  private static AnonymousUser anonymousUser(Prolog engine) {
-    PrologEnvironment env = (PrologEnvironment) engine.control;
-    return env.getInjector().getInstance(AnonymousUser.class);
+    return new JavaObjectTerm(user);
   }
 
   private static IdentifiedUser.GenericFactory userFactory(Prolog engine) {
diff --git a/gerrit-server/src/main/java/gerrit/PRED_get_legacy_approval_types_1.java b/gerrit-server/src/main/java/gerrit/PRED_get_legacy_approval_types_1.java
index 9399865..cbe0fd8 100644
--- a/gerrit-server/src/main/java/gerrit/PRED_get_legacy_approval_types_1.java
+++ b/gerrit-server/src/main/java/gerrit/PRED_get_legacy_approval_types_1.java
@@ -43,8 +43,6 @@
  * </ul>
  */
 class PRED_get_legacy_approval_types_1 extends Predicate.P1 {
-  private static final long serialVersionUID = 1L;
-
   PRED_get_legacy_approval_types_1(Term a1, Operation n) {
     arg1 = a1;
     cont = n;
diff --git a/gerrit-server/src/main/prolog/gerrit_common.pl b/gerrit-server/src/main/prolog/gerrit_common.pl
index 3313162..a75acc0 100644
--- a/gerrit-server/src/main/prolog/gerrit_common.pl
+++ b/gerrit-server/src/main/prolog/gerrit_common.pl
@@ -25,8 +25,7 @@
 %%   predicate that needs to obtain it.
 %%
 init :-
-  define_hash(commit_labels),
-  define_hash(current_user).
+  define_hash(commit_labels).
 
 define_hash(A) :- hash_exists(A), !, hash_clear(A).
 define_hash(A) :- atom(A), !, new_hash(_, [alias(A)]).
@@ -98,6 +97,10 @@
 %%   Lookup the range allowed to be used.
 %%
 user_label_range(Label, Who, Min, Max) :-
+  hash_get(commit_labels, '$fast_range', true), !,
+  atom(Label),
+  assume_range_from_label(Label, Who, Min, Max).
+user_label_range(Label, Who, Min, Max) :-
   Who = user(_), !,
   atom(Label),
   current_user(Who, User),
@@ -106,6 +109,14 @@
   clause(user:test_grant(Label, test_user(Name), range(Min, Max)), _)
   .
 
+assume_range_from_label :-
+  hash_put(commit_labels, '$fast_range', true).
+
+assume_range_from_label(Label, Who, Min, Max) :-
+  commit_label(label(Label, Value), Who), !,
+  Min = Value, Max = Value.
+assume_range_from_label(_, _, 0, 0).
+
 
 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
 %%
@@ -138,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.
 
 
@@ -198,8 +210,8 @@
 %%
 legacy_submit_rule('MaxWithBlock', Label, Id, Min, Max, T) :- !, max_with_block(Label, Min, Max, T).
 legacy_submit_rule('MaxNoBlock', Label, Id, Min, Max, T) :- !, max_no_block(Label, Max, T).
-legacy_submit_rule('NoBlock', Label, Id, Min, Max, T) :- !, T = ok(_).
-legacy_submit_rule('NoOp', Label, Id, Min, Max, T) :- !, T = ok(_).
+legacy_submit_rule('NoBlock', Label, Id, Min, Max, T) :- !, T = may(_).
+legacy_submit_rule('NoOp', Label, Id, Min, Max, T) :- !, T = may(_).
 legacy_submit_rule(Fun, Label, Id, Min, Max, T) :- T = impossible(unsupported(Fun)).
 
 
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/documentation/pegdown.css b/gerrit-server/src/main/resources/com/google/gerrit/server/documentation/pegdown.css
new file mode 100644
index 0000000..eada653
--- /dev/null
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/documentation/pegdown.css
@@ -0,0 +1,39 @@
+body {
+  margin: 1em;
+}
+
+h1, h2, h3, h4, h5, h6 {
+  color: #527bbd;
+  font-family: sans-serif;
+}
+
+h1, h2, h3 {
+  border-bottom: 2px solid silver;
+}
+
+pre {
+  border: 2px solid silver;
+  background: #ebebeb;
+  margin-left: 2em;
+  width: 100em;
+  color: darkgreen;
+  padding: 2px;
+}
+
+dl dt {
+  margin-top: 1em;
+}
+
+table.plugin_info {
+  border-collapse: separate;
+  border-spacing: 0;
+  text-align: left;
+  margin-left: 2em;
+}
+table.plugin_info th {
+  padding-right: 0.5em;
+  border-right: 2px solid silver;
+}
+table.plugin_info td {
+  padding-left: 0.5em;
+}
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooter.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooter.vm
index f2f0fc76..1eb6842 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooter.vm
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeFooter.vm
@@ -31,7 +31,8 @@
 ## The ChangeFooter.vm template will determine the contents of the footer
 ## text that will be appended to ALL emails related to changes.
 ##
---
+#set ($SPACE = " ")
+--$SPACE
 #if ($email.changeUrl)
 To view, visit $email.changeUrl
 #set ($notblank = 1)
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Comment.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Comment.vm
index 547c1b4..9af98a6 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Comment.vm
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Comment.vm
@@ -43,5 +43,11 @@
 $email.coverLetter
 
 #end
+##
+## It is possible to increase the span of the quoted lines by using the line
+## count parameter when calling $email.inlineComments as a function.
+##
+## Example: #if($email.inlineComments)$email.getInlineComments(5)#end
+##
 #if($email.inlineComments)$email.inlineComments#end
 #end
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/NewChange.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/NewChange.vm
index 8e08dc4..42f2ca9 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/NewChange.vm
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/NewChange.vm
@@ -43,6 +43,11 @@
 #end
 #else
 $fromName has uploaded a new change for review.
+#if($email.changeUrl)
+
+  $email.changeUrl
+
+#end
 #end
 
 Change subject: $change.subject
@@ -52,3 +57,7 @@
 #if($email.sshHost)
   git pull ssh://$email.sshHost/$projectName $patchSet.refName
 #end
+#if($email.includeDiff)
+
+$email.UnifiedDiff
+#end
\ No newline at end of file
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/tools/root/hooks/commit-msg b/gerrit-server/src/main/resources/com/google/gerrit/server/tools/root/hooks/commit-msg
index 212ffb1..06926df 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/tools/root/hooks/commit-msg
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/tools/root/hooks/commit-msg
@@ -17,6 +17,8 @@
 # limitations under the License.
 #
 
+unset GREP_OPTIONS
+
 CHANGE_ID_AFTER="Bug|Issue"
 MSG="$1"
 
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/config/ConfigUtilTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/config/ConfigUtilTest.java
index 37197ec..5d72916 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/config/ConfigUtilTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/config/ConfigUtilTest.java
@@ -26,9 +26,11 @@
 
 public class ConfigUtilTest extends TestCase {
   public void testTimeUnit() {
+    assertEquals(ms(0, MILLISECONDS), parse("0"));
     assertEquals(ms(2, MILLISECONDS), parse("2ms"));
     assertEquals(ms(200, MILLISECONDS), parse("200 milliseconds"));
 
+    assertEquals(ms(0, SECONDS), parse("0s"));
     assertEquals(ms(2, SECONDS), parse("2s"));
     assertEquals(ms(231, SECONDS), parse("231sec"));
     assertEquals(ms(1, SECONDS), parse("1second"));
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/git/ProjectConfigTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/ProjectConfigTest.java
index ca2b03a..a849e68 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/git/ProjectConfigTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/git/ProjectConfigTest.java
@@ -22,11 +22,13 @@
 import static org.junit.Assert.fail;
 
 import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.ContributorAgreement;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
@@ -43,6 +45,7 @@
 import org.junit.Test;
 
 import java.io.IOException;
+import java.util.Collections;
 
 public class ProjectConfigTest extends LocalDiskRepositoryTestCase {
   private final GroupReference developers = new GroupReference(
@@ -70,10 +73,31 @@
             + "  exclusiveGroupPermissions = read submit create\n" //
             + "  submit = group Developers\n" //
             + "  push = group Developers\n" //
-            + "  read = group Developers\n")) //
+            + "  read = group Developers\n" //
+            + "[accounts]\n" //
+            + "  sameGroupVisibility = deny group Developers\n" //
+            + "  sameGroupVisibility = block group Staff\n" //
+            + "[contributor-agreement \"Individual\"]\n" //
+            + "  description = A simple description\n" //
+            + "  accepted = group Developers\n" //
+            + "  accepted = group Staff\n" //
+            + "  requireContactInformation = true\n" //
+            + "  autoVerify = group Developers\n" //
+            + "  agreementUrl = http://www.example.com/agree\n")) //
         ));
 
     ProjectConfig cfg = read(rev);
+    assertEquals(2, cfg.getAccountsSection().getSameGroupVisibility().size());
+    ContributorAgreement ca = cfg.getContributorAgreement("Individual");
+    assertEquals("Individual", ca.getName());
+    assertEquals("A simple description", ca.getDescription());
+    assertEquals("http://www.example.com/agree", ca.getAgreementUrl());
+    assertEquals(2, ca.getAccepted().size());
+    assertEquals(developers, ca.getAccepted().get(0).getGroup());
+    assertEquals("Staff", ca.getAccepted().get(1).getGroup().getName());
+    assertEquals("Developers", ca.getAutoVerify().getName());
+    assertTrue(ca.isRequireContactInformation());
+
     AccessSection section = cfg.getAccessSection("refs/heads/*");
     assertNotNull("has refs/heads/*", section);
     assertNull("no refs/*", cfg.getAccessSection("refs/*"));
@@ -98,14 +122,30 @@
             + "  exclusiveGroupPermissions = read submit\n" //
             + "  submit = group Developers\n" //
             + "  upload = group Developers\n" //
-            + "  read = group Developers\n")) //
+            + "  read = group Developers\n" //
+            + "[accounts]\n" //
+            + "  sameGroupVisibility = deny group Developers\n" //
+            + "  sameGroupVisibility = block group Staff\n" //
+            + "[contributor-agreement \"Individual\"]\n" //
+            + "  description = A simple description\n" //
+            + "  accepted = group Developers\n" //
+            + "  requireContactInformation = true\n" //
+            + "  autoVerify = group Developers\n" //
+            + "  agreementUrl = http://www.example.com/agree\n")) //
         ));
     update(rev);
 
     ProjectConfig cfg = read(rev);
     AccessSection section = cfg.getAccessSection("refs/heads/*");
+    cfg.getAccountsSection().setSameGroupVisibility(
+        Collections.singletonList(new PermissionRule(cfg.resolve(staff))));
     Permission submit = section.getPermission(Permission.SUBMIT);
     submit.add(new PermissionRule(cfg.resolve(staff)));
+    ContributorAgreement ca = cfg.getContributorAgreement("Individual");
+    ca.setRequireContactInformation(false);
+    ca.setAccepted(Collections.singletonList(new PermissionRule(cfg.resolve(staff))));
+    ca.setAutoVerify(null);
+    ca.setDescription("A new description");
     rev = commit(cfg);
     assertEquals(""//
         + "[access \"refs/heads/*\"]\n" //
@@ -114,6 +154,12 @@
         + "\tsubmit = group Staff\n" //
         + "  upload = group Developers\n" //
         + "  read = group Developers\n"//
+        + "[accounts]\n" //
+        + "  sameGroupVisibility = group Staff\n" //
+        + "[contributor-agreement \"Individual\"]\n" //
+        + "  description = A new description\n" //
+        + "  accepted = group Staff\n" //
+        + "  agreementUrl = http://www.example.com/agree\n" //
         + "[project]\n"//
         + "\tstate = active\n", text(rev, "project.config"));
   }
@@ -156,13 +202,14 @@
 
   private RevCommit commit(ProjectConfig cfg) throws IOException,
       MissingObjectException, IncorrectObjectTypeException {
-    MetaDataUpdate md = new MetaDataUpdate(new NoReplication(), //
-        cfg.getProject().getNameKey(), //
+    MetaDataUpdate md = new MetaDataUpdate(
+        GitReferenceUpdated.DISABLED,
+        cfg.getProject().getNameKey(),
         db);
     util.tick(5);
     util.setAuthorAndCommitter(md.getCommitBuilder());
     md.setMessage("Edit\n");
-    assertTrue("commit finished", cfg.commit(md));
+    cfg.commit(md);
 
     Ref ref = db.getRef(GitRepositoryManager.REF_CONFIG);
     return util.getRevWalk().parseCommit(ref.getObjectId());
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/git/PushReplicationTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/PushReplicationTest.java
deleted file mode 100644
index 7ae705f..0000000
--- a/gerrit-server/src/test/java/com/google/gerrit/server/git/PushReplicationTest.java
+++ /dev/null
@@ -1,44 +0,0 @@
-// Copyright (C) 2011 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.git;
-
-import static com.google.gerrit.server.git.PushReplication.ReplicationConfig.encode;
-import static com.google.gerrit.server.git.PushReplication.ReplicationConfig.needsUrlEncoding;
-
-import junit.framework.TestCase;
-
-import org.eclipse.jgit.transport.URIish;
-
-import java.net.URISyntaxException;
-
-public class PushReplicationTest extends TestCase {
-  public void testNeedsUrlEncoding() throws URISyntaxException {
-    assertTrue(needsUrlEncoding(new URIish("http://host/path")));
-    assertTrue(needsUrlEncoding(new URIish("https://host/path")));
-    assertTrue(needsUrlEncoding(new URIish("amazon-s3://config/bucket/path")));
-
-    assertFalse(needsUrlEncoding(new URIish("host:path")));
-    assertFalse(needsUrlEncoding(new URIish("user@host:path")));
-    assertFalse(needsUrlEncoding(new URIish("git://host/path")));
-    assertFalse(needsUrlEncoding(new URIish("ssh://host/path")));
-  }
-
-  public void testUrlEncoding() {
-    assertEquals("foo/bar/thing", encode("foo/bar/thing"));
-    assertEquals("--%20All%20Projects%20--", encode("-- All Projects --"));
-    assertEquals("name/with%20a%20space", encode("name/with a space"));
-    assertEquals("name%0Awith-LF", encode("name\nwith-LF"));
-  }
-}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/git/SubmoduleOpTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/SubmoduleOpTest.java
index b32d54c..8d061eb 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/git/SubmoduleOpTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/git/SubmoduleOpTest.java
@@ -26,6 +26,7 @@
 import com.google.gerrit.reviewdb.client.SubmoduleSubscription;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.reviewdb.server.SubmoduleSubscriptionAccess;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gwtorm.client.KeyUtil;
 import com.google.gwtorm.server.ListResultSet;
 import com.google.gwtorm.server.ResultSet;
@@ -71,8 +72,9 @@
   private ReviewDb schema;
   private Provider<String> urlProvider;
   private GitRepositoryManager repoManager;
-  private ReplicationQueue replication;
+  private GitReferenceUpdated replication;
 
+  @SuppressWarnings("unchecked")
   @Override
   @Before
   public void setUp() throws Exception {
@@ -83,7 +85,7 @@
     subscriptions = createStrictMock(SubmoduleSubscriptionAccess.class);
     urlProvider = createStrictMock(Provider.class);
     repoManager = createStrictMock(GitRepositoryManager.class);
-    replication = createStrictMock(ReplicationQueue.class);
+    replication = createStrictMock(GitReferenceUpdated.class);
   }
 
   private void doReplay() {
@@ -637,7 +639,7 @@
     expect(repoManager.openRepository(targetBranchNameKey.getParentKey()))
         .andReturn(targetRepository);
 
-    replication.scheduleUpdate(targetBranchNameKey.getParentKey(),
+    replication.fire(targetBranchNameKey.getParentKey(),
         targetBranchNameKey.get());
 
     expect(schema.submoduleSubscriptions()).andReturn(subscriptions);
@@ -738,7 +740,7 @@
     expect(repoManager.openRepository(targetBranchNameKey.getParentKey()))
         .andReturn(targetRepository);
 
-    replication.scheduleUpdate(targetBranchNameKey.getParentKey(),
+    replication.fire(targetBranchNameKey.getParentKey(),
         targetBranchNameKey.get());
 
     expect(schema.submoduleSubscriptions()).andReturn(subscriptions);
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/ioutil/ColumnFormatterTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/ioutil/ColumnFormatterTest.java
new file mode 100644
index 0000000..2d432e6
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/ioutil/ColumnFormatterTest.java
@@ -0,0 +1,138 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.ioutil;
+
+import com.google.gerrit.server.ioutil.ColumnFormatter;
+
+import junit.framework.TestCase;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+public class ColumnFormatterTest extends TestCase {
+  /**
+   * Holds an in-memory {@link java.io.PrintWriter} object and allows
+   * comparisons of its contents to a supplied string via an assert statement.
+   */
+  class PrintWriterComparator {
+    private PrintWriter printWriter;
+    private StringWriter stringWriter;
+
+    public PrintWriterComparator() {
+      stringWriter = new StringWriter();
+      printWriter = new PrintWriter(stringWriter);
+    }
+
+    public void assertEquals(String str) {
+      printWriter.flush();
+      TestCase.assertEquals(stringWriter.toString(), str);
+    }
+
+    public PrintWriter getPrintWriter() {
+      return printWriter;
+    }
+  }
+
+  /**
+   * Test that only lines with at least one column of text emit output.
+   */
+  public void testEmptyLine() {
+    final PrintWriterComparator comparator = new PrintWriterComparator();
+    final ColumnFormatter formatter =
+        new ColumnFormatter(comparator.getPrintWriter(), '\t');
+    formatter.addColumn("foo");
+    formatter.addColumn("bar");
+    formatter.nextLine();
+    formatter.nextLine();
+    formatter.nextLine();
+    formatter.addColumn("foo");
+    formatter.addColumn("bar");
+    formatter.finish();
+    comparator.assertEquals("foo\tbar\nfoo\tbar\n");
+  }
+
+  /**
+   * Test that there is no output if no columns are ever added.
+   */
+  public void testEmptyOutput() {
+    final PrintWriterComparator comparator = new PrintWriterComparator();
+    final ColumnFormatter formatter =
+        new ColumnFormatter(comparator.getPrintWriter(), '\t');
+    formatter.nextLine();
+    formatter.nextLine();
+    formatter.finish();
+    comparator.assertEquals("");
+  }
+
+  /**
+   * Test that there is no output (nor any exceptions) if we finalize
+   * the output immediately after the creation of the {@link ColumnFormatter}.
+   */
+  public void testNoNextLine() {
+    final PrintWriterComparator comparator = new PrintWriterComparator();
+    final ColumnFormatter formatter =
+        new ColumnFormatter(comparator.getPrintWriter(), '\t');
+    formatter.finish();
+    comparator.assertEquals("");
+  }
+
+  /**
+   * Test that the text in added columns is escaped while the column separator
+   * (which of course shouldn't be escaped) is left alone.
+   */
+  public void testEscapingTakesPlace() {
+    final PrintWriterComparator comparator = new PrintWriterComparator();
+    final ColumnFormatter formatter =
+        new ColumnFormatter(comparator.getPrintWriter(), '\t');
+    formatter.addColumn("foo");
+    formatter.addColumn(
+        "\tan indented multi-line\ntext");
+    formatter.nextLine();
+    formatter.finish();
+    comparator.assertEquals("foo\t\\tan indented multi-line\\ntext\n");
+  }
+
+  /**
+   * Test that we get the correct output with multi-line input where the number
+   * of columns in each line varies.
+   */
+  public void testMultiLineDifferentColumnCount() {
+    final PrintWriterComparator comparator = new PrintWriterComparator();
+    final ColumnFormatter formatter =
+        new ColumnFormatter(comparator.getPrintWriter(), '\t');
+    formatter.addColumn("foo");
+    formatter.addColumn("bar");
+    formatter.addColumn("baz");
+    formatter.nextLine();
+    formatter.addColumn("foo");
+    formatter.addColumn("bar");
+    formatter.nextLine();
+    formatter.finish();
+    comparator.assertEquals("foo\tbar\tbaz\nfoo\tbar\n");
+  }
+
+  /**
+   * Test that we get the correct output with a single column of input.
+   */
+  public void testOneColumn() {
+    final PrintWriterComparator comparator = new PrintWriterComparator();
+    final ColumnFormatter formatter =
+        new ColumnFormatter(comparator.getPrintWriter(), '\t');
+    formatter.addColumn("foo");
+    formatter.nextLine();
+    formatter.finish();
+    comparator.assertEquals("foo\n");
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java
index cd6fa69..d4b07ae 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,9 +20,9 @@
 import static com.google.gerrit.common.data.Permission.READ;
 import static com.google.gerrit.common.data.Permission.SUBMIT;
 
-import com.google.common.collect.Iterables;
+import com.google.common.cache.Cache;
+import com.google.common.cache.CacheBuilder;
 import com.google.common.collect.Lists;
-import com.google.common.collect.Sets;
 import com.google.gerrit.common.data.Capable;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.PermissionRange;
@@ -31,22 +31,18 @@
 import com.google.gerrit.reviewdb.client.AccountProjectWatch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.rules.PrologEnvironment;
 import com.google.gerrit.rules.RulesCache;
 import com.google.gerrit.server.AccessPath;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.CapabilityControl;
-import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.account.GroupMembership;
 import com.google.gerrit.server.account.ListGroupMembership;
-import com.google.gerrit.server.cache.ConcurrentHashMapCache;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.FactoryModule;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.ProjectConfig;
-import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Guice;
 import com.google.inject.Injector;
 
@@ -55,11 +51,9 @@
 import org.eclipse.jgit.lib.Config;
 
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
 
@@ -297,6 +291,10 @@
       }
 
       @Override
+      public void remove(Project p) {
+      }
+
+      @Override
       public Iterable<Project.NameKey> all() {
         return Collections.emptySet();
       }
@@ -335,10 +333,9 @@
     local = new ProjectConfig(new Project.NameKey("local"));
     local.createInMemory();
 
-    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) {
@@ -390,12 +387,10 @@
   }
 
   private ProjectControl user(String name, AccountGroup.UUID... memberOf) {
-    SchemaFactory<ReviewDb> schema = null;
-    GroupCache groupCache = null;
     String canonicalWebUrl = "http://localhost";
 
     return new ProjectControl(Collections.<AccountGroup.UUID> emptySet(),
-        Collections.<AccountGroup.UUID> emptySet(), schema, groupCache,
+        Collections.<AccountGroup.UUID> emptySet(), projectCache,
         sectionSorter,
         canonicalWebUrl, new MockUser(name, memberOf),
         newProjectState());
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 39cbfe4..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
@@ -19,6 +19,9 @@
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.GerritPersonIdentProvider;
 import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.AnonymousCowardName;
+import com.google.gerrit.server.config.AnonymousCowardNameProvider;
+import com.google.gerrit.server.config.FactoryModule;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -27,7 +30,6 @@
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.gwtorm.server.StatementExecutor;
-import com.google.inject.AbstractModule;
 import com.google.inject.Guice;
 import com.google.inject.TypeLiteral;
 
@@ -63,7 +65,7 @@
 
     final File site = new File(UUID.randomUUID().toString());
     final SitePaths paths = new SitePaths(site);
-    SchemaUpdater u = Guice.createInjector(new AbstractModule() {
+    SchemaUpdater u = Guice.createInjector(new FactoryModule() {
       @Override
       protected void configure() {
         bind(new TypeLiteral<SchemaFactory<ReviewDb>>() {}).toInstance(db);
@@ -88,6 +90,10 @@
 
         bind(GitRepositoryManager.class) //
             .to(LocalDiskRepositoryManager.class);
+
+        bind(String.class) //
+          .annotatedWith(AnonymousCowardName.class) //
+          .toProvider(AnonymousCowardNameProvider.class);
       }
     }).getInstance(SchemaUpdater.class);
 
@@ -102,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-server/src/test/java/com/google/gerrit/server/util/SubmoduleSectionParserTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/util/SubmoduleSectionParserTest.java
index c8e684f..93d86e5 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/util/SubmoduleSectionParserTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/util/SubmoduleSectionParserTest.java
@@ -224,7 +224,7 @@
             break;
           } else {
             expect(repoManager.list()).andReturn(
-                new TreeSet<Project.NameKey>(Collections.EMPTY_LIST));
+                new TreeSet<Project.NameKey>(Collections.<Project.NameKey> emptyList()));
           }
         }
       }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryDatabase.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryDatabase.java
index a44f84f..b1f956f 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryDatabase.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryDatabase.java
@@ -20,6 +20,8 @@
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.GerritPersonIdentProvider;
 import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.AnonymousCowardName;
+import com.google.gerrit.server.config.AnonymousCowardNameProvider;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePath;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -122,6 +124,10 @@
 
               bind(GitRepositoryManager.class) //
                   .to(LocalDiskRepositoryManager.class);
+
+              bind(String.class) //
+                .annotatedWith(AnonymousCowardName.class) //
+                .toProvider(AnonymousCowardNameProvider.class);
             }
           }).getBinding(Key.get(SchemaVersion.class, Current.class))
               .getProvider().get();
diff --git a/gerrit-sshd/.gitignore b/gerrit-sshd/.gitignore
index 194bedc..8deb9bd 100644
--- a/gerrit-sshd/.gitignore
+++ b/gerrit-sshd/.gitignore
@@ -2,4 +2,5 @@
 /.classpath
 /.project
 /.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-sshd.iml
\ No newline at end of file
diff --git a/gerrit-sshd/.settings/org.eclipse.core.resources.prefs b/gerrit-sshd/.settings/org.eclipse.core.resources.prefs
index c780f44..839d647 100644
--- a/gerrit-sshd/.settings/org.eclipse.core.resources.prefs
+++ b/gerrit-sshd/.settings/org.eclipse.core.resources.prefs
@@ -1,4 +1,5 @@
-#Thu Jul 28 11:02:36 PDT 2011
 eclipse.preferences.version=1
 encoding//src/main/java=UTF-8
+encoding//src/main/resources=UTF-8
+encoding//src/test/java=UTF-8
 encoding/<project>=UTF-8
diff --git a/gerrit-sshd/pom.xml b/gerrit-sshd/pom.xml
index 55e8725..a26b1b2 100644
--- a/gerrit-sshd/pom.xml
+++ b/gerrit-sshd/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.4-SNAPSHOT</version>
+    <version>2.5-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-sshd</artifactId>
@@ -44,6 +44,11 @@
     </dependency>
 
     <dependency>
+      <groupId>org.apache.mina</groupId>
+      <artifactId>mina-core</artifactId>
+    </dependency>
+
+    <dependency>
       <groupId>org.apache.sshd</groupId>
       <artifactId>sshd-core</artifactId>
     </dependency>
@@ -67,7 +72,7 @@
 
     <dependency>
       <groupId>com.google.gerrit</groupId>
-      <artifactId>gerrit-ehcache</artifactId>
+      <artifactId>gerrit-cache-h2</artifactId>
       <version>${project.version}</version>
     </dependency>
   </dependencies>
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AbstractGitCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AbstractGitCommand.java
index eb8a5c2..af5df25 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AbstractGitCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AbstractGitCommand.java
@@ -36,6 +36,9 @@
   protected ProjectControl projectControl;
 
   @Inject
+  private SshScope sshScope;
+
+  @Inject
   private GitRepositoryManager repoManager;
 
   @Inject
@@ -56,7 +59,7 @@
   @Override
   public void start(final Environment env) {
     Context ctx = context.subContext(newSession(), context.getCommandLine());
-    final Context old = SshScope.set(ctx);
+    final Context old = sshScope.set(ctx);
     try {
       startThread(new ProjectCommandRunnable() {
         @Override
@@ -76,7 +79,7 @@
         }
       });
     } finally {
-      SshScope.set(old);
+      sshScope.set(old);
     }
   }
 
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommand.java
new file mode 100644
index 0000000..9582c93
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommand.java
@@ -0,0 +1,125 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.sshd;
+
+import com.google.common.collect.Lists;
+import com.google.common.util.concurrent.Atomics;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.CapabilityControl;
+import com.google.inject.Provider;
+
+import org.apache.sshd.server.Command;
+import org.apache.sshd.server.Environment;
+
+import java.io.IOException;
+import java.util.LinkedList;
+import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
+
+/** Command that executes some other command. */
+public class AliasCommand extends BaseCommand {
+  private final DispatchCommandProvider root;
+  private final Provider<CurrentUser> currentUser;
+  private final CommandName command;
+  private final AtomicReference<Command> atomicCmd;
+
+  AliasCommand(@CommandName(Commands.ROOT) DispatchCommandProvider root,
+      Provider<CurrentUser> currentUser, CommandName command) {
+    this.root = root;
+    this.currentUser = currentUser;
+    this.command = command;
+    this.atomicCmd = Atomics.newReference();
+  }
+
+  @Override
+  public void start(Environment env) throws IOException {
+    try {
+      begin(env);
+    } catch (UnloggedFailure e) {
+      String msg = e.getMessage();
+      if (!msg.endsWith("\n")) {
+        msg += "\n";
+      }
+      err.write(msg.getBytes(ENC));
+      err.flush();
+      onExit(e.exitCode);
+    }
+  }
+
+  private void begin(Environment env) throws UnloggedFailure, IOException {
+    Map<String, Provider<Command>> map = root.getMap();
+    for (String name : chain(command)) {
+      Provider<? extends Command> p = map.get(name);
+      if (p == null) {
+        throw new UnloggedFailure(1, getName() + ": not found");
+      }
+
+      Command cmd = p.get();
+      if (!(cmd instanceof DispatchCommand)) {
+        throw new UnloggedFailure(1, getName() + ": not found");
+      }
+      map = ((DispatchCommand) cmd).getMap();
+    }
+
+    Provider<? extends Command> p = map.get(command.value());
+    if (p == null) {
+      throw new UnloggedFailure(1, getName() + ": not found");
+    }
+
+    Command cmd = p.get();
+    checkRequiresCapability(cmd);
+    if (cmd instanceof BaseCommand) {
+      BaseCommand bc = (BaseCommand)cmd;
+      bc.setName(getName());
+      bc.setArguments(getArguments());
+    }
+    provideStateTo(cmd);
+    atomicCmd.set(cmd);
+    cmd.start(env);
+  }
+
+  @Override
+  public void destroy() {
+    Command cmd = atomicCmd.getAndSet(null);
+    if (cmd != null) {
+        cmd.destroy();
+    }
+  }
+
+  private void checkRequiresCapability(Command cmd) throws UnloggedFailure {
+    RequiresCapability rc = cmd.getClass().getAnnotation(RequiresCapability.class);
+    if (rc != null) {
+      CurrentUser user = currentUser.get();
+      CapabilityControl ctl = user.getCapabilities();
+      if (!ctl.canPerform(rc.value()) && !ctl.canAdministrateServer()) {
+        String msg = String.format(
+            "fatal: %s does not have \"%s\" capability.",
+            user.getUserName(), rc.value());
+        throw new UnloggedFailure(BaseCommand.STATUS_NOT_ADMIN, msg);
+      }
+    }
+  }
+
+  private static LinkedList<String> chain(CommandName command) {
+    LinkedList<String> chain = Lists.newLinkedList();
+    while (command != null) {
+      chain.addFirst(command.value());
+      command = Commands.parentOf(command);
+    }
+    chain.removeLast();
+    return chain;
+  }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommandProvider.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommandProvider.java
new file mode 100644
index 0000000..ee28e03
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommandProvider.java
@@ -0,0 +1,42 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.sshd;
+
+import com.google.gerrit.server.CurrentUser;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.apache.sshd.server.Command;
+
+/** Resolves an alias to another command. */
+public class AliasCommandProvider implements Provider<Command> {
+  private final CommandName command;
+
+  @Inject
+  @CommandName(Commands.ROOT)
+  private DispatchCommandProvider root;
+
+  @Inject
+  private Provider<CurrentUser> currentUser;
+
+  public AliasCommandProvider(CommandName command) {
+    this.command = command;
+  }
+
+  @Override
+  public Command get() {
+    return new AliasCommand(root, currentUser, command);
+  }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java
index a926e77..9e04f05 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.sshd;
 
+import com.google.common.util.concurrent.Atomics;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.Project.NameKey;
 import com.google.gerrit.server.CurrentUser;
@@ -50,6 +51,7 @@
 import java.io.StringWriter;
 import java.io.UnsupportedEncodingException;
 import java.util.concurrent.Future;
+import java.util.concurrent.atomic.AtomicReference;
 
 public abstract class BaseCommand implements Command {
   private static final Logger log = LoggerFactory.getLogger(BaseCommand.class);
@@ -71,6 +73,9 @@
   private ExitCallback exit;
 
   @Inject
+  private SshScope sshScope;
+
+  @Inject
   private CmdLineParser.Factory cmdLineParserFactory;
 
   @Inject
@@ -87,7 +92,7 @@
   private Provider<SshScope.Context> contextProvider;
 
   /** The task, as scheduled on a worker thread. */
-  private Future<?> task;
+  private final AtomicReference<Future<?>> task;
 
   /** Text of the command line which lead up to invoking this instance. */
   private String commandName = "";
@@ -95,6 +100,10 @@
   /** Unparsed command line options. */
   private String[] argv;
 
+  public BaseCommand() {
+    task = Atomics.newReference();
+  }
+
   public void setInputStream(final InputStream in) {
     this.in = in;
   }
@@ -111,18 +120,27 @@
     this.exit = callback;
   }
 
+  String getName() {
+    return commandName;
+  }
+
   void setName(final String prefix) {
     this.commandName = prefix;
   }
 
+  String[] getArguments() {
+    return argv;
+  }
+
   public void setArguments(final String[] argv) {
     this.argv = argv;
   }
 
   @Override
   public void destroy() {
-    if (task != null && !task.isDone()) {
-      task.cancel(true);
+    Future<?> future = task.getAndSet(null);
+    if (future != null && !future.isDone()) {
+      future.cancel(true);
     }
   }
 
@@ -243,25 +261,21 @@
    * @param thunk the runnable to execute on the thread, performing the
    *        command's logic.
    */
-  protected synchronized void startThread(final CommandRunnable thunk) {
+  protected void startThread(final CommandRunnable thunk) {
     final TaskThunk tt = new TaskThunk(thunk);
 
-    if (isAdminCommand() || (isAdminHighPriorityCommand()
-        && userProvider.get().getCapabilities().canAdministrateServer())) {
+    if (isAdminHighPriorityCommand()
+        && userProvider.get().getCapabilities().canAdministrateServer()) {
       // Admin commands should not block the main work threads (there
       // might be an interactive shell there), nor should they wait
       // for the main work threads.
       //
       new Thread(tt, tt.toString()).start();
     } else {
-      task = executor.submit(tt);
+      task.set(executor.submit(tt));
     }
   }
 
-  private final boolean isAdminCommand() {
-    return getClass().getAnnotation(AdminCommand.class) != null;
-  }
-
   private final boolean isAdminHighPriorityCommand() {
     return getClass().getAnnotation(AdminHighPriorityCommand.class) != null;
   }
@@ -277,7 +291,9 @@
    */
   protected void onExit(final int rc) {
     exit.onExit(rc);
-    cleanup.run();
+    if (cleanup != null) {
+      cleanup.run();
+    }
   }
 
   /** Wrap the supplied output stream in a UTF-8 encoded PrintWriter. */
@@ -355,6 +371,14 @@
     return new UnloggedFailure(1, "fatal: " + why.getMessage(), why);
   }
 
+  public void checkExclusivity(final Object arg1, final String arg1name,
+      final Object arg2, final String arg2name) throws UnloggedFailure {
+    if (arg1 != null && arg2 != null) {
+      throw new UnloggedFailure(String.format(
+          "%s and %s options are mutually exclusive.", arg1name, arg2name));
+    }
+  }
+
   private final class TaskThunk implements CancelableRunnable, ProjectRunnable {
     private final CommandRunnable thunk;
     private final Context context;
@@ -376,55 +400,59 @@
 
     @Override
     public void cancel() {
-      final Context old = SshScope.set(context);
-      try {
-        onExit(STATUS_CANCEL);
-      } finally {
-        SshScope.set(old);
+      synchronized (this) {
+        final Context old = sshScope.set(context);
+        try {
+          onExit(STATUS_CANCEL);
+        } finally {
+          sshScope.set(old);
+        }
       }
     }
 
     @Override
     public void run() {
-      final Thread thisThread = Thread.currentThread();
-      final String thisName = thisThread.getName();
-      int rc = 0;
-      final Context old = SshScope.set(context);
-      try {
-        context.started = System.currentTimeMillis();
-        thisThread.setName("SSH " + taskName);
-
-        if (thunk instanceof ProjectCommandRunnable) {
-          ((ProjectCommandRunnable) thunk).executeParseCommand();
-          projectName = ((ProjectCommandRunnable) thunk).getProjectName();
-        }
-
+      synchronized (this) {
+        final Thread thisThread = Thread.currentThread();
+        final String thisName = thisThread.getName();
+        int rc = 0;
+        final Context old = sshScope.set(context);
         try {
-          thunk.run();
-        } catch (NoSuchProjectException e) {
-          throw new UnloggedFailure(1, e.getMessage() + " no such project");
-        } catch (NoSuchChangeException e) {
-          throw new UnloggedFailure(1, e.getMessage() + " no such change");
-        }
+          context.started = System.currentTimeMillis();
+          thisThread.setName("SSH " + taskName);
 
-        out.flush();
-        err.flush();
-      } catch (Throwable e) {
-        try {
+          if (thunk instanceof ProjectCommandRunnable) {
+            ((ProjectCommandRunnable) thunk).executeParseCommand();
+            projectName = ((ProjectCommandRunnable) thunk).getProjectName();
+          }
+
+          try {
+            thunk.run();
+          } catch (NoSuchProjectException e) {
+            throw new UnloggedFailure(1, e.getMessage() + " no such project");
+          } catch (NoSuchChangeException e) {
+            throw new UnloggedFailure(1, e.getMessage() + " no such change");
+          }
+
           out.flush();
-        } catch (Throwable e2) {
-        }
-        try {
           err.flush();
-        } catch (Throwable e2) {
-        }
-        rc = handleError(e);
-      } finally {
-        try {
-          onExit(rc);
+        } catch (Throwable e) {
+          try {
+            out.flush();
+          } catch (Throwable e2) {
+          }
+          try {
+            err.flush();
+          } catch (Throwable e2) {
+          }
+          rc = handleError(e);
         } finally {
-          SshScope.set(old);
-          thisThread.setName(thisName);
+          try {
+            onExit(rc);
+          } finally {
+            sshScope.set(old);
+            thisThread.setName(thisName);
+          }
         }
       }
     }
@@ -505,6 +533,15 @@
     /**
      * Create a new failure.
      *
+     * @param msg message to also send to the client's stderr.
+     */
+    public UnloggedFailure(final String msg) {
+      this(1, msg);
+    }
+
+    /**
+     * Create a new failure.
+     *
      * @param exitCode exit code to return the client, which indicates the
      *        failure status of this command. Should be between 1 and 255,
      *        inclusive.
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandFactoryProvider.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandFactoryProvider.java
index 66e6add..7f08d49 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandFactoryProvider.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandFactoryProvider.java
@@ -53,6 +53,7 @@
 
   private final DispatchCommandProvider dispatcher;
   private final SshLog log;
+  private final SshScope sshScope;
   private final ScheduledExecutorService startExecutor;
   private final Executor destroyExecutor;
 
@@ -60,9 +61,10 @@
   CommandFactoryProvider(
       @CommandName(Commands.ROOT) final DispatchCommandProvider d,
       @GerritServerConfig final Config cfg, final WorkQueue workQueue,
-      final SshLog l) {
+      final SshLog l, final SshScope s) {
     dispatcher = d;
     log = l;
+    sshScope = s;
 
     int threads = cfg.getInt("sshd","commandStartThreads", 2);
     startExecutor = workQueue.createQueue(threads, "SshCommandStart");
@@ -120,7 +122,7 @@
 
     public void setSession(final ServerSession session) {
       final SshSession s = session.getAttribute(SshSession.KEY);
-      this.ctx = new Context(s, commandLine);
+      this.ctx = sshScope.newContext(s, commandLine);
     }
 
     public void start(final Environment env) throws IOException {
@@ -145,7 +147,7 @@
 
     private void onStart() throws IOException {
       synchronized (this) {
-        final Context old = SshScope.set(ctx);
+        final Context old = sshScope.set(ctx);
         try {
           cmd = dispatcher.get();
           cmd.setArguments(argv);
@@ -167,7 +169,7 @@
           });
           cmd.start(env);
         } finally {
-          SshScope.set(old);
+          sshScope.set(old);
         }
       }
     }
@@ -211,14 +213,14 @@
     private void onDestroy() {
       synchronized (this) {
         if (cmd != null) {
-          final Context old = SshScope.set(ctx);
+          final Context old = sshScope.set(ctx);
           try {
             cmd.destroy();
             log(BaseCommand.STATUS_CANCEL);
           } finally {
             ctx = null;
             cmd = null;
-            SshScope.set(old);
+            sshScope.set(old);
           }
         }
       }
@@ -226,7 +228,7 @@
   }
 
   /** Split a command line into a string array. */
-  static String[] split(String commandLine) {
+  static public String[] split(String commandLine) {
     final List<String> list = new ArrayList<String>();
     boolean inquote = false;
     boolean inDblQuote = false;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/Commands.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/Commands.java
index 12cff9c..5340d6f 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/Commands.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/Commands.java
@@ -97,6 +97,13 @@
     return false;
   }
 
+  static CommandName parentOf(CommandName name) {
+    if (name instanceof NestedCommandNameImpl) {
+      return ((NestedCommandNameImpl) name).parent;
+    }
+    return null;
+  }
+
   private static final class NestedCommandNameImpl implements CommandName {
     private final CommandName parent;
     private final String name;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
index a96a661..a69a2f1 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DatabasePubKeyAuth.java
@@ -65,6 +65,7 @@
   private final IdentifiedUser.GenericFactory userFactory;
   private final PeerDaemonUser.Factory peerFactory;
   private final Config config;
+  private final SshScope sshScope;
   private final Set<PublicKey> myHostKeys;
   private volatile PeerKeyCache peerKeyCache;
 
@@ -72,12 +73,13 @@
   DatabasePubKeyAuth(final SshKeyCacheImpl skc, final SshLog l,
       final IdentifiedUser.GenericFactory uf, final PeerDaemonUser.Factory pf,
       final SitePaths site, final KeyPairProvider hostKeyProvider,
-      final @GerritServerConfig Config cfg) {
+      final @GerritServerConfig Config cfg, final SshScope s) {
     sshKeyCache = skc;
     sshLog = l;
     userFactory = uf;
     peerFactory = pf;
     config = cfg;
+    sshScope = s;
     myHostKeys = myHostKeys(hostKeyProvider);
     peerKeyCache = new PeerKeyCache(site.peer_keys);
   }
@@ -171,24 +173,24 @@
       // session, record a login event in the log and add
       // a close listener to record a logout event.
       //
-      Context ctx = new Context(sd, null);
-      Context old = SshScope.set(ctx);
+      Context ctx = sshScope.newContext(sd, null);
+      Context old = sshScope.set(ctx);
       try {
         sshLog.onLogin();
       } finally {
-        SshScope.set(old);
+        sshScope.set(old);
       }
 
       session.getIoSession().getCloseFuture().addListener(
           new IoFutureListener<IoFuture>() {
             @Override
             public void operationComplete(IoFuture future) {
-              final Context ctx = new Context(sd, null);
-              final Context old = SshScope.set(ctx);
+              final Context ctx = sshScope.newContext(sd, null);
+              final Context old = sshScope.set(ctx);
               try {
                 sshLog.onLogout();
               } finally {
-                SshScope.set(old);
+                sshScope.set(old);
               }
             }
           });
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java
index 8daa7f4..691f3a0 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java
@@ -14,8 +14,12 @@
 
 package com.google.gerrit.sshd;
 
+import com.google.common.collect.Sets;
+import com.google.common.util.concurrent.Atomics;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.sshd.args4j.SubcommandHandler;
+import com.google.gerrit.server.account.CapabilityControl;
+import com.google.gerrit.server.args4j.SubcommandHandler;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
@@ -28,19 +32,19 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
+import java.util.concurrent.atomic.AtomicReference;
 
 /**
  * Command that dispatches to a subcommand from its command table.
  */
 final class DispatchCommand extends BaseCommand {
   interface Factory {
-    DispatchCommand create(String prefix, Map<String, Provider<Command>> map);
+    DispatchCommand create(Map<String, Provider<Command>> map);
   }
 
   private final Provider<CurrentUser> currentUser;
-  private final String prefix;
   private final Map<String, Provider<Command>> commands;
-  private Command cmd;
+  private final AtomicReference<Command> atomicCmd;
 
   @Argument(index = 0, required = true, metaVar = "COMMAND", handler = SubcommandHandler.class)
   private String commandName;
@@ -49,11 +53,15 @@
   private List<String> args = new ArrayList<String>();
 
   @Inject
-  DispatchCommand(final Provider<CurrentUser> cu, @Assisted final String pfx,
+  DispatchCommand(final Provider<CurrentUser> cu,
       @Assisted final Map<String, Provider<Command>> all) {
     currentUser = cu;
-    prefix = pfx;
     commands = all;
+    atomicCmd = Atomics.newReference();
+  }
+
+  Map<String, Provider<Command>> getMap() {
+    return commands;
   }
 
   @Override
@@ -64,25 +72,19 @@
       final Provider<Command> p = commands.get(commandName);
       if (p == null) {
         String msg =
-            (prefix.isEmpty() ? "Gerrit Code Review" : prefix) + ": "
+            (getName().isEmpty() ? "Gerrit Code Review" : getName()) + ": "
                 + commandName + ": not found";
         throw new UnloggedFailure(1, msg);
       }
 
       final Command cmd = p.get();
-
-      if (isAdminCommand(cmd)
-          && !currentUser.get().getCapabilities().canAdministrateServer()) {
-        final String msg = "fatal: Not a Gerrit administrator";
-        throw new UnloggedFailure(BaseCommand.STATUS_NOT_ADMIN, msg);
-      }
-
+      checkRequiresCapability(cmd);
       if (cmd instanceof BaseCommand) {
         final BaseCommand bc = (BaseCommand) cmd;
-        if (prefix.isEmpty())
+        if (getName().isEmpty())
           bc.setName(commandName);
         else
-          bc.setName(prefix + " " + commandName);
+          bc.setName(getName() + " " + commandName);
         bc.setArguments(args.toArray(new String[args.size()]));
 
       } else if (!args.isEmpty()) {
@@ -90,10 +92,7 @@
       }
 
       provideStateTo(cmd);
-
-      synchronized (this) {
-        this.cmd = cmd;
-      }
+      atomicCmd.set(cmd);
       cmd.start(env);
 
     } catch (UnloggedFailure e) {
@@ -107,17 +106,25 @@
     }
   }
 
-  private boolean isAdminCommand(final Command cmd) {
-    return cmd.getClass().getAnnotation(AdminCommand.class) != null;
+  private void checkRequiresCapability(Command cmd) throws UnloggedFailure {
+    RequiresCapability rc = cmd.getClass().getAnnotation(RequiresCapability.class);
+    if (rc != null) {
+      CurrentUser user = currentUser.get();
+      CapabilityControl ctl = user.getCapabilities();
+      if (!ctl.canPerform(rc.value()) && !ctl.canAdministrateServer()) {
+        String msg = String.format(
+            "fatal: %s does not have \"%s\" capability.",
+            user.getUserName(), rc.value());
+        throw new UnloggedFailure(BaseCommand.STATUS_NOT_ADMIN, msg);
+      }
+    }
   }
 
   @Override
   public void destroy() {
-    synchronized (this) {
-      if (cmd != null) {
+    Command cmd = atomicCmd.getAndSet(null);
+    if (cmd != null) {
         cmd.destroy();
-        cmd = null;
-      }
     }
   }
 
@@ -125,26 +132,30 @@
   protected String usage() {
     final StringBuilder usage = new StringBuilder();
     usage.append("Available commands");
-    if (!prefix.isEmpty()) {
+    if (!getName().isEmpty()) {
       usage.append(" of ");
-      usage.append(prefix);
+      usage.append(getName());
     }
     usage.append(" are:\n");
     usage.append("\n");
-    for (Map.Entry<String, Provider<Command>> e : commands.entrySet()) {
+    for (String name : Sets.newTreeSet(commands.keySet())) {
       usage.append("   ");
-      usage.append(e.getKey());
+      usage.append(name);
       usage.append("\n");
     }
     usage.append("\n");
 
     usage.append("See '");
-    if (prefix.indexOf(' ') < 0) {
-      usage.append(prefix);
+    if (getName().indexOf(' ') < 0) {
+      usage.append(getName());
       usage.append(' ');
     }
     usage.append("COMMAND --help' for more information.\n");
     usage.append("\n");
     return usage.toString();
   }
+
+  public String getCommandName() {
+    return commandName;
+  }
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommandProvider.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommandProvider.java
index 0b69228..b76ff71 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommandProvider.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommandProvider.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.sshd;
 
+import com.google.common.collect.Maps;
+import com.google.gerrit.extensions.registration.RegistrationHandle;
 import com.google.inject.Binding;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
@@ -23,11 +25,8 @@
 import org.apache.sshd.server.Command;
 
 import java.lang.annotation.Annotation;
-import java.util.Collections;
-import java.util.LinkedHashMap;
 import java.util.List;
-import java.util.Map;
-import java.util.TreeMap;
+import java.util.concurrent.ConcurrentMap;
 
 /**
  * Creates DispatchCommand using commands registered by {@link CommandModule}.
@@ -39,27 +38,45 @@
   @Inject
   private DispatchCommand.Factory factory;
 
-  private final String dispatcherName;
   private final CommandName parent;
-
-  private volatile Map<String, Provider<Command>> map;
+  private volatile ConcurrentMap<String, Provider<Command>> map;
 
   public DispatchCommandProvider(final CommandName cn) {
-    this(Commands.nameOf(cn), cn);
-  }
-
-  public DispatchCommandProvider(final String dispatcherName,
-      final CommandName cn) {
-    this.dispatcherName = dispatcherName;
     this.parent = cn;
   }
 
   @Override
   public DispatchCommand get() {
-    return factory.create(dispatcherName, getMap());
+    return factory.create(getMap());
   }
 
-  private Map<String, Provider<Command>> getMap() {
+  public RegistrationHandle register(final CommandName name,
+      final Provider<Command> cmd) {
+    final ConcurrentMap<String, Provider<Command>> m = getMap();
+    if (m.putIfAbsent(name.value(), cmd) != null) {
+      throw new IllegalArgumentException(name.value() + " exists");
+    }
+    return new RegistrationHandle() {
+      @Override
+      public void remove() {
+        m.remove(name.value(), cmd);
+      }
+    };
+  }
+
+  public RegistrationHandle replace(final CommandName name,
+      final Provider<Command> cmd) {
+    final ConcurrentMap<String, Provider<Command>> m = getMap();
+    m.put(name.value(), cmd);
+    return new RegistrationHandle() {
+      @Override
+      public void remove() {
+        m.remove(name.value(), cmd);
+      }
+    };
+  }
+
+  ConcurrentMap<String, Provider<Command>> getMap() {
     if (map == null) {
       synchronized (this) {
         if (map == null) {
@@ -71,10 +88,8 @@
   }
 
   @SuppressWarnings("unchecked")
-  private Map<String, Provider<Command>> createMap() {
-    final Map<String, Provider<Command>> m =
-        new TreeMap<String, Provider<Command>>();
-
+  private ConcurrentMap<String, Provider<Command>> createMap() {
+    ConcurrentMap<String, Provider<Command>> m = Maps.newConcurrentMap();
     for (final Binding<?> b : allCommands()) {
       final Annotation annotation = b.getKey().getAnnotation();
       if (annotation instanceof CommandName) {
@@ -84,9 +99,7 @@
         }
       }
     }
-
-    return Collections.unmodifiableMap(
-        new LinkedHashMap<String, Provider<Command>>(m));
+    return m;
   }
 
   private static final TypeLiteral<Command> type =
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/NoShell.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/NoShell.java
index be513b3..d2fdf78 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/NoShell.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/NoShell.java
@@ -58,6 +58,7 @@
 
   static class SendMessage implements Command, SessionAware {
     private final Provider<MessageFactory> messageFactory;
+    private final SshScope sshScope;
 
     private InputStream in;
     private OutputStream out;
@@ -66,8 +67,9 @@
     private Context context;
 
     @Inject
-    SendMessage(Provider<MessageFactory> messageFactory) {
+    SendMessage(Provider<MessageFactory> messageFactory, SshScope sshScope) {
       this.messageFactory = messageFactory;
+      this.sshScope = sshScope;
     }
 
     public void setInputStream(final InputStream in) {
@@ -87,16 +89,16 @@
     }
 
     public void setSession(final ServerSession session) {
-      this.context = new Context(session.getAttribute(SshSession.KEY), "");
+      this.context = sshScope.newContext(session.getAttribute(SshSession.KEY), "");
     }
 
     public void start(final Environment env) throws IOException {
-      Context old = SshScope.set(context);
+      Context old = sshScope.set(context);
       String message;
       try {
         message = messageFactory.get().getMessage();
       } finally {
-        SshScope.set(old);
+        sshScope.set(old);
       }
       err.write(Constants.encode(message.toString()));
       err.flush();
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/PluginCommandModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/PluginCommandModule.java
new file mode 100644
index 0000000..4dbb8d7
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/PluginCommandModule.java
@@ -0,0 +1,49 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.sshd;
+
+import com.google.common.base.Preconditions;
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.sshd.CommandName;
+import com.google.gerrit.sshd.Commands;
+import com.google.gerrit.sshd.DispatchCommandProvider;
+import com.google.inject.AbstractModule;
+import com.google.inject.binder.LinkedBindingBuilder;
+
+import org.apache.sshd.server.Command;
+
+import javax.inject.Inject;
+
+public abstract class PluginCommandModule extends AbstractModule {
+  private CommandName command;
+
+  @Inject
+  void setPluginName(@PluginName String name) {
+    this.command = Commands.named(name);
+  }
+
+  @Override
+  protected final void configure() {
+    Preconditions.checkState(command != null, "@PluginName must be provided");
+    bind(Commands.key(command)).toProvider(new DispatchCommandProvider(command));
+    configureCommands();
+  }
+
+  protected abstract void configureCommands();
+
+  protected LinkedBindingBuilder<Command> command(String subCmd) {
+    return bind(Commands.key(command, subCmd));
+  }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshAutoRegisterModuleGenerator.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshAutoRegisterModuleGenerator.java
new file mode 100644
index 0000000..03485f7
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshAutoRegisterModuleGenerator.java
@@ -0,0 +1,74 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.sshd;
+
+import com.google.common.base.Preconditions;
+import com.google.common.collect.Maps;
+import com.google.gerrit.extensions.annotations.Export;
+import com.google.gerrit.server.plugins.InvalidPluginException;
+import com.google.gerrit.server.plugins.ModuleGenerator;
+import com.google.inject.AbstractModule;
+import com.google.inject.Module;
+
+import org.apache.sshd.server.Command;
+
+import java.util.Map;
+
+class SshAutoRegisterModuleGenerator
+    extends AbstractModule
+    implements ModuleGenerator {
+  private final Map<String, Class<Command>> commands = Maps.newHashMap();
+  private CommandName command;
+
+  @Override
+  protected void configure() {
+    bind(Commands.key(command))
+        .toProvider(new DispatchCommandProvider(command));
+    for (Map.Entry<String, Class<Command>> e : commands.entrySet()) {
+      bind(Commands.key(command, e.getKey())).to(e.getValue());
+    }
+  }
+
+  public void setPluginName(String name) {
+    command = Commands.named(name);
+  }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public void export(Export export, Class<?> type)
+      throws InvalidPluginException {
+    Preconditions.checkState(command != null, "pluginName must be provided");
+    if (Command.class.isAssignableFrom(type)) {
+      Class<Command> old = commands.get(export.value());
+      if (old != null) {
+        throw new InvalidPluginException(String.format(
+            "@Export(\"%s\") has duplicate bindings:\n  %s\n  %s",
+            export.value(), old.getName(), type.getName()));
+      }
+      commands.put(export.value(), (Class<Command>) type);
+    } else {
+      throw new InvalidPluginException(String.format(
+          "Class %s with @Export(\"%s\") must extend %s or implement %s",
+          type.getName(), export.value(),
+          SshCommand.class.getName(), Command.class.getName()));
+    }
+  }
+
+  @Override
+  public Module create() throws InvalidPluginException {
+    Preconditions.checkState(command != null, "pluginName must be provided");
+    return !commands.isEmpty() ? this : null;
+  }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshCommand.java
new file mode 100644
index 0000000..f6209ba
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshCommand.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.sshd;
+
+import org.apache.sshd.server.Environment;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+
+public abstract class SshCommand extends BaseCommand {
+  protected PrintWriter stdout;
+  protected PrintWriter stderr;
+
+  @Override
+  public void start(Environment env) throws IOException {
+    startThread(new CommandRunnable() {
+      @Override
+      public void run() throws Exception {
+        parseCommandLine();
+        stdout = toPrintWriter(out);
+        stderr = toPrintWriter(err);
+        try {
+          SshCommand.this.run();
+        } finally {
+          stdout.flush();
+          stderr.flush();
+        }
+      }
+    });
+  }
+
+  protected abstract void run() throws UnloggedFailure, Failure, Exception;
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshCurrentUserProvider.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshCurrentUserProvider.java
deleted file mode 100644
index 5839cf1..0000000
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshCurrentUserProvider.java
+++ /dev/null
@@ -1,42 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.sshd;
-
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.Singleton;
-
-@Singleton
-class SshCurrentUserProvider implements Provider<CurrentUser> {
-  private final Provider<SshSession> session;
-  private final Provider<IdentifiedUser> identifiedProvider;
-
-  @Inject
-  SshCurrentUserProvider(Provider<SshSession> s, Provider<IdentifiedUser> p) {
-    session = s;
-    identifiedProvider = p;
-  }
-
-  @Override
-  public CurrentUser get() {
-    final CurrentUser user = session.get().getCurrentUser();
-    if (user instanceof IdentifiedUser) {
-      return identifiedProvider.get();
-    }
-    return session.get().getCurrentUser();
-  }
-}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java
index d382a57..664ce45 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshDaemon.java
@@ -18,7 +18,7 @@
 import static java.util.concurrent.TimeUnit.SECONDS;
 
 import com.google.gerrit.common.Version;
-import com.google.gerrit.lifecycle.LifecycleListener;
+import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.ssh.SshInfo;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshIdentifiedUserProvider.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshIdentifiedUserProvider.java
deleted file mode 100644
index 516b59c..0000000
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshIdentifiedUserProvider.java
+++ /dev/null
@@ -1,47 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.sshd;
-
-import com.google.gerrit.common.errors.NotSignedInException;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.ProvisionException;
-import com.google.inject.Singleton;
-
-@Singleton
-class SshIdentifiedUserProvider implements Provider<IdentifiedUser> {
-  private final Provider<SshSession> session;
-  private final IdentifiedUser.RequestFactory factory;
-
-  @Inject
-  SshIdentifiedUserProvider(Provider<SshSession> s,
-      IdentifiedUser.RequestFactory f) {
-    session = s;
-    factory = f;
-  }
-
-  @Override
-  public IdentifiedUser get() {
-    final CurrentUser user = session.get().getCurrentUser();
-    if (user instanceof IdentifiedUser) {
-      return factory.create(user.getAccessPath(), //
-          ((IdentifiedUser) user).getAccountId());
-    }
-    throw new ProvisionException(NotSignedInException.MESSAGE,
-        new NotSignedInException());
-  }
-}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheImpl.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
index 1f5ac28..0a1f708 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
@@ -16,13 +16,13 @@
 
 import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_USERNAME;
 
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
 import com.google.gerrit.common.errors.InvalidSshKeyException;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gerrit.reviewdb.client.AccountSshKey;
 import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.cache.Cache;
 import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.cache.EntryCreator;
 import com.google.gerrit.server.ssh.SshKeyCache;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.SchemaFactory;
@@ -42,6 +42,7 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.List;
+import java.util.concurrent.ExecutionException;
 
 /** Provides the {@link SshKeyCacheEntry}. */
 @Singleton
@@ -57,9 +58,10 @@
     return new CacheModule() {
       @Override
       protected void configure() {
-        final TypeLiteral<Cache<String, Iterable<SshKeyCacheEntry>>> type =
-            new TypeLiteral<Cache<String, Iterable<SshKeyCacheEntry>>>() {};
-        core(type, CACHE_NAME).populateWith(Loader.class);
+        cache(CACHE_NAME,
+            String.class,
+            new TypeLiteral<Iterable<SshKeyCacheEntry>>(){})
+          .loader(Loader.class);
         bind(SshKeyCacheImpl.class);
         bind(SshKeyCache.class).to(SshKeyCacheImpl.class);
       }
@@ -71,20 +73,27 @@
         .asList(new SshKeyCacheEntry[0]));
   }
 
-  private final Cache<String, Iterable<SshKeyCacheEntry>> cache;
+  private final LoadingCache<String, Iterable<SshKeyCacheEntry>> cache;
 
   @Inject
   SshKeyCacheImpl(
-      @Named(CACHE_NAME) final Cache<String, Iterable<SshKeyCacheEntry>> cache) {
+      @Named(CACHE_NAME) LoadingCache<String, Iterable<SshKeyCacheEntry>> cache) {
     this.cache = cache;
   }
 
-  public Iterable<SshKeyCacheEntry> get(String username) {
-    return cache.get(username);
+  Iterable<SshKeyCacheEntry> get(String username) {
+    try {
+      return cache.get(username);
+    } catch (ExecutionException e) {
+      log.warn("Cannot load SSH keys for " + username, e);
+      return Collections.emptyList();
+    }
   }
 
   public void evict(String username) {
-    cache.remove(username);
+    if (username != null) {
+      cache.invalidate(username);
+    }
   }
 
   @Override
@@ -107,7 +116,7 @@
     }
   }
 
-  static class Loader extends EntryCreator<String, Iterable<SshKeyCacheEntry>> {
+  static class Loader extends CacheLoader<String, Iterable<SshKeyCacheEntry>> {
     private final SchemaFactory<ReviewDb> schema;
 
     @Inject
@@ -116,8 +125,7 @@
     }
 
     @Override
-    public Iterable<SshKeyCacheEntry> createEntry(String username)
-        throws Exception {
+    public Iterable<SshKeyCacheEntry> load(String username) throws Exception {
       final ReviewDb db = schema.open();
       try {
         final AccountExternalId.Key key =
@@ -143,11 +151,6 @@
       }
     }
 
-    @Override
-    public Iterable<SshKeyCacheEntry> missing(String username) {
-      return Collections.emptyList();
-    }
-
     private void add(ReviewDb db, List<SshKeyCacheEntry> kl, AccountSshKey k) {
       try {
         kl.add(new SshKeyCacheEntry(k.getKey(), SshUtil.parse(k)));
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLog.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLog.java
index 923ac98..c9ac3e7 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLog.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLog.java
@@ -14,7 +14,9 @@
 
 package com.google.gerrit.sshd;
 
-import com.google.gerrit.lifecycle.LifecycleListener;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.audit.AuditEvent;
+import com.google.gerrit.audit.AuditService;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PeerDaemonUser;
@@ -40,6 +42,7 @@
 import java.io.File;
 import java.io.IOException;
 import java.text.SimpleDateFormat;
+import java.util.Arrays;
 import java.util.Calendar;
 import java.util.Date;
 import java.util.TimeZone;
@@ -58,12 +61,14 @@
   private final Provider<SshSession> session;
   private final Provider<Context> context;
   private final AsyncAppender async;
+  private final AuditService auditService;
 
   @Inject
   SshLog(final Provider<SshSession> session, final Provider<Context> context,
-      final SitePaths site, @GerritServerConfig Config config) {
+      final SitePaths site, @GerritServerConfig Config config, AuditService auditService) {
     this.session = session;
     this.context = context;
+    this.auditService = auditService;
 
     final DailyRollingFileAppender dst = new DailyRollingFileAppender();
     dst.setName(LOG_NAME);
@@ -96,6 +101,7 @@
 
   void onLogin() {
     async.append(log("LOGIN FROM " + session.get().getRemoteAddressAsString()));
+    audit(context.get(), "0", "LOGIN", new String[] {});
   }
 
   void onAuthFail(final SshSession sd) {
@@ -121,6 +127,7 @@
     }
 
     async.append(event);
+    audit(null, "FAIL", "AUTH", new String[] {sd.getRemoteAddressAsString()});
   }
 
   void onExecute(int exitValue) {
@@ -158,10 +165,19 @@
     event.setProperty(P_STATUS, status);
 
     async.append(event);
+    audit(context.get(), status, getCommand(commandLine),
+        CommandFactoryProvider.split(commandLine));
+  }
+
+  private String getCommand(String commandLine) {
+    commandLine = commandLine.trim();
+    int spacePos = commandLine.indexOf(' ');
+    return (spacePos > 0 ? commandLine.substring(0, spacePos):commandLine);
   }
 
   void onLogout() {
     async.append(log("LOGOUT"));
+    audit(context.get(), "0", "LOGOUT", new String[] {});
   }
 
   private LoggingEvent log(final String msg) {
@@ -192,7 +208,6 @@
 
     } else if (user instanceof PeerDaemonUser) {
       userName = PeerDaemonUser.USER_NAME;
-
     }
 
     event.setProperty(P_USER_NAME, userName);
@@ -400,4 +415,43 @@
     public void setLogger(Logger logger) {
     }
   }
+
+  void audit(Context ctx, Object result, String commandName, String[] args) {
+    final String sid = extractSessionId(ctx);
+    final long created = extractCreated(ctx);
+    final String what = extractWhat(commandName, args);
+    auditService.dispatch(new AuditEvent(sid, extractCurrentUser(ctx), "ssh:"
+        + what, created, Arrays.asList(args), result));
+  }
+
+  private String extractWhat(String commandName, String[] args) {
+    String result = commandName;
+    if ("gerrit".equals(commandName)) {
+      if (args.length > 1)
+        result = "gerrit"+"."+args[1];
+    }
+    return result;
+  }
+
+  private long extractCreated(final Context ctx) {
+    return (ctx != null) ? ctx.created : System.currentTimeMillis();
+  }
+
+  private CurrentUser extractCurrentUser(final Context ctx) {
+    if (ctx != null) {
+      SshSession session = ctx.getSession();
+      return (session == null) ? null : session.getCurrentUser();
+    } else {
+      return null;
+    }
+  }
+
+  private String extractSessionId(final Context ctx) {
+    if (ctx != null) {
+      SshSession session = ctx.getSession();
+      return (session == null) ? null : IdGenerator.format(session.getSessionId());
+    } else {
+      return null;
+    }
+  }
 }
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 54b0bb5..7f4a1f7 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshModule.java
@@ -15,52 +15,59 @@
 package com.google.gerrit.sshd;
 
 import static com.google.inject.Scopes.SINGLETON;
+import static com.google.gerrit.extensions.registration.PrivateInternals_DynamicTypes.registerInParentInjectors;
 
+import com.google.common.collect.Maps;
 import com.google.gerrit.lifecycle.LifecycleModule;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.CmdLineParserModule;
 import com.google.gerrit.server.PeerDaemonUser;
 import com.google.gerrit.server.RemotePeer;
 import com.google.gerrit.server.account.AccountManager;
 import com.google.gerrit.server.account.ChangeUserName;
 import com.google.gerrit.server.config.FactoryModule;
 import com.google.gerrit.server.config.GerritRequestModule;
+import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.QueueProvider;
 import com.google.gerrit.server.git.WorkQueue;
-import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.plugins.ModuleGenerator;
+import com.google.gerrit.server.plugins.ReloadPluginListener;
+import com.google.gerrit.server.plugins.StartPluginListener;
 import com.google.gerrit.server.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.PatchSetIdHandler;
-import com.google.gerrit.sshd.args4j.ProjectControlHandler;
-import com.google.gerrit.sshd.args4j.SocketAddressHandler;
 import com.google.gerrit.sshd.commands.DefaultCommandModule;
 import com.google.gerrit.sshd.commands.QueryShell;
-import com.google.gerrit.util.cli.CmdLineParser;
-import com.google.gerrit.util.cli.OptionHandlerUtil;
+import com.google.inject.Inject;
+import com.google.inject.internal.UniqueAnnotations;
 import com.google.inject.servlet.RequestScoped;
 
 import org.apache.sshd.common.KeyPairProvider;
 import org.apache.sshd.server.CommandFactory;
 import org.apache.sshd.server.PublickeyAuthenticator;
-import org.kohsuke.args4j.spi.OptionHandler;
-
+import org.eclipse.jgit.lib.Config;
 import java.net.SocketAddress;
+import java.util.Map;
 
 /** Configures standard dependencies for {@link SshDaemon}. */
 public class SshModule extends FactoryModule {
+  private final Map<String, String> aliases;
+
+  @Inject
+  SshModule(@GerritServerConfig Config cfg) {
+    aliases = Maps.newHashMap();
+    for (String name : cfg.getNames("ssh-alias")) {
+      aliases.put(name, cfg.getString("ssh-alias", null, name));
+    }
+  }
+
   @Override
   protected void configure() {
     bindScope(RequestScoped.class, SshScope.REQUEST);
     bind(RequestScopePropagator.class).to(SshScope.Propagator.class);
+    bind(SshScope.class).in(SINGLETON);
 
     configureRequestScope();
-    configureCmdLineParser();
+    install(new CmdLineParserModule());
+    configureAliases();
 
     install(SshKeyCacheImpl.module());
     bind(SshLog.class);
@@ -70,7 +77,7 @@
     factory(PeerDaemonUser.Factory.class);
 
     bind(DispatchCommandProvider.class).annotatedWith(Commands.CMD_ROOT)
-        .toInstance(new DispatchCommandProvider("", Commands.CMD_ROOT));
+        .toInstance(new DispatchCommandProvider(Commands.CMD_ROOT));
     bind(CommandFactoryProvider.class);
     bind(CommandFactory.class).toProvider(CommandFactoryProvider.class);
     bind(WorkQueue.Executor.class).annotatedWith(StreamCommandExecutor.class)
@@ -87,12 +94,37 @@
     install(new LifecycleModule() {
       @Override
       protected void configure() {
+        bind(ModuleGenerator.class).to(SshAutoRegisterModuleGenerator.class);
+        bind(SshPluginStarterCallback.class);
+        bind(StartPluginListener.class)
+          .annotatedWith(UniqueAnnotations.create())
+          .to(SshPluginStarterCallback.class);
+
+        bind(ReloadPluginListener.class)
+          .annotatedWith(UniqueAnnotations.create())
+          .to(SshPluginStarterCallback.class);
+
+        listener().toInstance(registerInParentInjectors());
         listener().to(SshLog.class);
         listener().to(SshDaemon.class);
       }
     });
   }
 
+  private void configureAliases() {
+    CommandName gerrit = Commands.named("gerrit");
+    for (Map.Entry<String, String> e : aliases.entrySet()) {
+      String name = e.getKey();
+      String[] dest = e.getValue().split("[ \\t]+");
+      CommandName cmd = Commands.named(dest[0]);
+      for (int i = 1; i < dest.length; i++) {
+        cmd = Commands.named(cmd, dest[i]);
+      }
+      bind(Commands.key(gerrit, name))
+        .toProvider(new AliasCommandProvider(cmd));
+    }
+  }
+
   private void configureRequestScope() {
     bind(SshScope.Context.class).toProvider(SshScope.ContextProvider.class);
 
@@ -101,30 +133,9 @@
     bind(SocketAddress.class).annotatedWith(RemotePeer.class).toProvider(
         SshRemotePeerProvider.class).in(SshScope.REQUEST);
 
-    bind(CurrentUser.class).toProvider(SshCurrentUserProvider.class).in(
-        SshScope.REQUEST);
-    bind(IdentifiedUser.class).toProvider(SshIdentifiedUserProvider.class).in(
-        SshScope.REQUEST);
-
     bind(WorkQueue.Executor.class).annotatedWith(CommandExecutor.class)
         .toProvider(CommandExecutorProvider.class).in(SshScope.REQUEST);
 
     install(new GerritRequestModule());
   }
-
-  private void configureCmdLineParser() {
-    factory(CmdLineParser.Factory.class);
-
-    registerOptionHandler(Account.Id.class, AccountIdHandler.class);
-    registerOptionHandler(AccountGroup.Id.class, AccountGroupIdHandler.class);
-    registerOptionHandler(AccountGroup.UUID.class, AccountGroupUUIDHandler.class);
-    registerOptionHandler(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/SshPluginStarterCallback.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshPluginStarterCallback.java
new file mode 100644
index 0000000..4f9fe33
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshPluginStarterCallback.java
@@ -0,0 +1,73 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.sshd;
+
+import com.google.gerrit.server.plugins.Plugin;
+import com.google.gerrit.server.plugins.ReloadPluginListener;
+import com.google.gerrit.server.plugins.StartPluginListener;
+import com.google.inject.Key;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import org.apache.sshd.server.Command;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import javax.inject.Inject;
+
+@Singleton
+class SshPluginStarterCallback
+    implements StartPluginListener, ReloadPluginListener {
+  private static final Logger log = LoggerFactory
+      .getLogger(SshPluginStarterCallback.class);
+
+  private final DispatchCommandProvider root;
+
+  @Inject
+  SshPluginStarterCallback(
+      @CommandName(Commands.ROOT) DispatchCommandProvider root) {
+    this.root = root;
+  }
+
+  @Override
+  public void onStartPlugin(Plugin plugin) {
+    Provider<Command> cmd = load(plugin);
+    if (cmd != null) {
+      plugin.add(root.register(Commands.named(plugin.getName()), cmd));
+    }
+  }
+
+  @Override
+  public void onReloadPlugin(Plugin oldPlugin, Plugin newPlugin) {
+    Provider<Command> cmd = load(newPlugin);
+    if (cmd != null) {
+      newPlugin.add(root.replace(Commands.named(newPlugin.getName()), cmd));
+    }
+  }
+
+  private Provider<Command> load(Plugin plugin) {
+    if (plugin.getSshInjector() != null) {
+      Key<Command> key = Commands.key(plugin.getName());
+      try {
+        return plugin.getSshInjector().getProvider(key);
+      } catch (RuntimeException err) {
+        log.warn(String.format(
+            "Plugin %s did not define its top-level command",
+            plugin.getName()), err);
+      }
+    }
+    return null;
+  }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshScope.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshScope.java
index 92609b5..d6f66ca 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshScope.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshScope.java
@@ -14,8 +14,13 @@
 
 package com.google.gerrit.sshd;
 
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.RequestCleanup;
+import com.google.gerrit.server.util.RequestContext;
+import com.google.gerrit.server.util.ThreadLocalRequestContext;
 import com.google.gerrit.server.util.ThreadLocalRequestScopePropagator;
+import com.google.inject.Inject;
 import com.google.inject.Key;
 import com.google.inject.OutOfScopeException;
 import com.google.inject.Provider;
@@ -26,10 +31,10 @@
 
 /** Guice scopes for state during an SSH connection. */
 class SshScope {
-  static class Context {
-    private static final Key<RequestCleanup> RC_KEY =
-        Key.get(RequestCleanup.class);
+  private static final Key<RequestCleanup> RC_KEY =
+      Key.get(RequestCleanup.class);
 
+  class Context implements RequestContext {
     private final RequestCleanup cleanup;
     private final SshSession session;
     private final String commandLine;
@@ -56,10 +61,6 @@
       finished = p.finished;
     }
 
-    Context(final SshSession s, final String c) {
-      this(s, c, System.currentTimeMillis());
-    }
-
     String getCommandLine() {
       return commandLine;
     }
@@ -68,6 +69,16 @@
       return session;
     }
 
+    @Override
+    public CurrentUser getCurrentUser() {
+      final CurrentUser user = session.getCurrentUser();
+      if (user instanceof IdentifiedUser) {
+        return userFactory.create(user.getAccessPath(), //
+            ((IdentifiedUser) user).getAccountId());
+      }
+      return user;
+    }
+
     synchronized <T> T get(Key<T> key, Provider<T> creator) {
       @SuppressWarnings("unchecked")
       T t = (T) map.get(key);
@@ -100,15 +111,19 @@
   }
 
   static class Propagator extends ThreadLocalRequestScopePropagator<Context> {
-    Propagator() {
-      super(REQUEST, current);
+    private final SshScope sshScope;
+
+    @Inject
+    Propagator(SshScope sshScope, ThreadLocalRequestContext local) {
+      super(REQUEST, current, local);
+      this.sshScope = sshScope;
     }
 
     @Override
     protected Context continuingContext(Context ctx) {
       // The cleanup is not chained, since the RequestScopePropagator executors
       // the Context's cleanup when finished executing.
-      return new Context(ctx, ctx.getSession(), ctx.getCommandLine());
+      return sshScope.newContinuingContext(ctx);
     }
   }
 
@@ -123,9 +138,28 @@
     return ctx;
   }
 
-  static Context set(Context ctx) {
+  private final ThreadLocalRequestContext local;
+  private final IdentifiedUser.RequestFactory userFactory;
+
+  @Inject
+  SshScope(ThreadLocalRequestContext local,
+      IdentifiedUser.RequestFactory userFactory) {
+    this.local = local;
+    this.userFactory = userFactory;
+  }
+
+  Context newContext(SshSession session, String commandLine) {
+    return new Context(session, commandLine, System.currentTimeMillis());
+  }
+
+  private Context newContinuingContext(Context ctx) {
+    return new Context(ctx, ctx.getSession(), ctx.getCommandLine());
+  }
+
+  Context set(Context ctx) {
     Context old = current.get();
     current.set(ctx);
+    local.setContext(ctx);
     return old;
   }
 
@@ -149,7 +183,4 @@
       return "SshScopes.REQUEST";
     }
   };
-
-  private SshScope() {
-  }
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java
index 492966e..33459c2 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.sshd;
 
+import com.google.common.util.concurrent.Atomics;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.AccessPath;
 import com.google.gerrit.server.CurrentUser;
@@ -32,6 +33,7 @@
 import java.net.SocketAddress;
 import java.util.ArrayList;
 import java.util.List;
+import java.util.concurrent.atomic.AtomicReference;
 
 /**
  * Executes any other command as a different user identity.
@@ -41,6 +43,7 @@
  * key, or a key on this daemon's peer host key ring.
  */
 public final class SuExec extends BaseCommand {
+  private final SshScope sshScope;
   private final DispatchCommandProvider dispatcher;
 
   private Provider<CurrentUser> caller;
@@ -57,18 +60,21 @@
   @Argument(index = 0, multiValued = true, metaVar = "COMMAND")
   private List<String> args = new ArrayList<String>();
 
-  private Command cmd;
+  private final AtomicReference<Command> atomicCmd;
 
   @Inject
-  SuExec(@CommandName(Commands.ROOT) final DispatchCommandProvider dispatcher,
+  SuExec(final SshScope sshScope,
+      @CommandName(Commands.ROOT) final DispatchCommandProvider dispatcher,
       final Provider<CurrentUser> caller, final Provider<SshSession> session,
       final IdentifiedUser.GenericFactory userFactory,
       final SshScope.Context callingContext) {
+    this.sshScope = sshScope;
     this.dispatcher = dispatcher;
     this.caller = caller;
     this.session = session;
     this.userFactory = userFactory;
     this.callingContext = callingContext;
+    atomicCmd = Atomics.newReference();
   }
 
   @Override
@@ -78,18 +84,15 @@
         parseCommandLine();
 
         final Context ctx = callingContext.subContext(newSession(), join(args));
-        final Context old = SshScope.set(ctx);
+        final Context old = sshScope.set(ctx);
         try {
           final BaseCommand cmd = dispatcher.get();
           cmd.setArguments(args.toArray(new String[args.size()]));
           provideStateTo(cmd);
-
-          synchronized (this) {
-            this.cmd = cmd;
-          }
+          atomicCmd.set(cmd);
           cmd.start(env);
         } finally {
-          SshScope.set(old);
+          sshScope.set(old);
         }
 
       } else {
@@ -136,11 +139,9 @@
 
   @Override
   public void destroy() {
-    synchronized (this) {
-      if (cmd != null) {
+    Command cmd = atomicCmd.getAndSet(null);
+    if (cmd != null) {
         cmd.destroy();
-        cmd = null;
-      }
     }
   }
 }
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 545554c..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
@@ -14,16 +14,18 @@
 
 package com.google.gerrit.sshd.commands;
 
-import com.google.gerrit.sshd.AdminCommand;
-import com.google.gerrit.sshd.BaseCommand;
+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.SshCommand;
 import com.google.inject.Inject;
 
-import org.apache.sshd.server.Environment;
 import org.kohsuke.args4j.Option;
 
 /** Opens a query processor. */
-@AdminCommand
-final class AdminQueryShell extends BaseCommand {
+@AdminHighPriorityCommand
+@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+final class AdminQueryShell extends SshCommand {
   @Inject
   private QueryShell.Factory factory;
 
@@ -34,19 +36,13 @@
   private String query;
 
   @Override
-  public void start(final Environment env) {
-    startThread(new CommandRunnable() {
-      @Override
-      public void run() throws Exception {
-        parseCommandLine();
-        final QueryShell shell = factory.create(in, out);
-        shell.setOutputFormat(format);
-        if (query != null) {
-          shell.execute(query);
-        } else {
-          shell.run();
-        }
-      }
-    });
+  protected void run() {
+    final QueryShell shell = factory.create(in, out);
+    shell.setOutputFormat(format);
+    if (query != null) {
+      shell.execute(query);
+    } else {
+      shell.run();
+    }
   }
 }
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 950bf341..6483e24 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
@@ -14,6 +14,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.reviewdb.client.Project;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.git.MetaDataUpdate;
@@ -21,11 +23,9 @@
 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.AdminCommand;
-import com.google.gerrit.sshd.BaseCommand;
+import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
 
-import org.apache.sshd.server.Environment;
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.kohsuke.args4j.Argument;
@@ -34,14 +34,13 @@
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
-import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 
-@AdminCommand
-final class AdminSetParent extends BaseCommand {
+@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+final class AdminSetParent extends SshCommand {
   private static final Logger log = LoggerFactory.getLogger(AdminSetParent.class);
 
   @Option(name = "--parent", aliases = {"-p"}, metaVar = "NAME", usage = "new parent project")
@@ -68,26 +67,10 @@
   @Inject
   private AllProjectsName allProjectsName;
 
-  private PrintWriter stdout;
   private Project.NameKey newParentKey = null;
 
   @Override
-  public void start(final Environment env) {
-    startThread(new CommandRunnable() {
-      @Override
-      public void run() throws Exception {
-        stdout = toPrintWriter(out);
-        try {
-          parseCommandLine();
-          updateParents();
-        } finally {
-          stdout.flush();
-        }
-      }
-    });
-  }
-
-  private void updateParents() throws Failure {
+  protected void run() throws Failure {
     if (oldParent == null && children.isEmpty()) {
       throw new UnloggedFailure(1, "fatal: child projects have to be specified as " +
                                    "arguments or the --children-of option has to be set");
@@ -154,9 +137,7 @@
           config.getProject().setParentName(newParentKey);
           md.setMessage("Inherit access from "
               + (newParentKey != null ? newParentKey.get() : allProjectsName.get()) + "\n");
-          if (!config.commit(md)) {
-            err.append("error: Could not update project " + name + "\n");
-          }
+          config.commit(md);
         } finally {
           md.close();
         }
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
new file mode 100644
index 0000000..939d68a
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java
@@ -0,0 +1,101 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.sshd.commands;
+
+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.MergeException;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.sshd.SshCommand;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.Option;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+
+public class BanCommitCommand extends SshCommand {
+  @Option(name = "--reason", aliases = {"-r"}, metaVar = "REASON", usage = "reason for banning the commit")
+  private String reason;
+
+  @Argument(index = 0, required = true, metaVar = "PROJECT",
+      usage = "name of the project for which the commit should be banned")
+  private ProjectControl projectControl;
+
+  @Argument(index = 1, required = true, multiValued = true, metaVar = "COMMIT",
+      usage = "commit(s) that should be banned")
+  private List<ObjectId> commitsToBan = new ArrayList<ObjectId>();
+
+  @Inject
+  private BanCommit.Factory banCommitFactory;
+
+  @Override
+  protected void run() throws Failure {
+    try {
+      final BanCommitResult result =
+          banCommitFactory.create().ban(projectControl, commitsToBan, reason);
+
+      final List<ObjectId> newlyBannedCommits =
+          result.getNewlyBannedCommits();
+      if (!newlyBannedCommits.isEmpty()) {
+        stdout.print("The following commits were banned:\n");
+        printCommits(stdout, newlyBannedCommits);
+      }
+
+      final List<ObjectId> alreadyBannedCommits =
+          result.getAlreadyBannedCommits();
+      if (!alreadyBannedCommits.isEmpty()) {
+        stdout.print("The following commits were already banned:\n");
+        printCommits(stdout, alreadyBannedCommits);
+      }
+
+      final List<ObjectId> ignoredIds = result.getIgnoredObjectIds();
+      if (!ignoredIds.isEmpty()) {
+        stdout.print("The following ids do not represent commits"
+            + " and were ignored:\n");
+        printCommits(stdout, ignoredIds);
+      }
+    } catch (PermissionDeniedException e) {
+      throw die(e);
+    } catch (IOException e) {
+      throw die(e);
+    } catch (MergeException e) {
+      throw die(e);
+    } catch (InterruptedException e) {
+      throw die(e);
+    } catch (ConcurrentRefUpdateException e) {
+      throw die(e);
+    }
+  }
+
+  private static void printCommits(final PrintWriter stdout,
+      final List<ObjectId> commits) {
+    boolean first = true;
+    for (final ObjectId c : commits) {
+      if (!first) {
+        stdout.print(",\n");
+      }
+      stdout.print(c.getName());
+      first = false;
+    }
+    stdout.print("\n\n");
+  }
+}
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 083759c..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.gerrit.sshd.BaseCommand;
+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 BaseCommand {
+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 38e0bcb..d6fecab 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
@@ -14,7 +14,9 @@
 
 package com.google.gerrit.sshd.commands;
 
+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;
@@ -26,12 +28,11 @@
 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.BaseCommand;
+import com.google.gerrit.sshd.SshCommand;
 import com.google.gwtorm.server.OrmDuplicateKeyException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 
-import org.apache.sshd.server.Environment;
 import org.kohsuke.args4j.Argument;
 import org.kohsuke.args4j.Option;
 
@@ -45,7 +46,8 @@
 import java.util.List;
 
 /** Create a new user account. **/
-final class CreateAccountCommand extends BaseCommand {
+@RequiresCapability(GlobalCapability.CREATE_ACCOUNT)
+final class CreateAccountCommand extends SshCommand {
   @Option(name = "--group", aliases = {"-g"}, metaVar = "GROUP", usage = "groups to add account to")
   private List<AccountGroup.Id> groups = new ArrayList<AccountGroup.Id>();
 
@@ -58,6 +60,9 @@
   @Option(name = "--ssh-key", metaVar = "-|KEY", usage = "public key for SSH authentication")
   private String sshKey;
 
+  @Option(name = "--http-password", metaVar = "PASSWORD", usage = "password for HTTP authentication")
+  private String httpPassword;
+
   @Argument(index = 0, required = true, metaVar = "USERNAME", usage = "name of the user account")
   private String username;
 
@@ -77,24 +82,7 @@
   private AccountByEmailCache byEmailCache;
 
   @Override
-  public void start(final Environment env) {
-    startThread(new CommandRunnable() {
-      @Override
-      public void run() throws Exception {
-        if (!currentUser.getCapabilities().canCreateAccount()) {
-          String msg = String.format(
-            "fatal: %s does not have \"Create Account\" capability.",
-            currentUser.getUserName());
-          throw new UnloggedFailure(BaseCommand.STATUS_NOT_ADMIN, msg);
-        }
-
-        parseCommandLine();
-        createAccount();
-      }
-    });
-  }
-
-  private void createAccount() throws OrmException, IOException,
+  protected void run() throws OrmException, IOException,
       InvalidSshKeyException, UnloggedFailure {
     if (!username.matches(Account.USER_NAME_PATTERN)) {
       throw die("Username '" + username + "'"
@@ -108,6 +96,10 @@
         new AccountExternalId(id, new AccountExternalId.Key(
             AccountExternalId.SCHEME_USERNAME, username));
 
+    if (httpPassword != null) {
+      extUser.setPassword(httpPassword);
+    }
+
     if (db.accountExternalIds().get(extUser.getKey()) != null) {
       throw die("username '" + username + "' already exists");
     }
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 98202e2..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
@@ -14,15 +14,17 @@
 
 package com.google.gerrit.sshd.commands;
 
+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.BaseCommand;
+import com.google.gerrit.sshd.SshCommand;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 
-import org.apache.sshd.server.Environment;
 import org.kohsuke.args4j.Argument;
 import org.kohsuke.args4j.Option;
 
@@ -34,7 +36,8 @@
  * <p>
  * Optionally, puts an initial set of user in the newly created group.
  */
-final class CreateGroupCommand extends BaseCommand {
+@RequiresCapability(GlobalCapability.CREATE_GROUP)
+final class CreateGroupCommand extends SshCommand {
   @Option(name = "--owner", aliases = {"-o"}, metaVar = "GROUP", usage = "owning group, if not specified the group will be self-owning")
   private AccountGroup.Id ownerGroupId;
 
@@ -65,25 +68,19 @@
   private PerformCreateGroup.Factory performCreateGroupFactory;
 
   @Override
-  public void start(Environment env) {
-    startThread(new CommandRunnable() {
-      @Override
-      public void run() throws Exception {
-        parseCommandLine();
-        try {
-          performCreateGroupFactory.create().createGroup(groupName,
-              groupDescription,
-              visibleToAll,
-              ownerGroupId,
-              initialMembers,
-              initialGroups);
-        } catch (PermissionDeniedException e) {
-          throw die(e);
+  protected void run() throws Failure, OrmException {
+    try {
+      performCreateGroupFactory.create().createGroup(groupName,
+          groupDescription,
+          visibleToAll,
+          ownerGroupId,
+          initialMembers,
+          initialGroups);
+    } catch (PermissionDeniedException e) {
+      throw die(e);
 
-        } catch (NameAlreadyUsedException e) {
-          throw die(e);
-        }
-      }
-    });
+    } catch (NameAlreadyUsedException e) {
+      throw die(e);
+    }
   }
 }
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 a93cab1..5f5b1e3 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
@@ -14,28 +14,27 @@
 
 package com.google.gerrit.sshd.commands;
 
+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;
-import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.project.CreateProject;
 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.BaseCommand;
+import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
 
-import org.apache.sshd.server.Environment;
-import org.eclipse.jgit.lib.Constants;
 import org.kohsuke.args4j.Argument;
 import org.kohsuke.args4j.Option;
 
-import java.io.PrintWriter;
 import java.util.List;
 
 /** Create a new project. **/
-final class CreateProjectCommand extends BaseCommand {
+@RequiresCapability(GlobalCapability.CREATE_PROJECT)
+final class CreateProjectCommand extends SshCommand {
   @Option(name = "--name", aliases = {"-n"}, metaVar = "NAME", usage = "name of project to be created (deprecated option)")
   void setProjectNameFromOption(String name) {
     if (projectName != null) {
@@ -79,7 +78,7 @@
 
   @Option(name = "--branch", aliases = {"-b"}, metaVar = "BRANCH", usage = "initial branch name\n"
       + "(default: master)")
-  private String branch = Constants.MASTER;
+  private List<String> branch;
 
   @Option(name = "--empty-commit", usage = "to create initial empty commit")
   private boolean createEmptyCommit;
@@ -96,64 +95,45 @@
   }
 
   @Inject
-  private IdentifiedUser currentUser;
-
-  @Inject
   private CreateProject.Factory CreateProjectFactory;
 
   @Inject
   private SuggestParentCandidates.Factory suggestParentCandidatesFactory;
 
   @Override
-  public void start(final Environment env) {
-    startThread(new CommandRunnable() {
-      @Override
-      public void run() throws Exception {
-        if (!currentUser.getCapabilities().canCreateProject()) {
-          String msg =
-              String.format(
-                  "fatal: %s does not have \"Create Project\" capability.",
-                  currentUser.getUserName());
-          throw new UnloggedFailure(BaseCommand.STATUS_NOT_ADMIN, msg);
+  protected void run() throws Exception {
+    try {
+      if (!suggestParent) {
+        if (projectName == null) {
+          throw new UnloggedFailure(1, "fatal: Project name is required.");
         }
-        PrintWriter p = toPrintWriter(out);
-        parseCommandLine();
-        try {
-          if (!suggestParent) {
-            if (projectName == null) {
-              throw new UnloggedFailure(1, "fatal: Project name is required.");
-            }
-            final CreateProjectArgs args = new CreateProjectArgs();
-            args.setProjectName(projectName);
-            args.ownerIds = ownerIds;
-            args.newParent = newParent;
-            args.permissionsOnly = permissionsOnly;
-            args.projectDescription = projectDescription;
-            args.submitType = submitType;
-            args.contributorAgreements = contributorAgreements;
-            args.signedOffBy = signedOffBy;
-            args.contentMerge = contentMerge;
-            args.changeIdRequired = requireChangeID;
-            args.branch = branch;
-            args.createEmptyCommit = createEmptyCommit;
+        final CreateProjectArgs args = new CreateProjectArgs();
+        args.setProjectName(projectName);
+        args.ownerIds = ownerIds;
+        args.newParent = newParent;
+        args.permissionsOnly = permissionsOnly;
+        args.projectDescription = projectDescription;
+        args.submitType = submitType;
+        args.contributorAgreements = contributorAgreements;
+        args.signedOffBy = signedOffBy;
+        args.contentMerge = contentMerge;
+        args.changeIdRequired = requireChangeID;
+        args.branch = branch;
+        args.createEmptyCommit = createEmptyCommit;
 
-            final CreateProject createProject =
-                CreateProjectFactory.create(args);
-            createProject.createProject();
-          } else {
-            List<Project.NameKey> parentCandidates =
-                suggestParentCandidatesFactory.create().getNameKeys();
+        final CreateProject createProject =
+            CreateProjectFactory.create(args);
+        createProject.createProject();
+      } else {
+        List<Project.NameKey> parentCandidates =
+            suggestParentCandidatesFactory.create().getNameKeys();
 
-            for (Project.NameKey parent : parentCandidates) {
-              p.print(parent + "\n");
-            }
-          }
-        } catch (ProjectCreationFailedException err) {
-          throw new UnloggedFailure(1, "fatal: " + err.getMessage(), err);
-        } finally {
-          p.flush();
+        for (Project.NameKey parent : parentCandidates) {
+          stdout.print(parent + "\n");
         }
       }
-    });
+    } catch (ProjectCreationFailedException err) {
+      throw new UnloggedFailure(1, "fatal: " + err.getMessage(), err);
+    }
   }
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java
index 16461b6..2a4dedc 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/DefaultCommandModule.java
@@ -28,6 +28,7 @@
   protected void configure() {
     final CommandName git = Commands.named("git");
     final CommandName gerrit = Commands.named("gerrit");
+    final CommandName plugin = Commands.named(gerrit, "plugin");
 
     // The following commands can be ran on a server in either Master or Slave
     // mode. If a command should only be used on a server in one mode, but not
@@ -35,6 +36,7 @@
     // SlaveCommandModule.
 
     command(gerrit).toProvider(new DispatchCommandProvider(gerrit));
+    command(gerrit, "ban-commit").to(BanCommitCommand.class);
     command(gerrit, "flush-caches").to(FlushCaches.class);
     command(gerrit, "ls-projects").to(ListProjectsCommand.class);
     command(gerrit, "ls-groups").to(ListGroupsCommand.class);
@@ -45,6 +47,15 @@
     command(gerrit, "stream-events").to(StreamEvents.class);
     command(gerrit, "version").to(VersionCommand.class);
 
+    command(gerrit, "plugin").toProvider(new DispatchCommandProvider(plugin));
+    command(plugin, "ls").to(PluginLsCommand.class);
+    command(plugin, "enable").to(PluginEnableCommand.class);
+    command(plugin, "install").to(PluginInstallCommand.class);
+    command(plugin, "reload").to(PluginReloadCommand.class);
+    command(plugin, "remove").to(PluginRemoveCommand.class);
+    command(plugin, "add").to(Commands.key(plugin, "install"));
+    command(plugin, "rm").to(Commands.key(plugin, "remove"));
+
     command(git).toProvider(new DispatchCommandProvider(git));
     command(git, "receive-pack").to(Commands.key(gerrit, "receive-pack"));
     command(git, "upload-pack").to(Upload.class);
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 639cc42..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,21 +14,23 @@
 
 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.inject.Inject;
+import com.google.inject.Provider;
 
-import net.sf.ehcache.Ehcache;
-
-import org.apache.sshd.server.Environment;
 import org.kohsuke.args4j.Option;
 
-import java.io.PrintWriter;
 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. */
+@RequiresCapability(GlobalCapability.FLUSH_CACHES)
 final class FlushCaches extends CacheCommand {
   private static final String WEB_SESSIONS = "web_sessions";
 
@@ -44,27 +46,8 @@
   @Inject
   IdentifiedUser currentUser;
 
-  private PrintWriter p;
-
   @Override
-  public void start(final Environment env) {
-    startThread(new CommandRunnable() {
-      @Override
-      public void run() throws Exception {
-        if (!currentUser.getCapabilities().canFlushCaches()) {
-          String msg = String.format(
-            "fatal: %s does not have \"Flush Caches\" capability.",
-            currentUser.getUserName());
-          throw new UnloggedFailure(BaseCommand.STATUS_NOT_ADMIN, msg);
-        }
-
-        parseCommandLine();
-        flush();
-      }
-    });
-  }
-
-  private void flush() throws Failure {
+  protected void run() throws Failure {
     if (caches.contains(WEB_SESSIONS)
         && !currentUser.getCapabilities().canAdministrateServer()) {
       String msg = String.format(
@@ -73,7 +56,6 @@
       throw new UnloggedFailure(BaseCommand.STATUS_NOT_ADMIN, msg);
     }
 
-    p = toPrintWriter(err);
     if (list) {
       if (all || caches.size() > 0) {
         throw error("error: cannot use --list with --all or --cache");
@@ -106,26 +88,29 @@
 
   private void doList() {
     for (final String name : cacheNames()) {
-      p.print(name);
-      p.print('\n');
+      stderr.print(name);
+      stderr.print('\n');
     }
-    p.flush();
+    stderr.flush();
   }
 
   private void doBulkFlush() {
     try {
-      for (final Ehcache c : getAllCaches()) {
-        final String name = c.getName();
-        if (flush(name)) {
-          try {
-            c.removeAll();
-          } catch (Throwable e) {
-            p.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);
+            }
           }
         }
       }
     } finally {
-      p.flush();
+      stderr.flush();
     }
   }
 
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 69018af..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
@@ -14,25 +14,24 @@
 
 package com.google.gerrit.sshd.commands;
 
-import com.google.gerrit.server.IdentifiedUser;
+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.BaseCommand;
+import com.google.gerrit.sshd.AdminHighPriorityCommand;
+import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
 
-import org.apache.sshd.server.Environment;
 import org.kohsuke.args4j.Argument;
 
-import java.io.PrintWriter;
 import java.util.HashSet;
 import java.util.Set;
 
 /** Kill a task in the work queue. */
-final class KillCommand extends BaseCommand {
-  @Inject
-  private IdentifiedUser currentUser;
-
+@AdminHighPriorityCommand
+@RequiresCapability(GlobalCapability.KILL_TASK)
+final class KillCommand extends SshCommand {
   @Inject
   private WorkQueue workQueue;
 
@@ -48,33 +47,14 @@
   }
 
   @Override
-  public void start(final Environment env) {
-    startThread(new CommandRunnable() {
-      @Override
-      public void run() throws Exception {
-        if (!currentUser.getCapabilities().canKillTask()) {
-          String msg = String.format(
-            "fatal: %s does not have \"Kill Task\" capability.",
-            currentUser.getUserName());
-          throw new UnloggedFailure(BaseCommand.STATUS_NOT_ADMIN, msg);
-        }
-
-        parseCommandLine();
-        KillCommand.this.commitMurder();
-      }
-    });
-  }
-
-  private void commitMurder() {
-    final PrintWriter p = toPrintWriter(err);
+  protected void run() {
     for (final Integer id : taskIds) {
       final Task<?> task = workQueue.getTask(id);
       if (task != null) {
         task.cancel(true);
       } else {
-        p.print("kill: " + IdGenerator.format(id) + ": No such task\n");
+        stderr.print("kill: " + IdGenerator.format(id) + ": No such task\n");
       }
     }
-    p.flush();
   }
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java
index e0b988e..f8856f2 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java
@@ -14,27 +14,27 @@
 
 package com.google.gerrit.sshd.commands;
 
-import com.google.gerrit.common.data.GroupDetail;
 import com.google.gerrit.common.data.GroupList;
 import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.account.VisibleGroups;
+import com.google.gerrit.server.ioutil.ColumnFormatter;
 import com.google.gerrit.server.project.ProjectControl;
-import com.google.gerrit.sshd.BaseCommand;
-import com.google.gwtorm.server.OrmException;
+import com.google.gerrit.sshd.SshCommand;
+import com.google.gwtorm.client.KeyUtil;
 import com.google.inject.Inject;
 
-import org.apache.sshd.server.Environment;
 import org.kohsuke.args4j.Option;
 
-import java.io.IOException;
-import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.List;
 
-public class ListGroupsCommand extends BaseCommand {
+public class ListGroupsCommand extends SshCommand {
+  @Inject
+  private GroupCache groupCache;
 
   @Inject
   private VisibleGroups.Factory visibleGroupsFactory;
@@ -56,19 +56,14 @@
       usage = "user for which the groups should be listed")
   private Account.Id user;
 
-  @Override
-  public void start(final Environment env) throws IOException {
-    startThread(new CommandRunnable() {
-      @Override
-      public void run() throws Exception {
-        parseCommandLine();
-        ListGroupsCommand.this.display();
-      }
-    });
-  }
+  @Option(name = "--verbose", aliases = {"-v"},
+      usage = "verbose output format with tab-separated columns for the " +
+          "group name, UUID, description, type, owner group name, " +
+          "owner group UUID, and whether the group is visible to all")
+  private boolean verboseOutput;
 
-  private void display() throws Failure {
-    final PrintWriter stdout = toPrintWriter(out);
+  @Override
+  protected void run() throws Failure {
     try {
       if (user != null && !projects.isEmpty()) {
         throw new UnloggedFailure(1, "fatal: --user and --project options are not compatible.");
@@ -85,15 +80,27 @@
       } else {
         groupList = visibleGroups.get();
       }
-      for (final GroupDetail groupDetail : groupList.getGroups()) {
-        stdout.print(groupDetail.group.getName() + "\n");
+
+      final ColumnFormatter formatter = new ColumnFormatter(stdout, '\t');
+      for (final AccountGroup g : groupList.getGroups()) {
+        formatter.addColumn(g.getName());
+        if (verboseOutput) {
+          formatter.addColumn(KeyUtil.decode(g.getGroupUUID().toString()));
+          formatter.addColumn(
+              g.getDescription() != null ? g.getDescription() : "");
+          formatter.addColumn(g.getType().toString());
+          final AccountGroup owningGroup =
+              groupCache.get(g.getOwnerGroupUUID());
+          formatter.addColumn(
+              owningGroup != null ? owningGroup.getName() : "n/a");
+          formatter.addColumn(KeyUtil.decode(g.getOwnerGroupUUID().toString()));
+          formatter.addColumn(Boolean.toString(g.isVisibleToAll()));
+        }
+        formatter.nextLine();
       }
-    } catch (OrmException e) {
-      throw die(e);
+      formatter.finish();
     } catch (NoSuchGroupException e) {
       throw die(e);
-    } finally {
-      stdout.flush();
     }
   }
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java
index d0bbc72..13e3f17 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java
@@ -30,11 +30,13 @@
       @Override
       public void run() throws Exception {
         parseCommandLine(impl);
-        if (impl.isShowTree() && (impl.getShowBranch() != null)) {
-          throw new UnloggedFailure(1, "fatal: --tree and --show-branch options are not compatible.");
-        }
-        if (impl.isShowTree() && impl.isShowDescription()) {
-          throw new UnloggedFailure(1, "fatal: --tree and --description options are not compatible.");
+        if (!impl.getFormat().isJson()) {
+          if (impl.isShowTree() && (impl.getShowBranch() != null)) {
+            throw new UnloggedFailure(1, "fatal: --tree and --show-branch options are not compatible.");
+          }
+          if (impl.isShowTree() && impl.isShowDescription()) {
+            throw new UnloggedFailure(1, "fatal: --tree and --description options are not compatible.");
+          }
         }
         impl.display(out);
       }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/MasterCommandModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/MasterCommandModule.java
index 34f64da..90bc07e 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/MasterCommandModule.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/MasterCommandModule.java
@@ -31,10 +31,12 @@
     command(gerrit, "rename-group").to(RenameGroupCommand.class);
     command(gerrit, "create-project").to(CreateProjectCommand.class);
     command(gerrit, "gsql").to(AdminQueryShell.class);
+    command(gerrit, "test-submit-rule").to(TestSubmitRule.class);
     command(gerrit, "set-reviewers").to(SetReviewersCommand.class);
     command(gerrit, "receive-pack").to(Receive.class);
-    command(gerrit, "replicate").to(Replicate.class);
     command(gerrit, "set-project-parent").to(AdminSetParent.class);
     command(gerrit, "review").to(ReviewCommand.class);
+    command(gerrit, "set-account").to(SetAccountCommand.class);
+    command(gerrit, "set-project").to(SetProjectCommand.class);
   }
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginEnableCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginEnableCommand.java
new file mode 100644
index 0000000..4df3aee
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginEnableCommand.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.sshd.commands;
+
+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.PluginInstallException;
+import com.google.gerrit.server.plugins.PluginLoader;
+import com.google.gerrit.sshd.SshCommand;
+import com.google.inject.Inject;
+
+import org.kohsuke.args4j.Argument;
+
+import java.util.List;
+
+@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+final class PluginEnableCommand extends SshCommand {
+  @Argument(index = 0, metaVar = "NAME", required = true, usage = "plugin(s) to enable")
+  List<String> names;
+
+  @Inject
+  private PluginLoader loader;
+
+  @Override
+  protected void run() throws UnloggedFailure {
+    if (names != null && !names.isEmpty()) {
+      try {
+        loader.enablePlugins(Sets.newHashSet(names));
+      } catch (PluginInstallException e) {
+        e.printStackTrace(stderr);
+        throw die("plugin failed to enable");
+      }
+    }
+  }
+}
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
new file mode 100644
index 0000000..12722ec
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginInstallCommand.java
@@ -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.
+
+package com.google.gerrit.sshd.commands;
+
+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.SshCommand;
+import com.google.inject.Inject;
+
+import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.Option;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+final class PluginInstallCommand extends SshCommand {
+  @Option(name = "--name", aliases = {"-n"}, usage = "install under name")
+  private String name;
+
+  @Option(name = "-")
+  void useInput(boolean on) {
+    source = "-";
+  }
+
+  @Argument(index = 0, metaVar = "-|URL", usage = "JAR to load")
+  private String source;
+
+  @Inject
+  private PluginLoader loader;
+
+  @Override
+  protected void run() throws UnloggedFailure {
+    if (Strings.isNullOrEmpty(source)) {
+      throw die("Argument \"-|URL\" is required");
+    }
+    if (Strings.isNullOrEmpty(name) && "-".equalsIgnoreCase(source)) {
+      throw die("--name required when source is stdin");
+    }
+
+    if (Strings.isNullOrEmpty(name)) {
+      int s = source.lastIndexOf('/');
+      if (0 <= s) {
+        name = source.substring(s + 1);
+      } else {
+        name = source;
+      }
+    }
+
+    InputStream data;
+    if ("-".equalsIgnoreCase(source)) {
+      data = in;
+    } else if (new File(source).isFile()
+        && source.equals(new File(source).getAbsolutePath())) {
+      try {
+        data = new FileInputStream(new File(source));
+      } catch (FileNotFoundException e) {
+        throw die("cannot read " + source);
+      }
+    } else {
+      try {
+        data = new URL(source).openStream();
+      } catch (MalformedURLException e) {
+        throw die("invalid url " + source);
+      } catch (IOException e) {
+        throw die("cannot read " + source);
+      }
+    }
+    try {
+      loader.installPluginFromStream(name, data);
+    } catch (IOException e) {
+      throw die("cannot install plugin");
+    } catch (PluginInstallException e) {
+      e.printStackTrace(stderr);
+      throw die("plugin failed to install");
+    } finally {
+      try {
+        data.close();
+      } catch (IOException err) {
+      }
+    }
+  }
+}
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
new file mode 100644
index 0000000..6d7490f
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginLsCommand.java
@@ -0,0 +1,42 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.sshd.commands;
+
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.server.plugins.ListPlugins;
+import com.google.gerrit.sshd.BaseCommand;
+import com.google.inject.Inject;
+
+import org.apache.sshd.server.Environment;
+
+import java.io.IOException;
+
+@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+final class PluginLsCommand extends BaseCommand {
+  @Inject
+  private ListPlugins impl;
+
+  @Override
+  public void start(Environment env) throws IOException {
+    startThread(new CommandRunnable() {
+      @Override
+      public void run() throws Exception {
+        parseCommandLine(impl);
+        impl.display(out);
+      }
+    });
+  }
+}
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
new file mode 100644
index 0000000..d2429a9
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginReloadCommand.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.sshd.commands;
+
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.server.plugins.InvalidPluginException;
+import com.google.gerrit.server.plugins.PluginInstallException;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.server.plugins.PluginLoader;
+import com.google.gerrit.sshd.SshCommand;
+import com.google.inject.Inject;
+
+import org.kohsuke.args4j.Argument;
+
+import java.util.List;
+
+@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+final class PluginReloadCommand extends SshCommand {
+  @Argument(index = 0, metaVar = "NAME", usage = "plugins to reload/restart")
+  private List<String> names;
+
+  @Inject
+  private PluginLoader loader;
+
+  @Override
+  protected void run() throws UnloggedFailure {
+    if (names == null || names.isEmpty()) {
+      loader.rescan();
+    } else {
+      try {
+        loader.reload(names);
+      } catch (InvalidPluginException e) {
+        throw die(e.getMessage());
+      } catch (PluginInstallException e) {
+        throw die(e.getMessage());
+      }
+    }
+  }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginRemoveCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginRemoveCommand.java
new file mode 100644
index 0000000..8baab77
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginRemoveCommand.java
@@ -0,0 +1,42 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.sshd.commands;
+
+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.SshCommand;
+import com.google.inject.Inject;
+
+import org.kohsuke.args4j.Argument;
+
+import java.util.List;
+
+@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+final class PluginRemoveCommand extends SshCommand {
+  @Argument(index = 0, metaVar = "NAME", required = true, usage = "plugin to remove")
+  List<String> names;
+
+  @Inject
+  private PluginLoader loader;
+
+  @Override
+  protected void run() {
+    if (names != null && !names.isEmpty()) {
+      loader.disablePlugins(Sets.newHashSet(names));
+    }
+  }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Query.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Query.java
index d9a1c3f..63680f8 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Query.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Query.java
@@ -15,16 +15,15 @@
 package com.google.gerrit.sshd.commands;
 
 import com.google.gerrit.server.query.change.QueryProcessor;
-import com.google.gerrit.sshd.BaseCommand;
+import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
 
-import org.apache.sshd.server.Environment;
 import org.kohsuke.args4j.Argument;
 import org.kohsuke.args4j.Option;
 
 import java.util.List;
 
-class Query extends BaseCommand {
+class Query extends SshCommand {
   @Inject
   private QueryProcessor processor;
 
@@ -71,23 +70,23 @@
     processor.setIncludeDependencies(on);
   }
 
+  @Option(name = "--submit-records", usage = "Include submit and label status")
+  void setSubmitRecords(boolean on) {
+    processor.setIncludeSubmitRecords(on);
+  }
+
   @Argument(index = 0, required = true, multiValued = true, metaVar = "QUERY", usage = "Query to execute")
   private List<String> query;
 
   @Override
-  public void start(Environment env) {
-    startThread(new CommandRunnable() {
-      @Override
-      public void run() throws Exception {
-        processor.setOutput(out, QueryProcessor.OutputFormat.TEXT);
-        parseCommandLine();
-        verifyCommandLine();
-        processor.query(join(query, " "));
-      }
-    });
+  protected void run() throws Exception {
+    processor.query(join(query, " "));
   }
 
-  private void verifyCommandLine() throws UnloggedFailure {
+  @Override
+  protected void parseCommandLine() throws UnloggedFailure {
+    processor.setOutput(out, QueryProcessor.OutputFormat.TEXT);
+    super.parseCommandLine();
     if (processor.getIncludeFiles() &&
         !(processor.getIncludePatchSets() || processor.getIncludeCurrentPatchSet())) {
       throw new UnloggedFailure(1, "--files option needs --patch-sets or --current-patch-set");
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Receive.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Receive.java
index 85f53bf..b4de75b 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Receive.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Receive.java
@@ -30,9 +30,10 @@
 import org.eclipse.jgit.transport.AdvertiseRefsHook;
 import org.eclipse.jgit.transport.ReceivePack;
 import org.kohsuke.args4j.Option;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
-import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
@@ -41,6 +42,8 @@
 
 /** Receives change upload over SSH using the Git receive-pack protocol. */
 final class Receive extends AbstractGitCommand {
+  private static final Logger log = LoggerFactory.getLogger(Receive.class);
+
   @Inject
   private AsyncReceiveCommits.Factory factory;
 
@@ -98,9 +101,17 @@
       // is larger than the receive.maxObjectSizeLimit gerrit.config parameter
       // we want to present this error to the user
       if (badStream.getCause() instanceof TooLargeObjectInPackException) {
-        PrintWriter p = toPrintWriter(err);
-        p.print("error: " + badStream.getCause().getMessage() + "\n");
-        p.flush();
+        StringBuilder msg = new StringBuilder();
+        msg.append("Receive error on project \""
+            + projectControl.getProject().getName() + "\"");
+        msg.append(" (user ");
+        msg.append(currentUser.getAccount().getUserName());
+        msg.append(" account ");
+        msg.append(currentUser.getAccountId());
+        msg.append("): ");
+        msg.append(badStream.getCause().getMessage());
+        log.info(msg.toString());
+        throw new UnloggedFailure(128, "error: " + badStream.getCause().getMessage());
       }
 
       // This may have been triggered by branch level access controls.
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/RenameGroupCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/RenameGroupCommand.java
index 5b6cf39..b9abc92 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/RenameGroupCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/RenameGroupCommand.java
@@ -14,20 +14,17 @@
 
 package com.google.gerrit.sshd.commands;
 
+import com.google.gerrit.common.errors.InvalidNameException;
 import com.google.gerrit.common.errors.NameAlreadyUsedException;
 import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.server.account.PerformRenameGroup;
-import com.google.gerrit.sshd.BaseCommand;
+import com.google.gerrit.sshd.SshCommand;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 
-import org.apache.sshd.server.Environment;
 import org.kohsuke.args4j.Argument;
 
-import java.io.IOException;
-
-public class RenameGroupCommand extends BaseCommand {
-
+public class RenameGroupCommand extends SshCommand {
   @Argument(index = 0, required = true, metaVar = "GROUP", usage = "name of the group to be renamed")
   private String groupName;
 
@@ -38,21 +35,17 @@
   private PerformRenameGroup.Factory performRenameGroupFactory;
 
   @Override
-  public void start(final Environment env) throws IOException {
-    startThread(new CommandRunnable() {
-      @Override
-      public void run() throws Exception {
-        parseCommandLine();
-        try {
-          performRenameGroupFactory.create().renameGroup(groupName, newGroupName);
-        } catch (OrmException e) {
-          throw die(e);
-        } catch (NameAlreadyUsedException e) {
-          throw die(e);
-        } catch (NoSuchGroupException e) {
-          throw die(e);
-        }
-      }
-    });
+  protected void run() throws Failure {
+    try {
+      performRenameGroupFactory.create().renameGroup(groupName, newGroupName);
+    } catch (OrmException e) {
+      throw die(e);
+    } catch (InvalidNameException e) {
+      throw die(e);
+    } catch (NameAlreadyUsedException e) {
+      throw die(e);
+    } catch (NoSuchGroupException e) {
+      throw die(e);
+    }
   }
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Replicate.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Replicate.java
deleted file mode 100644
index bc4e0bb..0000000
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Replicate.java
+++ /dev/null
@@ -1,97 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.sshd.commands;
-
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.git.PushAllProjectsOp;
-import com.google.gerrit.server.git.ReplicationQueue;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.sshd.BaseCommand;
-import com.google.inject.Inject;
-
-import org.apache.sshd.server.Environment;
-import org.kohsuke.args4j.Argument;
-import org.kohsuke.args4j.Option;
-
-import java.util.ArrayList;
-import java.util.List;
-import java.util.concurrent.TimeUnit;
-
-/** Force a project to replicate, again. */
-final class Replicate extends BaseCommand {
-  @Option(name = "--all", usage = "push all known projects")
-  private boolean all;
-
-  @Option(name = "--url", metaVar = "PATTERN", usage = "pattern to match URL on")
-  private String urlMatch;
-
-  @Argument(index = 0, multiValued = true, metaVar = "PROJECT", usage = "project name")
-  private List<String> projectNames = new ArrayList<String>(2);
-
-  @Inject
-  IdentifiedUser currentUser;
-
-  @Inject
-  private PushAllProjectsOp.Factory pushAllOpFactory;
-
-  @Inject
-  private ReplicationQueue replication;
-
-  @Inject
-  private ProjectCache projectCache;
-
-  @Override
-  public void start(final Environment env) {
-    startThread(new CommandRunnable() {
-      @Override
-      public void run() throws Exception {
-        if (!currentUser.getCapabilities().canStartReplication()) {
-          String msg = String.format(
-            "fatal: %s does not have \"Start Replication\" capability.",
-            currentUser.getUserName());
-          throw new UnloggedFailure(BaseCommand.STATUS_NOT_ADMIN, msg);
-        }
-
-        parseCommandLine();
-        Replicate.this.schedule();
-      }
-    });
-  }
-
-  private void schedule() throws Failure {
-    if (all && projectNames.size() > 0) {
-      throw new Failure(1, "error: cannot combine --all and PROJECT");
-    }
-
-    if (!replication.isEnabled()) {
-      throw new Failure(1, "error: replication not enabled");
-    }
-
-    if (all) {
-      pushAllOpFactory.create(urlMatch).start(0, TimeUnit.SECONDS);
-
-    } else {
-      for (final String name : projectNames) {
-        final Project.NameKey key = new Project.NameKey(name);
-        if (projectCache.get(key) != null) {
-          replication.scheduleFullSync(key, urlMatch);
-        } else {
-          throw new Failure(1, "error: '" + name + "': not a Gerrit project");
-        }
-      }
-    }
-  }
-}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
index 640adbf..5ebb6c7 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
@@ -33,13 +33,14 @@
 import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.ProjectControl;
-import com.google.gerrit.sshd.BaseCommand;
+import com.google.gerrit.sshd.SshCommand;
 import com.google.gerrit.util.cli.CmdLineParser;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 
-import org.apache.sshd.server.Environment;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.kohsuke.args4j.Argument;
 import org.kohsuke.args4j.Option;
 import org.slf4j.Logger;
@@ -52,7 +53,7 @@
 import java.util.List;
 import java.util.Set;
 
-public class ReviewCommand extends BaseCommand {
+public class ReviewCommand extends SshCommand {
   private static final Logger log =
       LoggerFactory.getLogger(ReviewCommand.class);
 
@@ -67,7 +68,8 @@
 
   private final Set<PatchSet.Id> patchSetIds = new HashSet<PatchSet.Id>();
 
-  @Argument(index = 0, required = true, multiValued = true, metaVar = "{COMMIT | CHANGE,PATCHSET}", usage = "patch to review")
+  @Argument(index = 0, required = true, multiValued = true, metaVar = "{COMMIT | CHANGE,PATCHSET}",
+      usage = "list of commits or patch sets to review")
   void addPatchSetId(final String token) {
     try {
       patchSetIds.addAll(parsePatchSetId(token));
@@ -78,29 +80,29 @@
     }
   }
 
-  @Option(name = "--project", aliases = "-p", usage = "project containing the patch set")
+  @Option(name = "--project", aliases = "-p", usage = "project containing the specified patch set(s)")
   private ProjectControl projectControl;
 
-  @Option(name = "--message", aliases = "-m", usage = "cover message to publish on change", metaVar = "MESSAGE")
+  @Option(name = "--message", aliases = "-m", usage = "cover message to publish on change(s)", metaVar = "MESSAGE")
   private String changeComment;
 
-  @Option(name = "--abandon", usage = "abandon the patch set")
+  @Option(name = "--abandon", usage = "abandon the specified change(s)")
   private boolean abandonChange;
 
-  @Option(name = "--restore", usage = "restore an abandoned the patch set")
+  @Option(name = "--restore", usage = "restore the specified abandoned change(s)")
   private boolean restoreChange;
 
-  @Option(name = "--submit", aliases = "-s", usage = "submit the patch set")
+  @Option(name = "--submit", aliases = "-s", usage = "submit the specified patch set(s)")
   private boolean submitChange;
 
   @Option(name = "--force-message", usage = "publish the message, "
-      + "even if the label score cannot be applied due to change being closed")
+      + "even if the label score cannot be applied due to the change being closed")
   private boolean forceMessage = false;
 
-  @Option(name = "--publish", usage = "publish a draft patch set")
+  @Option(name = "--publish", usage = "publish the specified draft patch set(s)")
   private boolean publishPatchSet;
 
-  @Option(name = "--delete", usage = "delete a draft patch set")
+  @Option(name = "--delete", usage = "delete the specified draft patch set(s)")
   private boolean deleteDraftPatchSet;
 
   @Inject
@@ -113,7 +115,7 @@
   private DeleteDraftPatchSet.Factory deleteDraftPatchSetFactory;
 
   @Inject
-  private AbandonChange.Factory abandonChangeFactory;
+  private Provider<AbandonChange> abandonChangeProvider;
 
   @Inject
   private PublishComments.Factory publishCommentsFactory;
@@ -122,7 +124,7 @@
   private PublishDraft.Factory publishDraftFactory;
 
   @Inject
-  private RestoreChange.Factory restoreChangeFactory;
+  private Provider<RestoreChange> restoreChangeProvider;
 
   @Inject
   private Submit.Factory submitFactory;
@@ -130,67 +132,60 @@
   private List<ApproveOption> optionList;
 
   @Override
-  public final void start(final Environment env) {
-    startThread(new CommandRunnable() {
-      @Override
-      public void run() throws Failure {
-        initOptionList();
-        parseCommandLine();
-        if (abandonChange) {
-          if (restoreChange) {
-            throw error("abandon and restore actions are mutually exclusive");
-          }
-          if (submitChange) {
-            throw error("abandon and submit actions are mutually exclusive");
-          }
-          if (publishPatchSet) {
-            throw error("abandon and publish actions are mutually exclusive");
-          }
-          if (deleteDraftPatchSet) {
-            throw error("abandon and delete actions are mutually exclusive");
-          }
-        }
-        if (publishPatchSet) {
-          if (restoreChange) {
-            throw error("publish and restore actions are mutually exclusive");
-          }
-          if (submitChange) {
-            throw error("publish and submit actions are mutually exclusive");
-          }
-          if (deleteDraftPatchSet) {
-            throw error("publish and delete actions are mutually exclusive");
-          }
-        }
-
-        boolean ok = true;
-        for (final PatchSet.Id patchSetId : patchSetIds) {
-          try {
-            approveOne(patchSetId);
-          } catch (UnloggedFailure e) {
-            ok = false;
-            writeError("error: " + e.getMessage() + "\n");
-          } catch (NoSuchChangeException e) {
-            ok = false;
-            writeError("no such change " + patchSetId.getParentKey().get());
-          } catch (Exception e) {
-            ok = false;
-            writeError("fatal: internal server error while approving "
-                + patchSetId + "\n");
-            log.error("internal error while approving " + patchSetId, e);
-          }
-        }
-
-        if (!ok) {
-          throw new UnloggedFailure(1, "one or more approvals failed;"
-              + " review output above");
-        }
-
+  protected void run() throws UnloggedFailure {
+    if (abandonChange) {
+      if (restoreChange) {
+        throw error("abandon and restore actions are mutually exclusive");
       }
-    });
+      if (submitChange) {
+        throw error("abandon and submit actions are mutually exclusive");
+      }
+      if (publishPatchSet) {
+        throw error("abandon and publish actions are mutually exclusive");
+      }
+      if (deleteDraftPatchSet) {
+        throw error("abandon and delete actions are mutually exclusive");
+      }
+    }
+    if (publishPatchSet) {
+      if (restoreChange) {
+        throw error("publish and restore actions are mutually exclusive");
+      }
+      if (submitChange) {
+        throw error("publish and submit actions are mutually exclusive");
+      }
+      if (deleteDraftPatchSet) {
+        throw error("publish and delete actions are mutually exclusive");
+      }
+    }
+
+    boolean ok = true;
+    for (final PatchSet.Id patchSetId : patchSetIds) {
+      try {
+        approveOne(patchSetId);
+      } catch (UnloggedFailure e) {
+        ok = false;
+        writeError("error: " + e.getMessage() + "\n");
+      } catch (NoSuchChangeException e) {
+        ok = false;
+        writeError("no such change " + patchSetId.getParentKey().get());
+      } catch (Exception e) {
+        ok = false;
+        writeError("fatal: internal server error while approving "
+            + patchSetId + "\n");
+        log.error("internal error while approving " + patchSetId, e);
+      }
+    }
+
+    if (!ok) {
+      throw new UnloggedFailure(1, "one or more approvals failed;"
+          + " review output above");
+    }
   }
 
-  private void approveOne(final PatchSet.Id patchSetId) throws
-      NoSuchChangeException, OrmException, EmailException, Failure {
+  private void approveOne(final PatchSet.Id patchSetId)
+      throws NoSuchChangeException, OrmException, EmailException, Failure,
+      RepositoryNotFoundException, IOException {
 
     if (changeComment == null) {
       changeComment = "";
@@ -208,12 +203,16 @@
       publishCommentsFactory.create(patchSetId, changeComment, aps, forceMessage).call();
 
       if (abandonChange) {
-        final ReviewResult result = abandonChangeFactory.create(
-            patchSetId, changeComment).call();
+        final AbandonChange abandonChange = abandonChangeProvider.get();
+        abandonChange.setChangeId(patchSetId.getParentKey());
+        abandonChange.setMessage(changeComment);
+        final ReviewResult result = abandonChange.call();
         handleReviewResultErrors(result);
       } else if (restoreChange) {
-        final ReviewResult result = restoreChangeFactory.create(
-            patchSetId, changeComment).call();
+        final RestoreChange restoreChange = restoreChangeProvider.get();
+        restoreChange.setChangeId(patchSetId.getParentKey());
+        restoreChange.setMessage(changeComment);
+        final ReviewResult result = restoreChange.call();
         handleReviewResultErrors(result);
       }
       if (submitChange) {
@@ -255,6 +254,9 @@
         case CHANGE_IS_CLOSED:
           errMsg += "change is closed";
           break;
+        case CHANGE_NOT_ABANDONED:
+          errMsg += "change is not abandoned";
+          break;
         case PUBLISH_NOT_PERMITTED:
           errMsg += "not permitted to publish change";
           break;
@@ -265,11 +267,14 @@
           errMsg += "rule error";
           break;
         case NOT_A_DRAFT:
-          errMsg += "change is not a draft";
+          errMsg += "change/patch set is not a draft";
           break;
         case GIT_ERROR:
           errMsg += "error writing change to git repository";
           break;
+        case DEST_BRANCH_NOT_FOUND:
+          errMsg += "destination branch not found";
+          break;
         default:
           errMsg += "failure in review";
       }
@@ -344,7 +349,8 @@
     return projectControl.getProject().getNameKey().equals(change.getProject());
   }
 
-  private void initOptionList() {
+  @Override
+  protected void parseCommandLine() throws UnloggedFailure {
     optionList = new ArrayList<ApproveOption>();
 
     for (ApprovalType type : approvalTypes.getApprovalTypes()) {
@@ -360,6 +366,8 @@
           "--" + category.getName().toLowerCase().replace(' ', '-');
       optionList.add(new ApproveOption(name, usage, type));
     }
+
+    super.parseCommandLine();
   }
 
   private void writeError(final String msg) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
new file mode 100644
index 0000000..9940fc8
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
@@ -0,0 +1,290 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.sshd.commands;
+
+
+import com.google.gerrit.common.errors.InvalidSshKeyException;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Account.FieldName;
+import com.google.gerrit.reviewdb.client.AccountExternalId;
+import com.google.gerrit.reviewdb.client.AccountSshKey;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.AccountException;
+import com.google.gerrit.server.account.AccountManager;
+import com.google.gerrit.server.account.AuthRequest;
+import com.google.gerrit.server.account.Realm;
+import com.google.gerrit.server.ssh.SshKeyCache;
+import com.google.gerrit.sshd.BaseCommand;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
+import com.google.inject.Inject;
+
+import org.apache.sshd.server.Environment;
+import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.Option;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.UnsupportedEncodingException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+/** Set a user's account settings. **/
+final class SetAccountCommand extends BaseCommand {
+
+  @Argument(index = 0, required = true, metaVar = "USER", usage = "full name, email-address, ssh username or account id")
+  private Account.Id id;
+
+  @Option(name = "--full-name", metaVar = "NAME", usage = "display name of the account")
+  private String fullName;
+
+  @Option(name = "--active", usage = "set account's state to active")
+  private boolean active;
+
+  @Option(name = "--inactive", usage = "set account's state to inactive")
+  private boolean inactive;
+
+  @Option(name = "--add-email", multiValued = true, metaVar = "EMAIL", usage = "email addresses to add to the account")
+  private List<String> addEmails = new ArrayList<String>();
+
+  @Option(name = "--delete-email", multiValued = true, metaVar = "EMAIL", usage = "email addresses to delete from the account")
+  private List<String> deleteEmails = new ArrayList<String>();
+
+  @Option(name = "--add-ssh-key", multiValued = true, metaVar = "-|KEY", usage = "public keys to add to the account")
+  private List<String> addSshKeys = new ArrayList<String>();
+
+  @Option(name = "--delete-ssh-key", multiValued = true, metaVar = "-|KEY", usage = "public keys to delete from the account")
+  private List<String> deleteSshKeys = new ArrayList<String>();
+
+  @Option(name = "--http-password", metaVar = "PASSWORD", usage = "password for HTTP authentication for the account")
+  private String httpPassword;
+
+  @Inject
+  private IdentifiedUser currentUser;
+
+  @Inject
+  private ReviewDb db;
+
+  @Inject
+  private AccountManager manager;
+
+  @Inject
+  private SshKeyCache sshKeyCache;
+
+  @Inject
+  private AccountCache byIdCache;
+
+  @Inject
+  private Realm realm;
+
+  @Override
+  public void start(final Environment env) {
+    startThread(new CommandRunnable() {
+      @Override
+      public void run() throws Exception {
+        if (!currentUser.getCapabilities().canAdministrateServer()) {
+          String msg =
+              String.format(
+                  "fatal: %s does not have \"Administrator\" capability.",
+                  currentUser.getUserName());
+          throw new UnloggedFailure(1, msg);
+        }
+        parseCommandLine();
+        validate();
+        setAccount();
+      }
+    });
+  }
+
+  private void validate() throws UnloggedFailure {
+    if (active && inactive) {
+      throw new UnloggedFailure(1,
+          "--active and --inactive options are mutually exclusive.");
+    }
+    if (addSshKeys.contains("-") && deleteSshKeys.contains("-")) {
+      throw new UnloggedFailure(1, "Only one option may use the stdin");
+    }
+    if (deleteSshKeys.contains("ALL")) {
+      deleteSshKeys = Collections.singletonList("ALL");
+    }
+    if (deleteEmails.contains("ALL")) {
+      deleteEmails = Collections.singletonList("ALL");
+    }
+  }
+
+  private void setAccount() throws OrmException, IOException, UnloggedFailure {
+
+    final Account account = db.accounts().get(id);
+    boolean accountUpdated = false;
+    boolean sshKeysUpdated = false;
+
+    for (String email : addEmails) {
+      link(id, email);
+    }
+
+    for (String email : deleteEmails) {
+      deleteMail(id, email);
+    }
+
+    if (fullName != null) {
+      if (realm.allowsEdit(FieldName.FULL_NAME)) {
+        account.setFullName(fullName);
+      } else {
+        throw new UnloggedFailure(1, "The realm doesn't allow editing names");
+      }
+    }
+
+    if (httpPassword != null) {
+      setHttpPassword(id, httpPassword);
+    }
+
+    if (active) {
+      accountUpdated = true;
+      account.setActive(true);
+    } else if (inactive) {
+      accountUpdated = true;
+      account.setActive(false);
+    }
+
+    addSshKeys = readSshKey(addSshKeys);
+    if (!addSshKeys.isEmpty()) {
+      sshKeysUpdated = true;
+      addSshKeys(addSshKeys, account);
+    }
+
+    deleteSshKeys = readSshKey(deleteSshKeys);
+    if (!deleteSshKeys.isEmpty()) {
+      sshKeysUpdated = true;
+      deleteSshKeys(deleteSshKeys, account);
+    }
+
+    if (accountUpdated) {
+      db.accounts().update(Collections.singleton(account));
+      byIdCache.evict(id);
+    }
+
+    if (sshKeysUpdated) {
+      sshKeyCache.evict(account.getUserName());
+    }
+  }
+
+  private void addSshKeys(final List<String> keys, final Account account)
+      throws OrmException, UnloggedFailure {
+    List<AccountSshKey> accountKeys = new ArrayList<AccountSshKey>();
+    int seq = db.accountSshKeys().byAccount(account.getId()).toList().size();
+    for (String key : keys) {
+      try {
+        seq++;
+        AccountSshKey accountSshKey = sshKeyCache.create(
+            new AccountSshKey.Id(account.getId(), seq), key.trim());
+        accountKeys.add(accountSshKey);
+      } catch (InvalidSshKeyException e) {
+        throw new UnloggedFailure(1, "fatal: invalid ssh key");
+      }
+    }
+    db.accountSshKeys().insert(accountKeys);
+  }
+
+  private void deleteSshKeys(final List<String> keys, final Account account)
+      throws OrmException {
+    ResultSet<AccountSshKey> allKeys = db.accountSshKeys().byAccount(account.getId());
+    if (keys.contains("ALL")) {
+      db.accountSshKeys().delete(allKeys);
+    } else {
+      List<AccountSshKey> accountKeys = new ArrayList<AccountSshKey>();
+      for (String key : keys) {
+        for (AccountSshKey accountSshKey : allKeys) {
+          if (key.trim().equals(accountSshKey.getSshPublicKey())
+              || accountSshKey.getComment().trim().equals(key)) {
+            accountKeys.add(accountSshKey);
+          }
+        }
+      }
+      db.accountSshKeys().delete(accountKeys);
+    }
+  }
+
+  private void deleteMail(Account.Id id, final String mailAddress)
+      throws UnloggedFailure, OrmException {
+    if (mailAddress.equals("ALL")) {
+      ResultSet<AccountExternalId> ids = db.accountExternalIds().byAccount(id);
+      for (AccountExternalId extId : ids) {
+        if (extId.isScheme(AccountExternalId.SCHEME_MAILTO)) {
+          unlink(id, extId.getEmailAddress());
+        }
+      }
+    } else {
+      AccountExternalId.Key key = new AccountExternalId.Key(
+          AccountExternalId.SCHEME_MAILTO, mailAddress);
+      AccountExternalId extId = db.accountExternalIds().get(key);
+      if (extId != null) {
+        unlink(id, mailAddress);
+      }
+    }
+  }
+
+  private void setHttpPassword(Account.Id id, final String httpPassword)
+      throws UnloggedFailure, OrmException {
+    ResultSet<AccountExternalId> ids = db.accountExternalIds().byAccount(id);
+    for (AccountExternalId extId: ids) {
+      if (extId.isScheme(AccountExternalId.SCHEME_USERNAME)) {
+        extId.setPassword(httpPassword);
+        db.accountExternalIds().update(Collections.singleton(extId));
+        byIdCache.evict(id);
+      }
+    }
+  }
+
+  private void unlink(Account.Id id, final String mailAddress)
+      throws UnloggedFailure {
+    try {
+      manager.unlink(id, AuthRequest.forEmail(mailAddress));
+    } catch (AccountException ex) {
+      throw die(ex.getMessage());
+    }
+  }
+
+  private void link(Account.Id id, final String mailAddress)
+      throws UnloggedFailure {
+    try {
+      manager.link(id, AuthRequest.forEmail(mailAddress));
+    } catch (AccountException ex) {
+      throw die(ex.getMessage());
+    }
+  }
+
+  private List<String> readSshKey(final List<String> sshKeys)
+      throws UnsupportedEncodingException, IOException {
+    if (!sshKeys.isEmpty()) {
+      String sshKey = "";
+      int idx = sshKeys.indexOf("-");
+      if (idx >= 0) {
+        sshKey = "";
+        BufferedReader br =
+            new BufferedReader(new InputStreamReader(in, "UTF-8"));
+        String line;
+        while ((line = br.readLine()) != null) {
+          sshKey += line + "\n";
+        }
+        sshKeys.set(idx, sshKey);
+      }
+    }
+    return sshKeys;
+  }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetProjectCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetProjectCommand.java
new file mode 100644
index 0000000..9143f5b
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetProjectCommand.java
@@ -0,0 +1,171 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.sshd.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.reviewdb.client.Project.State;
+import com.google.gerrit.reviewdb.client.Project.SubmitType;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.sshd.SshCommand;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.Option;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+
+@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+final class SetProjectCommand extends SshCommand {
+  private static final Logger log = LoggerFactory
+      .getLogger(SetProjectCommand.class);
+
+  @Argument(index = 0, required = true, metaVar = "NAME", usage = "name of the project")
+  private ProjectControl projectControl;
+
+  @Option(name = "--description", aliases = {"-d"}, metaVar = "DESCRIPTION", usage = "description of project")
+  private String projectDescription;
+
+  @Option(name = "--submit-type", aliases = {"-t"}, usage = "project submit type\n"
+      + "(default: MERGE_IF_NECESSARY)")
+  private SubmitType submitType;
+
+  @Option(name = "--use-contributor-agreements", aliases = {"--ca"}, usage = "if contributor agreement is required")
+  private Boolean contributorAgreements;
+
+  @Option(name = "--no-contributor-agreements", aliases = {"--nca"}, usage = "if contributor agreement is not required")
+  private Boolean noContributorAgreements;
+
+  @Option(name = "--use-signed-off-by", aliases = {"--so"}, usage = "if signed-off-by is required")
+  private Boolean signedOffBy;
+
+  @Option(name = "--no-signed-off-by", aliases = {"--nso"}, usage = "if signed-off-by is not required")
+  private Boolean noSignedOffBy;
+
+  @Option(name = "--use-content-merge", usage = "allow automatic conflict resolving within files")
+  private Boolean contentMerge;
+
+  @Option(name = "--no-content-merge", usage = "don't allow automatic conflict resolving within files")
+  private Boolean noContentMerge;
+
+  @Option(name = "--require-change-id", aliases = {"--id"}, usage = "if change-id is required")
+  private Boolean requireChangeID;
+
+  @Option(name = "--no-change-id", aliases = {"--nid"}, usage = "if change-id is not required")
+  private Boolean noRequireChangeID;
+
+  @Option(name = "--project-state", aliases = {"--ps"}, usage = "project's visibility state")
+  private State state;
+
+  @Inject
+  private MetaDataUpdate.User metaDataUpdateFactory;
+
+  @Inject
+  private ProjectCache projectCache;
+
+  @Override
+  protected void run() throws Failure {
+    validate();
+    Project ctlProject = projectControl.getProject();
+    Project.NameKey nameKey = ctlProject.getNameKey();
+    String name = ctlProject.getName();
+    final StringBuilder err = new StringBuilder();
+
+    try {
+      MetaDataUpdate md = metaDataUpdateFactory.create(nameKey);
+      try {
+        ProjectConfig config = ProjectConfig.read(md);
+        Project project = config.getProject();
+
+        project.setRequireChangeID(requireChangeID != null ? requireChangeID
+            : project.isRequireChangeID());
+
+        project.setRequireChangeID(noRequireChangeID != null
+            ? !noRequireChangeID : project.isRequireChangeID());
+
+        project.setSubmitType(submitType != null ? submitType : project
+            .getSubmitType());
+
+        project.setUseContentMerge(contentMerge != null ? contentMerge
+            : project.isUseContentMerge());
+
+        project.setUseContentMerge(noContentMerge != null ? !noContentMerge
+            : project.isUseContentMerge());
+
+        project.setUseContributorAgreements(contributorAgreements != null
+            ? contributorAgreements : project.isUseContributorAgreements());
+
+        project.setUseContributorAgreements(noContributorAgreements != null
+            ? !noContributorAgreements : project.isUseContributorAgreements());
+
+        project.setUseSignedOffBy(signedOffBy != null ? signedOffBy : project
+            .isUseSignedOffBy());
+
+        project.setUseContentMerge(noSignedOffBy != null ? !noSignedOffBy
+            : project.isUseContentMerge());
+
+        project.setDescription(projectDescription != null ? projectDescription
+            : project.getDescription());
+
+        project.setState(state != null ? state : project.getState());
+
+        md.setMessage("Project settings updated");
+        config.commit(md);
+      } finally {
+        md.close();
+      }
+    } catch (RepositoryNotFoundException notFound) {
+      err.append("error: Project " + name + " not found\n");
+    } catch (IOException e) {
+      final String msg = "Cannot update project " + name;
+      log.error(msg, e);
+      err.append("error: " + msg + "\n");
+    } catch (ConfigInvalidException e) {
+      final String msg = "Cannot update project " + name;
+      log.error(msg, e);
+      err.append("error: " + msg + "\n");
+    }
+    projectCache.evict(ctlProject);
+
+    if (err.length() > 0) {
+      while (err.charAt(err.length() - 1) == '\n') {
+        err.setLength(err.length() - 1);
+      }
+      throw new UnloggedFailure(1, err.toString());
+    }
+  }
+
+  private void validate() throws UnloggedFailure {
+    checkExclusivity(contentMerge, "--use-content-merge",
+        noContentMerge, "--no-content-merge");
+
+    checkExclusivity(contributorAgreements, "--use-contributor-agreements",
+        noContributorAgreements, "--no-contributor-agreements");
+
+    checkExclusivity(signedOffBy, "--use-signed-off-by",
+        noSignedOffBy, "--no-signed-off-by");
+
+    checkExclusivity(requireChangeID, "--require-change-id",
+        noRequireChangeID, "--no-change-id");
+  }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
index 6e1a32b..f873824 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
@@ -25,12 +25,11 @@
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.ProjectControl;
-import com.google.gerrit.sshd.BaseCommand;
+import com.google.gerrit.sshd.SshCommand;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Inject;
 
-import org.apache.sshd.server.Environment;
 import org.kohsuke.args4j.Argument;
 import org.kohsuke.args4j.Option;
 import org.slf4j.Logger;
@@ -43,7 +42,7 @@
 import java.util.List;
 import java.util.Set;
 
-public class SetReviewersCommand extends BaseCommand {
+public class SetReviewersCommand extends SshCommand {
   private static final Logger log =
       LoggerFactory.getLogger(SetReviewersCommand.class);
 
@@ -85,28 +84,21 @@
   private Set<Change.Id> changes = new HashSet<Change.Id>();
 
   @Override
-  public final void start(final Environment env) {
-    startThread(new CommandRunnable() {
-      @Override
-      public void run() throws Failure {
-        parseCommandLine();
-
-        boolean ok = true;
-        for (Change.Id changeId : changes) {
-          try {
-            ok &= modifyOne(changeId);
-          } catch (Exception err) {
-            ok = false;
-            log.error("Error updating reviewers on change " + changeId, err);
-            writeError("fatal", "internal error while updating " + changeId);
-          }
-        }
-
-        if (!ok) {
-          throw error("fatal: one or more updates failed; review output above");
-        }
+  protected void run() throws UnloggedFailure {
+    boolean ok = true;
+    for (Change.Id changeId : changes) {
+      try {
+        ok &= modifyOne(changeId);
+      } catch (Exception err) {
+        ok = false;
+        log.error("Error updating reviewers on change " + changeId, err);
+        writeError("fatal", "internal error while updating " + changeId);
       }
-    });
+    }
+
+    if (!ok) {
+      throw error("fatal: one or more updates failed; review output above");
+    }
   }
 
   private boolean modifyOne(Change.Id changeId) throws Exception {
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 4de10d6..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,19 +14,20 @@
 
 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.lifecycle.LifecycleListener;
-import com.google.gerrit.server.IdentifiedUser;
+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.BaseCommand;
 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;
@@ -36,7 +37,6 @@
 
 import java.io.File;
 import java.io.IOException;
-import java.io.PrintWriter;
 import java.lang.management.ManagementFactory;
 import java.lang.management.OperatingSystemMXBean;
 import java.lang.management.RuntimeMXBean;
@@ -45,8 +45,11 @@
 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)
 final class ShowCaches extends CacheCommand {
   private static volatile long serverStarted;
 
@@ -68,9 +71,6 @@
   private boolean showJVM;
 
   @Inject
-  private IdentifiedUser currentUser;
-
-  @Inject
   private WorkQueue workQueue;
 
   @Inject
@@ -80,98 +80,81 @@
   @SitePath
   private File sitePath;
 
-  private PrintWriter p;
+  @Option(name = "--width", aliases = {"-w"}, metaVar = "COLS", usage = "width of output table")
+  private int columns = 80;
+  private int nw;
 
   @Override
-  public void start(final Environment env) {
-    startThread(new CommandRunnable() {
-      @Override
-      public void run() throws Exception {
-        if (!currentUser.getCapabilities().canViewCaches()) {
-          String msg = String.format(
-            "fatal: %s does not have \"View Caches\" capability.",
-            currentUser.getUserName());
-          throw new UnloggedFailure(BaseCommand.STATUS_NOT_ADMIN, msg);
-        }
-
-        parseCommandLine();
-        display();
+  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);
   }
 
-  private void display() {
-    p = toPrintWriter(out);
-
+  @Override
+  protected void run() {
+    nw = columns - 50;
     Date now = new Date();
-    p.format(
+    stdout.format(
         "%-25s %-20s      now  %16s\n",
         "Gerrit Code Review",
         Version.getVersion() != null ? Version.getVersion() : "",
         new SimpleDateFormat("HH:mm:ss   zzz").format(now));
-    p.format(
+    stdout.format(
         "%-25s %-20s   uptime %16s\n",
         "", "",
         uptime(now.getTime() - serverStarted));
-    p.print('\n');
+    stdout.print('\n');
 
-    p.print(String.format(//
-        "%1s %-18s %-4s|%-20s|  %-5s  |%-14s|\n" //
+    stdout.print(String.format(//
+        "%1s %-"+nw+"s|%-21s|  %-5s |%-9s|\n" //
         , "" //
         , "Name" //
-        , "Max" //
-        , "Object Count" //
+        , "Entries" //
         , "AvgGet" //
         , "Hit Ratio" //
     ));
-    p.print(String.format(//
-        "%1s %-18s %-4s|%6s %6s %6s|  %-5s   |%-4s %-4s %-4s|\n" //
+    stdout.print(String.format(//
+        "%1s %-"+nw+"s|%6s %6s %7s|  %-5s  |%-4s %-4s|\n" //
         , "" //
         , "" //
-        , "Age" //
-        , "Disk" //
         , "Mem" //
-        , "Cnt" //
-        , "" //
         , "Disk" //
+        , "Space" //
+        , "" //
         , "Mem" //
-        , "Agg" //
+        , "Disk" //
     ));
-    p.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();
-
-      if (useDisk) {
-        p.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 {
-        p.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) //
-            ));
-      }
+    stdout.print("--");
+    for (int i = 0; i < nw; i++) {
+      stdout.print('-');
     }
-    p.print('\n');
+    stdout.print("+---------------------+---------+---------+\n");
+
+    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');
 
     if (gc) {
       System.gc();
@@ -187,7 +170,52 @@
       jvmSummary();
     }
 
-    p.flush();
+    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() {
@@ -200,17 +228,17 @@
     final int jgitOpen = WindowCacheStatAccessor.getOpenFiles();
     final long jgitBytes = WindowCacheStatAccessor.getOpenBytes();
 
-    p.format("Mem: %s total = %s used + %s free + %s buffers\n",
+    stdout.format("Mem: %s total = %s used + %s free + %s buffers\n",
         bytes(mTotal),
         bytes(mInuse - jgitBytes),
         bytes(mFree),
         bytes(jgitBytes));
-    p.format("     %s max\n", bytes(mMax));
-    p.format("    %8d open files, %8d cpus available, %8d threads\n",
+    stdout.format("     %s max\n", bytes(mMax));
+    stdout.format("    %8d open files, %8d cpus available, %8d threads\n",
         jgitOpen,
         r.availableProcessors(),
         ManagementFactory.getThreadMXBean().getThreadCount());
-    p.print('\n');
+    stdout.print('\n');
   }
 
   private void taskSummary() {
@@ -224,7 +252,7 @@
         case SLEEPING: tasksSleeping++; break;
       }
     }
-    p.format(
+    stdout.format(
         "Tasks: %4d  total = %4d running +   %4d ready + %4d sleeping\n",
         tasksTotal,
         tasksRunning,
@@ -245,7 +273,7 @@
       oldest = Math.min(oldest, s.getCreationTime());
     }
 
-    p.format(
+    stdout.format(
         "SSH:   %4d  users, oldest session started %s ago\n",
         list.size(),
         uptime(now - oldest));
@@ -254,22 +282,22 @@
   private void jvmSummary() {
     OperatingSystemMXBean osBean = ManagementFactory.getOperatingSystemMXBean();
     RuntimeMXBean runtimeBean = ManagementFactory.getRuntimeMXBean();
-    p.format("JVM: %s %s %s\n",
+    stdout.format("JVM: %s %s %s\n",
         runtimeBean.getVmVendor(),
         runtimeBean.getVmName(),
         runtimeBean.getVmVersion());
-    p.format("  on %s %s %s\n", "",
+    stdout.format("  on %s %s %s\n", "",
         osBean.getName(),
         osBean.getVersion(),
         osBean.getArch());
     try {
-      p.format("  running as %s on %s\n",
+      stdout.format("  running as %s on %s\n",
           System.getProperty("user.name"),
           InetAddress.getLocalHost().getHostName());
     } catch (UnknownHostException e) {
     }
-    p.format("  cwd  %s\n", path(new File(".").getAbsoluteFile().getParentFile()));
-    p.format("  site %s\n", path(sitePath));
+    stdout.format("  cwd  %s\n", path(new File(".").getAbsoluteFile().getParentFile()));
+    stdout.format("  site %s\n", path(sitePath));
   }
 
   private String path(File file) {
@@ -325,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 a72ce90..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
@@ -14,21 +14,21 @@
 
 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.BaseCommand;
+import com.google.gerrit.sshd.SshCommand;
 import com.google.gerrit.sshd.SshDaemon;
 import com.google.gerrit.sshd.SshSession;
 import com.google.inject.Inject;
 
 import org.apache.mina.core.service.IoAcceptor;
 import org.apache.mina.core.session.IoSession;
-import org.apache.sshd.server.Environment;
 import org.apache.sshd.server.session.ServerSession;
 import org.kohsuke.args4j.Option;
 
-import java.io.PrintWriter;
 import java.net.InetAddress;
 import java.net.InetSocketAddress;
 import java.net.SocketAddress;
@@ -40,39 +40,16 @@
 import java.util.List;
 
 /** Show the current SSH connections. */
-final class ShowConnections extends BaseCommand {
+@RequiresCapability(GlobalCapability.VIEW_CONNECTIONS)
+final class ShowConnections extends SshCommand {
   @Option(name = "--numeric", aliases = {"-n"}, usage = "don't resolve names")
   private boolean numeric;
 
-  private PrintWriter p;
-
-  @Inject
-  IdentifiedUser currentUser;
-
   @Inject
   private SshDaemon daemon;
 
   @Override
-  public void start(final Environment env) {
-    startThread(new CommandRunnable() {
-      @Override
-      public void run() throws Exception {
-        if (!currentUser.getCapabilities().canViewConnections()) {
-          String msg = String.format(
-            "fatal: %s does not have \"View Connections\" capability.",
-            currentUser.getUserName());
-          throw new UnloggedFailure(BaseCommand.STATUS_NOT_ADMIN, msg);
-        }
-
-        parseCommandLine();
-        ShowConnections.this.display();
-      }
-    });
-  }
-
-  private void display() throws Failure {
-    p = toPrintWriter(out);
-
+  protected void run() throws Failure {
     final IoAcceptor acceptor = daemon.getIoAcceptor();
     if (acceptor == null) {
       throw new Failure(1, "fatal: sshd no longer running");
@@ -93,9 +70,9 @@
     });
 
     final long now = System.currentTimeMillis();
-    p.print(String.format("%-8s %8s %8s   %-15s %s\n", //
+    stdout.print(String.format("%-8s %8s %8s   %-15s %s\n", //
         "Session", "Start", "Idle", "User", "Remote Host"));
-    p.print("--------------------------------------------------------------\n");
+    stdout.print("--------------------------------------------------------------\n");
     for (final IoSession io : list) {
       ServerSession s = (ServerSession) ServerSession.getSession(io, true);
       SshSession sd = s != null ? s.getAttribute(SshSession.KEY) : null;
@@ -104,16 +81,14 @@
       final long start = io.getCreationTime();
       final long idle = now - io.getLastIoTime();
 
-      p.print(String.format("%8s %8s %8s  %-15.15s %.30s\n", //
+      stdout.print(String.format("%8s %8s %8s  %-15.15s %.30s\n", //
           id(sd), //
           time(now, start), //
           age(idle), //
           username(sd), //
           hostname(remoteAddress)));
     }
-    p.print("--\n");
-
-    p.flush();
+    stdout.print("--\n");
   }
 
   private static String id(final SshSession sd) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowQueue.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowQueue.java
index e835ffe..f862484 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowQueue.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowQueue.java
@@ -23,13 +23,13 @@
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.util.IdGenerator;
 import com.google.gerrit.sshd.AdminHighPriorityCommand;
-import com.google.gerrit.sshd.BaseCommand;
+import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
 
 import org.apache.sshd.server.Environment;
 import org.kohsuke.args4j.Option;
 
-import java.io.PrintWriter;
+import java.io.IOException;
 import java.text.SimpleDateFormat;
 import java.util.Collections;
 import java.util.Comparator;
@@ -39,7 +39,7 @@
 
 /** Display the current work queue. */
 @AdminHighPriorityCommand
-final class ShowQueue extends BaseCommand {
+final class ShowQueue extends SshCommand {
   @Option(name = "-w", usage = "display without line width truncation")
   private boolean wide;
 
@@ -52,12 +52,11 @@
   @Inject
   private IdentifiedUser currentUser;
 
-  private PrintWriter p;
   private int columns = 80;
   private int taskNameWidth;
 
   @Override
-  public void start(final Environment env) {
+  public void start(final Environment env) throws IOException {
     String s = env.getEnv().get(Environment.ENV_COLUMNS);
     if (s != null && !s.isEmpty()) {
       try {
@@ -66,19 +65,11 @@
         columns = 80;
       }
     }
-
-    startThread(new CommandRunnable() {
-      @Override
-      public void run() throws Exception {
-        parseCommandLine();
-        ShowQueue.this.display();
-      }
-    });
+    super.start(env);
   }
 
-  private void display() {
-    p = toPrintWriter(out);
-
+  @Override
+  protected void run() {
     final List<Task<?>> pending = workQueue.getTasks();
     Collections.sort(pending, new Comparator<Task<?>>() {
       public int compare(Task<?> a, Task<?> b) {
@@ -103,9 +94,9 @@
 
     taskNameWidth = wide ? Integer.MAX_VALUE : columns - 8 - 12 - 8 - 4;
 
-    p.print(String.format("%-8s %-12s %-8s %s\n", //
+    stdout.print(String.format("%-8s %-12s %-8s %s\n", //
         "Task", "State", "", "Command"));
-    p.print("----------------------------------------------"
+    stdout.print("----------------------------------------------"
         + "--------------------------------\n");
 
     int numberOfPendingTasks = 0;
@@ -158,7 +149,7 @@
 
       // Shows information about tasks depending on the user rights
       if (viewAll || (!hasCustomizedPrint && regularUserCanSee)) {
-        p.print(String.format("%8s %-12s %-8s %s\n", //
+        stdout.print(String.format("%8s %-12s %-8s %s\n", //
             id(task.getTaskId()), start, "", format(task)));
       } else if (regularUserCanSee) {
         if (remoteName == null) {
@@ -167,20 +158,18 @@
           remoteName = remoteName + "/" + projectName;
         }
 
-        p.print(String.format("%8s %-12s %-8s %s\n", //
+        stdout.print(String.format("%8s %-12s %-8s %s\n", //
             id(task.getTaskId()), start, "", remoteName));
       }
     }
-    p.print("----------------------------------------------"
+    stdout.print("----------------------------------------------"
         + "--------------------------------\n");
 
     if (viewAll) {
       numberOfPendingTasks = pending.size();
     }
 
-    p.print("  " + numberOfPendingTasks + " tasks\n");
-
-    p.flush();
+    stdout.print("  " + numberOfPendingTasks + " tasks\n");
   }
 
   private static String id(final int id) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SlaveCommandModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SlaveCommandModule.java
index 32ab2db..0e1a1fe 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SlaveCommandModule.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SlaveCommandModule.java
@@ -27,11 +27,14 @@
 
     command(gerrit, "approve").to(ErrorSlaveMode.class);
     command(gerrit, "create-account").to(ErrorSlaveMode.class);
+    command(gerrit, "create-group").to(ErrorSlaveMode.class);
     command(gerrit, "create-project").to(ErrorSlaveMode.class);
     command(gerrit, "gsql").to(ErrorSlaveMode.class);
     command(gerrit, "receive-pack").to(ErrorSlaveMode.class);
+    command(gerrit, "rename-group").to(ErrorSlaveMode.class);
     command(gerrit, "replicate").to(ErrorSlaveMode.class);
     command(gerrit, "review").to(ErrorSlaveMode.class);
     command(gerrit, "set-project-parent").to(ErrorSlaveMode.class);
+    command(gerrit, "set-reviewers").to(ErrorSlaveMode.class);
   }
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StreamEvents.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/StreamEvents.java
old mode 100755
new mode 100644
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/TestSubmitRule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/TestSubmitRule.java
new file mode 100644
index 0000000..c8544e4
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/TestSubmitRule.java
@@ -0,0 +1,241 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.sshd.commands;
+
+import com.google.gerrit.common.data.AccountInfo;
+import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.rules.PrologEnvironment;
+import com.google.gerrit.rules.StoredValues;
+import com.google.gerrit.server.OutputFormat;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.config.AnonymousCowardName;
+import com.google.gerrit.server.events.AccountAttribute;
+import com.google.gerrit.server.events.SubmitLabelAttribute;
+import com.google.gerrit.server.events.SubmitRecordAttribute;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.sshd.SshCommand;
+import com.google.gson.reflect.TypeToken;
+import com.google.inject.Inject;
+
+import com.googlecode.prolog_cafe.compiler.CompileException;
+import com.googlecode.prolog_cafe.lang.BufferingPrologControl;
+import com.googlecode.prolog_cafe.lang.JavaObjectTerm;
+import com.googlecode.prolog_cafe.lang.ListTerm;
+import com.googlecode.prolog_cafe.lang.Prolog;
+import com.googlecode.prolog_cafe.lang.PrologClassLoader;
+import com.googlecode.prolog_cafe.lang.PrologException;
+import com.googlecode.prolog_cafe.lang.PrologMachineCopy;
+import com.googlecode.prolog_cafe.lang.SymbolTerm;
+import com.googlecode.prolog_cafe.lang.Term;
+import com.googlecode.prolog_cafe.lang.VariableTerm;
+
+import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.Option;
+
+import java.io.InputStreamReader;
+import java.io.PushbackReader;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Set;
+
+/** Command that allows testing of prolog submit-rules in a live instance. */
+final class TestSubmitRule extends SshCommand {
+  @Inject
+  private ReviewDb db;
+
+  @Inject
+  private PrologEnvironment.Factory envFactory;
+
+  @Inject
+  private ChangeControl.Factory ccFactory;
+
+  @Inject
+  private AccountCache accountCache;
+
+  final @AnonymousCowardName String anonymousCowardName;
+
+  @Argument(index = 0, required = true, usage = "ChangeId to load in prolog environment")
+  private String changeId;
+
+  @Option(name = "-s",
+      usage = "Read prolog script from stdin instead of reading rules.pl from the refs/meta/config branch")
+  private boolean useStdin;
+
+  @Option(name = "--format", metaVar = "FMT", usage = "Output display format")
+  private OutputFormat format = OutputFormat.TEXT;
+
+  @Option(name = "--no-filters", aliases = {"-n"},
+      usage = "Don't run the submit_filter/2 from the parent projects")
+  private boolean skipSubmitFilters;
+
+  private static final String[] PACKAGE_LIST = {Prolog.BUILTIN, "gerrit"};
+
+  @Inject
+  public TestSubmitRule(@AnonymousCowardName String anonymous) {
+    anonymousCowardName = anonymous;
+  }
+  private PrologMachineCopy newMachine() {
+    BufferingPrologControl ctl = new BufferingPrologControl();
+    ctl.setMaxDatabaseSize(16 * 1024);
+    ctl.setPrologClassLoader(new PrologClassLoader(getClass().getClassLoader()));
+    return PrologMachineCopy.save(ctl);
+  }
+
+  @Override
+  protected void run() throws UnloggedFailure {
+    PushbackReader inReader = new PushbackReader(new InputStreamReader(in));
+
+    try {
+      PrologEnvironment pcl;
+
+      List<Change> changeList =
+          db.changes().byKey(new Change.Key(changeId)).toList();
+      if (changeList.size() != 1)
+        throw new UnloggedFailure(1, "Invalid ChangeId");
+
+      Change c = changeList.get(0);
+      PatchSet ps = db.patchSets().get(c.currentPatchSetId());
+      // Will throw exception if current user can not access this change, and
+      // thus will leak information that a change-id is valid even though the
+      // user are not allowed to see the change.
+      // See http://code.google.com/p/gerrit/issues/detail?id=1586
+      ChangeControl cc = ccFactory.controlFor(c);
+      ProjectState projectState = cc.getProjectControl().getProjectState();
+
+      if (useStdin) {
+        pcl = envFactory.create(newMachine());
+      } else {
+        pcl = projectState.newPrologEnvironment();
+      }
+
+      pcl.set(StoredValues.REVIEW_DB, db);
+      pcl.set(StoredValues.CHANGE, c);
+      pcl.set(StoredValues.PATCH_SET, ps);
+      pcl.set(StoredValues.CHANGE_CONTROL, cc);
+      if (useStdin) {
+        pcl.initialize(PACKAGE_LIST);
+        pcl.execute(Prolog.BUILTIN, "consult_stream",
+            SymbolTerm.intern("stdin"), new JavaObjectTerm(inReader));
+      }
+
+      List<Term> results = new ArrayList<Term>();
+      Term submitRule =
+          pcl.once("gerrit", "locate_submit_rule", new VariableTerm());
+
+      for (Term[] template : pcl.all("gerrit", "can_submit", submitRule,
+          new VariableTerm())) {
+        results.add(template[1]);
+      }
+
+      if (!skipSubmitFilters) {
+        runSubmitFilters(projectState, results, pcl);
+      }
+
+      List<SubmitRecord> res = cc.resultsToSubmitRecord(submitRule, results);
+      for (SubmitRecord r : res) {
+        if (format.isJson()) {
+          SubmitRecordAttribute submitRecord = new SubmitRecordAttribute();
+          submitRecord.status = r.status.name();
+
+          List<SubmitLabelAttribute> submitLabels = new LinkedList<SubmitLabelAttribute>();
+          for(SubmitRecord.Label l : r.labels) {
+            SubmitLabelAttribute label = new SubmitLabelAttribute();
+            label.label = l.label;
+            label.status= l.status.name();
+            if(l.appliedBy != null) {
+              Account a = accountCache.get(l.appliedBy).getAccount();
+              label.by = new AccountAttribute();
+              label.by.email = a.getPreferredEmail();
+              label.by.name = a.getFullName();
+              label.by.username = a.getUserName();
+            }
+            submitLabels.add(label);
+          }
+          submitRecord.labels = submitLabels;
+          format.newGson().toJson(submitRecord, new TypeToken<SubmitRecordAttribute>() {}.getType(), stdout);
+          stdout.print('\n');
+        } else {
+          for(SubmitRecord.Label l : r.labels) {
+            stdout.print(l.label + ": " + l.status);
+            if(l.appliedBy != null) {
+              AccountInfo a = new AccountInfo(accountCache.get(l.appliedBy).getAccount());
+              stdout.print(" by " + a.getNameEmail(anonymousCowardName));
+            }
+            stdout.print('\n');
+          }
+          stdout.print("\n" + r.status.name() + "\n");
+        }
+      }
+    } catch (Exception e) {
+      throw new UnloggedFailure("Processing of prolog script failed: " + e);
+    }
+  }
+
+  private void runSubmitFilters(ProjectState projectState, List<Term> results,
+      PrologEnvironment pcl) throws UnloggedFailure {
+    ProjectState parentState = projectState.getParentState();
+    PrologEnvironment childEnv = pcl;
+    Set<Project.NameKey> projectsSeen = new HashSet<Project.NameKey>();
+    projectsSeen.add(projectState.getProject().getNameKey());
+
+    while (parentState != null) {
+      if (!projectsSeen.add(parentState.getProject().getNameKey())) {
+        // parent has been seen before, stop walk up inheritance tree
+        break;
+      }
+      PrologEnvironment parentEnv;
+      try {
+        parentEnv = parentState.newPrologEnvironment();
+      } catch (CompileException err) {
+        throw new UnloggedFailure("Cannot consult rules.pl for "
+            + parentState.getProject().getName() + err);
+      }
+
+      parentEnv.copyStoredValues(childEnv);
+      Term filterRule =
+          parentEnv.once("gerrit", "locate_submit_filter", new VariableTerm());
+      if (filterRule != null) {
+        try {
+          Term resultsTerm = ChangeControl.toListTerm(results);
+          results.clear();
+          Term[] template =
+              parentEnv.once("gerrit", "filter_submit_results", filterRule,
+                  resultsTerm, new VariableTerm());
+          @SuppressWarnings("unchecked")
+          final List<? extends Term> termList =
+              ((ListTerm) template[2]).toJava();
+          results.addAll(termList);
+        } catch (PrologException err) {
+          throw new UnloggedFailure("Exception calling " + filterRule + " of "
+              + parentState.getProject().getName() + err);
+        } catch (RuntimeException err) {
+          throw new UnloggedFailure("Exception calling " + filterRule + " of "
+              + parentState.getProject().getName() + err);
+        }
+      }
+
+      parentState = parentState.getParentState();
+      childEnv = parentEnv;
+    }
+  }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/VersionCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/VersionCommand.java
index 001863b..addbb84 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/VersionCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/VersionCommand.java
@@ -15,29 +15,16 @@
 package com.google.gerrit.sshd.commands;
 
 import com.google.gerrit.common.Version;
-import com.google.gerrit.sshd.BaseCommand;
+import com.google.gerrit.sshd.SshCommand;
 
-import org.apache.sshd.server.Environment;
-
-import java.io.PrintWriter;
-
-final class VersionCommand extends BaseCommand {
+final class VersionCommand extends SshCommand {
   @Override
-  public void start(final Environment env) {
-    startThread(new CommandRunnable() {
-      @Override
-      public void run() throws Failure {
-        parseCommandLine();
+  protected void run() throws Failure {
+    String v = Version.getVersion();
+    if (v == null) {
+      throw new Failure(1, "fatal: version unavailable");
+    }
 
-        String v = Version.getVersion();
-        if (v == null) {
-          throw new Failure(1, "fatal: version unavailable");
-        }
-
-        final PrintWriter stdout = toPrintWriter(out);
-        stdout.println("gerrit version " + v);
-        stdout.flush();
-      }
-    });
+    stdout.println("gerrit version " + v);
   }
 }
diff --git a/gerrit-util-cli/.gitignore b/gerrit-util-cli/.gitignore
index 194bedc..35069e7 100644
--- a/gerrit-util-cli/.gitignore
+++ b/gerrit-util-cli/.gitignore
@@ -2,4 +2,5 @@
 /.classpath
 /.project
 /.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-util-cli.iml
\ No newline at end of file
diff --git a/gerrit-util-cli/.settings/org.eclipse.core.resources.prefs b/gerrit-util-cli/.settings/org.eclipse.core.resources.prefs
index c780f44..e9441bb 100644
--- a/gerrit-util-cli/.settings/org.eclipse.core.resources.prefs
+++ b/gerrit-util-cli/.settings/org.eclipse.core.resources.prefs
@@ -1,4 +1,3 @@
-#Thu Jul 28 11:02:36 PDT 2011
 eclipse.preferences.version=1
 encoding//src/main/java=UTF-8
 encoding/<project>=UTF-8
diff --git a/gerrit-util-cli/pom.xml b/gerrit-util-cli/pom.xml
index 4ecbda4..4886d09 100644
--- a/gerrit-util-cli/pom.xml
+++ b/gerrit-util-cli/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.4-SNAPSHOT</version>
+    <version>2.5-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-util-cli</artifactId>
diff --git a/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/CmdLineParser.java b/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/CmdLineParser.java
index de2f7e9..a9f5229 100644
--- a/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/CmdLineParser.java
+++ b/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/CmdLineParser.java
@@ -46,6 +46,7 @@
 import org.kohsuke.args4j.Option;
 import org.kohsuke.args4j.OptionDef;
 import org.kohsuke.args4j.spi.BooleanOptionHandler;
+import org.kohsuke.args4j.spi.EnumOptionHandler;
 import org.kohsuke.args4j.spi.OptionHandler;
 import org.kohsuke.args4j.spi.Setter;
 
@@ -53,9 +54,11 @@
 import java.io.Writer;
 import java.lang.annotation.Annotation;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.ResourceBundle;
+import java.util.Set;
 
 /**
  * Extended command line parser which handles --foo=value arguments.
@@ -66,7 +69,6 @@
  * args4j style format prior to invoking args4j for parsing.
  */
 public class CmdLineParser {
-
   public interface Factory {
     CmdLineParser create(Object bean);
   }
@@ -118,6 +120,67 @@
     out.write('\n');
   }
 
+  public void printQueryStringUsage(String name, StringWriter out) {
+    out.write(name);
+
+    char next = '?';
+    List<NamedOptionDef> booleans = new ArrayList<NamedOptionDef>();
+    for (@SuppressWarnings("rawtypes") OptionHandler handler : parser.options) {
+      if (handler.option instanceof NamedOptionDef) {
+        NamedOptionDef n = (NamedOptionDef) handler.option;
+
+        if (handler instanceof BooleanOptionHandler) {
+          booleans.add(n);
+          continue;
+        }
+
+        if (!n.required()) {
+          out.write('[');
+        }
+        out.write(next);
+        next = '&';
+        if (n.name().startsWith("--")) {
+          out.write(n.name().substring(2));
+        } else if (n.name().startsWith("-")) {
+          out.write(n.name().substring(1));
+        } else {
+          out.write(n.name());
+        }
+        out.write('=');
+
+        String var = handler.getDefaultMetaVariable();
+        if (handler instanceof EnumOptionHandler) {
+          var = var.substring(1, var.length() - 1);
+          var = var.replaceAll(" ", "");
+        }
+        out.write(var);
+        if (!n.required()) {
+          out.write(']');
+        }
+        if (n.isMultiValued()) {
+          out.write('*');
+        }
+      }
+    }
+    for (NamedOptionDef n : booleans) {
+      if (!n.required()) {
+        out.write('[');
+      }
+      out.write(next);
+      next = '&';
+      if (n.name().startsWith("--")) {
+        out.write(n.name().substring(2));
+      } else if (n.name().startsWith("-")) {
+        out.write(n.name().substring(1));
+      } else {
+        out.write(n.name());
+      }
+      if (!n.required()) {
+        out.write(']');
+      }
+    }
+  }
+
   public boolean wasHelpRequestedByOption() {
     return parser.help.value;
   }
@@ -148,6 +211,12 @@
 
   public void parseOptionMap(Map<String, String[]> parameters)
       throws CmdLineException {
+    parseOptionMap(parameters, Collections.<String>emptySet());
+  }
+
+  public void parseOptionMap(Map<String, String[]> parameters,
+      Set<String> argNames)
+      throws CmdLineException {
     ArrayList<String> tmp = new ArrayList<String>();
     for (Map.Entry<String, String[]> ent : parameters.entrySet()) {
       String name = ent.getKey();
@@ -169,7 +238,9 @@
         }
       } else {
         for (String value : ent.getValue()) {
-          tmp.add(name);
+          if (!argNames.contains(ent.getKey())) {
+            tmp.add(name);
+          }
           tmp.add(value);
         }
       }
diff --git a/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/OptionHandlerUtil.java b/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/OptionHandlerUtil.java
index 7af544c..1ea73cc 100644
--- a/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/OptionHandlerUtil.java
+++ b/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/OptionHandlerUtil.java
@@ -38,7 +38,6 @@
     return (Key<OptionHandler<T>>) Key.get(handlerType);
   }
 
-  @SuppressWarnings("unchecked")
   public static <T> Module moduleFor(final Class<T> type, Class<? extends OptionHandler<T>> impl) {
     return new FactoryModuleBuilder()
         .implement(handlerOf(type), impl)
diff --git a/gerrit-util-ssl/.gitignore b/gerrit-util-ssl/.gitignore
index 194bedc..e552ad5 100644
--- a/gerrit-util-ssl/.gitignore
+++ b/gerrit-util-ssl/.gitignore
@@ -2,4 +2,5 @@
 /.classpath
 /.project
 /.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-util-ssl.iml
\ No newline at end of file
diff --git a/gerrit-util-ssl/.settings/org.eclipse.core.resources.prefs b/gerrit-util-ssl/.settings/org.eclipse.core.resources.prefs
index 589908f..e9441bb 100644
--- a/gerrit-util-ssl/.settings/org.eclipse.core.resources.prefs
+++ b/gerrit-util-ssl/.settings/org.eclipse.core.resources.prefs
@@ -1,4 +1,3 @@
-#Thu Jul 28 11:02:35 PDT 2011
 eclipse.preferences.version=1
 encoding//src/main/java=UTF-8
 encoding/<project>=UTF-8
diff --git a/gerrit-util-ssl/pom.xml b/gerrit-util-ssl/pom.xml
index 2e49d47..beedb8f 100644
--- a/gerrit-util-ssl/pom.xml
+++ b/gerrit-util-ssl/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.4-SNAPSHOT</version>
+    <version>2.5-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-util-ssl</artifactId>
diff --git a/gerrit-war/.gitignore b/gerrit-war/.gitignore
index 194bedc..dc8c7ad 100644
--- a/gerrit-war/.gitignore
+++ b/gerrit-war/.gitignore
@@ -2,4 +2,5 @@
 /.classpath
 /.project
 /.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-war.iml
\ No newline at end of file
diff --git a/gerrit-war/.settings/org.eclipse.core.resources.prefs b/gerrit-war/.settings/org.eclipse.core.resources.prefs
index d404b00..abdea9ac 100644
--- a/gerrit-war/.settings/org.eclipse.core.resources.prefs
+++ b/gerrit-war/.settings/org.eclipse.core.resources.prefs
@@ -1,4 +1,3 @@
-#Thu Jul 28 11:02:37 PDT 2011
 eclipse.preferences.version=1
 encoding//src/main/java=UTF-8
 encoding//src/main/resources=UTF-8
diff --git a/gerrit-war/pom.xml b/gerrit-war/pom.xml
index 733d976a..1f3750e 100644
--- a/gerrit-war/pom.xml
+++ b/gerrit-war/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.4-SNAPSHOT</version>
+    <version>2.5-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-war</artifactId>
diff --git a/gerrit-war/src/main/java/com/google/gerrit/httpd/ReviewDbDataSourceProvider.java b/gerrit-war/src/main/java/com/google/gerrit/httpd/ReviewDbDataSourceProvider.java
index 233d53d..52467a0 100644
--- a/gerrit-war/src/main/java/com/google/gerrit/httpd/ReviewDbDataSourceProvider.java
+++ b/gerrit-war/src/main/java/com/google/gerrit/httpd/ReviewDbDataSourceProvider.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.httpd;
 
-import com.google.gerrit.lifecycle.LifecycleListener;
+import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.inject.Provider;
 import com.google.inject.ProvisionException;
 import com.google.inject.Singleton;
diff --git a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
index 01b4a44..1a556c2 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,11 +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;
@@ -32,11 +33,12 @@
 import com.google.gerrit.server.config.SitePath;
 import com.google.gerrit.server.contact.HttpContactStoreConnection;
 import com.google.gerrit.server.git.LocalDiskRepositoryManager;
-import com.google.gerrit.server.git.PushReplication;
 import com.google.gerrit.server.git.ReceiveCommitsExecutorModule;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.mail.SignedTokenEmailTokenVerifier;
 import com.google.gerrit.server.mail.SmtpEmailSender;
+import com.google.gerrit.server.plugins.PluginGuiceEnvironment;
+import com.google.gerrit.server.plugins.PluginModule;
 import com.google.gerrit.server.schema.DataSourceProvider;
 import com.google.gerrit.server.schema.DatabaseModule;
 import com.google.gerrit.server.schema.SchemaModule;
@@ -112,6 +114,11 @@
       sshInjector = createSshInjector();
       webInjector = createWebInjector();
 
+      PluginGuiceEnvironment env = sysInjector.getInstance(PluginGuiceEnvironment.class);
+      env.setCfgInjector(cfgInjector);
+      env.setSshInjector(sshInjector);
+      env.setHttpInjector(webInjector);
+
       // Push the Provider<HttpServletRequest> down into the canonical
       // URL provider. Its optional for that provider, but since we can
       // supply one we should do so, in case the administrator has not
@@ -193,10 +200,11 @@
     modules.add(new ChangeHookRunner.Module());
     modules.add(new ReceiveCommitsExecutorModule());
     modules.add(cfgInjector.getInstance(GerritGlobalModule.class));
-    modules.add(new EhcachePoolImpl.Module());
+    modules.add(new DefaultCacheFactory.Module());
     modules.add(new SmtpEmailSender.Module());
     modules.add(new SignedTokenEmailTokenVerifier.Module());
-    modules.add(new PushReplication.Module());
+    modules.add(new SignedTokenRestTokenVerifier.Module());
+    modules.add(new PluginModule());
     modules.add(new CanonicalWebUrlModule() {
       @Override
       protected Class<? extends Provider<String>> provider() {
@@ -209,18 +217,21 @@
 
   private Injector createSshInjector() {
     final List<Module> modules = new ArrayList<Module>();
-    modules.add(new SshModule());
+    modules.add(sysInjector.getInstance(SshModule.class));
     modules.add(new MasterCommandModule());
     return sysInjector.createChildInjector(modules);
   }
 
   private Injector createWebInjector() {
     final List<Module> modules = new ArrayList<Module>();
+    modules.add(RequestContextFilter.module());
+    modules.add(AllRequestFilter.module());
     modules.add(sysInjector.getInstance(GitOverHttpModule.class));
     modules.add(sshInjector.getInstance(WebModule.class));
     modules.add(sshInjector.getInstance(WebSshGlueModule.class));
     modules.add(CacheBasedWebSession.module());
     modules.add(HttpContactStoreConnection.module());
+    modules.add(new HttpPluginModule());
 
     AuthConfig authConfig = cfgInjector.getInstance(AuthConfig.class);
     if (authConfig.getAuthType() == AuthType.OPENID) {
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/gerrit-war/src/main/webapp/WEB-INF/extra/jetty7/gerrit.xml b/gerrit-war/src/main/webapp/WEB-INF/extra/jetty7/gerrit.xml
index 117bf61..3ae9440 100644
--- a/gerrit-war/src/main/webapp/WEB-INF/extra/jetty7/gerrit.xml
+++ b/gerrit-war/src/main/webapp/WEB-INF/extra/jetty7/gerrit.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE Configure PUBLIC "-//Mort Bay Consulting//DTD Configure//EN" "http://jetty.eclipse.org/configure.dtd">
+<!DOCTYPE Configure PUBLIC "-//Mort Bay Consulting//DTD Configure//EN" "http://www.eclipse.org/jetty/configure.dtd">
 <!--
 
   Jetty configuration to place "gerrit.war" into the root context,
diff --git a/gerrit-war/src/main/webapp/WEB-INF/extra/jetty7/jetty_sslproxy.xml b/gerrit-war/src/main/webapp/WEB-INF/extra/jetty7/jetty_sslproxy.xml
index 652acad..59cc040 100644
--- a/gerrit-war/src/main/webapp/WEB-INF/extra/jetty7/jetty_sslproxy.xml
+++ b/gerrit-war/src/main/webapp/WEB-INF/extra/jetty7/jetty_sslproxy.xml
@@ -1,5 +1,5 @@
 <?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE Configure PUBLIC "-//Mort Bay Consulting//DTD Configure//EN" "http://jetty.eclipse.org/configure.dtd">
+<!DOCTYPE Configure PUBLIC "-//Mort Bay Consulting//DTD Configure//EN" "http://www.eclipse.org/jetty/configure.dtd">
 <!--
 
   Jetty configuration to correctly handle SSL/HTTPS traffic when
diff --git a/pom.xml b/pom.xml
index c36da4e..754eae0 100644
--- a/pom.xml
+++ b/pom.xml
@@ -22,7 +22,7 @@
   <groupId>com.google.gerrit</groupId>
   <artifactId>gerrit-parent</artifactId>
   <packaging>pom</packaging>
-  <version>2.4-SNAPSHOT</version>
+  <version>2.5-SNAPSHOT</version>
 
   <name>Gerrit Code Review - Parent</name>
   <url>http://code.google.com/p/gerrit/</url>
@@ -46,11 +46,11 @@
   </issueManagement>
 
   <properties>
-    <jgitVersion>1.3.0.201202151440-r.76-g0135ed8</jgitVersion>
+    <jgitVersion>2.0.0.201206130900-r.24-g6cc51a5</jgitVersion>
     <gwtormVersion>1.4</gwtormVersion>
     <gwtjsonrpcVersion>1.3</gwtjsonrpcVersion>
-    <gwtexpuiVersion>1.2.5</gwtexpuiVersion>
-    <gwtVersion>2.3.0</gwtVersion>
+    <gwtexpuiVersion>1.2.6</gwtexpuiVersion>
+    <gwtVersion>2.4.0</gwtVersion>
     <slf4jVersion>1.6.1</slf4jVersion>
     <guiceVersion>3.0</guiceVersion>
     <jettyVersion>7.2.1.v20101111</jettyVersion>
@@ -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>
@@ -87,9 +87,27 @@
     <module>gerrit-gwtdebug</module>
     <module>gerrit-war</module>
 
+    <module>gerrit-extension-api</module>
+
     <module>gerrit-gwtui</module>
   </modules>
 
+  <profiles>
+    <profile>
+      <id>all</id>
+      <modules>
+        <module>gerrit-plugin-api</module>
+        <module>gerrit-plugin-archetype</module>
+      </modules>
+    </profile>
+    <profile>
+      <activation>
+        <activeByDefault>true</activeByDefault>
+      </activation>
+      <id>no-plugins</id>
+    </profile>
+  </profiles>
+
   <licenses>
     <license>
       <name>Apache License, 2.0</name>
@@ -333,7 +351,7 @@
         <plugin>
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-shade-plugin</artifactId>
-          <version>1.4</version>
+          <version>1.6</version>
         </plugin>
 
         <plugin>
@@ -363,7 +381,7 @@
         <plugin>
           <groupId>org.codehaus.mojo</groupId>
           <artifactId>gwt-maven-plugin</artifactId>
-          <version>2.3.0</version>
+          <version>2.4.0</version>
         </plugin>
 
         <plugin>
@@ -425,6 +443,14 @@
         </configuration>
       </plugin>
     </plugins>
+
+    <extensions>
+      <extension>
+        <groupId>net.anzix.aws</groupId>
+        <artifactId>s3-maven-wagon</artifactId>
+        <version>3.2</version>
+      </extension>
+    </extensions>
   </build>
 
   <dependencies>
@@ -444,6 +470,18 @@
   <dependencyManagement>
     <dependencies>
       <dependency>
+        <groupId>com.google.code.gson</groupId>
+        <artifactId>gson</artifactId>
+        <version>2.1</version>
+      </dependency>
+
+      <dependency>
+        <groupId>com.google.guava</groupId>
+        <artifactId>guava</artifactId>
+        <version>12.0.1</version>
+      </dependency>
+
+      <dependency>
         <groupId>gwtorm</groupId>
         <artifactId>gwtorm</artifactId>
         <version>${gwtormVersion}</version>
@@ -481,9 +519,8 @@
 
       <dependency>
         <groupId>org.openid4java</groupId>
-        <artifactId>openid4java-consumer</artifactId>
-        <version>0.9.6</version>
-        <type>pom</type>
+        <artifactId>openid4java</artifactId>
+        <version>0.9.8</version>
         <exclusions>
           <exclusion>
             <!-- conflicts with our use of guice 3.0 -->
@@ -518,6 +555,12 @@
       </dependency>
 
       <dependency>
+        <groupId>org.apache.mina</groupId>
+        <artifactId>mina-core</artifactId>
+        <version>2.0.5</version>
+      </dependency>
+
+      <dependency>
         <groupId>org.apache.sshd</groupId>
         <artifactId>sshd-core</artifactId>
         <version>0.5.1-r1095809</version>
@@ -536,12 +579,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>
@@ -805,13 +842,24 @@
         <artifactId>PrologCafe</artifactId>
         <version>1.3</version>
       </dependency>
+
+      <dependency>
+        <groupId>org.pegdown</groupId>
+        <artifactId>pegdown</artifactId>
+        <version>1.1.0</version>
+      </dependency>
     </dependencies>
   </dependencyManagement>
 
   <repositories>
     <repository>
-      <id>gerrit-maven-repository</id>
-      <url>https://gerrit-maven.storage.googleapis.com</url>
+      <id>gerrit-maven</id>
+      <url>https://gerrit-maven.commondatastorage.googleapis.com</url>
+    </repository>
+
+    <repository>
+      <id>jgit-repository</id>
+      <url>http://download.eclipse.org/jgit/maven</url>
     </repository>
 
     <repository>
@@ -820,11 +868,6 @@
     </repository>
 
     <repository>
-      <id>gson</id>
-      <url>https://google-gson.googlecode.com/svn/mavenrepo/</url>
-    </repository>
-
-    <repository>
       <id>objectweb-repository</id>
       <url>http://maven.objectweb.org/maven2/</url>
     </repository>
@@ -833,5 +876,10 @@
       <id>clojars-repo</id>
       <url>http://clojars.org/repo</url>
     </repository>
+
+    <repository>
+      <id>scala-tools</id>
+      <url>http://scala-tools.org/repo-releases</url>
+    </repository>
   </repositories>
 </project>
diff --git a/tools/deploy_api.sh b/tools/deploy_api.sh
new file mode 100755
index 0000000..e5909e54
--- /dev/null
+++ b/tools/deploy_api.sh
@@ -0,0 +1,60 @@
+#!/bin/sh
+
+set -e
+
+SRC=$(ls gerrit-plugin-api/target/gerrit-plugin-api-*-sources.jar)
+VER=${SRC#gerrit-plugin-api/target/gerrit-plugin-api-}
+VER=${VER%-sources.jar}
+
+type=release
+case $VER in
+*-SNAPSHOT)
+  echo >&2 "fatal: Cannot deploy $VER"
+  echo >&2 "       Use ./tools/version.sh --release && mvn clean package"
+  exit 1
+  ;;
+*-[0-9]*-g*) type=snapshot ;;
+esac
+URL=s3://gerrit-api@commondatastorage.googleapis.com/$type
+
+
+echo "Deploying $type gerrit-extension-api $VER"
+mvn deploy:deploy-file \
+  -DgroupId=com.google.gerrit \
+  -DartifactId=gerrit-extension-api \
+  -Dversion=$VER \
+  -Dpackaging=jar \
+  -Dfile=gerrit-extension-api/target/gerrit-extension-api-$VER-all.jar \
+  -DrepositoryId=gerrit-api-repository \
+  -Durl=$URL
+
+mvn deploy:deploy-file \
+  -DgroupId=com.google.gerrit \
+  -DartifactId=gerrit-extension-api \
+  -Dversion=$VER \
+  -Dpackaging=java-source \
+  -Dfile=gerrit-extension-api/target/gerrit-extension-api-$VER-all-sources.jar \
+  -Djava-source=false \
+  -DrepositoryId=gerrit-api-repository \
+  -Durl=$URL
+
+
+echo "Deploying $type gerrit-plugin-api $VER"
+mvn deploy:deploy-file \
+  -DgroupId=com.google.gerrit \
+  -DartifactId=gerrit-plugin-api \
+  -Dversion=$VER \
+  -Dpackaging=jar \
+  -Dfile=gerrit-plugin-api/target/gerrit-plugin-api-$VER.jar \
+  -DrepositoryId=gerrit-api-repository \
+  -Durl=$URL
+
+mvn deploy:deploy-file \
+  -DgroupId=com.google.gerrit \
+  -DartifactId=gerrit-plugin-api \
+  -Dversion=$VER \
+  -Dpackaging=java-source \
+  -Dfile=gerrit-plugin-api/target/gerrit-plugin-api-$VER-sources.jar \
+  -Djava-source=false \
+  -DrepositoryId=gerrit-api-repository \
+  -Durl=$URL
diff --git a/tools/gwtui_dbg.launch b/tools/gwtui_dbg.launch
index ea76ee1..f007da4 100644
--- a/tools/gwtui_dbg.launch
+++ b/tools/gwtui_dbg.launch
@@ -10,6 +10,10 @@
 <booleanAttribute key="org.eclipse.debug.core.appendEnvironmentVariables" value="true"/>
 <stringAttribute key="org.eclipse.debug.core.source_locator_id" value="org.eclipse.jdt.launching.sourceLocator.JavaSourceLookupDirector"/>
 <stringAttribute key="org.eclipse.debug.core.source_locator_memento" value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;sourceLookupDirector&gt;&#10;&lt;sourceContainers duplicates=&quot;false&quot;&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-common&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-httpd&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-patch-commonsnet&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-prettify&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-patch-jgit&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-pgm&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-server&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-sshd&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-util-cli&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-util-ssl&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-reviewdb&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-gwtui&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-gwtdebug&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;default/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.debug.core.containerType.default&quot;/&gt;&#10;&lt;/sourceContainers&gt;&#10;&lt;/sourceLookupDirector&gt;&#10;"/>
+<listAttribute key="org.eclipse.debug.ui.favoriteGroups">
+<listEntry value="org.eclipse.debug.ui.launchGroup.debug"/>
+<listEntry value="org.eclipse.debug.ui.launchGroup.run"/>
+</listAttribute>
 <booleanAttribute key="org.eclipse.jdt.debug.ui.CONSIDER_INHERITED_MAIN" value="true"/>
 <listAttribute key="org.eclipse.jdt.launching.CLASSPATH">
 <listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;runtimeClasspathEntry containerPath=&quot;org.eclipse.jdt.launching.JRE_CONTAINER/org.eclipse.jdt.internal.debug.ui.launcher.StandardVMType/JavaSE-1.6&quot; path=&quot;1&quot; type=&quot;4&quot;/&gt;&#10;"/>
@@ -32,5 +36,5 @@
 <stringAttribute key="org.eclipse.jdt.launching.PROGRAM_ARGUMENTS" value="-startupUrl /&#10;-war ${resource_loc:/gerrit-gwtui/target}/gwt-hosted-mode&#10;-server com.google.gerrit.gwtdebug.GerritDebugLauncher&#10;com.google.gerrit.GerritGwtUI"/>
 <stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="gerrit-gwtdebug"/>
 <stringAttribute key="org.eclipse.jdt.launching.SOURCE_PATH_PROVIDER" value="org.eclipse.m2e.launchconfig.sourcepathProvider"/>
-<stringAttribute key="org.eclipse.jdt.launching.VM_ARGUMENTS" value="-Xmx256M&#10;&#10;-Dgerrit.site_path=${resource_loc:/gerrit-parent}/../test_site"/>
+<stringAttribute key="org.eclipse.jdt.launching.VM_ARGUMENTS" value="-Xmx256M&#10;-da:com.google.gwtexpui.globalkey.client.KeyCommandSet&#10;&#10;-Dgerrit.site_path=${resource_loc:/gerrit-parent}/../test_site"/>
 </launchConfiguration>
diff --git a/tools/pgm_daemon.launch b/tools/pgm_daemon.launch
index fd7b50f..cedf470 100644
--- a/tools/pgm_daemon.launch
+++ b/tools/pgm_daemon.launch
@@ -10,6 +10,10 @@
 <booleanAttribute key="org.eclipse.debug.core.appendEnvironmentVariables" value="true"/>
 <stringAttribute key="org.eclipse.debug.core.source_locator_id" value="org.eclipse.jdt.launching.sourceLocator.JavaSourceLookupDirector"/>
 <stringAttribute key="org.eclipse.debug.core.source_locator_memento" value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;sourceLookupDirector&gt;&#10;&lt;sourceContainers duplicates=&quot;false&quot;&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-common&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-httpd&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-patch-commonsnet&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-patch-jgit&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-pgm&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-server&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-sshd&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-util-cli&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-util-ssl&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-war&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-reviewdb&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-main&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;default/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.debug.core.containerType.default&quot;/&gt;&#10;&lt;/sourceContainers&gt;&#10;&lt;/sourceLookupDirector&gt;&#10;"/>
+<listAttribute key="org.eclipse.debug.ui.favoriteGroups">
+<listEntry value="org.eclipse.debug.ui.launchGroup.debug"/>
+<listEntry value="org.eclipse.debug.ui.launchGroup.run"/>
+</listAttribute>
 <booleanAttribute key="org.eclipse.jdt.debug.ui.CONSIDER_INHERITED_MAIN" value="true"/>
 <stringAttribute key="org.eclipse.jdt.launching.CLASSPATH_PROVIDER" value="org.eclipse.m2e.launchconfig.classpathProvider"/>
 <stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value="Main"/>
diff --git a/tools/release.sh b/tools/release.sh
index 4a872a1..de18357 100755
--- a/tools/release.sh
+++ b/tools/release.sh
@@ -25,7 +25,7 @@
 fi
 
 ./tools/version.sh --release &&
-mvn clean package $include_docs
+mvn clean install $include_docs -P all
 rc=$?
 ./tools/version.sh --reset
 
diff --git a/tools/version.sh b/tools/version.sh
index c3d9417..d3e4cd5 100755
--- a/tools/version.sh
+++ b/tools/version.sh
@@ -6,7 +6,7 @@
 # Java based Maven plugin so its fully portable.
 #
 
-POM_FILES=$(git ls-files | grep pom.xml)
+POM_FILES=$(git ls-files | grep pom.xml | grep -v gerrit-plugin-archetype/src/main/resources/archetype-resources/pom.xml)
 
 case "$1" in
 --snapshot=*)