Merge 'v2.2.2.2'

* v2.2.2.2:
  Release notes for 2.2.2.2
  Fix permissions bug caused by directly inheriting from All-Projects

Change-Id: Id0a3c91ae300cb7ae177d45578021701ec189f2a
diff --git a/.gitignore b/.gitignore
index 0476c1d..f318b65 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,6 @@
 /.classpath
 /.project
+/.settings
 /.settings/org.eclipse.jdt.core.prefs
 /.settings/org.maven.ide.eclipse.prefs
 /test_site
diff --git a/Documentation/access-control.txt b/Documentation/access-control.txt
index 8e93623..d2078ab 100644
--- a/Documentation/access-control.txt
+++ b/Documentation/access-control.txt
@@ -712,6 +712,7 @@
 `Code Review`, above) must enable submit, and also must not block it.
 See above for details on each category.
 
+
 [[category_makeoneup]]
 Your Category Here
 ~~~~~~~~~~~~~~~~~~
@@ -779,6 +780,183 @@
 and `-1 Do not have copyright` will block submit, while `+1 Copyright
 clear` is required to enable submit.
 
+[[restart_changes]]
+[NOTE]
+Restart the Gerrit web application and reload all browsers after
+making any database changes to approval categories.  Browsers are
+sent the list of known categories when they first visit the site,
+and don't notice changes until the page is closed and opened again,
+or is reloaded.
+
+
+Examples of typical roles in a project
+--------------------------------------
+
+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
+brand new Gerrit instance.
+
+[[examples_contributor]]
+Contributor
+~~~~~~~~~~~
+
+This is the typical user on a public server. They are able to read
+your project and upload new changes to it. They are able to give
+feedback on other changes as well, but are unable to block or approve
+any changes.
+
+Suggested access rights to grant:
+
+* <<category_read,`Read`>> on 'refs/heads/\*' and 'refs/tags/*'
+* <<category_push,`Push`>> to 'refs/for/refs/heads/\*' and 'refs/changes/*'
+* <<category_label-Code-Review,`Code review`>> with range '-1' to '+1'
+
+
+[[examples_developer]]
+Developer
+~~~~~~~~~
+
+This is the typical core developer on a public server.  They are able
+to read the project, upload changes to a branch.  They are allowed to
+push merge commits to merge branches together.  Also, they are allowed
+to forge author identity, thus handling commits belonging to others
+than themselves, effectively allowing them to transfer commits
+between different branches.
+
+They are furthermore able to code review and verify commits, and
+eventually submit them.  If you have an automated CI system that
+builds all uploaded patch sets you might want to skip the
+verification rights for the developer and let the CI system do that
+exclusively.
+
+Suggested access rights to grant:
+
+* <<category_read,`Read`>> on 'refs/heads/\*' and 'refs/tags/*'
+* <<category_push,`Push`>> to 'refs/for/refs/heads/\*' and 'refs/changes/*'
+* <<category_push_merge,`Push merge commit`>> to 'refs/for/refs/heads/\*' and 'refs/changes/*'
+* <<category_forge_author,`Forge Author Identity`>>
+* <<category_label-Code-Review,`Label: Code review`>> with range '-2' to '+2'
+* <<category_label-Verified,`Label: Verify`>> with range '-1' to '+1'
+* <<category_submit,`Submit`>>
+
+If the project is small or the developers are seasoned it might make
+sense to give them the freedom to push commits directly to a branch.
+
+Optional access rights to grant:
+
+* <<category_push,`Push`>> to 'refs/heads/*'
+* <<category_push_merge,`Push merge commit`>> to 'refs/heads/*'
+
+
+[[examples_cisystem]]
+CI system
+~~~~~~~~~
+
+A typical Continous Integration system should be able to download new changes
+to build and then leave a verdict somehow.
+
+As an example, the popular
+link:https://wiki.jenkins-ci.org/display/JENKINS/Gerrit+Trigger[gerrit-trigger plugin]
+for Jenkins/Hudson can set labels at:
+
+* The start of a build
+* A successful build
+* An unstable build (tests fails)
+* A failed build
+
+Usually the range chosen for this verdict is the verify label.  Depending on
+the size of your project and discipline of involved developers you might want
+to limit access right to the +1 `Verify` label to the CI system only.  That
+way it's guaranteed that submitted commits always get built and pass tests
+successfully.
+
+If the build doesn't complete successfully the CI system can set the
+`Verify` label to -1.  However that means that a failed build will block
+submit of the change even if someone else sets `Verify` +1.  Depending on the
+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
+possible to set `Code-review` +1 as well.
+
+If pushing new changes is granted, it's possible to automate cherry-pick of
+submitted changes for upload to other branches under certain conditions.  This
+is probably not the first step of what a project wants to automate however,
+and so the push right can be found under the optional section.
+
+Suggested access rights to grant, that won't block changes:
+
+* <<category_read,`Read`>> on 'refs/heads/\*' and 'refs/tags/*'
+* <<category_label-Code-Review,`Label: Code review`>> with range '-1' to '0'
+* <<category_label-Verified,`Label: Verify`>> with range '0' to '+1'
+
+Optional access rights to grant:
+
+* <<category_label-Code-Review,`Label: Code review`>> with range '-1' to '+1'
+* <<category_push,`Push`>> to 'refs/for/refs/heads/\*' and 'refs/changes/*'
+
+
+[[examples_integrator]]
+Integrator
+~~~~~~~~~~
+
+Integrators are like developers but with some additional rights granted due
+to their administrative role in a project.  They can upload or push any commit
+with any committer email (not just their own) and they can also create new
+tags and branches.
+
+Suggested access rights to grant:
+
+* <<examples_developer,Developer rights>>
+* <<category_push,`Push`>> to 'refs/heads/*'
+* <<category_push_merge,`Push merge commit`>> to 'refs/heads/*'
+* <<category_forge_committer,`Forge Committer Identity`>> to 'refs/for/refs/heads/\*' and 'refs/changes/*'
+* <<category_create,`Create Reference`>> to 'refs/heads/*'
+* <<category_push_annotated,`Push Annotated Tag`>> to 'refs/tags/*'
+
+
+[[examples_project-owner]]
+Project owner
+~~~~~~~~~~~~~
+
+The project owner is almost like an integrator but with additional destructive
+power in the form of being able to delete branches.  Optionally these users
+also have the power to configure access rights in gits assigned to them.
+
+[WARNING]
+These users should be really knowledgable about git, for instance knowing why
+tags never should be removed from a server.  This role is granted potentially
+destructive access rights and cleaning up after such a mishap could be time
+consuming!
+
+Suggested access rights to grant:
+
+* <<examples_integrator,Integrator rights>>
+* <<category_push,`Push`>> with the force option to 'refs/heads/\*' and 'refs/tags/*'
+
+Optional access right to grant:
+
+* <<category_owner,`Owner`>> in the gits they mostly work with.
+
+[[examples_administrator]]
+Administrator
+~~~~~~~~~~~~~
+
+The administrator role is the most powerful role known in the Gerrit universe.
+This role may grant itself (or others) any access right, and it already has
+all capabilities granted as well.  By default the
+<<administrators,`Administrators` group>> is the group that has this role.
+
+Mandatory access rights:
+
+* <<capability_administrateServer,The `Administrate Server` capability>>
+
+Suggested access rights to grant:
+
+* <<examples_project-owner,Project owner rights>>
+
 
 [[conversion_table]]
 Conversion table from 2.1.x series to 2.2.x series
@@ -815,13 +993,163 @@
 on `refs/for/refs/heads/<branch>` rather than permissions to upload changes
 on `refs/heads/<branch>`.
 
-[[restart_changes]]
+
+System capabilities
+-------------------
+
+The system capabilities control actions that the administrators of
+the server can perform which usually affect the entire
+server in some way.  The administrators may delegate these
+capabilities to trusted groups of users.
+
+Delegation of capabilities allows groups to be granted a subset of
+administrative capabilities without being given complete
+administrative control of the server.  This makes it possible to
+keep fewer users in the administrators group, even while spreading
+much of the server administration burden out to more users.
+
+Below you find a list of capabilities available:
+
+
+[[capability_administrateServer]]
+Administrate Server
+~~~~~~~~~~~~~~~~~~~
+
+This is in effect the owner and administrator role of the Gerrit
+instance.  Any members of a group granted this capability will be
+able to grant any access right to any group. They will also have all
+capabilities granted to them automatically.
+
+
+[[capability_createAccount]]
+Create Account
+~~~~~~~~~~~~~~
+
+Allow link:cmd-create-account.html[account creation over the ssh prompt].
+This capability allows the granted group members to create non-interactive
+service accounts.  These service accounts are generally used for automation
+and made to be members of the
+link:access-control.html#non-interactive_users['Non-Interactive users'] group.
+
+
+[[capability_createGroup]]
+Create Group
+~~~~~~~~~~~~
+
+Allow group creation.  Groups are used to grant users access to different
+actions in projects.  This capability allows the granted group members to
+either link:cmd-create-group.html[create new groups via ssh] or via the web UI.
+
+
+[[capability_createProject]]
+Create Project
+~~~~~~~~~~~~~~
+
+Allow project creation.  This capability allows the granted group to
+either link:cmd-create-project.html[create new git projects via ssh]
+or via the web UI.
+
+
+[[capability_flushCaches]]
+Flush Caches
+~~~~~~~~~~~~
+
+Allow the flushing of Gerrit's caches.  This capability allows the granted
+group to link:cmd-flush-caches.html[flush some or all Gerrit caches via ssh].
+
 [NOTE]
-Restart the Gerrit web application and reload all browsers after
-making any database changes to approval categories.  Browsers are
-sent the list of known categories when they first visit the site,
-and don't notice changes until the page is closed and opened again,
-or is reloaded.
+This capability doesn't imply permissions to the show-caches command.  For that
+you need the <<capability_viewCaches,view caches capability>>.
+
+
+[[capability_kill]]
+Kill Task
+~~~~~~~~~
+
+Allow the operation of the link:cmd-kill.html[kill command over ssh].  The
+kill command ends tasks that currently occupy the Gerrit server, usually
+a replication task or a user initiated task such as an upload-pack or
+recieve-pack.
+
+
+[[capability_priority]]
+Priority
+~~~~~~~~
+
+This capability allows users to use
+link:config-gerrit.html#sshd.batchThreads[the thread pool reserved] for
+link:access-control.html#non-interactive_users['Non-Interactive Users'].
+It's a binary value in that granted users either have access to the thread
+pool, or they don't.
+
+There are three modes for this capability and they're listed by rising
+priority:
+
+No capability configured.::
+The user isn't a member of a group with any priority capability granted. By
+default the user is then in the 'INTERACTIVE' thread pool.
+
+'BATCH'::
+If there's a thread pool configured for 'Non-Interactive Users' and a user is
+granted the priority capability with the 'BATCH' mode selected, the user ends
+up in the separate batch user thread pool. This is true unless the user is
+also granted the below 'INTERACTIVE' option.
+
+'INTERACTIVE'::
+If a user is granted the priority capability with the 'INTERACTIVE' option,
+regardless if they also have the 'BATCH' option or not, they are in the
+'INTERACTIVE' thread pool.
+
+
+[[capability_queryLimit]]
+Query Limit
+~~~~~~~~~~~
+
+Allow site administrators to configure the query limit for users to
+be above the default hard-coded value of 500.  Administrators can add
+a global block to `All-Projects` with group(s) that
+should have different limits:
+
+When applying a query limit to a user the largest value granted by
+any of their groups is used.
+
+This limit applies not only to the link:cmd-query.html[`gerrit query`]
+command, but also to the web UI results pagination size.
+
+
+[[capability_startReplication]]
+Start Replication
+~~~~~~~~~~~~~~~~~
+
+Allow access to execute link:cmd-replicate.html[the `gerrit replicate` command].
+
+
+[[capability_viewCaches]]
+View Caches
+~~~~~~~~~~~
+
+Allow querying for status of Gerrit's internal caches.  This capability allows
+the granted group to
+link:cmd-show-caches.html[look at some or all Gerrit caches via ssh].
+
+
+[[capability_viewConnections]]
+View Connections
+~~~~~~~~~~~~~~~~
+
+Allow querying for status of Gerrit's current client connections.  This
+capability allows the granted group to
+link:cmd-show-connections.html[look at Gerrit's current connections via ssh].
+
+
+[[capability_viewQueue]]
+View Queue
+~~~~~~~~~~
+
+Allow querying for status of Gerrit's internal task queue.  This capability
+allows the granted group to
+link:cmd-show-queue.html[look at the Gerrit task queue via ssh].
+
 
 GERRIT
 ------
diff --git a/Documentation/cmd-create-account.txt b/Documentation/cmd-create-account.txt
index 31bc482..98f950f 100644
--- a/Documentation/cmd-create-account.txt
+++ b/Documentation/cmd-create-account.txt
@@ -28,7 +28,8 @@
 ACCESS
 ------
 Caller must be a member of the privileged 'Administrators' group,
-or have been granted the 'Create Account' global capability.
+or have been granted
+link:access-control.html#capability_createAccount[the 'Create Account' global capability].
 
 SCRIPTING
 ---------
diff --git a/Documentation/cmd-create-group.txt b/Documentation/cmd-create-group.txt
index 1a6c168..475d2c5 100644
--- a/Documentation/cmd-create-group.txt
+++ b/Documentation/cmd-create-group.txt
@@ -13,6 +13,7 @@
   [--description <DESC>]
   [--member <USERNAME>]
   [--group <GROUP>]
+  [--visible-to-all]
   <GROUP>
 
 DESCRIPTION
@@ -27,7 +28,8 @@
 ACCESS
 ------
 Caller must be a member of the privileged 'Administrators' group,
-or have been granted the 'Create Group' global capability.
+or have been granted
+link:access-control.html#capability_createGroup[the 'Create Group' global capability].
 
 SCRIPTING
 ---------
@@ -56,6 +58,9 @@
 	Group name to include in the group.  Multiple --group options may
 	be specified to include more initial groups.
 
+--visible-to-all::
+	If specified, the group members will be visible to all users.
+
 EXAMPLES
 --------
 Create a new account group called `gerritdev` with two initial members
diff --git a/Documentation/cmd-create-project.txt b/Documentation/cmd-create-project.txt
index 9d2b98b..f22141c 100644
--- a/Documentation/cmd-create-project.txt
+++ b/Documentation/cmd-create-project.txt
@@ -11,6 +11,7 @@
 'ssh' -p <port> <host> 'gerrit create-project'
   [--owner <GROUP> ... | -o <GROUP> ...]
   [--parent <NAME> | -p <NAME> ]
+  [--suggest-parents | -S ]
   [--permissions-only]
   [--description <DESC> | -d <DESC>]
   [--submit-type <TYPE> |  -t <TYPE>]
@@ -38,7 +39,8 @@
 ACCESS
 ------
 Caller must be a member of the privileged 'Administrators' group,
-or have been granted the 'Create Project' global capability.
+or have been granted
+link:access-control.html#capability_createProject[the 'Create Project' global capability].
 
 SCRIPTING
 ---------
@@ -75,6 +77,13 @@
 	through. If not specified, the parent is set to the default
 	project `All-Projects`.
 
+--suggest-parents::
+-S::
+	Suggest parent candidates. This option cannot be used with
+	other arguments. Print out a list of projects that are
+	already parents to other projects, thus it can help the user
+	find a suitable parent for the new project.
+
 --permissions-only::
 	Create the project only to serve as a parent for other
 	projects.  The new project's Git repository will be
diff --git a/Documentation/cmd-flush-caches.txt b/Documentation/cmd-flush-caches.txt
index f8882c8..bc6fac5 100644
--- a/Documentation/cmd-flush-caches.txt
+++ b/Documentation/cmd-flush-caches.txt
@@ -26,7 +26,8 @@
 ACCESS
 ------
 Caller must be a member of the privileged 'Administrators' group,
-or have granted the 'Flush Caches' global capability.
+or in a group that have been granted
+link:access-control.html#capability_flushCaches[the 'Flush Caches' global capability].
 
 SCRIPTING
 ---------
diff --git a/Documentation/cmd-index.txt b/Documentation/cmd-index.txt
index dc4e72e..b09c3b3 100644
--- a/Documentation/cmd-index.txt
+++ b/Documentation/cmd-index.txt
@@ -54,9 +54,15 @@
 'gerrit approve'::
 	'Deprecated alias for `gerrit review`.'
 
+link:cmd-ls-groups.html[gerrit ls-groups]::
+	List groups visible to the caller.
+
 link:cmd-ls-projects.html[gerrit ls-projects]::
 	List projects visible to the caller.
 
+link:cmd-rename-group.html[gerrit rename-group]::
+	Rename an account group.
+
 link:cmd-set-reviewers.html[gerrit set-reviewers]::
         Add or remove reviewers on a change.
 
@@ -64,13 +70,13 @@
 	Query the change database.
 
 'gerrit receive-pack'::
-	'Depreated alias for `git receive-pack`.'
+	'Deprecated alias for `git receive-pack`.'
 
 link:cmd-review.html[gerrit review]::
 	Verify, approve and/or submit a patch set from the command line.
 
 link:cmd-stream-events.html[gerrit stream-events]::
-	Monitor events occuring in real time.
+	Monitor events occurring in real time.
 
 git upload-pack::
 	Standard Git server side command for client side `git fetch`.
@@ -81,7 +87,7 @@
 Also implements the magic associated with uploading commits for
 review.  See link:user-upload.html#push_create[Creating Changes].
 
-[[admin_commands]]Adminstrator Commands
+[[admin_commands]]Administrator Commands
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 link:cmd-create-account.html[gerrit create-account]::
diff --git a/Documentation/cmd-kill.txt b/Documentation/cmd-kill.txt
index 57a98f7..f09053e 100644
--- a/Documentation/cmd-kill.txt
+++ b/Documentation/cmd-kill.txt
@@ -19,7 +19,7 @@
 ACCESS
 ------
 Caller must be a member of the privileged 'Administrators' group,
-or have been granted the 'Kill Task' global capability.
+or have been granted link:access-control.html#capability_kill[the 'Kill Task' global capability].
 
 SCRIPTING
 ---------
diff --git a/Documentation/cmd-ls-groups.txt b/Documentation/cmd-ls-groups.txt
new file mode 100644
index 0000000..8564db2
--- /dev/null
+++ b/Documentation/cmd-ls-groups.txt
@@ -0,0 +1,96 @@
+gerrit ls-groups
+================
+
+NAME
+----
+gerrit ls-groups - List groups visible to caller
+
+SYNOPSIS
+--------
+[verse]
+'ssh' -p <port> <host> 'gerrit ls-groups'
+  [--project <NAME>]
+  [--user <NAME>]
+  [--visible-to-all]
+  [--type {internal | ldap | system}]
+
+DESCRIPTION
+-----------
+Displays the list of group names, one per line, that are visible to
+the account of the calling user.
+
+If the caller is a member of the privileged 'Administrators' group,
+all groups are listed.
+
+ACCESS
+------
+Any user who has configured an SSH key.
+
+SCRIPTING
+---------
+This command is intended to be used in scripts.
+
+OPTIONS
+-------
+--project::
+-p::
+	Name of the project for which the groups should be listed. Only
+	groups are listed for which any permission is set on this project
+	(or for which a permission is inherited from a parent project).
+	Multiple --project options may be specified to specify additional
+	projects. In this case all groups are listed that have a
+	permission for any of the specified projects.
++
+This option can't be used together with the '--user' option.
+
+--user::
+-u::
+	User for which the groups should be listed. Only groups are
+	listed that contain this user as a member.
++
+The calling user can list the groups for the own user or must be a
+member of the privileged 'Administrators' group to list the groups
+for other users.
++
+This option can't be used together with the '--project' option.
+
+--visible-to-all::
+	Displays only groups that are visible to all registered users
+	(groups that are explicitly marked as visible to all registered
+	users).
+
+--type::
+	Display only groups of the specified type. If not specified,
+	groups of all types are displayed. Supported types:
++
+--
+`internal`:: Any group defined within Gerrit.
+`ldap`:: Any group defined by an external LDAP database.
+`system`:: Any system defined and managed group.
+--
+
+EXAMPLES
+--------
+
+List visible groups:
+=====
+	$ ssh -p 29418 review.example.com gerrit ls-groups
+	Administrators
+	Anonymous Users
+	MyProject_Committers
+	Project Owners
+	Registered Users
+=====
+
+List all groups for which any permission is set for the project
+"MyProject":
+=====
+	$ ssh -p 29418 review.example.com gerrit ls-groups --project MyProject
+	MyProject_Committers
+	Project Owners
+	Registered Users
+=====
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/cmd-ls-projects.txt b/Documentation/cmd-ls-projects.txt
index 0332c69..b6e67c4 100644
--- a/Documentation/cmd-ls-projects.txt
+++ b/Documentation/cmd-ls-projects.txt
@@ -41,6 +41,14 @@
 	If the user does not have access to any branch in the project then the
 	whole project is not shown.
 
+--description::
+--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.
+
 --tree::
 -t::
 	Displays project inheritance in a tree-like format.
@@ -49,6 +57,14 @@
 --type::
 	Display only projects of the specified type.  If not
 	specified, defaults to `code`. Supported types:
+
+--all::
+	Display all projects that are accessible by the calling user
+	account. Besides the projects that the calling user account has
+	been granted 'READ' access to, this includes all projects that
+	are owned by the calling user account (even if for these projects
+	the 'READ' access right is not assigned to the calling user
+	account).
 +
 --
 `code`:: Any project likely to contain user files.
diff --git a/Documentation/cmd-query.txt b/Documentation/cmd-query.txt
index b20ac92..1bd4862 100644
--- a/Documentation/cmd-query.txt
+++ b/Documentation/cmd-query.txt
@@ -12,7 +12,9 @@
   [--format {TEXT | JSON}]
   [--current-patch-set]
   [--patch-sets | --all-approvals]
+  [--files]
   [--comments]
+  [--commit-message]
   [--]
   <query>
   [limit:<n>]
@@ -59,11 +61,20 @@
 	the --current-patch-set flag then the current patch set
 	information will be output twice, once in each field.
 
+--files::
+	Support for listing files with patch sets and their
+	attributes (ADDED, MODIFIED, DELETED, RENAMED, COPIED).
+	Note that this option requires either the --current-patch-set
+	or the --patch-sets option in order to give any file information.
+
 --comments::
 	Include comments for all changes. If combined with the
 	--patch-sets flag then all in-line comments are included for
 	each patch set.
 
+--commit-message::
+	Include the full commit message in the change description.
+
 limit:<n>::
 	Maximum number of results to return.  This is actually a
 	query operator, and not a command line option.	If more
diff --git a/Documentation/cmd-rename-group.txt b/Documentation/cmd-rename-group.txt
new file mode 100644
index 0000000..e810727
--- /dev/null
+++ b/Documentation/cmd-rename-group.txt
@@ -0,0 +1,46 @@
+gerrit rename-group
+===================
+
+NAME
+----
+gerrit rename-group - Rename an account group.
+
+SYNOPSIS
+--------
+[verse]
+'ssh' -p <port> <host> 'gerrit rename-group'
+  <GROUP>
+  <NEWNAME>
+
+DESCRIPTION
+-----------
+Renames an account group.
+
+ACCESS
+------
+Caller must be a member of the group owning the group to be renamed
+or be a member of the privileged 'Administrators' group.
+
+SCRIPTING
+---------
+This command is intended to be used in scripts.
+
+OPTIONS
+-------
+<GROUP>::
+	Required; name of the group to be renamed.
+
+<NEWNAME>::
+	Required; new name of the group.
+
+EXAMPLES
+--------
+Rename the group "MyGroup" to "MyCommitters".
+
+====
+	$ ssh -p 29418 user@review.example.com gerrit rename-group MyGroup MyCommitters
+====
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/cmd-replicate.txt b/Documentation/cmd-replicate.txt
index b4ffb0e..7722027 100644
--- a/Documentation/cmd-replicate.txt
+++ b/Documentation/cmd-replicate.txt
@@ -53,7 +53,8 @@
 ACCESS
 ------
 Caller must be a member of the privileged 'Administrators' group,
-or have been granted the 'Start Replication' global capability.
+or have been granted
+link:access-control.html#capability_startReplication[the 'Start Replication' global capability].
 
 SCRIPTING
 ---------
diff --git a/Documentation/cmd-review.txt b/Documentation/cmd-review.txt
index f174034..ac613e5 100644
--- a/Documentation/cmd-review.txt
+++ b/Documentation/cmd-review.txt
@@ -11,8 +11,11 @@
 'ssh' -p <port> <host> 'gerrit review'
   [--project <PROJECT>]
   [--message <MESSAGE>]
+  [--force-message]
   [--submit]
   [--abandon | --restore]
+  [--publish]
+  [--delete]
   [--verified <N>] [--code-review <N>]
   {COMMIT | CHANGEID,PATCHSET}...
 
@@ -47,6 +50,19 @@
 	Optional cover letter to include as part of the message
 	sent to reviewers when the approval states are updated.
 
+--force-message::
+	Option which allows Gerrit to publish the --message, even
+	when the labels could not be applied due to 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
+successfully, even if the label could not be changed.
+
 --help::
 -h::
 	Display site-specific usage information, including the
@@ -57,7 +73,7 @@
 	(option is mutually exclusive with --submit and --restore)
 
 --restore::
-	Restore the specified abandonned patch set(s).
+	Restore the specified abandoned patch set(s).
 	(option is mutually exclusive with --abandon)
 
 --submit::
@@ -65,6 +81,14 @@
 	Submit the specified patch set(s) for merging.
 	(option is mutually exclusive with --abandon)
 
+--publish::
+	Publish the specified draft patch set(s).
+	(option is mutually exclusive with --submit, --restore, --abandon, and --delete)
+
+--delete::
+	Delete the specified draft patch set(s).
+	(option is mutually exclusive with --submit, --restore, --abandon, and --publish)
+
 --code-review::
 --verified::
 	Set the approval category to the value 'N'.  The exact
diff --git a/Documentation/cmd-set-project-parent.txt b/Documentation/cmd-set-project-parent.txt
index 9dd11d7..1e7e6c5 100644
--- a/Documentation/cmd-set-project-parent.txt
+++ b/Documentation/cmd-set-project-parent.txt
@@ -10,6 +10,8 @@
 [verse]
 'ssh' -p <port> <host> 'gerrit set-project-parent'
   [--parent <NAME>]
+  [--children-of <NAME>]
+  [--exclude <NAME>]
   <NAME> ...
 
 DESCRIPTION
@@ -33,6 +35,19 @@
 	Name of the parent to inherit through. If not specified,
 	the parent is set back to the default `All-Projects`.
 
+--children-of::
+	Name of the parent project for which all child projects should be
+	reparented. If the new parent project or any project in its
+	parent line is a child of this parent project it is automatically
+	excluded from reparenting.
+
+--exclude::
+	Name of a child project that should not be reparented. This
+	option can only be used if the option --children-of is set.
+	Multiple child projects can be excluded from reparenting by
+	specifying the --exclude option multiple times. Excluding a
+	project that is not a child project has no effect.
+
 EXAMPLES
 --------
 Configure `kernel/omap` to inherit permissions from `kernel/common`:
@@ -41,6 +56,13 @@
 	$ ssh -p 29418 review.example.com gerrit set-project-parent --parent kernel/common kernel/omap
 ====
 
+Reparent all children of `myParent` to `myOtherParent`:
+
+====
+	$ ssh -p 29418 review.example.com gerrit set-project-parent \
+	  --children-of myParent --parent myOtherParent
+====
+
 SEE ALSO
 --------
 
diff --git a/Documentation/cmd-show-caches.txt b/Documentation/cmd-show-caches.txt
index a841fc1..126b2a0 100644
--- a/Documentation/cmd-show-caches.txt
+++ b/Documentation/cmd-show-caches.txt
@@ -28,7 +28,8 @@
 ACCESS
 ------
 Caller must be a member of the privileged 'Administrators' group,
-or have been granted the 'View Caches' global capability.
+or have been granted
+link:access-control.html#capability_viewCaches[the 'View Caches' global capability].
 
 SCRIPTING
 ---------
diff --git a/Documentation/cmd-show-connections.txt b/Documentation/cmd-show-connections.txt
index 16cd5b4..b5d41bd 100644
--- a/Documentation/cmd-show-connections.txt
+++ b/Documentation/cmd-show-connections.txt
@@ -19,7 +19,8 @@
 ACCESS
 ------
 Caller must be a member of the privileged 'Administrators' group,
-or have been granted the 'View Connections' global capability.
+or have been granted
+link:access-control.html#capability_viewConnections[the 'View Connections' global capability].
 
 SCRIPTING
 ---------
diff --git a/Documentation/cmd-show-queue.txt b/Documentation/cmd-show-queue.txt
index 98a954e..f99e342 100644
--- a/Documentation/cmd-show-queue.txt
+++ b/Documentation/cmd-show-queue.txt
@@ -29,7 +29,9 @@
 projects, or that do not have a specific project are hidden.
 
 Members of the group 'Administrators', or any group that has been
-granted the 'View Queue' capability can see all queue entries.
+granted
+link:access-control.html#capability_viewQueue[the 'View Queue' capability]
+can see all queue entries.
 
 SCRIPTING
 ---------
diff --git a/Documentation/cmd-stream-events.txt b/Documentation/cmd-stream-events.txt
index bd23baa..bf78051 100644
--- a/Documentation/cmd-stream-events.txt
+++ b/Documentation/cmd-stream-events.txt
@@ -3,7 +3,7 @@
 
 NAME
 ----
-gerrit stream-events - Monitor events occuring in real time
+gerrit stream-events - Monitor events occurring in real time
 
 SYNOPSIS
 --------
@@ -13,7 +13,7 @@
 DESCRIPTION
 -----------
 
-Provides a portal into the major events occuring on the server,
+Provides a portal into the major events occurring on the server,
 outputing activity data in real-time to the client.  Events are
 filtered by the caller's access permissions, ensuring the caller
 only receives events for changes they can view on the web, or in
diff --git a/Documentation/config-contact.txt b/Documentation/config-contact.txt
index d84a499..5c633cf 100644
--- a/Documentation/config-contact.txt
+++ b/Documentation/config-contact.txt
@@ -69,7 +69,7 @@
 
 Install a contact store implementation somewhere to receive
 the contact records.  To be really paranoid, Gerrit always
-ships the data to another HTTP server, preferrably over HTTPS.
+ships the data to another HTTP server, preferably over HTTPS.
 Existing open-source server implementations can be found in the
 gerrit-contactstore project.
 
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 7af7d30..9fa7ee1 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -70,7 +70,7 @@
 * `HTTP`
 +
 Gerrit relies upon data presented in the HTTP request.  This includes
-HTTP basic authentication, or some types of commerical single-sign-on
+HTTP basic authentication, or some types of commercial single-sign-on
 solutions.  With this setting enabled the authentication must
 take place in the web server or servlet container, and not from
 within Gerrit.
@@ -206,7 +206,7 @@
 * y, year, years (`1 year` is treated as `365 days`)
 
 +
-Default is 5 days.
+Default is 12 hours.
 
 [[auth.httpHeader]]auth.httpHeader::
 +
@@ -298,6 +298,25 @@
 +
 By default this is set to false.
 
+[[auth.userNameToLowerCase]]auth.userNameToLowerCase::
++
+If set the username that is received to authenticate a git operation
+is converted to lower case for looking up the user account in Gerrit.
++
+By setting this parameter a case insensitive authentication for the
+git operations can be achieved, if it is ensured that the usernames in
+Gerrit (scheme `username`) are stored in lower case (e.g. if the
+parameter link:#ldap.accountSshUserName[ldap.accountSshUserName] is
+set to `${sAMAccountName.toLowerCase}`). It is important that for all
+existing accounts this username is already in lower case. It is not
+possible to convert the usernames of the existing accounts to lower
+case because this would break the access to existing per-user
+branches.
++
+This parameter only affects git over http and git over SSH traffic.
++
+By default this is set to false.
+
 [[cache]]Section cache
 ~~~~~~~~~~~~~~~~~~~~~~
 
@@ -573,9 +592,8 @@
 +
 Default is 5 minutes.
 
-
 [[changeMerge]]Section changeMerge
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 Controls whether or not the mergeability test of changes is
 enabled.  If enabled, when the change page is loaded, the test is
@@ -587,11 +605,10 @@
   test = true
 ----
 
-+
 By default this is false (test is not enabled).
 
 [[commentlink]]Section commentlink
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 Comment links are find/replace strings applied to change descriptions,
 patch comments, and in-line code comments to turn set strings into
 hyperlinks.  One common use is for linking to bug-tracking systems.
@@ -1021,6 +1038,16 @@
 by the system administrator, and might not even be running on the
 same host as Gerrit.
 
+[[gerrit.gitHttpUrl]]gerrit.gitHttpUrl::
++
+Optional base URL for repositories available over the HTTP
+protocol.  For example, set this to `http://mirror.example.com/base/`
+to have Gerrit display URLs from this server, rather than itself.
++
+By default unset, as the HTTP daemon must be configured externally
+by the system administrator, and might not even be running on the
+same host as Gerrit.
+
 [[gerrit.replicateOnStartup]]gerrit.replicateOnStartup::
 +
 If true, replicates to all remotes on startup to ensure they are
@@ -1054,9 +1081,10 @@
 [[gitweb.type]]gitweb.type::
 +
 Optional type of affiliated gitweb service. This allows using
-alternatives to gitweb, such as cgit.
+alternatives to gitweb, such as cgit. If set to disabled there
+is no gitweb hyperlinking support.
 +
-Valid values are `gitweb`, `cgit` or `custom`.
+Valid values are `gitweb`, `cgit`, `disabled` or `custom`.
 
 [[gitweb.type]]gitweb.revision::
 +
@@ -1081,9 +1109,32 @@
 Valid replacements are `${project}` for the project name in Gerrit
 and `${branch}` for the name of the branch.
 
+[[gitweb.linkname]]gitweb.linkname::
++
+Optional setting for modifying the link name presented to the user
+in the Gerrit web-UI.
++
+Default linkname for custom type is "gitweb".
+
+[[gitweb.pathSeparator]]gitweb.pathSeparator::
++
+Optional character to substitute the standard path separator (slash) in
+project names and branch names.
++
+By default, Gerrit will use hexadecimal encoding for slashes in project and
+branch names. Some web servers, such as Tomcat, reject this hexadecimal
+encoding in the URL.
++
+Some alternative gitweb services, such as link:http://gitblit.com[Gitblit],
+allow using an alternative path separator character. In Gitblit, this can be
+configured through the property link:http://gitblit.com/properties.html[web.forwardSlashCharacter].
+In Gerrit, the alternative path separator can be configured correspondingly
+using the property 'gitweb.pathSeparator'.
++
+Valid values are the characters '*', '(' and ')'.
 
 [[hooks]]Section hooks
-~~~~~~~~~~~~~~~~~~~~~~~~
+~~~~~~~~~~~~~~~~~~~~~~
 
 See also link:config-hooks.html[Hooks].
 
@@ -1370,6 +1421,16 @@
 +
 By default, `ignore`.
 
+[[ldap.readTimeout]]ldap.readTimeout::
++
+_(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.
++
+By default there is no timeout and Gerrit will wait for the LDAP
+server to respond until the TCP connection times out.
+
 [[ldap.accountBase]]ldap.accountBase::
 +
 Root of the tree containing all user accounts.  This is typically
@@ -1515,6 +1576,24 @@
 Default is `(memberUid=${username})` for RFC 2307,
 and unset (disabled) for Active Directory.
 
+[[ldap.localUsernameToLowerCase]]ldap.localUsernameToLowerCase::
++
+Converts the local username, that is used to login into the Gerrit
+WebUI, to lower case before doing the LDAP authentication. By setting
+this parameter to true, a case insensitive login to the Gerrit WebUI
+can be achieved.
++
+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
+anymore. The local usernames for the existing accounts can be
+converted to lower case by running the server program
+link:pgm-LocalUsernamesToLowerCase.html[LocalUsernamesToLowerCase].
+Please be aware that the conversion of the local usernames to lower
+case can't be undone. For newly created accounts the local username
+will be directly stored in lower case.
++
+By default, unset/false.
 
 [[mimetype]]Section mimetype
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -1572,14 +1651,16 @@
 
 [[receive]]Section receive
 ~~~~~~~~~~~~~~~~~~~~~~~~~~
-Sets the group of users allowed to execute 'receive-pack' on the
-server, 'receive-pack' is what runs on the server during a user's
-push or repo upload command.
+This section is used to set who can execute the 'receive-pack' and
+to limit the maximum Git object size that 'receive-pack' will accept.
+'receive-pack' is what runs on the server during a user's push or
+repo upload command.
 
 ----
 [receive]
   allowGroup = GROUP_ALLOWED_TO_EXECUTE
   allowGroup = YET_ANOTHER_GROUP_ALLOWED_TO_EXECUTE
+  maxObjectSizeLimit = 40 m
 ----
 
 [[receive.allowGroup]]receive.allowGroup::
@@ -1590,6 +1671,20 @@
 If no groups are added, any user will be allowed to execute
 'receive-pack' on the server.
 
+[[receive.maxObjectSizeLimit]]receive.maxObjectSizeLimit::
++
+Maximum allowed Git object size that 'receive-pack' will accept.
+If an object is larger than the given size the pack-parsing will abort
+and the push operation will fail. If set to zero then there is no
+limit.
++
+Gerrit administrator can use this setting to prevent developers
+from pushing objects which are too large to Gerrit.
++
+Default is zero.
++
+Common unit suffixes of 'k', 'm', or 'g' are supported.
+
 
 [[repository]]Section repository
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -1613,6 +1708,17 @@
 groups are allowed.  Each on its own line.  Groups which don't exist
 in the database are ignored.
 
+[[rules]]Section rules
+~~~~~~~~~~~~~~~~~~~~~~
+
+[[rules.enable]]rules.enable::
++
+If true, Gerrit will load and excute 'rules.pl' files in each
+project's refs/meta/config branch, if present. When set to false,
+only the default internal rules will be used.
++
+Default is true, to execute project specific rules.
+
 [[sendemail]]Section sendemail
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -1724,6 +1830,25 @@
 +
 By default, unset, so no Expiry-Date header is generated.
 
+
+[[site]]Section site
+~~~~~~~~~~~~~~~~~~~~
+
+[[site.checkUserAgent]]site.checkUserAgent::
++
+If true the server checks the User-Agent HTTP header and sends the
+correct JavaScript to the client as part of the initial page load.
+This usually reduces a round-trip for the client, allowing the UI to
+start more quickly. If false, a tiny JavaScript loader is sent to the
+client instead to determine the correct code to use. Default is true.
+
+[[site.refreshHeaderFooter]]site.refreshHeaderFooter::
++
+If true the server checks the site header, footer and CSS files for
+updated versions. If false, a server restart is required to change
+any of these resources. Default is true, allowing automatic reloads.
+
+
 [[sshd]] Section sshd
 ~~~~~~~~~~~~~~~~~~~~~
 
@@ -1743,6 +1868,8 @@
 If multiple values are supplied, the daemon will listen on all
 of them.
 +
+To disable the internal SSHD, set listenAddress to `off`.
++
 By default, *:29418.
 
 [[sshd.advertisedAddress]]sshd.advertisedAddress::
@@ -1790,8 +1917,9 @@
 [[sshd.batchThreads]]sshd.batchThreads::
 +
 Number of threads to allocate for SSH command requests from
-non-interactive users. If equals to 0, then all non-interactive
-requests are executed in the same queue as interactive requests.
+link:access-control.html#non-interactive_users[non-interactive users].
+If equals to 0, then all non-interactive requests are executed in the same
+queue as interactive requests.
 +
 Any other value will remove the number of threads from the queue
 allocated to interactive users, and create a separate thread pool
@@ -1812,7 +1940,7 @@
 +
 By default, 1 plus the number of CPUs available to the JVM.
 
-[sshd.commandStartThreads]]sshd.commandStartThreads::
+[[sshd.commandStartThreads]]sshd.commandStartThreads::
 +
 Number of threads used to parse a command line submitted by a client
 over SSH for execution, create the internal data structures used by
@@ -1883,7 +2011,7 @@
 [[suggest]] Section suggest
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-[[suggest.accounts]]::
+[[suggest.accounts]]suggest.accounts::
 +
 If `ALL`, all matching user accounts will be offered as
 completion suggestions when adding a reviewer to a change,
@@ -2028,7 +2156,7 @@
 
 
 [[upload]]Section upload
-~~~~~~~~~~~~~~~~~~~~~~~~~~
+~~~~~~~~~~~~~~~~~~~~~~~~
 Sets the group of users allowed to execute 'upload-pack' on the
 server, 'upload-pack' is what runs on the server during a user's
 fetch, clone or repo sync command.
@@ -2068,9 +2196,16 @@
 +
 By default, not set, generating the value at startup.
 
+[[user.anonymousCoward]]user.anonymousCoward::
++
+Username that this displayed in the Gerrit WebUI and in e-mail
+notifications if the full name of the user is not set.
++
+By default "Anonymous Coward" is used.
+
 
 File `etc/secure.config`
--------------------------
+------------------------
 The optional file `'$site_path'/etc/secure.config` overrides (or
 supplements) the settings supplied by `'$site_path'/etc/gerrit.config`.
 The file should be readable only by the daemon process and can be
@@ -2108,6 +2243,16 @@
 
 * link:config-replication.html[Git Replication/Mirroring]
 
+File `etc/peer_keys`
+--------------------
+
+The optional file `'$site_path'/etc/peer_keys` controls who can
+login as the 'Gerrit Code Review' user, required for the link:cmd-suexec.html[suexec]
+command.
+
+The format is one Base-64 encoded public key per line.
+
+
 Database system_config
 ----------------------
 
diff --git a/Documentation/config-hooks.txt b/Documentation/config-hooks.txt
index 058a6bf..ceb7c78 100644
--- a/Documentation/config-hooks.txt
+++ b/Documentation/config-hooks.txt
@@ -11,7 +11,7 @@
 
 Make sure your hook scripts are executable if running on *nix.
 
-Hooks are run in the background after the relevent change has
+Hooks are run in the background after the relevant change has
 taken place so are unable to affect the outcome of any given
 change. Because of the fact the hooks are run in the background
 after the activity, a hook might not be notified about an event if
diff --git a/Documentation/config-replication.txt b/Documentation/config-replication.txt
index 3390a4f..772282e 100644
--- a/Documentation/config-replication.txt
+++ b/Documentation/config-replication.txt
@@ -14,7 +14,10 @@
 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.
+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
 --------------------
@@ -28,8 +31,8 @@
   sudo su -c 'ssh mirror1.us.some.org echo' gerrit2
 ====
 
-Next, create `'$site_path'/replication.config` as a Git-style config
-file, and restart Gerrit.
+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:
@@ -55,8 +58,9 @@
 [[replication_config]]File `replication.config`
 -----------------------------------------------
 
-The optional file `'$site_path'/replication.config` is a Git-style
-config file that controls the replication settings for Gerrit.
+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
diff --git a/Documentation/dev-contributing.txt b/Documentation/dev-contributing.txt
index fd98c1d..2609b05 100644
--- a/Documentation/dev-contributing.txt
+++ b/Documentation/dev-contributing.txt
@@ -148,6 +148,8 @@
     be near each other baring a good reason not to.
   * If you are using assisted injection, the factory for your class
     should be before the instance members.
+  * Annotations should go before language keywords (final, private...) +
+    Example: @Assisted @Nullable final type varName
   * Imports should be mostly aphabetical (uppercase sorts before
     all lowercase, which means classes come before packages at the
     same level).
diff --git a/Documentation/dev-design.txt b/Documentation/dev-design.txt
index 30826d8..bf5ba73 100644
--- a/Documentation/dev-design.txt
+++ b/Documentation/dev-design.txt
@@ -87,8 +87,9 @@
 
 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 PostgreSQL, where they can be queried to
-present customized user dashboards, enumerating any pending changes.
+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.
 
 A summary of each newly uploaded change is automatically emailed
 to reviewers, so they receive a direct hyperlink to review the
@@ -113,7 +114,7 @@
 After a change has been scored positively by reviewers, Gerrit
 enables a submit button on the web interface.  Authorized users
 can push the submit button to have the change enter the project
-repository.  The equivilant in Subversion or Perforce would be
+repository.  The equivalent in Subversion or Perforce would be
 that Gerrit is invoking `svn commit` or `p4 submit` on behalf of
 the web user pressing the button.  Due to the way Git audit trails
 are maintained, the user pressing the submit button does not need
@@ -160,11 +161,11 @@
 
 The Gerrit metadata contains a summary of the available changes,
 all comments (published and drafts), and individual user account
-information.  The metadata is housed in a PostgreSQL database,
+information.  The metadata is mostly housed in the database (*1),
 which can be located either on the same server as Gerrit, or on
 a different (but nearby) server.  Most installations would opt to
-install both Gerrit and PostgreSQL on the same server, to reduce
-administration overheads.
+install both Gerrit and the metadata database on the same server,
+to reduce administration overheads.
 
 User authentication is handled by OpenID, and therefore Gerrit
 requires that the OpenID provider selected by a user must be
@@ -175,6 +176,13 @@
 * link:http://www.postgresql.org/about/[About PostgreSQL]
 * link:http://openid.net/developers/specs/[OpenID Specifications]
 
+*1  Although an effort is underway to eliminate the use of the
+database altogether, and to store all the metadata directly in
+the git repositories themselves.  So far, as of Gerrit 2.2.1, of
+all Gerrit's metadata, only the project configuration metadata
+has been migrated out of the database and into the git
+repositories for each project.
+
 
 Project Information
 -------------------
@@ -426,7 +434,7 @@
 
 Both of these assumptions are also based upon the idea that Gerrit
 will be a lot less popular than blog software, and thus will be
-running on a lot less websites.  Spammers therefore have very little
+running on a lot fewer websites.  Spammers therefore have very little
 returned benefit for getting over the protocol hurdles.
 
 These assumptions may need to be revisited in the future if any
@@ -438,7 +446,7 @@
 
 Gerrit targets for sub-250 ms per page request, mostly by using
 very compact JSON payloads bewteen client and server.  However, as
-most of the serving stack (network, hardware, PostgreSQL metadata
+most of the serving stack (network, hardware, metadata
 database) is out of control of the Gerrit developers, no real
 guarantees can be made about latency.
 
@@ -632,19 +640,18 @@
 or becomes corrupt, Gerrit has no provisions to fallback or retry
 and errors will be returned to clients.
 
-Gerrit largely assumes that the metadata PostgreSQL database is
-online and answering both read and write queries.  Query failures
-immediately result in the operation aborting and errors being
-returned to the client, with no retry or fallback provisions.
+Gerrit largely assumes that the metadata database is online and
+answering both read and write queries.  Query failures immediately
+result in the operation aborting and errors being returned to the
+client, with no retry or fallback provisions.
 
 Due to the relatively small scale described above, it is very likely
-that the Git filesystem and PostgreSQL based metadata database
-are all housed on the same server that is running Gerrit.  If any
-failure arises in one of these components, it is likely to manifest
-in the others too.  It is also likely that the administrator cannot
-be bothered to deploy a cluster of load-balanced server hardware,
-as the scale and expected load does not justify the hardware or
-management costs.
+that the Git filesystem and metadata database are all housed on the
+same server that is running Gerrit.  If any failure arises in one of
+these components, it is likely to manifest in the others too.  It is
+also likely that the administrator cannot be bothered to deploy a
+cluster of load-balanced server hardware, as the scale and expected
+load does not justify the hardware or management costs.
 
 Most deployments caring about reliability will setup a warm-spare
 standby system and use a manual fail-over process to switch from the
diff --git a/Documentation/error-you-are-not-author.txt b/Documentation/error-you-are-not-author.txt
index 8c53262..a245252 100644
--- a/Documentation/error-you-are-not-author.txt
+++ b/Documentation/error-you-are-not-author.txt
@@ -95,7 +95,7 @@
 you have to amend the commit with explicitly setting the author
 before continuing the rebase.
 
-Here is an exmaple that shows how the interactive rebase is used to
+Here is an example that shows how the interactive rebase is used to
 update the author for the last 3 commits:
 
 ----
diff --git a/Documentation/i18n-readme.txt b/Documentation/i18n-readme.txt
index 74a8e49..080ecb6 100644
--- a/Documentation/i18n-readme.txt
+++ b/Documentation/i18n-readme.txt
@@ -12,7 +12,7 @@
 The getName() function produces only a single translation of the
 description string.  This name is set by the Gerrit administrator,
 which may cause problems if the site is translated into multiple
-langauges and different users want different translations.
+languages and different users want different translations.
 
 ApprovalCategoryValue
 ---------------------
@@ -20,7 +20,7 @@
 The getName() function produces only a single translation of the
 description string.  This name is set by the Gerrit administrator,
 which may cause problems if the site is translated into multiple
-langauges and different users want different translations.
+languages and different users want different translations.
 
 /Gerrit Gerrit.html
 -------------------
diff --git a/Documentation/index.txt b/Documentation/index.txt
index 5d3eb4c..5143bf7 100644
--- a/Documentation/index.txt
+++ b/Documentation/index.txt
@@ -18,6 +18,7 @@
 * link:user-signedoffby.html[Signed-off-by Lines]
 * link:access-control.html[Access Controls]
 * link:error-messages.html[Error Messages]
+* link:user-submodules.html[Subscribing to Git Submodules]
 
 Installation
 ------------
diff --git a/Documentation/install-j2ee.txt b/Documentation/install-j2ee.txt
index d51ba68..507d6c5 100644
--- a/Documentation/install-j2ee.txt
+++ b/Documentation/install-j2ee.txt
@@ -11,7 +11,7 @@
 
 Gerrit Code Review can be installed into any J2EE servlet container,
 including popular open source containers such as Jetty or Tomcat, or
-any commerical server which supports the J2EE servlet specification.
+any commercial server which supports the J2EE servlet specification.
 
 
 Installation
diff --git a/Documentation/json.txt b/Documentation/json.txt
index 99b158da..b1dbc32 100644
--- a/Documentation/json.txt
+++ b/Documentation/json.txt
@@ -28,6 +28,8 @@
 
 url:: Canonical URL to reach this change
 
+commitMessage:: The full commit message for the change.
+
 lastUpdated:: Time in seconds since the UNIX epoch when this change
 was last updated.
 
diff --git a/Documentation/pgm-ExportReviewNotes.txt b/Documentation/pgm-ExportReviewNotes.txt
index 602c245..17cc862 100644
--- a/Documentation/pgm-ExportReviewNotes.txt
+++ b/Documentation/pgm-ExportReviewNotes.txt
@@ -26,7 +26,7 @@
 -d::
 \--site-path::
 	Location of the gerrit.config file, and all other per-site
-	configuration data, supporting libaries and log files.
+	configuration data, supporting libraries and log files.
 
 \--threads::
 	Number of threads to perform the scan work with.  Default: 2.
diff --git a/Documentation/pgm-LocalUsernamesToLowerCase.txt b/Documentation/pgm-LocalUsernamesToLowerCase.txt
new file mode 100644
index 0000000..9189fee
--- /dev/null
+++ b/Documentation/pgm-LocalUsernamesToLowerCase.txt
@@ -0,0 +1,68 @@
+LocalUsernamesToLowerCase
+=========================
+
+NAME
+----
+LocalUsernamesToLowerCase - Convert the local username of every
+account to lower case
+
+SYNOPSIS
+--------
+[verse]
+'java' -jar gerrit.war 'LocalUsernamesToLowerCase' -d <SITE_PATH>
+
+DESCRIPTION
+-----------
+Converts the local username for every account to lower case. The
+local username is the username that is used to login into the Gerrit
+WebUI.
+
+This task is only intended to be run if the configuration parameter
+link:config-gerrit.html#ldap.localUsernameToLowerCase[ldap.localUsernameToLowerCase]
+was set to true to achieve case insensitive LDAP login to the Gerrit
+WebUI.
+
+Please be aware that the conversion of the local usernames to lower
+case can't be undone.
+
+The program will produce errors if there are accounts that have the
+same local username, but with different case. In this case the local
+username for these accounts is not converted to lower case.
+
+This task can run in the background concurrently to the server if the
+database is MySQL or PostgreSQL. If the database is H2, this task
+must be run by itself.
+
+OPTIONS
+-------
+
+-d::
+\--site-path::
+	Location of the gerrit.config file, and all other per-site
+	configuration data, supporting libraries and log files.
+
+\--threads::
+	Number of threads to perform the scan work with.  Defaults to
+	twice the number of CPUs available.
+
+CONTEXT
+-------
+This command can only be run on a server which has direct
+connectivity to the metadata database.
+
+EXAMPLES
+--------
+To convert the local username of every account to lower case:
+
+====
+	$ java -jar gerrit.war LocalUsernamesToLowerCase -d site_path
+====
+
+See Also
+--------
+
+* Configuration parameter link:config-gerrit.html#ldap.localUsernameToLowerCase[ldap.localUsernameToLowerCase]
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/pgm-ScanTrackingIds.txt b/Documentation/pgm-ScanTrackingIds.txt
index 4ab4a02..ea5d72e 100644
--- a/Documentation/pgm-ScanTrackingIds.txt
+++ b/Documentation/pgm-ScanTrackingIds.txt
@@ -26,7 +26,7 @@
 -d::
 \--site-path::
 	Location of the gerrit.config file, and all other per-site
-	configuration data, supporting libaries and log files.
+	configuration data, supporting libraries and log files.
 
 \--threads::
 	Number of threads to perform the scan work with.  Defaults to
diff --git a/Documentation/pgm-daemon.txt b/Documentation/pgm-daemon.txt
index 0b2e72f..1c1d343 100644
--- a/Documentation/pgm-daemon.txt
+++ b/Documentation/pgm-daemon.txt
@@ -33,7 +33,7 @@
 -d::
 \--site-path::
 	Location of the gerrit.config file, and all other per-site
-	configuration data, supporting libaries and log files.
+	configuration data, supporting libraries and log files.
 
 \--enable-httpd::
 \--disable-httpd::
@@ -48,7 +48,7 @@
 \--slave::
 	Run in slave mode, permitting only read operations
     by clients.  Commands which modify state such as
-    link:cmd-receive-pack.html[recieve-pack] (creates new changes
+    link:cmd-receive-pack.html[receive-pack] (creates new changes
     or updates existing ones) or link:cmd-review.html[review]
     (sets approve marks) are disabled.
 +
diff --git a/Documentation/pgm-gsql.txt b/Documentation/pgm-gsql.txt
index 938aafd..c3a492a 100644
--- a/Documentation/pgm-gsql.txt
+++ b/Documentation/pgm-gsql.txt
@@ -26,7 +26,7 @@
 -d::
 \--site-path::
 	Location of the gerrit.config file, and all other per-site
-	configuration data, supporting libaries and log files.
+	configuration data, supporting libraries and log files.
 
 CONTEXT
 -------
diff --git a/Documentation/pgm-index.txt b/Documentation/pgm-index.txt
index 4db4ab0..5cbe6ba0 100644
--- a/Documentation/pgm-index.txt
+++ b/Documentation/pgm-index.txt
@@ -36,6 +36,9 @@
 link:pgm-ScanTrackingIds.html[ScanTrackingIds]::
 	Rescan all changes after configuring trackingids.
 
+link:pgm-LocalUsernamesToLowerCase.html[LocalUsernamesToLowerCase]::
+	Convert the local username of every account to lower case.
+
 GERRIT
 ------
 Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/pgm-init.txt b/Documentation/pgm-init.txt
index d3094fd..c53c57d 100644
--- a/Documentation/pgm-init.txt
+++ b/Documentation/pgm-init.txt
@@ -31,14 +31,14 @@
 
 \--no-auto-start::
 	Don't automatically start the daemon after initializing a
-	newly created site path.  This permits the administartor
+	newly created site path.  This permits the administrator
 	to inspect and modify the configuration before the daemon
 	is started.
 
 -d::
 \--site-path::
 	Location of the gerrit.config file, and all other per-site
-	configuration data, supporting libaries and log files.
+	configuration data, supporting libraries and log files.
 
 CONTEXT
 -------
diff --git a/Documentation/pgm-rulec.txt b/Documentation/pgm-rulec.txt
index 40aad2d..6d0a632 100644
--- a/Documentation/pgm-rulec.txt
+++ b/Documentation/pgm-rulec.txt
@@ -21,7 +21,7 @@
 -d::
 --site-path::
 	Location of the gerrit.config file, and all other per-site
-	configuration data, supporting libaries and log files.
+	configuration data, supporting libraries and log files.
 
 --all::
 	Compile rules for all projects.
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index c291b89..6a6ce16 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -100,7 +100,7 @@
 [[project]]
 project:'PROJECT'::
 +
-Changes occuring in 'PROJECT'. If 'PROJECT' starts with `^` it
+Changes occurring in 'PROJECT'. If 'PROJECT' starts with `^` it
 matches project names by regular expression.  The
 link:http://www.brics.dk/automaton/[dk.brics.automaton
 library] is used for evaluation of such patterns.
diff --git a/Documentation/user-submodules.txt b/Documentation/user-submodules.txt
new file mode 100644
index 0000000..3d14437
--- /dev/null
+++ b/Documentation/user-submodules.txt
@@ -0,0 +1,143 @@
+Gerrit Code Review - Superprojects subscribed to submodules updates
+===================================================================
+
+Description
+-----------
+
+Gerrit supports a custom git superproject feature for tracking submodules.
+This feature is useful for automatic updates on superprojects whenever
+a change is merged on tracked submodules. To take advantage of this
+feature, one should add submodule(s) to a local working copy of a
+superproject, edit the created .gitmodules configuration file to
+have a branch field on each submodule section with the value of the
+submodule branch it is subscribing to, commit the changes, push and
+merge the commit.
+
+When a commit is merged to a project, the commit content is scanned
+to identify if it registers git submodules (if the commit registers
+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
+automatically updates the subscribers to the submodule with a new
+commit having the updated gitlinks.
+
+Git Submodules Overview
+-----------------------
+
+It is a git feature that allows an external repository to be
+attached inside a repository at a specific path. The objective here
+is to provide a brief overview, further details can be found
+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".
+With this feature, one could attach 'a' inside of 'super' repository
+at path 'a' by executing the following command when being inside
+'super':
+=====
+git submodule add ssh://server/a a
+====
+
+Still considering the above example, after its execution notice that
+inside the local repository 'super' the 'a' folder is considered a
+gitlink to the external repository 'a'. Also notice a file called
+.gitmodules is created (it is a config file containing the
+subscription of 'a'). To provide the sha-1 each gitlink points to in
+the external repository, one should use the command:
+====
+git submodule status
+====
+
+In the example provided, if 'a' is updated and 'super' is supposed
+to see the latest sha-1 (considering here 'a' has only the master
+branch), one should then commit the modified gitlink for 'a' in
+the 'super' project. Actually it would not even need to be an
+external update, one could move to 'a' folder (insider 'super'),
+modify its content, commit, then move back to 'super' and
+commit the modified gitlink for 'a'.
+
+Creating a New Subscription
+---------------------------
+
+Defining the Submodule Branch
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+This is required because Submodule subscription is actually the
+subscription of a submodule project and one of its branches for
+a branch of a super project.
+
+Since it manages subscriptions in the branch scope, we could have
+a scenario having a project called 'super' having a branch 'integration'
+subscribed to a project called 'a' in branch 'integration', and also
+having the same 'super' project but in branch 'dev' subscribed to the 'a'
+project in a branch called 'local-dev'.
+
+After adding the git submodule to a super project, one should edit
+the .gitmodules file to add a branch field to each submodule
+section which is supposed to be subscribed.
+
+The branch field is not filled by the git submodule command. Its value
+should indicate the branch of a submodule project that when updated
+will trigger automatic update of its registered gitlink.
+
+The branch value could be '.' if the submodule project branch
+has the same name as the destination branch of the commit having
+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
+editing .gitmodules file after adding submodules to a super project,
+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
+to automatically update the superproject.
+
+Detecting and Subscribing Submodules
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Whenever a commit is merged to a project, its content is scanned
+to identify if it registers any submodules (if the commit contains new
+gitlinks and a .gitmodules file with all required info) and if so,
+a new submodule subscription is registered.
+
+Automatic Update of Superprojects
+---------------------------------
+
+After a superproject is subscribed to a submodule, it is not
+required to push/merge commits to this superproject to update the
+gitlink to the submodule.
+
+Whenever a commit is merged in a submodule, its subscribed superproject
+is updated.
+
+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
+creates a new commit on branch 'dev' of 'super' updating the gitlink
+to point to the just merged commit.
+
+Canonical Web Url
+~~~~~~~~~~~~~~~~~
+
+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 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
+add/subscribe submodules.
+
+Removing Subscriptions
+----------------------
+
+If one has added a submodule subscription and drops it, it is
+required to merge a commit updating the subscribed super
+project/branch to remove the gitlink and the submodule section
+of the .gitmodules file.
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/user-upload.txt b/Documentation/user-upload.txt
index 3ab48c9..2cd959b 100644
--- a/Documentation/user-upload.txt
+++ b/Documentation/user-upload.txt
@@ -15,7 +15,8 @@
 
 Each user uploading changes to Gerrit must configure one or more SSH
 public keys.  The per-user SSH key list can be accessed over the web
-within Gerrit by `Settings`, and then accessing the `SSH Keys` tab.
+within Gerrit by `Settings`, and then accessing the `SSH Public Keys`
+tab.
 
 [[configure_ssh]]
 Configuration
@@ -153,8 +154,8 @@
   $ git push tr:kernel/common HEAD:refs/for/experimental
 ====
 
-Specific reviewers can be requested and/or additional ``carbon
-copies'' of the notification message may be sent by including these
+Specific reviewers can be requested and/or additional 'carbon
+copies' of the notification message may be sent by including these
 as arguments to `git receive-pack`:
 
 ====
@@ -279,19 +280,26 @@
 * `refs/tags/*`: annotated tag objects pointing to any other type
 of Git object can be created.
 
-To push branches, the link:access-control.html#category_push_direct['Push']
-right must be granted to one (or more) of the user's groups.  The
-allowed levels within this category are:
+To push branches, the proper access rights must be configured first.
+Here follows a few examples of how to configure this in Gerrit:
 
 * Update: Any existing branch can be fast-forwarded to a new commit.
 This is the safest mode as commits cannot be discarded.  Creation
-of new branches is rejected.
-* Create: Implies Update, but also allows creation of a new branch
-if the name does not not already designate an existing branch name.
-* Delete: Implies Create and Update, but also allows an existing
+of new branches is rejected. Can be configured with
+link:access-control.html#category_push_direct['Push'] access.
+* Create: Allows creation of a new branch if the name does not
+already designate an existing branch name.  Needs
+link:access-control.html#category_create['Create Reference']
+configured. Please note that once created, this permission doesn't
+grant the right to update the branch with further commits (see above
+for update details).
+* Delete: Implies Update, but also allows an existing
 branch to be deleted.  Since a force push is effectively a delete
 followed by a create, but performed atomically on the server and
 logged, this also permits forced push updates to branches.
+To grant this access, configure
+link:access-control.html#category_push_direct['Push'] with the
+'Force' option ticked.
 
 To push annotated tags, the `Push Annotated Tag` project right must
 be granted to one (or more) of the user's groups.  There is only
diff --git a/ReleaseNotes/ReleaseNotes-2.3.txt b/ReleaseNotes/ReleaseNotes-2.3.txt
new file mode 100644
index 0000000..965c8e3
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.3.txt
@@ -0,0 +1,474 @@
+Release notes for Gerrit 2.3
+============================
+
+Gerrit 2.3 is now available:
+
+link:http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.3.war[http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.3.war]
+
+Schema Change
+-------------
+*WARNING:* This release contains schema changes.  To upgrade:
+----
+  java -jar gerrit.war init -d site_path
+----
+
+*WARNING:* Upgrading to 2.3.x requires the server be first upgraded
+to 2.1.7 (or a later 2.1.x version), and then to 2.3.x.
+
+If you are upgrading from 2.2.x.x, you may ignore this warning and
+upgrade directly to 2.3.x.
+
+
+New Features
+------------
+Drafts
+~~~~~~
+* New draft statuses and magic branches
++
+Adds draft status to Change. DRAFT status in change occurs before NEW
+and will be for a change that is not meant for review (yet).
+Also adds magic branches refs/drafts/ and refs/publish/ that
+will handle whether or not a patchset is a draft or goes straight to
+review. refs/for/ should be deprecated in favor of explicitly marking
+a patchset as a draft or directly to review.
+
+* Draft patchset and change visibility in UI
++
+If a patchset is a draft, adds a (DRAFT) label next to the revision
+(or gitweb link if it exists). If a change is a draft, adds a (DRAFT)
+next to the subject and changes the status appropriately.
+
+* Publish draft patchsets in UI and SSH
++
+Adds Publish button to draft patchsets in UI and an option to
+publish draft patchsets in the review ssh command. Publishing a draft
+patchset makes it visible. Publishing a draft patchset in a draft
+change irreversibly upgrades the change status to NEW.
+
+* Delete draft changes and patchsets
++
+Adds ability to delete draft changes and patchsets that are not meant
+or fit for code review. Deleting a draft patchset also deletes the
+corresponding ref from the repository and decrements the next patch
+set number for the change if necessary. Deleting a draft change
+deletes all of its (draft) patchsets.
+
+* Add pushing drafts to refs/drafts/
++
+Pushing to refs/drafts/ will now push a draft patchset. If this is the
+first patch set, change created will be in draft status. Pushing a
+draft patchset to a draft change keeps it in draft status. Pushing
+a non-draft patchset (with refs/publish/ or refs/for/, they do the
+same thing) to a draft change turns it into a non-draft change.
+Draft patchsets cannot be submitted.
+
+* When pushing changes as drafts, output [DRAFT] next to the change link
+
+
+Web
+~~~
+* issue 203 Create project through web interface
++
+Add a new panel in the Admin->Projects Screen.  It
+enables the users that are allowed to create projects
+via command-line to create them also via web interface.
+
+* Suggest parent for 'create-project' in the UI.
++
+Add a list of parent suggestions for 'create project'
+in the UI, so the user can select a parent for the new
+project from a list of projects that are already parents to
+other projects.
+
+* issue 981 Fix diffs skipping one line
++
+Don't show '... skipping 1 common line ...'.  The text to show this
+takes up just as much space as showing the line which was skipped.
+
+* issue 18 Support expanding lines of context in diff
++
+Allow lines of context which were skipped in the side-by-side diff
+view to be expanded.  This makes it easier to get more code context
+when needed but not show huge amounts of unneeded data.
+
+* Move checkbox to mark file as reviewed into title bar
+
+* Redirect the user to the reverted change (when reverting).
+
+* On group rename update the group name in the page title
+
+* In ProjectAccessScreen add link to history of project.config in gitweb
+
+* Removed superfluous 'comment' for patch history table.
+
+* Make OpenID login images transparent
+
+* Disable SSH Keys in the web UI if SSHD is disabled
+
+
+SSH
+~~~
+* Adds --description (-d) option to ls-projects
++
+Allows listing of projects together with their respective
+description.
+
+* ls-projects: new option to list all accessible projects
++
+Add a new option '--all' to the 'ls-projects' SSH command to display
+all projects that are accessible by the calling user account. Besides
+the projects that the calling user account has been granted 'READ'
+access to, this includes all projects that are owned by the calling
+user account (even if for these projects the 'READ' access right is
+not assigned to the calling user account).
+
+* Suggest parent for 'create-project' in the SSH command
++
+Add an option '--suggest-parents' which will print out
+a list of projects that are already parents to another
+projects, thus it can help user to find a suitable
+parent for the new project.
+
+* Support reparenting all children of a parent project
++
+This change adds a new option to the 'set-project-parent' command that
+allows reparenting all child projects of one parent project to another
+parent project.
+
+* set-parent-project: evict child projects from project cache
+
+* Add ssh command to list groups.
+
+* ls-groups: add option to list groups for a project
++
+Add an option to the ls-groups SSH command that allows to list only
+those groups for which any permission is assigned to a project.
+
+* ls-groups: Add option to only list groups that are visible to all
+
+* ls-groups: Support listing groups by group type
+
+* ls-groups: Support listing of groups for a user
+
+* Add new SSH command to rename groups
+
+* Support for --file option for ssh queries.
++
+Allows user to list files and attributes (ADDED,
+MODIFIED, DELETED, RENAMED, COPIED) when querying for
+patch sets.
+
+* Output full commit message in query results
+
+* Option for SSHD review-cmd to always publish the message.
++
+"--force-message" option for the SSHD review command,
+which allows Gerrit to publish the "--message", even if the
+labels could not be applied due to change being closed.
+
+
+Config
+~~~~~~
+* issue 349 Apply states for projects (active, readonly and hidden)
++
+Active state indicates the project is regular and is the default value.
++
+Read Only means that users can see the project if read permission is
+granted, but all modification operations are disabled.
++
+Hidden means the project is not visible for those who are not owners
+
+* Enable case insensitive login to Gerrit WebUI for LDAP authentication
++
+Gerrit treats user names as case sensitive, while some LDAP servers
+don't. On first login to Gerrit the user enters his user name and
+Gerrit queries LDAP for it. Since LDAP is case-insensitive with regards
+to  the username, the LDAP authentication succeeds regardless in
+which case the user typed in his user name. The username is stored in
+Gerrit exactly as entered by the user. For further logins the user
+always has to use the same case. If the user specifies his user name in
+a different case Gerrit tries to create a new account which fails with
+"Cannot assign user name ... to account ...; name already in use.".
+This error occurs because the LDAP query resolves to the same LDAP
+user and storing the username for SSH (which is by default always
+lower case) fails because such an entry exists already for the first
+account that the user created.
++
+This change introduces a new configuration parameter that converts the
+user name always to lower case before doing the LDAP authentication.
+By this the login to the Gerrit WebUI gets case insensitive. If this
+configuration parameter is set, the user names for all existing
+accounts have to be converted to lower case. This change includes a
+server program to do this conversion.
+
+* Enable case insensitive authentication for git operations
++
+A new configuration parameter is introduced that converts the username
+that is received to authenticate a git operation to lower case for
+looking up the user account in Gerrit.
++
+By setting this parameter a case insensitive authentication for the
+git operations can be achieved, if it is ensured that the usernames in
+Gerrit (scheme 'username') are stored in lower case (e.g. if the
+parameter 'ldap.accountSshUserName' is set to
+'${sAMAccountName.toLowerCase}').
+
+* Support replication to local folder
+
+* Read timeout parameter for LDAP connections: ldap.readTimeout
++
+This helps prevent a very slow LDAP server from blocking
+all SSH command creation threads.
+
+* Introduce a git maxObjectSizeLimit in the [recieve] config
++
+This limits the size of uploaded files
+
+* Make 'Anonymous Coward' configurable
+
+* Add property to configure path separator in URLs for a gitweb service
+
+* Customize link-name pointing to gitweb-service.
++
+Previously the link to the external gitweb-type
+pages said "(gitweb)" regardless if using cgit
+or a custom service.
+
+* Support gitweb.type=disabled
+
+* rules.enable: Support disabling per project prolog rules in gerrit.config
+
+* Allow site administrators to define Git-over-HTTP mirror URL
+
+* Allow sshd.listenAddress = off to disable the daemon
+
+* daemon: Allow httpd without sshd
+
+* Allow disabling certain features of HostPageServlet
++
+These features are: user agent detection and automatic refresh
+logic associated with the site header, footer and CSS.
+
+
+Dev
+~~~
+* Fix 'No source code is available for type org.eclipse.jgit.lib.Constants'
+
+* Fix miscellaneous compiler warnings
+
+* Add entries to .gitignore for m2e settings/preference files
+
+* Package source JARs for antlr, httpd, server
+
+* pom.xml: change gerrit-war's dependency on gerrit-main to runtime
++
+This only seems to matter to IntelliJ, since the Main class is
+provided to the war via an overlay in gerrit-war/pom.xml
+
+* Fixed the full name of the MAVEN2_CLASSPATH_CONTAINER
++
+Fixes java.lang.NoClassDefFoundError: com/google/gwt/dev/DevMode
+
+
+Miscellaneous
+~~~~~~~~~~~~~
+* Allow superprojects to subscribe to submodules updates
++
+The feature introduced in this release allows superprojects to
+subscribe to submodules updates.
++
+When a commit is merged to a project, the commit content is scanned
+to identify if it registers submodules (if the commit contains new
+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
+automatically updates the subscribers to the submodule with new
+commit having the updated gitlinks.
++
+The most notable benefit of the feature is to not require
+to push/merge commits of super projects (subscribers) with gitlinks
+whenever a project being a submodule is updated. It is only
+required to push commits with gitlinks when they are created
+(and in this case it is also required to push .gitmodules file).
+
+* Allow Realm to participate when linking an account identity
++
+When linking a new user identity to an exisiting account, permit the
+Realm to observe the new incoming identity and the current account,
+and to alter the request. This enables a Realm to observe when a
+user verifies a new email address link.
+
+* issue 871 Show latest patchset with cherry-picked merge
++
+When a change is published via the cherry-pick merge strategy,
+show the final commit as a patchset in the change history.
+This now makes it possible to search for the cherry-picked SHA1.
+
+* issue 871 Display hash of the cherry-pick merge in comment
+
+* Added more verbose messages when changes are being rejected
+
+* Display proper error message when LDAP is unavailable
+
+* Clarify error msg when user's not allowed to '--force push'.
+
+* ContainerAuthFilter: fail with FORBIDDEN if username not set
+
+* Resolve 'Project Owners' group if it is included into another group
+
+* Hide SSH URL in email footers if SSH is disabled
+
+* Sort the jar files from the war before adding to classpath.
+
+* Apply user preferences when loading site
+
+* Ensure HttpLog can always get the user identity
+
+* Prevent comments spam for abandoned commit
++
+If some change was abandoned but later submitted (e.g. by
+cherry-picking it to a another branch) then pushing a new branch
+that contains this change no longer adds a new comment.
+
+* Make Address, EmailHeader visible to other EmailSenders
+
+* Use transactions to handle comments when possible
+
+* Try to use transactions when creating changes
+
+* gerrit.sh: disown doesn't accept pid as a argument, fix script
+
+* gerrit.sh: detach gerrit properly so it won't keep bad ssh sessions open.
+
+* Cache list of all groups in the group cache
+
+* issue 1161 Evict project in user cache on save of project meta data
+
+* Ensure that the site paths are resolved to their canonical form (for Windows)
+
+* Connect Velocity to slf4j
+
+* Expose project permissionOnly status via JSON-RPC
+
+* Make HEAD of All-Projects point to refs/meta/config
+
+* issue 1158 Added support for European style dates
+
+* Make macros in email templates local to the template
+
+* Support http://server/project for Git access
+
+* Use _ instead of $ for implementation-detail Prolog predicates
+
+* Update the Sign In anchor with current URL
++
+Always update the href of the Sign In anchor in the menu bar with
+the current page URL after /login/, making the redirect process
+bring users back to the current view after sign in.
+
+* Improve validation of email registration tokens
+
+
+Upgrades
+--------
+* Upgrade to gwtorm 1.2
+
+* Upgrade to JGit 1.1.0.201109151100-r.119-gb4495d1
++
+This is needed because of this change:
+https://gerrit-review.googlesource.com/#/c/30450/
+
+* Support Velocity 1.5 (as well as previous 1.6.4)
+
+
+Bug Fixes
+---------
+* Avoid NPE when group is missing
+
+* Do not fail with NPE if context path of request is null
+
+* Fix NPE in set-project-parent command if parent is not specified
+
+* Only send mail to author and committer if they are registered to prevent an NPE
+
+* Avoid potential NPE when querying the queue.
+
+* Allow loading Project Access when there is no refs/meta/config
+
+* Fix calculation of project name if repo is not existing
++
+If a project inherits from a non existing parent, prevent a
+StringIndexOutOfBoundsException.
+
+* Fix: Supress "Error on refs/cache-automerge" warnings.
+
+* Don't allow registering for cleanup after cleanup runs
++
+This prevents leaking a database connection.
+
+* issue 807 Fix: Tags are not replicated properly
+
+* Prevent smtp rejected users from rejecting emails for all users
+
+* Fix token saving redirect in container auth
++
+Update the jump page that redirects users from /#TOKEN to
+/login/TOKEN.  This forces using the container based
+authentication.  Also correct "/login//" to be just "/login/".
+
+* Use custom error messages for Git-over-HTTP
++
+Ensure clients see messages related to contributor agreement not
+being activated even if they push over HTTP.
+
+* Avoid double key event for GroupReferenceBox
+
+* Fix git push authentication over HTTP
+
+* Fix http://login/ redirect bug
+
+* Fix missing targets in /login/ URLs
+
+* set-project-parent: if update of 1 project fails continue with others
+
+* Verify the case of the project name before opening git repository
+
+* Update top level SUBMITTING_PATCHES URLs
+
+
+Documentation
+-------------
+* Some updates to the design docs
+
+* cmd-index: Fix link to documentation of rename-group command
+
+* Update documentation for testing SSH connection
+
+* Bypass review updated with 2.2.x permissions
+
+* Add documentation for 'peer_keys'
+
+* Improve 'Push Merge Commit' access right documentation
+
+* Access control: Capabilities documented
+** Administrate Server
+** Create Account
+** Create Group
+** Create Project
+** Flush Caches
+** Kill Task
+** Priority
+** Query Limit
+** Start Replication
+** View caches
+** View connections
+** View queue
+
+* Access control: Example roles documented
+** Contributor
+** Developer
+** CI System
+** Integrator
+** Project owner
+** Administrator
diff --git a/ReleaseNotes/index.txt b/ReleaseNotes/index.txt
index b971110..51d0b22 100644
--- a/ReleaseNotes/index.txt
+++ b/ReleaseNotes/index.txt
@@ -1,6 +1,11 @@
 Gerrit Code Review - Release Notes
 ==================================
 
+[[2_3]]
+Version 2.3.x
+-------------
+* link:ReleaseNotes-2.3.html[2.3]
+
 [[2_2]]
 Version 2.2.x
 -------------
diff --git a/gerrit-antlr/.gitignore b/gerrit-antlr/.gitignore
index 903c6c8..194bedc 100644
--- a/gerrit-antlr/.gitignore
+++ b/gerrit-antlr/.gitignore
@@ -2,3 +2,4 @@
 /.classpath
 /.project
 /.settings/org.maven.ide.eclipse.prefs
+/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
diff --git a/gerrit-antlr/pom.xml b/gerrit-antlr/pom.xml
index 4b9e3ad..efcc190 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.2-SNAPSHOT</version>
+    <version>2.3-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-antlr</artifactId>
@@ -52,6 +52,18 @@
           </execution>
         </executions>
       </plugin>
+
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-source-plugin</artifactId>
+        <executions>
+          <execution>
+            <goals>
+              <goal>jar</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
     </plugins>
   </build>
 </project>
diff --git a/gerrit-common/.gitignore b/gerrit-common/.gitignore
index 903c6c8..194bedc 100644
--- a/gerrit-common/.gitignore
+++ b/gerrit-common/.gitignore
@@ -2,3 +2,4 @@
 /.classpath
 /.project
 /.settings/org.maven.ide.eclipse.prefs
+/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
diff --git a/gerrit-common/pom.xml b/gerrit-common/pom.xml
index 814afad..b4395be 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.2-SNAPSHOT</version>
+    <version>2.3-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 23b0d27..23bef63 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
@@ -41,6 +41,7 @@
   public static final String MINE = "/";
   public static final String ADMIN_GROUPS = "/admin/groups/";
   public static final String ADMIN_PROJECTS = "/admin/projects/";
+  public static final String ADMIN_CREATE_PROJECT = "/admin/create-project/";
 
   public static String toChange(final ChangeInfo c) {
     return toChange(c.getId());
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/auth/userpass/LoginResult.java b/gerrit-common/src/main/java/com/google/gerrit/common/auth/userpass/LoginResult.java
index dcabf81..4a9ab66 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/auth/userpass/LoginResult.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/auth/userpass/LoginResult.java
@@ -14,7 +14,40 @@
 
 package com.google.gerrit.common.auth.userpass;
 
+import com.google.gerrit.reviewdb.AuthType;
+
 public class LoginResult {
   public boolean success;
   public boolean isNew;
+
+  protected AuthType authType;
+  protected Error error;
+
+  protected LoginResult() {
+  }
+
+  public LoginResult(final AuthType authType) {
+    this.authType = authType;
+  }
+
+  public AuthType getAuthType() {
+    return authType;
+  }
+
+  public void setError(final Error error) {
+    this.error = error;
+    success = error == null;
+  }
+
+  public Error getError() {
+    return error;
+  }
+
+  public static enum Error {
+    /** Username or password are invalid */
+    INVALID_LOGIN,
+
+    /** The authentication server is unavailable or the query to it timed out */
+    AUTHENTICATION_UNAVAILABLE
+  }
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetail.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetail.java
index c4c56d8..809d462 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetail.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetail.java
@@ -28,8 +28,10 @@
   protected AccountInfoCache accounts;
   protected boolean allowsAnonymous;
   protected boolean canAbandon;
+  protected boolean canPublish;
   protected boolean canRestore;
   protected boolean canRevert;
+  protected boolean canDeleteDraft;
   protected Change change;
   protected boolean starred;
   protected List<ChangeInfo> dependsOn;
@@ -41,6 +43,7 @@
   protected List<ChangeMessage> messages;
   protected PatchSet.Id currentPatchSetId;
   protected PatchSetDetail currentDetail;
+  protected boolean canEdit;
 
   public ChangeDetail() {
   }
@@ -69,6 +72,14 @@
     canAbandon = a;
   }
 
+  public boolean canPublish() {
+    return canPublish;
+  }
+
+  public void setCanPublish(final boolean a) {
+    canPublish = a;
+  }
+
   public boolean canRestore() {
     return canRestore;
   }
@@ -93,6 +104,14 @@
     canSubmit = a;
   }
 
+  public boolean canDeleteDraft() {
+    return canDeleteDraft;
+  }
+
+  public void setCanDeleteDraft(boolean a) {
+    canDeleteDraft = a;
+  }
+
   public Change getChange() {
     return change;
   }
@@ -187,7 +206,19 @@
     currentDetail = d;
   }
 
+  public void setCurrentPatchSetId(final PatchSet.Id id) {
+    currentPatchSetId = id;
+  }
+
   public String getDescription() {
     return currentDetail != null ? currentDetail.getInfo().getMessage() : "";
   }
+
+  public void setCanEdit(boolean a) {
+    canEdit = a;
+  }
+
+  public boolean canEdit() {
+    return canEdit;
+  }
 }
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 cb38c3b..b667877 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
@@ -20,6 +20,7 @@
 import com.google.gwtjsonrpc.client.RemoteJsonService;
 import com.google.gwtjsonrpc.client.RpcImpl;
 import com.google.gwtjsonrpc.client.RpcImpl.Version;
+import com.google.gwtjsonrpc.client.VoidResult;
 
 @RpcImpl(version = Version.V2_0)
 public interface ChangeManageService extends RemoteJsonService {
@@ -37,4 +38,10 @@
   @SignInRequired
   void restoreChange(PatchSet.Id patchSetId, String message,
       AsyncCallback<ChangeDetail> callback);
+
+  @SignInRequired
+  void publish(PatchSet.Id patchSetId, AsyncCallback<ChangeDetail> callback);
+
+  @SignInRequired
+  void deleteDraftChange(PatchSet.Id patchSetId, AsyncCallback<VoidResult> callback);
 }
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 0ef68df..5581c18 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
@@ -26,6 +26,7 @@
 
 public class GerritConfig implements Cloneable {
   protected String registerUrl;
+  protected String httpPasswordUrl;
   protected List<OpenIdProviderPattern> allowedOpenIDs;
 
   protected GitwebLink gitweb;
@@ -35,6 +36,7 @@
   protected AuthType authType;
   protected Set<DownloadScheme> downloadSchemes;
   protected String gitDaemonUrl;
+  protected String gitHttpUrl;
   protected String sshdAddress;
   protected Project.NameKey wildProject;
   protected ApprovalTypes approvalTypes;
@@ -42,6 +44,7 @@
   protected List<RegexFindReplace> commentLinks;
   protected boolean documentationAvailable;
   protected boolean testChangeMerge;
+  protected String anonymousCowardName;
 
   public String getRegisterUrl() {
     return registerUrl;
@@ -51,6 +54,14 @@
     registerUrl = u;
   }
 
+  public String getHttpPasswordUrl() {
+    return httpPasswordUrl;
+  }
+
+  public void setHttpPasswordUrl(String url) {
+    httpPasswordUrl = url;
+  }
+
   public List<OpenIdProviderPattern> getAllowedOpenIDs() {
     return allowedOpenIDs;
   }
@@ -110,6 +121,17 @@
     gitDaemonUrl = url;
   }
 
+  public String getGitHttpUrl() {
+    return gitHttpUrl;
+  }
+
+  public void setGitHttpUrl(String url) {
+    if (url != null && !url.endsWith("/")) {
+      url += "/";
+    }
+    gitHttpUrl = url;
+  }
+
   public String getSshdAddress() {
     return sshdAddress;
   }
@@ -169,4 +191,12 @@
   public void setTestChangeMerge(final boolean test) {
     testChangeMerge = test;
   }
+
+  public String getAnonymousCowardName() {
+    return anonymousCowardName;
+  }
+
+  public void setAnonymousCowardName(final String anonymousCowardName) {
+    this.anonymousCowardName = anonymousCowardName;
+  }
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GitWebType.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GitWebType.java
index 7eb6955..0e2e50d 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GitWebType.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GitWebType.java
@@ -27,18 +27,27 @@
 
     if (name == null || name.isEmpty() || name.equalsIgnoreCase("gitweb")) {
       type = new GitWebType();
+      type.setLinkName("gitweb");
       type.setProject("?p=${project}.git;a=summary");
       type.setRevision("?p=${project}.git;a=commit;h=${commit}");
       type.setBranch("?p=${project}.git;a=shortlog;h=${branch}");
+      type.setFileHistory("?p=${project}.git;a=history;hb=${branch};f=${file}");
 
     } else if (name.equalsIgnoreCase("cgit")) {
       type = new GitWebType();
+      type.setLinkName("cgit");
       type.setProject("${project}/summary");
       type.setRevision("${project}/commit/?id=${commit}");
       type.setBranch("${project}/log/?h=${branch}");
+      type.setFileHistory("${project}/log/${file}?h=${branch}");
 
     } else if (name.equalsIgnoreCase("custom")) {
       type = new GitWebType();
+      // The custom name is not defined, let's keep the old style of using GitWeb
+      type.setLinkName("gitweb");
+
+    } else if (name.equalsIgnoreCase("disabled")) {
+      type = null;
 
     } else {
       type = null;
@@ -47,6 +56,9 @@
     return type;
   }
 
+  /** name of the type. */
+  private String name;
+
   /** String for revision view url. */
   private String revision;
 
@@ -56,6 +68,13 @@
   /** ParamertizedString for branch view url. */
   private String branch;
 
+  /** ParamertizedString for file history view url. */
+  private String fileHistory;
+
+  /** Character to substitute the standard path separator '/' in branch and
+    * project names */
+  private char pathSeparator = '/';
+
   /** Private default constructor for gson. */
   protected GitWebType() {
   }
@@ -70,6 +89,15 @@
   }
 
   /**
+   * Get the String for link-name of the type.
+   *
+   * @return The String for link-name of the type
+   */
+  public String getLinkName() {
+    return name;
+  }
+
+  /**
    * Get the String for project view.
    *
    * @return The String for project view
@@ -88,6 +116,15 @@
   }
 
   /**
+   * Get the String for file history view.
+   *
+   * @return The String for file history view
+   */
+  public String getFileHistory() {
+    return fileHistory;
+  }
+
+  /**
    * Set the pattern for branch view.
    *
    * @param pattern The pattern for branch view
@@ -99,6 +136,17 @@
   }
 
   /**
+   * Set the pattern for link-name type.
+   *
+   * @param pattern The pattern for link-name type
+   */
+  public void setLinkName(final String name) {
+    if (name != null && !name.isEmpty()) {
+      this.name = name;
+    }
+  }
+
+  /**
    * Set the pattern for project view.
    *
    * @param pattern The pattern for project view
@@ -119,4 +167,38 @@
       revision = pattern;
     }
   }
+
+  /**
+   * Set the pattern for file history view.
+   *
+   * @param pattern The pattern for file history view
+   */
+  public void setFileHistory(final String pattern) {
+    if (pattern != null && !pattern.isEmpty()) {
+      fileHistory = pattern;
+    }
+  }
+
+  /**
+   * Replace the standard path separator ('/') in a branch name or project
+   * name with a custom path separator configured by the property
+   * gitweb.pathSeparator.
+   * @param urlSegment The branch or project to replace the path separator in
+   * @return the urlSegment with the standard path separator replaced by the
+   * custom path separator
+   */
+  public String replacePathSeparator(String urlSegment) {
+    if ('/' != pathSeparator) {
+      return urlSegment.replace('/', pathSeparator);
+    }
+    return urlSegment;
+  }
+
+  /**
+   * Set the custom path separator
+   * @param separator The custom path separator
+   */
+  public void setPathSeparator(char separator) {
+    this.pathSeparator = separator;
+  }
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GitwebLink.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GitwebLink.java
index 0460bf2..5ee1b6e 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GitwebLink.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GitwebLink.java
@@ -36,12 +36,16 @@
     type = gitWebType;
   }
 
+  public String getLinkName() {
+    return "(" + type.getLinkName() + ")";
+  }
+
   public String toRevision(final Project.NameKey project, final PatchSet ps) {
     ParameterizedString pattern = new ParameterizedString(type.getRevision());
 
     final Map<String, String> p = new HashMap<String, String>();
-    p.put("project", URL.encodeQueryString(project.get()));
-    p.put("commit", URL.encodeQueryString(ps.getRevision().get()));
+    p.put("project", encode(project.get()));
+    p.put("commit", encode(ps.getRevision().get()));
     return baseUrl + pattern.replace(p);
   }
 
@@ -49,7 +53,7 @@
     ParameterizedString pattern = new ParameterizedString(type.getProject());
 
     final Map<String, String> p = new HashMap<String, String>();
-    p.put("project", URL.encodeQueryString(project.get()));
+    p.put("project", encode(project.get()));
     return baseUrl + pattern.replace(p);
   }
 
@@ -57,8 +61,22 @@
     ParameterizedString pattern = new ParameterizedString(type.getBranch());
 
     final Map<String, String> p = new HashMap<String, String>();
-    p.put("project", URL.encodeQueryString(branch.getParentKey().get()));
-    p.put("branch", URL.encodeQueryString(branch.get()));
+    p.put("project", encode(branch.getParentKey().get()));
+    p.put("branch", encode(branch.get()));
     return baseUrl + pattern.replace(p);
   }
+
+  public String toFileHistory(final Branch.NameKey branch, final String file) {
+    ParameterizedString pattern = new ParameterizedString(type.getFileHistory());
+
+    final Map<String, String> p = new HashMap<String, String>();
+    p.put("project", encode(branch.getParentKey().get()));
+    p.put("branch", encode(branch.get()));
+    p.put("file", encode(file));
+    return baseUrl + pattern.replace(p);
+  }
+
+  private String encode(String segment) {
+    return URL.encodeQueryString(type.replacePathSeparator(segment));
+  }
 }
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 bc978cc..9c79eb2 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
@@ -22,6 +22,14 @@
   protected List<GroupDetail> groups;
   protected boolean canCreateGroup;
 
+  protected GroupList() {
+  }
+
+  public GroupList(final List<GroupDetail> groups, final boolean canCreateGroup) {
+    this.groups = groups;
+    this.canCreateGroup = canCreateGroup;
+  }
+
   public List<GroupDetail> getGroups() {
     return groups;
   }
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 66b0c3b..d90d68b 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
@@ -21,6 +21,7 @@
 public class HostPageData {
   public Account account;
   public AccountDiffPreference accountDiffPref;
+  public String xsrfToken;
   public GerritConfig config;
   public Theme theme;
 
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 0899b6f..c9be1d4 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
@@ -44,6 +44,22 @@
   @SignInRequired
   void deleteDraft(PatchLineComment.Key key, AsyncCallback<VoidResult> callback);
 
+  /**
+   * Deletes the specified draft patch set. If the draft patch set is the only
+   * patch set of the change, then also the change gets deleted.
+   *
+   * @param psid ID of the draft patch set that should be deleted
+   * @param callback callback to report the result of the draft patch set
+   *        deletion operation; if the draft patch set was successfully deleted
+   *        {@link AsyncCallback#onSuccess(Object)} is invoked and the change
+   *        details are passed as parameter; if the change gets deleted because
+   *        the draft patch set that was deleted was the only patch set in the
+   *        change, then <code>null</code> is passed as result to
+   *        {@link AsyncCallback#onSuccess(Object)}
+   */
+  @SignInRequired
+  void deleteDraftPatchSet(PatchSet.Id psid, AsyncCallback<ChangeDetail> callback);
+
   @SignInRequired
   void publishComments(PatchSet.Id psid, String message,
       Set<ApprovalCategoryValue.Id> approvals,
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 2ddd8a7..9b6695e 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
@@ -15,6 +15,7 @@
 package com.google.gerrit.common.data;
 
 public class PermissionRule implements Comparable<PermissionRule> {
+  public static final String FORCE_PUSH = "Force Push";
   public static enum Action {
     ALLOW, DENY, BLOCK,
 
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 168544d..2a80d52 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
@@ -20,14 +20,24 @@
 import java.util.Set;
 
 public class ProjectAccess {
+  protected Project.NameKey projectName;
   protected String revision;
   protected Project.NameKey inheritsFrom;
   protected List<AccessSection> local;
   protected Set<String> ownerOf;
+  protected boolean isConfigVisible;
 
   public ProjectAccess() {
   }
 
+  public Project.NameKey getProjectName() {
+    return projectName;
+  }
+
+  public void setProjectName(Project.NameKey projectName) {
+    this.projectName = projectName;
+  }
+
   public String getRevision() {
     return revision;
   }
@@ -76,4 +86,12 @@
   public void setOwnerOf(Set<String> refs) {
     ownerOf = refs;
   }
+
+  public boolean isConfigVisible() {
+    return isConfigVisible;
+  }
+
+  public void setConfigVisible(boolean isConfigVisible) {
+    this.isConfigVisible = isConfigVisible;
+  }
 }
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 cd02785..4d10a42 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
@@ -20,6 +20,7 @@
 import com.google.gwt.user.client.rpc.AsyncCallback;
 import com.google.gwtjsonrpc.client.RemoteJsonService;
 import com.google.gwtjsonrpc.client.RpcImpl;
+import com.google.gwtjsonrpc.client.VoidResult;
 import com.google.gwtjsonrpc.client.RpcImpl.Version;
 
 import java.util.List;
@@ -27,11 +28,19 @@
 
 @RpcImpl(version = Version.V2_0)
 public interface ProjectAdminService extends RemoteJsonService {
-  void visibleProjects(AsyncCallback<List<Project>> callback);
+  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);
 
+  @SignInRequired
+  void createNewProject(String projectName, String parentName,
+      boolean emptyCommit, boolean permissionsOnly,
+      AsyncCallback<VoidResult> callback);
+
   void projectAccess(Project.NameKey projectName,
       AsyncCallback<ProjectAccess> callback);
 
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectDetail.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectDetail.java
index 02aaf80..7d59c8b 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectDetail.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectDetail.java
@@ -22,6 +22,8 @@
   public boolean canModifyMergeType;
   public boolean canModifyAgreements;
   public boolean canModifyAccess;
+  public boolean canModifyState;
+  public boolean isPermissionOnly;
 
   public ProjectDetail() {
   }
@@ -38,6 +40,10 @@
     canModifyMergeType = cmmt;
   }
 
+  public void setCanModifyState(final boolean cms) {
+    canModifyState = cms;
+  }
+
   public void setCanModifyAgreements(final boolean cma) {
     canModifyAgreements = cma;
   }
@@ -45,4 +51,8 @@
   public void setCanModifyAccess(final boolean cma) {
     canModifyAccess = cma;
   }
+
+  public void setPermissionOnly(final boolean ipo) {
+    isPermissionOnly = ipo;
+  }
 }
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
new file mode 100644
index 0000000..0da45b2
--- /dev/null
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectList.java
@@ -0,0 +1,43 @@
+// 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.Project;
+
+import java.util.List;
+
+public class ProjectList {
+  protected List<Project> projects;
+  protected boolean canCreateProject;
+
+  public ProjectList() {
+  }
+
+  public List<Project> getProjects() {
+    return projects;
+  }
+
+  public void setProjects(List<Project> projects) {
+    this.projects = projects;
+  }
+
+  public boolean canCreateProject() {
+    return canCreateProject;
+  }
+
+  public void setCanCreateProject(boolean canCreateProject) {
+    this.canCreateProject = canCreateProject;
+  }
+}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ReviewResult.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ReviewResult.java
new file mode 100644
index 0000000..9378526
--- /dev/null
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ReviewResult.java
@@ -0,0 +1,122 @@
+// 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.Change;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * Result from performing a review (comment, abandon, etc.)
+ */
+public class ReviewResult {
+  protected List<Error> errors;
+  protected Change.Id changeId;
+
+  public ReviewResult() {
+    errors = new ArrayList<Error>();
+  }
+
+  public void addError(final Error e) {
+    errors.add(e);
+  }
+
+  public List<Error> getErrors() {
+    return errors;
+  }
+
+  public Change.Id getChangeId() {
+    return changeId;
+  }
+
+  public void setChangeId(Change.Id changeId) {
+    this.changeId = changeId;
+  }
+
+  public static class Error {
+    public static enum Type {
+      /** Not permitted to abandon this change. */
+      ABANDON_NOT_PERMITTED,
+
+      /** Not permitted to restore this change. */
+      RESTORE_NOT_PERMITTED,
+
+      /** Not permitted to submit this change. */
+      SUBMIT_NOT_PERMITTED,
+
+      /** Approvals or dependencies are lacking for submission. */
+      SUBMIT_NOT_READY,
+
+      /** Review operation invalid because change is closed. */
+      CHANGE_IS_CLOSED,
+
+      /** Not permitted to publish this draft patch set */
+      PUBLISH_NOT_PERMITTED,
+
+      /** Not permitted to delete this draft patch set */
+      DELETE_NOT_PERMITTED,
+
+      /** Review operation not permitted by rule. */
+      RULE_ERROR,
+
+      /** Review operation invalid because patch set is not a draft. */
+      NOT_A_DRAFT,
+
+      /** Error writing change to git repository */
+      GIT_ERROR
+    }
+
+    protected Type type;
+    protected String message;
+
+    protected Error() {
+    }
+
+    public Error(final Type type) {
+      this.type = type;
+      this.message = null;
+    }
+
+    public Error(final Type type, final String message) {
+      this.type = type;
+      this.message = message;
+    }
+
+    public Type getType() {
+      return type;
+    }
+
+    public String getMessage() {
+      return message;
+    }
+
+    public String getMessageOrType() {
+      if (message != null) {
+        return message;
+      }
+      return "" + type;
+    }
+
+    @Override
+    public String toString() {
+      String ret = type + "";
+      if (message != null) {
+        ret += " " + message;
+      }
+      return ret;
+    }
+  }
+}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/errors/ProjectCreationFailedException.java b/gerrit-common/src/main/java/com/google/gerrit/common/errors/ProjectCreationFailedException.java
new file mode 100644
index 0000000..0437d77
--- /dev/null
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/errors/ProjectCreationFailedException.java
@@ -0,0 +1,29 @@
+// 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.errors;
+
+/** Error indicating failed to create new project. */
+public class ProjectCreationFailedException extends Exception {
+  private static final long serialVersionUID = 1L;
+
+  public ProjectCreationFailedException(final String message) {
+    this(message, null);
+  }
+
+  public ProjectCreationFailedException(final String message,
+      final Throwable why) {
+    super(message, why);
+  }
+}
diff --git a/gerrit-common/src/test/java/com/google/gerrit/common/data/EncodePathSeparatorTest.java b/gerrit-common/src/test/java/com/google/gerrit/common/data/EncodePathSeparatorTest.java
new file mode 100644
index 0000000..e00fa48
--- /dev/null
+++ b/gerrit-common/src/test/java/com/google/gerrit/common/data/EncodePathSeparatorTest.java
@@ -0,0 +1,36 @@
+// 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 junit.framework.TestCase;
+
+public class EncodePathSeparatorTest extends TestCase {
+
+  public void testDefaultBehaviour() {
+
+    GitWebType gitWebType = GitWebType.fromName(null);
+
+    assertEquals("a/b", gitWebType.replacePathSeparator("a/b"));
+  }
+
+  public void testExclamationMark() {
+
+    GitWebType gitWebType = GitWebType.fromName(null);
+    gitWebType.setPathSeparator('!');
+
+    assertEquals("a!b", gitWebType.replacePathSeparator("a/b"));
+  }
+
+}
diff --git a/gerrit-ehcache/.gitignore b/gerrit-ehcache/.gitignore
new file mode 100644
index 0000000..903c6c8
--- /dev/null
+++ b/gerrit-ehcache/.gitignore
@@ -0,0 +1,4 @@
+/target
+/.classpath
+/.project
+/.settings/org.maven.ide.eclipse.prefs
diff --git a/gerrit-ehcache/.settings/org.eclipse.core.resources.prefs b/gerrit-ehcache/.settings/org.eclipse.core.resources.prefs
new file mode 100644
index 0000000..82eb859
--- /dev/null
+++ b/gerrit-ehcache/.settings/org.eclipse.core.resources.prefs
@@ -0,0 +1,3 @@
+#Tue Sep 02 16:59:24 PDT 2008
+eclipse.preferences.version=1
+encoding/<project>=UTF-8
diff --git a/gerrit-ehcache/.settings/org.eclipse.core.runtime.prefs b/gerrit-ehcache/.settings/org.eclipse.core.runtime.prefs
new file mode 100644
index 0000000..8667cfd
--- /dev/null
+++ b/gerrit-ehcache/.settings/org.eclipse.core.runtime.prefs
@@ -0,0 +1,3 @@
+#Tue Sep 02 16:59:24 PDT 2008
+eclipse.preferences.version=1
+line.separator=\n
diff --git a/gerrit-ehcache/.settings/org.eclipse.jdt.core.prefs b/gerrit-ehcache/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 0000000..e89c048
--- /dev/null
+++ b/gerrit-ehcache/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,269 @@
+#Thu Jan 19 12:55:44 PST 2012
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
+org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
+org.eclipse.jdt.core.compiler.compliance=1.6
+org.eclipse.jdt.core.compiler.debug.lineNumber=generate
+org.eclipse.jdt.core.compiler.debug.localVariable=generate
+org.eclipse.jdt.core.compiler.debug.sourceFile=generate
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
+org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
+org.eclipse.jdt.core.compiler.source=1.6
+org.eclipse.jdt.core.formatter.align_type_members_on_columns=false
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=16
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call=16
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation=16
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression=16
+org.eclipse.jdt.core.formatter.alignment_for_assignment=16
+org.eclipse.jdt.core.formatter.alignment_for_binary_expression=16
+org.eclipse.jdt.core.formatter.alignment_for_compact_if=16
+org.eclipse.jdt.core.formatter.alignment_for_conditional_expression=16
+org.eclipse.jdt.core.formatter.alignment_for_enum_constants=16
+org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer=16
+org.eclipse.jdt.core.formatter.alignment_for_multiple_fields=16
+org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation=16
+org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration=16
+org.eclipse.jdt.core.formatter.blank_lines_after_imports=1
+org.eclipse.jdt.core.formatter.blank_lines_after_package=1
+org.eclipse.jdt.core.formatter.blank_lines_before_field=0
+org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration=0
+org.eclipse.jdt.core.formatter.blank_lines_before_imports=0
+org.eclipse.jdt.core.formatter.blank_lines_before_member_type=0
+org.eclipse.jdt.core.formatter.blank_lines_before_method=1
+org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk=1
+org.eclipse.jdt.core.formatter.blank_lines_before_package=0
+org.eclipse.jdt.core.formatter.blank_lines_between_import_groups=1
+org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations=2
+org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_array_initializer=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_block=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_block_in_case=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_enum_constant=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_method_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_switch=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_type_declaration=end_of_line
+org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment=false
+org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment=false
+org.eclipse.jdt.core.formatter.comment.format_block_comments=true
+org.eclipse.jdt.core.formatter.comment.format_header=true
+org.eclipse.jdt.core.formatter.comment.format_html=true
+org.eclipse.jdt.core.formatter.comment.format_javadoc_comments=true
+org.eclipse.jdt.core.formatter.comment.format_line_comments=true
+org.eclipse.jdt.core.formatter.comment.format_source_code=true
+org.eclipse.jdt.core.formatter.comment.indent_parameter_description=false
+org.eclipse.jdt.core.formatter.comment.indent_root_tags=true
+org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags=insert
+org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter=do not insert
+org.eclipse.jdt.core.formatter.comment.line_length=80
+org.eclipse.jdt.core.formatter.compact_else_if=true
+org.eclipse.jdt.core.formatter.continuation_indentation=2
+org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer=2
+org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line=false
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header=true
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header=true
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header=true
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header=true
+org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases=true
+org.eclipse.jdt.core.formatter.indent_empty_lines=false
+org.eclipse.jdt.core.formatter.indent_statements_compare_to_block=true
+org.eclipse.jdt.core.formatter.indent_statements_compare_to_body=true
+org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases=true
+org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch=true
+org.eclipse.jdt.core.formatter.indentation.size=4
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_member=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_annotation_declaration=insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_anonymous_type_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_block=insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_declaration=insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_method_body=insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter=insert
+org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_binary_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters=insert
+org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block=insert
+org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters=insert
+org.eclipse.jdt.core.formatter.insert_space_after_ellipsis=insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional=insert
+org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_after_unary_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter=insert
+org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_binary_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert=insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional=insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_ellipsis=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while=insert
+org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return=insert
+org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw=insert
+org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional=insert
+org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_semicolon=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_unary_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation=do not insert
+org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line=false
+org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line=false
+org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line=true
+org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line=false
+org.eclipse.jdt.core.formatter.lineSplit=80
+org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column=false
+org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column=false
+org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body=0
+org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve=3
+org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line=false
+org.eclipse.jdt.core.formatter.tabulation.char=space
+org.eclipse.jdt.core.formatter.tabulation.size=2
+org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations=false
+org.eclipse.jdt.core.formatter.wrap_before_binary_operator=true
diff --git a/gerrit-ehcache/.settings/org.eclipse.jdt.ui.prefs b/gerrit-ehcache/.settings/org.eclipse.jdt.ui.prefs
new file mode 100644
index 0000000..d4218a5
--- /dev/null
+++ b/gerrit-ehcache/.settings/org.eclipse.jdt.ui.prefs
@@ -0,0 +1,61 @@
+#Wed Jul 29 11:31:38 PDT 2009
+eclipse.preferences.version=1
+editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true
+formatter_profile=_Google Format
+formatter_settings_version=11
+org.eclipse.jdt.ui.ignorelowercasenames=true
+org.eclipse.jdt.ui.importorder=com.google;com;junit;net;org;java;javax;
+org.eclipse.jdt.ui.ondemandthreshold=99
+org.eclipse.jdt.ui.staticondemandthreshold=99
+org.eclipse.jdt.ui.text.custom_code_templates=<?xml version\="1.0" encoding\="UTF-8" standalone\="no"?><templates/>
+sp_cleanup.add_default_serial_version_id=true
+sp_cleanup.add_generated_serial_version_id=false
+sp_cleanup.add_missing_annotations=false
+sp_cleanup.add_missing_deprecated_annotations=true
+sp_cleanup.add_missing_methods=false
+sp_cleanup.add_missing_nls_tags=false
+sp_cleanup.add_missing_override_annotations=true
+sp_cleanup.add_serial_version_id=false
+sp_cleanup.always_use_blocks=true
+sp_cleanup.always_use_parentheses_in_expressions=false
+sp_cleanup.always_use_this_for_non_static_field_access=false
+sp_cleanup.always_use_this_for_non_static_method_access=false
+sp_cleanup.convert_to_enhanced_for_loop=false
+sp_cleanup.correct_indentation=false
+sp_cleanup.format_source_code=false
+sp_cleanup.format_source_code_changes_only=false
+sp_cleanup.make_local_variable_final=true
+sp_cleanup.make_parameters_final=true
+sp_cleanup.make_private_fields_final=true
+sp_cleanup.make_type_abstract_if_missing_method=false
+sp_cleanup.make_variable_declarations_final=false
+sp_cleanup.never_use_blocks=false
+sp_cleanup.never_use_parentheses_in_expressions=true
+sp_cleanup.on_save_use_additional_actions=true
+sp_cleanup.organize_imports=false
+sp_cleanup.qualify_static_field_accesses_with_declaring_class=false
+sp_cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true
+sp_cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true
+sp_cleanup.qualify_static_member_accesses_with_declaring_class=false
+sp_cleanup.qualify_static_method_accesses_with_declaring_class=false
+sp_cleanup.remove_private_constructors=true
+sp_cleanup.remove_trailing_whitespaces=true
+sp_cleanup.remove_trailing_whitespaces_all=true
+sp_cleanup.remove_trailing_whitespaces_ignore_empty=false
+sp_cleanup.remove_unnecessary_casts=false
+sp_cleanup.remove_unnecessary_nls_tags=false
+sp_cleanup.remove_unused_imports=false
+sp_cleanup.remove_unused_local_variables=false
+sp_cleanup.remove_unused_private_fields=true
+sp_cleanup.remove_unused_private_members=false
+sp_cleanup.remove_unused_private_methods=true
+sp_cleanup.remove_unused_private_types=true
+sp_cleanup.sort_members=false
+sp_cleanup.sort_members_all=false
+sp_cleanup.use_blocks=false
+sp_cleanup.use_blocks_only_for_return_and_throw=false
+sp_cleanup.use_parentheses_in_expressions=false
+sp_cleanup.use_this_for_non_static_field_access=false
+sp_cleanup.use_this_for_non_static_field_access_only_if_necessary=true
+sp_cleanup.use_this_for_non_static_method_access=false
+sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=true
diff --git a/gerrit-ehcache/pom.xml b/gerrit-ehcache/pom.xml
new file mode 100644
index 0000000..417a978
--- /dev/null
+++ b/gerrit-ehcache/pom.xml
@@ -0,0 +1,47 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<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.3-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>gerrit-ehcache</artifactId>
+  <name>Gerrit Code Review - Ehcache Bindings</name>
+
+  <description>
+    Bindings to Ehcache
+  </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>
+  </dependencies>
+</project>
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
new file mode 100644
index 0000000..c25c381
--- /dev/null
+++ b/gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/EhcachePoolImpl.java
@@ -0,0 +1,271 @@
+// 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-server/src/main/java/com/google/gerrit/server/cache/PopulatingCache.java b/gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/PopulatingCache.java
similarity index 92%
rename from gerrit-server/src/main/java/com/google/gerrit/server/cache/PopulatingCache.java
rename to gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/PopulatingCache.java
index 0822cc0..f5c6c45 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/PopulatingCache.java
+++ b/gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/PopulatingCache.java
@@ -12,9 +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.ehcache;
 
-import static java.util.concurrent.TimeUnit.SECONDS;
+import com.google.gerrit.server.cache.Cache;
+import com.google.gerrit.server.cache.EntryCreator;
 
 import net.sf.ehcache.CacheException;
 import net.sf.ehcache.Ehcache;
@@ -24,8 +25,6 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.util.concurrent.TimeUnit;
-
 /**
  * A decorator for {@link Cache} which automatically constructs missing entries.
  * <p>
@@ -109,12 +108,6 @@
   }
 
   @Override
-  public long getTimeToLive(final TimeUnit unit) {
-    final long maxAge = self.getCacheConfiguration().getTimeToLiveSeconds();
-    return unit.convert(maxAge, SECONDS);
-  }
-
-  @Override
   public String toString() {
     return "Cache[" + self.getName() + "]";
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/SimpleCache.java b/gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/SimpleCache.java
similarity index 86%
rename from gerrit-server/src/main/java/com/google/gerrit/server/cache/SimpleCache.java
rename to gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/SimpleCache.java
index 2283f96..e4428e3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/SimpleCache.java
+++ b/gerrit-ehcache/src/main/java/com/google/gerrit/ehcache/SimpleCache.java
@@ -12,9 +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.ehcache;
 
-import static java.util.concurrent.TimeUnit.SECONDS;
+import com.google.gerrit.server.cache.Cache;
 
 import net.sf.ehcache.CacheException;
 import net.sf.ehcache.Ehcache;
@@ -23,8 +23,6 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.util.concurrent.TimeUnit;
-
 /**
  * A fast in-memory and/or on-disk based cache.
  *
@@ -77,12 +75,6 @@
   }
 
   @Override
-  public long getTimeToLive(final TimeUnit unit) {
-    final long maxAge = self.getCacheConfiguration().getTimeToLiveSeconds();
-    return unit.convert(maxAge, SECONDS);
-  }
-
-  @Override
   public String toString() {
     return "Cache[" + self.getName() + "]";
   }
diff --git a/gerrit-gwtdebug/.gitignore b/gerrit-gwtdebug/.gitignore
index 903c6c8..194bedc 100644
--- a/gerrit-gwtdebug/.gitignore
+++ b/gerrit-gwtdebug/.gitignore
@@ -2,3 +2,4 @@
 /.classpath
 /.project
 /.settings/org.maven.ide.eclipse.prefs
+/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
diff --git a/gerrit-gwtdebug/pom.xml b/gerrit-gwtdebug/pom.xml
index b3b1083..33b6851 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.2-SNAPSHOT</version>
+    <version>2.3-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-gwtdebug</artifactId>
diff --git a/gerrit-gwtui/.gitignore b/gerrit-gwtui/.gitignore
index 903c6c8..194bedc 100644
--- a/gerrit-gwtui/.gitignore
+++ b/gerrit-gwtui/.gitignore
@@ -2,3 +2,4 @@
 /.classpath
 /.project
 /.settings/org.maven.ide.eclipse.prefs
+/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
diff --git a/gerrit-gwtui/pom.xml b/gerrit-gwtui/pom.xml
index bb0da11..b09b8e3 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.2-SNAPSHOT</version>
+    <version>2.3-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-gwtui</artifactId>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ConfirmationDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ConfirmationDialog.java
index c4cb770..36169db 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ConfirmationDialog.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ConfirmationDialog.java
@@ -19,8 +19,9 @@
 import com.google.gwt.user.client.DOM;
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.HTML;
+import com.google.gwt.user.client.ui.Widget;
 import com.google.gwtexpui.globalkey.client.GlobalKey;
+import com.google.gwtexpui.safehtml.client.SafeHtml;
 import com.google.gwtexpui.user.client.AutoCenterDialogBox;
 
 public class ConfirmationDialog extends AutoCenterDialogBox {
@@ -28,7 +29,7 @@
 
   private Button cancelButton;
 
-  public ConfirmationDialog(final String dialogTitle, final HTML message,
+  public ConfirmationDialog(final String dialogTitle, final SafeHtml message,
       final ConfirmationCallback callback) {
     super(/* auto hide */false, /* modal */true);
     setGlassEnabled(true);
@@ -59,11 +60,12 @@
     buttons.add(cancelButton);
 
     final FlowPanel center = new FlowPanel();
-    center.add(message);
+    final Widget msgWidget = message.toBlockWidget();
+    center.add(msgWidget);
     center.add(buttons);
     add(center);
 
-    message.setWidth("400px");
+    msgWidget.setWidth("400px");
 
     setWidget(center);
   }
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 b439e6c..2e8cd5e 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,6 +14,7 @@
 
 package com.google.gerrit.client;
 
+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.MINE;
@@ -45,6 +46,7 @@
 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.CreateProjectScreen;
 import com.google.gerrit.client.admin.GroupListScreen;
 import com.google.gerrit.client.admin.ProjectAccessScreen;
 import com.google.gerrit.client.admin.ProjectBranchesScreen;
@@ -277,7 +279,6 @@
 
   private static String legacyChange(final String token) {
     final String s = skip(token);
-    final String q = "patchset=";
     final String t[] = s.split(",", 2);
     if (t.length > 1 && matchPrefix("patchset=", t[1])) {
       return PageLinks.toChange(PatchSet.Id.parse(t[0] + "," + skip(t[1])));
@@ -580,6 +581,10 @@
         } else if (matchPrefix("/admin/projects/", token)) {
           Gerrit.display(token, selectProject());
 
+        } else if (matchExact(ADMIN_CREATE_PROJECT, token)
+            || matchExact("/admin/create-project", token)) {
+          Gerrit.display(token, new CreateProjectScreen());
+
         } else {
           Gerrit.display(token, new NotFoundScreen());
         }
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 c915d1e..16a2d35 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
@@ -109,7 +109,7 @@
   public static String nameEmail(final AccountInfo acct) {
     String name = acct.getFullName();
     if (name == null) {
-      name = Gerrit.C.anonymousCoward();
+      name = Gerrit.getConfig().getAnonymousCowardName();
     }
 
     final StringBuilder b = new StringBuilder();
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 f775534..c04167e 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
@@ -42,6 +42,7 @@
 import com.google.gwt.event.logical.shared.ValueChangeEvent;
 import com.google.gwt.event.logical.shared.ValueChangeHandler;
 import com.google.gwt.http.client.URL;
+import com.google.gwt.http.client.UrlBuilder;
 import com.google.gwt.user.client.Command;
 import com.google.gwt.user.client.Cookies;
 import com.google.gwt.user.client.History;
@@ -73,13 +74,13 @@
   public static final GerritResources RESOURCES =
       GWT.create(GerritResources.class);
   public static final SystemInfoService SYSTEM_SVC;
-  private static final String SESSION_COOKIE = "GerritAccount";
 
   private static String myHost;
   private static GerritConfig myConfig;
   private static HostPageData.Theme myTheme;
   private static Account myAccount;
   private static AccountDiffPreference myAccountDiffPref;
+  private static String xsrfToken;
 
   private static MorphingTabPanel menuLeft;
   private static LinkMenuBar menuRight;
@@ -188,13 +189,7 @@
    */
   public static void updateImpl(final String token) {
     History.newItem(token, false);
-
-    if (historyHooks != null) {
-      // Because we blocked firing the event our history hooks won't be
-      // informed of the current token. Manually fire the event to them.
-      //
-      dispatchHistoryHooks(token);
-    }
+    dispatchHistoryHooks(token);
   }
 
   public static void setQueryString(String query) {
@@ -241,16 +236,17 @@
   }
 
   /** Sign the user into the application. */
-  public static void doSignIn(final String token) {
+  public static void doSignIn(String token) {
     switch (myConfig.getAuthType()) {
       case HTTP:
       case HTTP_LDAP:
       case CLIENT_SSL_CERT_LDAP:
-        Location.assign(Location.getPath() + "login/" + token);
+      case CUSTOM_EXTENSION:
+        Location.assign(loginRedirect(token));
         break;
 
       case DEVELOPMENT_BECOME_ANY_ACCOUNT:
-        Location.assign(Location.getPath() + "become");
+        Location.assign(selfRedirect("/become"));
         break;
 
       case OPENID:
@@ -264,11 +260,61 @@
     }
   }
 
+  private static String loginRedirect(String token) {
+    if (token == null) {
+      token = "";
+    } else if (token.startsWith("/")) {
+      token = token.substring(1);
+    }
+    return selfRedirect("/login/" + token);
+  }
+
+  private 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();
+    if (path == null || path.isEmpty()) {
+      path = "/";
+    } else {
+      while (path.startsWith("//")) {
+        path = path.substring(1);
+      }
+      while (path.endsWith("//")) {
+        path = path.substring(0, path.length() - 1);
+      }
+      if (!path.endsWith("/")) {
+        path = path + "/";
+      }
+    }
+
+    if (suffix != null) {
+      while (suffix.startsWith("/")) {
+        suffix = suffix.substring(1);
+      }
+      path += suffix;
+    }
+
+    UrlBuilder builder = new UrlBuilder();
+    builder.setProtocol(Location.getProtocol());
+    builder.setHost(Location.getHost());
+    String port = Location.getPort();
+    if (port != null && !port.isEmpty()) {
+      builder.setPort(Integer.parseInt(port));
+    }
+    builder.setPath(path);
+    return builder.buildString();
+  }
+
   static void deleteSessionCookie() {
-    Cookies.removeCookie(SESSION_COOKIE);
     myAccount = null;
     myAccountDiffPref = null;
+    xsrfToken = null;
     refreshMenuBar();
+
+    // If the cookie was HttpOnly, this request to delete it will
+    // most likely not be successful.  We can try anyway though.
+    //
+    Cookies.removeCookie("GerritAccount");
   }
 
   public void onModuleLoad() {
@@ -305,9 +351,11 @@
         myTheme = result.theme;
         if (result.account != null) {
           myAccount = result.account;
+          xsrfToken = result.xsrfToken;
         }
         if (result.accountDiffPref != null) {
           myAccountDiffPref = result.accountDiffPref;
+          applyUserPreferences();
         }
         onModuleLoad2();
       }
@@ -327,6 +375,7 @@
   }
 
   private static ArrayList<JavaScriptObject> historyHooks;
+  private static Anchor signInAnchor;
 
   private static native void initHistoryHooks()
   /*-{ $wnd['gerrit_addHistoryHook'] = function(h) { @com.google.gerrit.client.Gerrit::addHistoryHook(Lcom/google/gwt/core/client/JavaScriptObject;)(h); }; }-*/;
@@ -348,9 +397,14 @@
   /*-{ hook(url); }-*/;
 
   private static void dispatchHistoryHooks(final String historyToken) {
-    final String url = Location.getPath() + "#" + historyToken;
-    for (final JavaScriptObject hook : historyHooks) {
-      callHistoryHook(hook, url);
+    if (signInAnchor != null) {
+      signInAnchor.setHref(loginRedirect(historyToken));
+    }
+    if (historyHooks != null) {
+      final String url = Location.getPath() + "#" + historyToken;
+      for (final JavaScriptObject hook : historyHooks) {
+        callHistoryHook(hook, url);
+      }
     }
   }
 
@@ -415,9 +469,7 @@
         final String token = view.getToken();
         if (!token.equals(History.getToken())) {
           History.newItem(token, false);
-          if (historyHooks != null) {
-            dispatchHistoryHooks(token);
-          }
+          dispatchHistoryHooks(token);
         }
 
         if (view instanceof ChangeListScreen) {
@@ -436,7 +488,7 @@
     JsonUtil.setDefaultXsrfManager(new XsrfManager() {
       @Override
       public String getToken(JsonDefTarget proxy) {
-        return Cookies.getCookie(SESSION_COOKIE);
+        return xsrfToken;
       }
 
       @Override
@@ -461,15 +513,16 @@
     });
     JumpKeys.register(body);
 
-    if ("".equals(History.getToken())) {
-      if (isSignedIn()) {
-        display(PageLinks.MINE);
-      } else {
-        display(PageLinks.toChangeQuery("status:open"));
-      }
-    } else {
-      display(History.getToken());
+    String token = History.getToken();
+    if (token.isEmpty()) {
+      token = isSignedIn()
+          ? PageLinks.MINE
+          : PageLinks.toChangeQuery("status:open");
     }
+    if (signInAnchor != null) {
+      signInAnchor.setHref(loginRedirect(token));
+    }
+    display(token);
   }
 
   public static void refreshMenuBar() {
@@ -528,7 +581,7 @@
       whoAmI();
       addLink(menuRight, C.menuSettings(), PageLinks.SETTINGS);
       if (cfg.getAuthType() != AuthType.CLIENT_SSL_CERT_LDAP) {
-        menuRight.add(anchor(C.menuSignOut(), "logout"));
+        menuRight.add(anchor(C.menuSignOut(), selfRedirect("/logout")));
       }
     } else {
       switch (cfg.getAuthType()) {
@@ -553,18 +606,16 @@
 
         case LDAP:
         case LDAP_BIND:
+        case CUSTOM_EXTENSION:
           if (cfg.getRegisterUrl() != null) {
             menuRight.add(anchor(C.menuRegister(), cfg.getRegisterUrl()));
           }
-          menuRight.addItem(C.menuSignIn(), new Command() {
-            public void execute() {
-              doSignIn(History.getToken());
-            }
-          });
+          signInAnchor = anchor(C.menuSignIn(), loginRedirect(History.getToken()));
+          menuRight.add(signInAnchor);
           break;
 
         case DEVELOPMENT_BECOME_ANY_ACCOUNT:
-          menuRight.add(anchor("Become", "become"));
+          menuRight.add(anchor("Become", selfRedirect("/become")));
           break;
       }
     }
@@ -635,7 +686,7 @@
 
   private static void addDocLink(final LinkMenuBar m, final String text,
       final String href) {
-    final Anchor atag = anchor(text, "Documentation/" + href);
+    final Anchor atag = anchor(text, selfRedirect("/Documentation/" + href));
     atag.setTarget("_blank");
     m.add(atag);
   }
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 b4293dc..f2a3d3e 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
@@ -82,7 +82,6 @@
   String searchButton();
 
   String rpcStatusLoading();
-  String anonymousCoward();
 
   String sectionNavigation();
   String sectionActions();
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 7b06788..3880f59 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
@@ -65,7 +65,6 @@
 searchButton = Search
 
 rpcStatusLoading = Loading ...
-anonymousCoward = Anonymous Coward
 
 sectionNavigation = Navigation
 sectionActions = Actions
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 57f9c39..70cfdbc 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
@@ -80,6 +80,8 @@
   String contributorAgreementLegal();
   String contributorAgreementShortDescription();
   String coverMessage();
+  String createProjectLink();
+  String createProjectPanel();
   String dataCell();
   String dataHeader();
   String diffLinkCell();
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 53a2b58..c886216 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
@@ -56,6 +56,7 @@
   String buttonChangeUserName();
   String buttonClearPassword();
   String buttonGeneratePassword();
+  String linkObtainPassword();
   String invalidUserName();
   String invalidUserEmail();
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountConstants.properties
index ffc5218..499f051 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
@@ -37,6 +37,7 @@
 buttonChangeUserName = Change Username
 buttonClearPassword = Clear Password
 buttonGeneratePassword = Generate Password
+linkObtainPassword = Obtain Password
 invalidUserName = Username must contain only letters, numbers, _, - or .
 invalidUserEmail = Email format is wrong.
 sshKeyInvalid = Invalid Key
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelShort.java
index aa179ab..8c6ba2e 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
@@ -28,6 +28,7 @@
 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.gwt.user.client.rpc.AsyncCallback;
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.Composite;
 import com.google.gwt.user.client.ui.FlowPanel;
@@ -118,7 +119,7 @@
     save.addClickHandler(new ClickHandler() {
       @Override
       public void onClick(final ClickEvent event) {
-        doSave();
+        doSave(null);
       }
     });
     new OnEditEnabler(save, nameTxt);
@@ -309,7 +310,7 @@
     inEmail.setFocus(true);
   }
 
-  void doSave() {
+  void doSave(final AsyncCallback<Account> onSave) {
     String newName = canEditFullName() ? nameTxt.getText() : null;
     if ("".equals(newName)) {
       newName = null;
@@ -336,6 +337,9 @@
           public void onSuccess(final Account result) {
             registerNewEmail.setEnabled(true);
             onSaveSuccess(result);
+            if (onSave != null) {
+              onSave.onSuccess(result);
+            }
           }
 
           @Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPasswordScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPasswordScreen.java
index 2202ac8..65525eb 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPasswordScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPasswordScreen.java
@@ -23,11 +23,12 @@
 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.gwt.user.client.ui.Anchor;
 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.Widget;
 import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
+import com.google.gwt.user.client.ui.Widget;
 import com.google.gwtexpui.clippy.client.CopyableLabel;
 
 import java.util.List;
@@ -42,6 +43,16 @@
   protected void onInitUI() {
     super.onInitUI();
 
+    String url = Gerrit.getConfig().getHttpPasswordUrl();
+    if (url != null) {
+      Anchor link = new Anchor();
+      link.setText(Util.C.linkObtainPassword());
+      link.setHref(url);
+      link.setTarget("_blank");
+      add(link);
+      return;
+    }
+
     password = new CopyableLabel("");
     password.addStyleName(Gerrit.RESOURCES.css().accountPassword());
 
@@ -84,6 +95,11 @@
   protected void onLoad() {
     super.onLoad();
 
+    if (password == null) {
+      display();
+      return;
+    }
+
     enableUI(false);
     Util.ACCOUNT_SEC
         .myExternalIds(new ScreenLoadCallback<List<AccountExternalId>>(this) {
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 41f0540..5a62eac 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
@@ -22,7 +22,7 @@
 import com.google.gerrit.client.ui.ProjectsTable;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.data.AccountProjectWatchInfo;
-import com.google.gerrit.reviewdb.Project;
+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;
@@ -362,10 +362,10 @@
 
   protected void populateProjects() {
     Util.PROJECT_SVC.visibleProjects(
-        new GerritCallback<List<Project>>() {
+        new GerritCallback<ProjectList>() {
       @Override
-      public void onSuccess(final List<Project> result) {
-        projectsTab.display(result);
+      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 15b0e4e..30638da 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,6 +22,7 @@
 import com.google.gerrit.client.ui.SmallHeading;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.data.AgreementInfo;
+import com.google.gerrit.reviewdb.Account;
 import com.google.gerrit.reviewdb.AccountAgreement;
 import com.google.gerrit.reviewdb.AccountGroupAgreement;
 import com.google.gerrit.reviewdb.ContributorAgreement;
@@ -33,6 +34,7 @@
 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.user.client.rpc.AsyncCallback;
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.FormPanel;
@@ -213,8 +215,22 @@
     }
 
     if (contactGroup.isVisible()) {
-      contactPanel.doSave();
+      contactPanel.doSave(new AsyncCallback<Account>() {
+        @Override
+        public void onSuccess(Account result) {
+          doEnterAgreement();
+        }
+
+        @Override
+        public void onFailure(Throwable caught) {
+        }
+      });
+    } else {
+      doEnterAgreement();
     }
+  }
+
+  private void doEnterAgreement() {
     Util.ACCOUNT_SEC.enterAgreement(current.getId(),
         new GerritCallback<VoidResult>() {
           public void onSuccess(final VoidResult result) {
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 8c99d45..605d7f3 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
@@ -99,18 +99,20 @@
       formBody.add(fp);
     }
 
-    final FlowPanel sshKeyGroup = new FlowPanel();
-    sshKeyGroup.setStyleName(Gerrit.RESOURCES.css().registerScreenSection());
-    sshKeyGroup.add(new SmallHeading(Util.C.welcomeSshKeyHeading()));
-    final HTML whySshKey = new HTML(Util.C.welcomeSshKeyText());
-    whySshKey.setStyleName(Gerrit.RESOURCES.css().registerScreenExplain());
-    sshKeyGroup.add(whySshKey);
-    sshKeyGroup.add(new SshPanel() {
-      {
-        setKeyTableVisible(false);
-      }
-    });
-    formBody.add(sshKeyGroup);
+    if (Gerrit.getConfig().getSshdAddress() != null) {
+      final FlowPanel sshKeyGroup = new FlowPanel();
+      sshKeyGroup.setStyleName(Gerrit.RESOURCES.css().registerScreenSection());
+      sshKeyGroup.add(new SmallHeading(Util.C.welcomeSshKeyHeading()));
+      final HTML whySshKey = new HTML(Util.C.welcomeSshKeyText());
+      whySshKey.setStyleName(Gerrit.RESOURCES.css().registerScreenExplain());
+      sshKeyGroup.add(whySshKey);
+      sshKeyGroup.add(new SshPanel() {
+        {
+          setKeyTableVisible(false);
+        }
+      });
+      formBody.add(sshKeyGroup);
+    }
 
     final FlowPanel choices = new FlowPanel();
     choices.setStyleName(Gerrit.RESOURCES.css().registerScreenNextLinks());
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SettingsScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SettingsScreen.java
index 9356018..97b2efb 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SettingsScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SettingsScreen.java
@@ -26,7 +26,9 @@
     link(Util.C.tabPreferences(), PageLinks.SETTINGS_PREFERENCES);
     link(Util.C.tabWatchedProjects(), PageLinks.SETTINGS_PROJECTS);
     link(Util.C.tabContactInformation(), PageLinks.SETTINGS_CONTACT);
-    link(Util.C.tabSshKeys(), PageLinks.SETTINGS_SSHKEYS);
+    if (Gerrit.getConfig().getSshdAddress() != null) {
+      link(Util.C.tabSshKeys(), PageLinks.SETTINGS_SSHKEYS);
+    }
     link(Util.C.tabHttpAccess(), PageLinks.SETTINGS_HTTP_PASSWORD);
     link(Util.C.tabWebIdentities(), PageLinks.SETTINGS_WEBIDENT);
     link(Util.C.tabMyGroups(), PageLinks.SETTINGS_MYGROUPS);
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 ab4ab7f..5ce90e7 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
@@ -130,6 +130,7 @@
             new GerritCallback<GroupDetail>() {
               public void onSuccess(final GroupDetail groupDetail) {
                 saveName.setEnabled(false);
+                setPageTitle(Util.M.group(groupDetail.group.getName()));
                 display(groupDetail);
               }
             });
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 adad511..c928eb9 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
@@ -31,10 +31,13 @@
   String buttonSaveDescription();
   String buttonRenameGroup();
   String buttonCreateGroup();
+  String buttonCreateProject();
   String buttonChangeGroupOwner();
   String buttonChangeGroupType();
   String buttonSelectGroup();
   String buttonSaveChanges();
+  String checkBoxEmptyCommit();
+  String checkBoxPermissionsOnly();
   String useContentMerge();
   String useContributorAgreements();
   String useSignedOffBy();
@@ -45,6 +48,7 @@
   String descriptionNotifications();
   String buttonSaveGroupOptions();
   String suggestedGroupLabel();
+  String parentSuggestions();
 
   String headingGroupUUID();
   String headingOwner();
@@ -56,6 +60,9 @@
   String noMembersInfo();
   String headingExternalGroup();
   String headingCreateGroup();
+  String headingCreateProject();
+  String headingParentProjectName();
+  String columnProjectName();
   String headingAgreements();
 
   String projectSubmitType_FAST_FORWARD_ONLY();
@@ -63,6 +70,10 @@
   String projectSubmitType_MERGE_IF_NECESSARY();
   String projectSubmitType_CHERRY_PICK();
 
+  String projectState_ACTIVE();
+  String projectState_READ_ONLY();
+  String projectState_HIDDEN();
+
   String groupType_SYSTEM();
   String groupType_INTERNAL();
   String groupType_LDAP();
@@ -89,6 +100,7 @@
   String groupTabGeneral();
   String groupTabMembers();
   String projectListTitle();
+  String createProjectTitle();
   String projectAdminTabGeneral();
   String projectAdminTabBranches();
   String projectAdminTabAccess();
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 f782a63..f2a8ad6 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
@@ -10,10 +10,13 @@
 buttonRenameGroup = Rename Group
 buttonSaveDescription = Save Description
 buttonCreateGroup = Create Group
+buttonCreateProject = Create Project
 buttonChangeGroupOwner = Change Owner
 buttonChangeGroupType = Change Type
 buttonSelectGroup = Select
 buttonSaveChanges = Save Changes
+checkBoxEmptyCommit = Create initial empty commit
+checkBoxPermissionsOnly = Only serve as parent for other projects
 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
@@ -22,6 +25,9 @@
 isVisibleToAll = Make group visible to all registered users.
 buttonSaveGroupOptions = Save Group Options
 suggestedGroupLabel = group
+headingParentProjectName = Rights Inherit From
+parentSuggestions = Parent Suggestion
+columnProjectName = Project Name
 
 emailOnlyAuthors = Authors
 descriptionNotifications = Send email notifications about comments and actions by users in this group only to:
@@ -36,6 +42,7 @@
 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
@@ -43,6 +50,10 @@
 projectSubmitType_MERGE_ALWAYS = Always Merge
 projectSubmitType_CHERRY_PICK = Cherry Pick
 
+projectState_ACTIVE = Active
+projectState_READ_ONLY = Read Only
+projectState_HIDDEN = Hidden
+
 groupType_SYSTEM = System Group
 groupType_INTERNAL = Internal Group
 groupType_LDAP = LDAP Group
@@ -69,6 +80,7 @@
 groupTabGeneral = General
 groupTabMembers = Members
 projectListTitle = Projects
+createProjectTitle = Create Project
 projectAdminTabGeneral = General
 projectAdminTabBranches = Branches
 projectAdminTabAccess = Access
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
new file mode 100644
index 0000000..7f2ae07
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateProjectScreen.java
@@ -0,0 +1,206 @@
+// 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.admin;
+
+import com.google.gerrit.client.Dispatcher;
+import com.google.gerrit.client.ErrorDialog;
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.ui.HintTextBox;
+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.reviewdb.Project;
+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.Anchor;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.CheckBox;
+import com.google.gwt.user.client.ui.Grid;
+import com.google.gwt.user.client.ui.SuggestBox;
+import com.google.gwt.user.client.ui.VerticalPanel;
+import com.google.gwtexpui.globalkey.client.NpTextBox;
+import com.google.gwtjsonrpc.client.VoidResult;
+
+import java.util.List;
+
+public class CreateProjectScreen extends Screen {
+  private NpTextBox project;
+  private Button create;
+  private HintTextBox parent;
+  private SuggestBox sugestParent;
+  private CheckBox emptyCommit;
+  private CheckBox permissionsOnly;
+  private ProjectsTable suggestedParentsTab;
+
+  public CreateProjectScreen() {
+    super();
+    setRequiresSignIn(true);
+  }
+
+  @Override
+  protected void onLoad() {
+    super.onLoad();
+    display();
+  }
+
+  @Override
+  protected void onInitUI() {
+    super.onInitUI();
+    setPageTitle(Util.C.createProjectTitle());
+    addCreateProjectPanel();
+  }
+
+  private void addCreateProjectPanel() {
+    final VerticalPanel fp = new VerticalPanel();
+    fp.setStyleName(Gerrit.RESOURCES.css().createProjectPanel());
+
+    initCreateTxt();
+    initCreateButton();
+    initParentBox();
+
+    addGrid(fp);
+
+    emptyCommit = new CheckBox(Util.C.checkBoxEmptyCommit());
+    permissionsOnly = new CheckBox(Util.C.checkBoxPermissionsOnly());
+    fp.add(emptyCommit);
+    fp.add(permissionsOnly);
+    fp.add(create);
+    VerticalPanel vp=new VerticalPanel();
+    vp.add(fp);
+    initSuggestedParents();
+    vp.add(suggestedParentsTab);
+    add(vp);
+
+  }
+
+  private void initCreateTxt() {
+    project = new NpTextBox();
+    project.setVisibleLength(50);
+    project.addKeyPressHandler(new KeyPressHandler() {
+      @Override
+      public void onKeyPress(KeyPressEvent event) {
+        if (event.getCharCode() == KeyCodes.KEY_ENTER) {
+          doCreateProject();
+        }
+      }
+    });
+  }
+
+  private void initCreateButton() {
+    create = new Button(Util.C.buttonCreateProject());
+    create.addClickHandler(new ClickHandler() {
+      @Override
+      public void onClick(final ClickEvent event) {
+        doCreateProject();
+      }
+    });
+  }
+
+  private void initParentBox() {
+    parent = new HintTextBox();
+    sugestParent =
+        new SuggestBox(new ProjectNameSuggestOracle(), parent);
+    parent.setVisibleLength(50);
+  }
+
+  private void initSuggestedParents() {
+    suggestedParentsTab = new ProjectsTable() {
+      {
+        table.setText(0, 1, Util.C.parentSuggestions());
+      }
+
+      @Override
+      protected void populate(final int row, final Project k) {
+        final Anchor projectLink = new Anchor(k.getName());
+        projectLink.addClickHandler(new ClickHandler() {
+
+          @Override
+          public void onClick(ClickEvent event) {
+            sugestParent.setText(getRowItem(row).getName());
+          }
+        });
+
+        table.setWidget(row, 1, projectLink);
+        table.setText(row, 2, k.getDescription());
+
+        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();
+            }
+          }
+        });
+  }
+
+  private void addGrid(final VerticalPanel fp) {
+    final Grid grid = new Grid(2, 2);
+    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);
+
+    fp.add(grid);
+  }
+
+  private void doCreateProject() {
+    final String projectName = project.getText().trim();
+    final String parentName = sugestParent.getText().trim();
+
+    if ("".equals(projectName)) {
+      project.setFocus(true);
+      return;
+    }
+
+    enableForm(false);
+    Util.PROJECT_SVC.createNewProject(projectName, parentName,
+        emptyCommit.getValue(), permissionsOnly.getValue(),
+        new GerritCallback<VoidResult>() {
+          @Override
+          public void onSuccess(VoidResult result) {
+            History.newItem(Dispatcher.toProjectAdmin(new Project.NameKey(
+                projectName), ProjectScreen.INFO));
+          }
+
+          @Override
+          public void onFailure(Throwable caught) {
+            new ErrorDialog(caught.getMessage()).center();
+            enableForm(true);
+          }
+        });
+  }
+
+  private void enableForm(final boolean enabled) {
+    project.setEnabled(enabled);
+    create.setEnabled(enabled);
+    parent.setEnabled(enabled);
+    emptyCommit.setEnabled(enabled);
+    permissionsOnly.setEnabled(enabled);
+  }
+}
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 f41535d..9da9c22 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
@@ -57,7 +57,7 @@
         suggestions);
     initWidget(suggestBox);
 
-    suggestBox.addKeyPressHandler(new KeyPressHandler() {
+    textBox.addKeyPressHandler(new KeyPressHandler() {
       @Override
       public void onKeyPress(KeyPressEvent event) {
         submitOnSelection = false;
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 76495e7..7585f8a 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
@@ -16,7 +16,6 @@
 
 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.Hyperlink;
 import com.google.gerrit.client.ui.NavigationTable;
 import com.google.gerrit.common.data.GroupDetail;
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 7f5b573..e4cced7 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
@@ -137,6 +137,7 @@
       String ref = section.getName();
       canForce = !ref.startsWith("refs/for/") && !ref.startsWith("^refs/for/");
     }
+    force.setText(PermissionRule.FORCE_PUSH);
     force.setVisible(canForce);
     force.setEnabled(!readOnly);
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionRuleEditor.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionRuleEditor.ui.xml
index e60f3df..26fc229 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionRuleEditor.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionRuleEditor.ui.xml
@@ -80,14 +80,7 @@
 
   <q:Hyperlink ui:field='groupNameLink' styleName='{style.groupName}'/>
   <span ui:field='groupNameSpan' styleName='{style.groupName}'/>
-
-  <g:CheckBox
-      ui:field='force'
-      addStyleNames='{style.forcePush}'
-      text='Force Push'>
-    <ui:attribute name='text'/>
-  </g:CheckBox>
-
+  <g:CheckBox ui:field='force' addStyleNames='{style.forcePush}'/>
   <g:Anchor
       ui:field='deleteRule'
       href='javascript:void'
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 f3c133b..e6e11b0 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
@@ -15,9 +15,12 @@
 package com.google.gerrit.client.admin;
 
 import com.google.gerrit.client.Dispatcher;
+import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.ui.Hyperlink;
 import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.GitwebLink;
 import com.google.gerrit.common.data.ProjectAccess;
+import com.google.gerrit.reviewdb.Branch;
 import com.google.gerrit.reviewdb.Project;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.dom.client.DivElement;
@@ -53,6 +56,12 @@
   Hyperlink parentProject;
 
   @UiField
+  DivElement history;
+
+  @UiField
+  Anchor gitweb;
+
+  @UiField
   FlowPanel localContainer;
   ListEditor<AccessSection, AccessSectionEditor> local;
 
@@ -101,6 +110,16 @@
       inheritsFrom.getStyle().setDisplay(Display.NONE);
     }
 
+    final GitwebLink c = Gerrit.getConfig().getGitwebLink();
+    if (value.isConfigVisible() && c != null) {
+      history.getStyle().setDisplay(Display.BLOCK);
+      gitweb.setText(c.getLinkName());
+      gitweb.setHref(c.toFileHistory(new Branch.NameKey(value.getProjectName(),
+          "refs/meta/config"), "project.config"));
+    } else {
+      history.getStyle().setDisplay(Display.NONE);
+    }
+
     addSection.setVisible(value != null && editing && !value.getOwnerOf().isEmpty());
   }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.ui.xml
index 9360daa..4942b83 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.ui.xml
@@ -33,6 +33,16 @@
     display: inline;
   }
 
+  .history {
+    margin-bottom: 0.5em;
+  }
+  .historyTitle {
+    font-weight: bold;
+  }
+  .gitwebLink {
+    display: inline;
+  }
+
   .addContainer {
     margin-top: 5px;
     font-size: 80%;
@@ -47,6 +57,10 @@
     <span class='{style.parentTitle}'><ui:msg>Rights Inherit From:</ui:msg></span>
     <q:Hyperlink ui:field='parentProject' styleName='{style.parentLink}'/>
   </div>
+  <div ui:field='history' class='{style.history}'>
+    <span class='{style.historyTitle}'><ui:msg>History:</ui:msg></span>
+    <g:Anchor ui:field='gitweb' styleName='{style.gitwebLink}'></g:Anchor>
+  </div>
 
   <g:FlowPanel ui:field='localContainer'/>
   <div class='{style.addContainer}'>
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 406333b..7c51700 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
@@ -40,12 +40,10 @@
 import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
 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.Label;
+import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
 import com.google.gwtjsonrpc.client.RemoteJsonException;
 
-import org.eclipse.jgit.lib.Constants;
-
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
@@ -234,29 +232,32 @@
     }
 
     void deleteChecked() {
-      final StringBuilder message = new StringBuilder();
-      message.append("<b>").append(Gerrit.C.branchDeletionConfirmationMessage()).append("</b>");
-      message.append("<p>");
+      final SafeHtmlBuilder b = new SafeHtmlBuilder();
+      b.openElement("b");
+      b.append(Gerrit.C.branchDeletionConfirmationMessage());
+      b.closeElement("b");
+
+      b.openElement("p");
       final HashSet<Branch.NameKey> ids = new HashSet<Branch.NameKey>();
       for (int row = 1; row < table.getRowCount(); row++) {
         final Branch k = getRowItem(row);
         if (k != null && table.getWidget(row, 1) instanceof CheckBox
             && ((CheckBox) table.getWidget(row, 1)).getValue()) {
           if (!ids.isEmpty()) {
-            message.append(", <br>");
+            b.append(",").br();
           }
-          message.append(k.getName());
+          b.append(k.getName());
           ids.add(k.getNameKey());
         }
       }
-      message.append("</p>");
+      b.closeElement("p");
       if (ids.isEmpty()) {
         return;
       }
 
       ConfirmationDialog confirmationDialog =
           new ConfirmationDialog(Gerrit.C.branchDeletionDialogTitle(),
-              new HTML(message.toString()), new ConfirmationCallback() {
+              b.toSafeHtml(), new ConfirmationCallback() {
         @Override
         public void onOk() {
           Util.PROJECT_SVC.deleteBranch(getProjectKey(), ids,
@@ -310,7 +311,7 @@
       }
 
       if (c != null) {
-        table.setWidget(row, 4, new Anchor("(gitweb)", false, c.toBranch(k
+        table.setWidget(row, 4, new Anchor(c.getLinkName(), false, c.toBranch(k
             .getNameKey())));
       }
 
@@ -318,7 +319,7 @@
       String iconCellStyle = Gerrit.RESOURCES.css().iconCell();
       String dataCellStyle = Gerrit.RESOURCES.css().dataCell();
       if ("refs/meta/config".equals(k.getShortName())
-          || Constants.HEAD.equals(k.getShortName())) {
+          || "HEAD".equals(k.getShortName())) {
         iconCellStyle = Gerrit.RESOURCES.css().specialBranchIconCell();
         dataCellStyle = Gerrit.RESOURCES.css().specialBranchDataCell();
         fmt.setStyleName(row, 0, iconCellStyle);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
index fb35855..a85fe0e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
@@ -39,6 +39,7 @@
   private Panel projectOptionsPanel;
   private CheckBox requireChangeID;
   private ListBox submitType;
+  private ListBox state;
   private CheckBox useContentMerge;
 
   private Panel agreementsPanel;
@@ -79,19 +80,22 @@
         new ScreenLoadCallback<ProjectDetail>(this) {
           public void preDisplay(final ProjectDetail result) {
             enableForm(result.canModifyAgreements,
-                result.canModifyDescription, result.canModifyMergeType);
+                result.canModifyDescription, result.canModifyMergeType, result.canModifyState);
             saveProject.setVisible(
                 result.canModifyAgreements ||
                 result.canModifyDescription ||
-                result.canModifyMergeType);
+                result.canModifyMergeType ||
+                result.canModifyState);
             display(result);
           }
         });
   }
 
   private void enableForm(final boolean canModifyAgreements,
-      final boolean canModifyDescription, final boolean canModifyMergeType) {
+      final boolean canModifyDescription, final boolean canModifyMergeType,
+      final boolean canModifyState) {
     submitType.setEnabled(canModifyMergeType);
+    state.setEnabled(canModifyState);
     useContentMerge.setEnabled(canModifyMergeType);
     descTxt.setEnabled(canModifyDescription);
     useContributorAgreements.setEnabled(canModifyAgreements);
@@ -130,6 +134,14 @@
     saveEnabler.listenTo(submitType);
     projectOptionsPanel.add(submitType);
 
+    state = new ListBox();
+    for (final Project.State stateValue : Project.State.values()) {
+      state.addItem(Util.toLongString(stateValue), stateValue.name());
+    }
+
+    saveEnabler.listenTo(state);
+    projectOptionsPanel.add(state);
+
     useContentMerge = new CheckBox(Util.C.useContentMerge(), true);
     saveEnabler.listenTo(useContentMerge);
     projectOptionsPanel.add(useContentMerge);
@@ -186,6 +198,17 @@
     }
   }
 
+  private void setState(final Project.State newState) {
+    if (state != null) {
+      for (int i = 0; i < state.getItemCount(); i++) {
+        if (newState.name().equals(state.getValue(i))) {
+          state.setSelectedIndex(i);
+          break;
+        }
+      }
+    }
+  }
+
   void display(final ProjectDetail result) {
     project = result.project;
 
@@ -202,6 +225,7 @@
     useContentMerge.setValue(project.isUseContentMerge());
     requireChangeID.setValue(project.isRequireChangeID());
     setSubmitType(project.getSubmitType());
+    setState(project.getState());
 
     saveProject.setEnabled(false);
   }
@@ -216,14 +240,18 @@
       project.setSubmitType(Project.SubmitType.valueOf(submitType
           .getValue(submitType.getSelectedIndex())));
     }
+    if (state.getSelectedIndex() >= 0) {
+      project.setState(Project.State.valueOf(state
+          .getValue(state.getSelectedIndex())));
+    }
 
-    enableForm(false, false, false);
+    enableForm(false, false, false, false);
 
     Util.PROJECT_SVC.changeProjectSettings(project,
         new GerritCallback<ProjectDetail>() {
           public void onSuccess(final ProjectDetail result) {
             enableForm(result.canModifyAgreements,
-                result.canModifyDescription, result.canModifyMergeType);
+                result.canModifyDescription, result.canModifyMergeType, result.canModifyState);
             display(result);
           }
         });
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 758f4de..d89676c 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
@@ -20,24 +20,24 @@
 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.client.ui.SmallHeading;
 import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.common.data.ProjectList;
 import com.google.gerrit.reviewdb.Project;
 import com.google.gwt.user.client.History;
 import com.google.gwt.user.client.ui.VerticalPanel;
 
-import java.util.List;
-
 public class ProjectListScreen extends Screen {
+  private VerticalPanel createProjectLinkPanel;
   private ProjectsTable projects;
 
   @Override
   protected void onLoad() {
     super.onLoad();
-    Util.PROJECT_SVC.visibleProjects(new ScreenLoadCallback<List<Project>>(this) {
+    Util.PROJECT_SVC.visibleProjects(new ScreenLoadCallback<ProjectList>(this) {
       @Override
-      protected void preDisplay(final List<Project> result) {
-        projects.display(result);
+      protected void preDisplay(final ProjectList result) {
+        createProjectLinkPanel.setVisible(result.canCreateProject());
+        projects.display(result.getProjects());
         projects.finishDisplay();
       }
     });
@@ -48,6 +48,13 @@
     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 onOpenRow(final int row) {
@@ -69,10 +76,6 @@
     projects.setSavePointerId(PageLinks.ADMIN_PROJECTS);
 
     add(projects);
-
-    final VerticalPanel fp = new VerticalPanel();
-    fp.setStyleName(Gerrit.RESOURCES.css().addSshKeyPanel());
-    fp.add(new SmallHeading(Util.C.headingCreateGroup()));
   }
 
   @Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/Util.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/Util.java
index c599ee9..e3bcab1 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/Util.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/Util.java
@@ -53,4 +53,20 @@
         return type.name();
     }
   }
+
+  public static String toLongString(final Project.State type) {
+    if (type == null) {
+      return "";
+    }
+    switch (type) {
+      case ACTIVE:
+        return C.projectState_ACTIVE();
+      case READ_ONLY:
+        return C.projectState_READ_ONLY();
+      case HIDDEN:
+        return C.projectState_HIDDEN();
+      default:
+        return type.name();
+    }
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/iconGoogle.gif b/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/iconGoogle.gif
index 782c2fd..748fd20 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/iconGoogle.gif
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/iconGoogle.gif
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/openidLogo.png b/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/openidLogo.png
index 520ff9c..9f8fa73 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/openidLogo.png
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/openidLogo.png
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/userpass/UserPassMessages.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/userpass/UserPassMessages.java
index ebb15d3..2991dc9 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/userpass/UserPassMessages.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/userpass/UserPassMessages.java
@@ -14,8 +14,10 @@
 
 package com.google.gerrit.client.auth.userpass;
 
+import com.google.gerrit.reviewdb.AuthType;
 import com.google.gwt.i18n.client.Messages;
 
 public interface UserPassMessages extends Messages {
   String signInAt(String hostname);
+  String authenticationUnavailable(AuthType authType);
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/userpass/UserPassMessages.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/userpass/UserPassMessages.properties
index ce40ff8..b1eab35 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/userpass/UserPassMessages.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/userpass/UserPassMessages.properties
@@ -1 +1,2 @@
 signInAt = Sign In to Gerrit Code Review at {0}
+authenticationUnavailable = {0} authentication unavailable
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/userpass/UserPassSignInDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/userpass/UserPassSignInDialog.java
index 09d41e5..3463640 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/userpass/UserPassSignInDialog.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/userpass/UserPassSignInDialog.java
@@ -209,7 +209,16 @@
           }
           Location.replace(Location.getPath() + "login" + to);
         } else {
-          showError(Util.C.invalidLogin());
+          final String message;
+          switch (result.getError()) {
+            case AUTHENTICATION_UNAVAILABLE:
+              message = Util.M.authenticationUnavailable(result.getAuthType());
+              break;
+            case INVALID_LOGIN:
+            default:
+              message = Util.C.invalidLogin();
+          }
+          showError(message);
           enable(true);
           password.selectAll();
           Scheduler.get().scheduleDeferred(new ScheduledCommand() {
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 9dac207..d775bbc 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
@@ -41,7 +41,6 @@
 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.HTML;
 import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
 import com.google.gwt.user.client.ui.Image;
 import com.google.gwt.user.client.ui.Panel;
@@ -330,15 +329,15 @@
 
           private void askForConfirmation(final String groupName,
               final int memberCount) {
-            final StringBuilder message = new StringBuilder();
-            message.append("<b>");
-            message.append(Util.M.groupManyMembersConfirmation(groupName,
-                memberCount));
-            message.append("</b>");
+            final SafeHtmlBuilder b = new SafeHtmlBuilder();
+            b.openElement("b");
+            b.append(Util.M
+                .groupManyMembersConfirmation(groupName, memberCount));
+            b.closeElement("b");
             final ConfirmationDialog confirmationDialog =
                 new ConfirmationDialog(Util.C
                     .approvalTableAddManyReviewersConfirmationDialogTitle(),
-                    new HTML(message.toString()), new ConfirmationCallback() {
+                    b.toSafeHtml(), new ConfirmationCallback() {
                       @Override
                       public void onOk() {
                         addReviewers(reviewers, true);
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 83121f9..3cddd2b 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
@@ -21,6 +21,7 @@
   String statusLongSubmitted();
   String statusLongMerged();
   String statusLongAbandoned();
+  String statusLongDraft();
 
   String changesRecentlyClosed();
 
@@ -137,13 +138,20 @@
   String headingRestoreMessage();
   String buttonRestoreChangeSend();
 
+  String buttonPublishPatchSet();
+
+  String buttonDeleteDraftChange();
+  String buttonDeleteDraftPatchSet();
+
   String pagedChangeListPrev();
   String pagedChangeListNext();
 
+  String draftPatchSetLabel();
+
   String reviewed();
   String submitFailed();
   String buttonClose();
 
   String buttonDiffAllSideBySide();
   String buttonDiffAllUnified();
-}
\ No newline at end of file
+}
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 44c96ed..35991f4 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
@@ -2,6 +2,7 @@
 statusLongSubmitted = Submitted, Merge Pending
 statusLongMerged = Merged
 statusLongAbandoned = Abandoned
+statusLongDraft = Draft
 
 starredHeading = Starred Changes
 watchedHeading = Open Changes of Watched Projects
@@ -114,9 +115,16 @@
 headingCoverMessage = Cover Message:
 headingPatchComments = Patch Comments:
 
+buttonPublishPatchSet = Publish
+
+buttonDeleteDraftChange = Delete Draft Change
+buttonDeleteDraftPatchSet = Delete Draft Patch Set
+
 pagedChangeListPrev = &#x21e6;Prev
 pagedChangeListNext = Next&#x21e8;
 
+draftPatchSetLabel = (DRAFT)
+
 upToChangeIconLink = &#x21e7;Up to change
 prevPatchLinkIcon = &#x21e6;
 nextPatchLinkIcon = &#x21e8;
@@ -126,4 +134,4 @@
 buttonClose = Close
 
 buttonDiffAllSideBySide = Diff All Side-by-Side
-buttonDiffAllUnified = Diff All Unified
\ No newline at end of file
+buttonDiffAllUnified = Diff All Unified
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 ed0f2f2..1b382a8 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
@@ -204,7 +204,7 @@
           toggleStar();
         }
       });
-      insertTitleWidget(starChange);
+      setTitleWest(starChange);
     }
 
     descriptionBlock = new ChangeDescriptionBlock();
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 003a731..d3c3f76 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
@@ -17,10 +17,12 @@
 import com.google.gerrit.client.Dispatcher;
 import com.google.gerrit.client.FormatUtil;
 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.ComplexDisclosurePanel;
 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.GitwebLink;
 import com.google.gerrit.common.data.PatchSetDetail;
@@ -48,10 +50,11 @@
 import com.google.gwt.user.client.ui.DisclosurePanel;
 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.InlineLabel;
 import com.google.gwt.user.client.ui.Panel;
-import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
 import com.google.gwtexpui.clippy.client.CopyableLabel;
+import com.google.gwtjsonrpc.client.VoidResult;
 
 import java.util.HashSet;
 import java.util.List;
@@ -111,11 +114,17 @@
     getHeader().add(revtxt);
     if (gw != null) {
       final Anchor revlink =
-          new Anchor("(gitweb)", false, gw.toRevision(detail.getChange()
+          new Anchor(gw.getLinkName(), false, gw.toRevision(detail.getChange()
               .getProject(), ps));
       revlink.addStyleName(Gerrit.RESOURCES.css().patchSetLink());
       getHeader().add(revlink);
     }
+
+    if (ps.isDraft()) {
+      final InlineLabel draftLabel = new InlineLabel(Util.C.draftPatchSetLabel());
+      draftLabel.addStyleName(Gerrit.RESOURCES.css().patchSetRevision());
+      getHeader().add(draftLabel);
+    }
   }
 
   public void setDiffBaseId(PatchSet.Id diffBaseId) {
@@ -163,9 +172,20 @@
       actionsPanel.setStyleName(Gerrit.RESOURCES.css().patchSetActions());
       body.add(actionsPanel);
       if (Gerrit.isSignedIn()) {
-        populateReviewAction();
-        if (changeDetail.isCurrentPatchSet(detail)) {
-          populateActions(detail);
+        if (changeDetail.canEdit()) {
+          populateReviewAction();
+          if (changeDetail.isCurrentPatchSet(detail)) {
+            populateActions(detail);
+          }
+        }
+        if (detail.getPatchSet().isDraft()) {
+          if (changeDetail.canPublish()) {
+            populatePublishAction();
+          }
+          if (changeDetail.canDeleteDraft() &&
+              changeDetail.getPatchSets().size() > 1) {
+            populateDeleteDraftPatchSetAction();
+          }
         }
       }
       populateDiffAllActions(detail);
@@ -200,12 +220,20 @@
           .anonymousDownload("Git"), r.toString()));
     }
 
+    String hostPageUrl = GWT.getHostPageBaseURL();
+    if (!hostPageUrl.endsWith("/")) {
+      hostPageUrl += "/";
+    }
+
     if (changeDetail.isAllowsAnonymous()
         && (allowedSchemes.contains(DownloadScheme.ANON_HTTP) ||
             allowedSchemes.contains(DownloadScheme.DEFAULT_DOWNLOADS))) {
       StringBuilder r = new StringBuilder();
-      r.append(GWT.getHostPageBaseURL());
-      r.append("p/");
+      if (Gerrit.getConfig().getGitHttpUrl() != null) {
+        r.append(Gerrit.getConfig().getGitHttpUrl());
+      } else {
+        r.append(hostPageUrl);
+      }
       r.append(projectName);
       r.append(" ");
       r.append(patchSet.getRefName());
@@ -241,24 +269,28 @@
         && Gerrit.getUserAccount().getUserName().length() > 0
         && (allowedSchemes.contains(DownloadScheme.HTTP) ||
             allowedSchemes.contains(DownloadScheme.DEFAULT_DOWNLOADS))) {
-      String base = GWT.getHostPageBaseURL();
-      int p = base.indexOf("://");
-      int s = base.indexOf('/', p + 3);
-      if (s < 0) {
-        s = base.length();
-      }
-      String host = base.substring(p + 3, s);
-      if (host.contains("@")) {
-        host = host.substring(host.indexOf('@') + 1);
-      }
-
       final StringBuilder r = new StringBuilder();
-      r.append(base.substring(0, p + 3));
-      r.append(Gerrit.getUserAccount().getUserName());
-      r.append('@');
-      r.append(host);
-      r.append(base.substring(s));
-      r.append("p/");
+      if (Gerrit.getConfig().getGitHttpUrl() != null
+          && changeDetail.isAllowsAnonymous()) {
+        r.append(Gerrit.getConfig().getGitHttpUrl());
+      } else {
+        String base = hostPageUrl;
+        int p = base.indexOf("://");
+        int s = base.indexOf('/', p + 3);
+        if (s < 0) {
+          s = base.length();
+        }
+        String host = base.substring(p + 3, s);
+        if (host.contains("@")) {
+          host = host.substring(host.indexOf('@') + 1);
+        }
+
+        r.append(base.substring(0, p + 3));
+        r.append(Gerrit.getUserAccount().getUserName());
+        r.append('@');
+        r.append(host);
+        r.append(base.substring(s));
+      }
       r.append(projectName);
       r.append(" ");
       r.append(patchSet.getRefName());
@@ -436,7 +468,7 @@
         @Override
         public void onClick(final ClickEvent event) {
           b.setEnabled(false);
-          new CommentedChangeActionDialog(patchSet.getId(), createCommentedCallback(b),
+          new CommentedChangeActionDialog(patchSet.getId(), createCommentedCallback(b, true),
               Util.C.revertChangeTitle(), Util.C.headingRevertMessage(),
               Util.C.buttonRevertChangeSend(), Util.C.buttonRevertChangeCancel(),
               Gerrit.RESOURCES.css().revertChangeDialog(), Gerrit.RESOURCES.css().revertMessage(),
@@ -456,7 +488,7 @@
         @Override
         public void onClick(final ClickEvent event) {
           b.setEnabled(false);
-          new CommentedChangeActionDialog(patchSet.getId(), createCommentedCallback(b),
+          new CommentedChangeActionDialog(patchSet.getId(), createCommentedCallback(b, false),
               Util.C.abandonChangeTitle(), Util.C.headingAbandonMessage(),
               Util.C.buttonAbandonChangeSend(), Util.C.buttonAbandonChangeCancel(),
               Gerrit.RESOURCES.css().abandonChangeDialog(), Gerrit.RESOURCES.css().abandonMessage()) {
@@ -469,13 +501,37 @@
       actionsPanel.add(b);
     }
 
+    if (changeDetail.getChange().getStatus() == Change.Status.DRAFT
+        && changeDetail.canDeleteDraft()) {
+      final Button b = new Button(Util.C.buttonDeleteDraftChange());
+      b.addClickHandler(new ClickHandler() {
+        @Override
+        public void onClick(final ClickEvent event) {
+          b.setEnabled(false);
+          Util.MANAGE_SVC.deleteDraftChange(patchSet.getId(),
+              new GerritCallback<VoidResult>() {
+                public void onSuccess(VoidResult result) {
+                  Gerrit.display(PageLinks.MINE);
+                }
+
+                @Override
+                public void onFailure(Throwable caught) {
+                  b.setEnabled(true);
+                  super.onFailure(caught);
+                }
+              });
+        }
+      });
+      actionsPanel.add(b);
+    }
+
     if (changeDetail.canRestore()) {
       final Button b = new Button(Util.C.buttonRestoreChangeBegin());
       b.addClickHandler(new ClickHandler() {
         @Override
         public void onClick(final ClickEvent event) {
           b.setEnabled(false);
-          new CommentedChangeActionDialog(patchSet.getId(), createCommentedCallback(b),
+          new CommentedChangeActionDialog(patchSet.getId(), createCommentedCallback(b, false),
               Util.C.restoreChangeTitle(), Util.C.headingRestoreMessage(),
               Util.C.buttonRestoreChangeSend(), Util.C.buttonRestoreChangeCancel(),
               Gerrit.RESOURCES.css().abandonChangeDialog(), Gerrit.RESOURCES.css().abandonMessage()) {
@@ -528,6 +584,56 @@
     actionsPanel.add(b);
   }
 
+  private void populatePublishAction() {
+    final Button b = new Button(Util.C.buttonPublishPatchSet());
+    b.addClickHandler(new ClickHandler() {
+      @Override
+      public void onClick(final ClickEvent event) {
+        b.setEnabled(false);
+        Util.MANAGE_SVC.publish(patchSet.getId(),
+            new GerritCallback<ChangeDetail>() {
+              public void onSuccess(ChangeDetail result) {
+                changeScreen.update(result);
+              }
+
+              @Override
+              public void onFailure(Throwable caught) {
+                b.setEnabled(true);
+                super.onFailure(caught);
+              }
+            });
+      }
+    });
+    actionsPanel.add(b);
+  }
+
+  private void populateDeleteDraftPatchSetAction() {
+    final Button b = new Button(Util.C.buttonDeleteDraftPatchSet());
+    b.addClickHandler(new ClickHandler() {
+      @Override
+      public void onClick(final ClickEvent event) {
+        b.setEnabled(false);
+        PatchUtil.DETAIL_SVC.deleteDraftPatchSet(patchSet.getId(),
+            new GerritCallback<ChangeDetail>() {
+              public void onSuccess(final ChangeDetail result) {
+                if (result != null) {
+                  changeScreen.update(result);
+                } else {
+                  Gerrit.display(PageLinks.MINE);
+                }
+              }
+
+              @Override
+              public void onFailure(Throwable caught) {
+                b.setEnabled(true);
+                super.onFailure(caught);
+              }
+            });
+      }
+    });
+    actionsPanel.add(b);
+  }
+
   public void refresh() {
     AccountDiffPreference diffPrefs;
     if (patchTable == null) {
@@ -633,10 +739,14 @@
     }
   }
 
-  private AsyncCallback<ChangeDetail> createCommentedCallback(final Button b) {
+  private AsyncCallback<ChangeDetail> createCommentedCallback(final Button b, final boolean redirect) {
     return new AsyncCallback<ChangeDetail>() {
       public void onSuccess(ChangeDetail result) {
-        changeScreen.update(result);
+        if (redirect) {
+          Gerrit.display(PageLinks.toChange(result.getChange().getId()));
+        } else {
+          changeScreen.update(result);
+        }
       }
 
       public void onFailure(Throwable caught) {
@@ -644,4 +754,4 @@
       }
     };
   }
-}
\ No newline at end of file
+}
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 a7d468e..3fe63ed 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
@@ -46,6 +46,8 @@
       return "";
     }
     switch (status) {
+      case DRAFT:
+        return C.statusLongDraft();
       case NEW:
         return C.statusLongNew();
       case SUBMITTED:
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 881c2a4..20837c4 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
@@ -1110,6 +1110,16 @@
   padding: 5px 5px 5px 5px;
 }
 
+.createProjectLink {
+  margin-bottom: 10px;
+}
+
+.createProjectPanel {
+  margin-bottom: 10px;
+  background-color: trimColor;
+  padding: 5px 5px 5px 5px;
+}
+
 .sshHostKeyPanel {
   margin-top: 10px;
   border: 1px solid trimColor;
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 96befb0..396f1bf 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
@@ -482,6 +482,22 @@
         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/HistoryTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/HistoryTable.java
index a29acf4..e35097e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/HistoryTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/HistoryTable.java
@@ -98,7 +98,7 @@
 
       fmt.setStyleName(3, col, Gerrit.RESOURCES.css().dataCell());
       if (k.getCommentCount() > 0) {
-        table.setText(3, col, Util.M.patchTableComments(k.getCommentCount()));
+        table.setText(3, col, Integer.toString(k.getCommentCount()));
       }
       col++;
     }
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 7d3f74a..32f7096 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
@@ -67,4 +67,7 @@
 
   String fileTypeSymlink();
   String fileTypeGitlink();
+
+  String patchSkipRegionStart();
+  String patchSkipRegionEnd();
 }
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 c97d7530..f9f407e 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
@@ -46,3 +46,6 @@
 
 fileTypeSymlink = Type: Symbolic Link
 fileTypeGitlink = Type: Git Commit in Subproject
+
+patchSkipRegionStart = (... skipping
+patchSkipRegionEnd = common lines ...)
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchMessages.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchMessages.java
index 0f4adab..18a358a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchMessages.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchMessages.java
@@ -19,6 +19,7 @@
 import java.util.Date;
 
 public interface PatchMessages extends Messages {
-  String patchSkipRegion(@PluralCount int lineCnt);
+  String expandBefore(int cnt);
+  String expandAfter(int cnt);
   String draftSaved(Date when);
 }
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 9d7a2fb..52dcba2 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,3 +1,4 @@
-patchSkipRegion = (... skipping {0} common lines ...)
+expandBefore = Expand {0} before
+expandAfter = Expand {0} after
 
 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 9fceabe..58c4f6c 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 @@
-patchSkipRegion[one] = (... skipping 1 common line ...)
-patchSkipRegion = (... skipping {0} common lines ...)
+expandBefore = Expand {0} before
+expandAfter = Expand {0} after
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 910bf61..1671d6e 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
@@ -39,6 +39,7 @@
 import com.google.gwt.event.logical.shared.ValueChangeHandler;
 import com.google.gwt.event.shared.HandlerRegistration;
 import com.google.gwt.user.client.rpc.AsyncCallback;
+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;
@@ -105,6 +106,7 @@
   protected PatchScriptSettingsPanel settingsPanel;
   protected TopView topView;
 
+  private CheckBox reviewed;
   private HistoryTable historyTable;
   private FlowPanel topPanel;
   private FlowPanel contentPanel;
@@ -151,6 +153,15 @@
     idSideB = diffSideB != null ? diffSideB : 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,13 +176,6 @@
         });
 
     settingsPanel = new PatchScriptSettingsPanel(prefs);
-    settingsPanel.getReviewedCheckBox().addValueChangeHandler(
-        new ValueChangeHandler<Boolean>() {
-          @Override
-          public void onValueChange(ValueChangeEvent<Boolean> event) {
-            setReviewedByCurrentUser(event.getValue());
-          }
-        });
   }
 
   @Override
@@ -234,6 +238,10 @@
   protected void onInitUI() {
     super.onInitUI();
 
+    if (Gerrit.isSignedIn()) {
+      setTitleFarEast(reviewed);
+    }
+
     keysNavigation = new KeyCommandSet(Gerrit.C.sectionNavigation());
     keysNavigation.add(new UpToChangeCommand(patchKey.getParentKey(), 0, 'u'));
     keysNavigation.add(new FileListCmd(0, 'f', PatchUtil.C.fileList()));
@@ -444,7 +452,7 @@
 
     // Mark this file reviewed as soon we display the diff screen
     if (Gerrit.isSignedIn() && isFirst) {
-      settingsPanel.getReviewedCheckBox().setValue(true);
+      reviewed.setValue(true);
       setReviewedByCurrentUser(true /* reviewed */);
     }
 
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 70aade1..df0fff5 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
@@ -79,9 +79,6 @@
   CheckBox showTabs;
 
   @UiField
-  CheckBox reviewed;
-
-  @UiField
   CheckBox skipDeleted;
 
   @UiField
@@ -119,7 +116,6 @@
     initIgnoreWhitespace(ignoreWhitespace);
     initContext(context);
     if (!Gerrit.isSignedIn()) {
-      reviewed.setVisible(false);
       save.setVisible(false);
     }
 
@@ -188,10 +184,6 @@
     syntaxHighlighting.setTitle(title);
   }
 
-  public CheckBox getReviewedCheckBox() {
-    return reviewed;
-  }
-
   public AccountDiffPreference getValue() {
     return listenablePrefs.get();
   }
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 f320ce5..e819ffa 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
@@ -164,15 +164,6 @@
         <ui:attribute name='text'/>
       </g:Button>
     </td>
-
-    <td>
-      <g:CheckBox
-          ui:field='reviewed'
-          text='Reviewed'
-          tabIndex='15'>
-        <ui:attribute name='text'/>
-      </g:CheckBox>
-    </td>
   </tr>
 
   <tr valign='top'>
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 6a8f60b..853cedc 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
@@ -26,10 +26,16 @@
 import com.google.gerrit.prettify.common.EditList;
 import com.google.gerrit.prettify.common.SparseHtmlFile;
 import com.google.gerrit.reviewdb.Patch;
-import com.google.gerrit.reviewdb.PatchLineComment;
 import com.google.gerrit.reviewdb.Patch.ChangeType;
+import com.google.gerrit.reviewdb.PatchLineComment;
 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.gwtexpui.safehtml.client.SafeHtml;
 import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
 import com.google.gwtorm.client.KeyUtil;
@@ -44,6 +50,11 @@
   private static final int COL_A = 2;
   private static final int COL_B = 4;
 
+  private static final int NUM_ROWS_TO_EXPAND = 10;
+
+  private SparseHtmlFile a;
+  private SparseHtmlFile b;
+
   @Override
   protected void onCellDoubleClick(final int row, int column) {
     if (column > 0 && getRowItem(row) instanceof PatchLine) {
@@ -78,9 +89,9 @@
 
   @Override
   protected void render(final PatchScript script) {
-    final SparseHtmlFile a = script.getSparseHtmlFileA();
-    final SparseHtmlFile b = script.getSparseHtmlFileB();
-    final ArrayList<PatchLine> lines = new ArrayList<PatchLine>();
+    a = script.getSparseHtmlFileA();
+    b = script.getSparseHtmlFileB();
+    final ArrayList<Object> lines = new ArrayList<Object>();
     final SafeHtmlBuilder nc = new SafeHtmlBuilder();
     final boolean intraline =
         script.getDiffPrefs().isIntralineDifference()
@@ -97,12 +108,13 @@
       lines.add(null);
     }
 
+    int lastA = 0;
     int lastB = 0;
     final boolean ignoreWS = script.isIgnoreWhitespace();
     for (final EditList.Hunk hunk : script.getHunks()) {
       if (!hunk.isStartOfFile()) {
         appendSkipLine(nc, hunk.getCurB() - lastB);
-        lines.add(null);
+        lines.add(new SkippedLine(lastA, lastB, hunk.getCurB() - lastB));
       }
 
       while (hunk.next()) {
@@ -156,16 +168,21 @@
           }
         }
       }
+      lastA = hunk.getCurA();
       lastB = hunk.getCurB();
     }
     if (lastB != b.size()) {
       appendSkipLine(nc, b.size() - lastB);
+      lines.add(new SkippedLine(lastA, lastB, b.size() - lastB));
     }
     resetHtml(nc);
     initScript(script);
 
     for (int row = 0; row < lines.size(); row++) {
       setRowItem(row, lines.get(row));
+      if (lines.get(row) instanceof SkippedLine) {
+        createSkipLine(row, (SkippedLine) lines.get(row));
+      }
     }
   }
 
@@ -315,11 +332,112 @@
     m.openTd();
     m.setStyleName(Gerrit.RESOURCES.css().skipLine());
     m.setAttribute("colspan", 4);
-    m.append(PatchUtil.M.patchSkipRegion(skipCnt));
     m.closeTd();
     m.closeTr();
   }
 
+  ClickHandler expandAllListener = new ClickHandler() {
+    @Override
+    public void onClick(ClickEvent event) {
+      expand(event, 0);
+    }
+  };
+
+  ClickHandler expandBeforeListener = new ClickHandler() {
+    @Override
+    public void onClick(ClickEvent event) {
+      expand(event, NUM_ROWS_TO_EXPAND);
+    }
+  };
+
+  ClickHandler expandAfterListener = new ClickHandler() {
+    @Override
+    public void onClick(ClickEvent event) {
+      expand(event, -NUM_ROWS_TO_EXPAND);
+    }
+  };
+
+  private void expand(ClickEvent event, final int numRows) {
+    Cell cell = table.getCellForEvent(event);
+    int row = cell.getRowIndex();
+    if (!(getRowItem(row) instanceof SkippedLine)) {
+      return;
+    }
+    SkippedLine line = (SkippedLine) getRowItem(row);
+    int loopTo = numRows;
+    if (numRows == 0) {
+      loopTo = line.getSize();
+    } else if (numRows < 0) {
+      loopTo = -numRows;
+    }
+    int offset = 0;
+    if (numRows < 0) {
+      offset = 1;
+    }
+    for (int i = 0 + offset; i < loopTo + offset; i++) {
+      insertRow(row + i);
+      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());
+
+      setHtml(row + i, 3, "<a href=\"javascript:void(0)\">" + (lineB + 1)
+          + "</a>");
+      addStyle(row + i, 3, Gerrit.RESOURCES.css().lineNumber());
+
+      setHtml(row + i, 4, b.getSafeHtmlLine(lineB).asString());
+      addStyle(row + i, 4, Gerrit.RESOURCES.css().fileLine());
+      addStyle(row + i, 4, Gerrit.RESOURCES.css().fileLineCONTEXT());
+
+      setRowItem(row + i, new PatchLine(CONTEXT, lineA, lineB));
+    }
+
+    if (numRows > 0) {
+      line.incrementStart(numRows);
+      createSkipLine(row + loopTo, line);
+    } else if (numRows < 0) {
+      line.reduceSize(-numRows);
+      createSkipLine(row, line);
+    } else {
+      removeRow(row + loopTo);
+    }
+  }
+
+  private void createSkipLine(int row, SkippedLine line) {
+    FlowPanel p = new FlowPanel();
+    Label l1 = new Label(" " + PatchUtil.C.patchSkipRegionStart() + " ");
+    Anchor all = new Anchor(String.valueOf(line.getSize()));
+    Label l2 = new Label(" " + PatchUtil.C.patchSkipRegionEnd() + " ");
+    all.addClickHandler(expandAllListener);
+    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);
+      p.add(l1);
+      p.add(all);
+      p.add(l2);
+      p.add(after);
+    } else {
+      p.add(l1);
+      p.add(all);
+      p.add(l2);
+    }
+    setWidget(row, 1, p);
+  }
+
   private void openLine(final SafeHtmlBuilder m) {
     m.openTr();
     m.setAttribute("valign", "top");
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/SkippedLine.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/SkippedLine.java
new file mode 100644
index 0000000..486e7b8
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/SkippedLine.java
@@ -0,0 +1,50 @@
+// 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.patches;
+
+public class SkippedLine {
+
+  private int a;
+  private int b;
+  private int sz;
+
+  public SkippedLine(int startA, int startB, int size) {
+    a = startA;
+    b = startB;
+    sz = size;
+  }
+
+  public int getStartA() {
+    return a;
+  }
+
+  public int getStartB() {
+    return b;
+  }
+
+  public int getSize() {
+    return sz;
+  }
+
+  public void incrementStart(int n) {
+    a += n;
+    b += n;
+    reduceSize(n);
+  }
+
+  public void reduceSize(int n) {
+    sz -= n;
+  }
+}
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 68026ae..5cb4727 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
@@ -18,12 +18,24 @@
 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;
 import com.google.gwt.user.client.ui.InlineLabel;
 import com.google.gwt.user.client.ui.Widget;
 import com.google.gwtexpui.user.client.View;
 
+/**
+  *  A Screen layout with a header and a body.
+  *
+  * The header is mainly a text title, but it can be decorated
+  * in the West, the East, and the FarEast by any Widget.  The
+  * West and East decorations will surround the text on the
+  * left and right respectively, and the FarEast will be right
+  * justified to the right edge of the screen.  The East
+  * decoration will expand to take up any extra space.
+  */
 public abstract class Screen extends View {
-  private FlowPanel header;
+  private Grid header;
   private InlineLabel headerText;
   private FlowPanel body;
   private String token;
@@ -46,13 +58,25 @@
   public void registerKeys() {
   }
 
+  private static enum Cols {
+    West, Title, East, FarEast;
+  }
+
   protected void onInitUI() {
     final FlowPanel me = (FlowPanel) getWidget();
-    me.add(header = new FlowPanel());
+    me.add(header = new Grid(1, Cols.values().length));
     me.add(body = new FlowPanel());
 
+    FlowPanel title = new FlowPanel();
+    title.add(headerText = new InlineLabel());
+    title.setStyleName(Gerrit.RESOURCES.css().screenHeader());
+    header.setWidget(0, Cols.Title.ordinal(), title);
+
     header.setStyleName(Gerrit.RESOURCES.css().screenHeader());
-    header.add(headerText = new InlineLabel());
+    header.getCellFormatter().setHorizontalAlignment(0, Cols.FarEast.ordinal(),
+      HasHorizontalAlignment.ALIGN_RIGHT);
+    // force FarEast all the way to the right
+    header.getCellFormatter().setWidth(0, Cols.FarEast.ordinal(), "100%");
   }
 
   protected void setWindowTitle(final String text) {
@@ -73,8 +97,16 @@
     }
   }
 
-  protected void insertTitleWidget(final Widget w) {
-    header.insert(w, 0);
+  protected void setTitleEast(final Widget w) {
+    header.setWidget(0, Cols.East.ordinal(), w);
+  }
+
+  protected void setTitleFarEast(final Widget w) {
+    header.setWidget(0, Cols.FarEast.ordinal(), w);
+  }
+
+  protected void setTitleWest(final Widget w) {
+    header.setWidget(0, Cols.West.ordinal(), w);
   }
 
   protected void add(final Widget w) {
diff --git a/gerrit-httpd/.gitignore b/gerrit-httpd/.gitignore
index 903c6c8..194bedc 100644
--- a/gerrit-httpd/.gitignore
+++ b/gerrit-httpd/.gitignore
@@ -2,3 +2,4 @@
 /.classpath
 /.project
 /.settings/org.maven.ide.eclipse.prefs
+/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
diff --git a/gerrit-httpd/pom.xml b/gerrit-httpd/pom.xml
index 5154515..6c2190d 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.2-SNAPSHOT</version>
+    <version>2.3-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-httpd</artifactId>
@@ -65,12 +65,6 @@
     </dependency>
 
     <dependency>
-      <groupId>org.openid4java</groupId>
-      <artifactId>openid4java-consumer</artifactId>
-      <type>pom</type>
-    </dependency>
-
-    <dependency>
       <groupId>gwtjsonrpc</groupId>
       <artifactId>gwtjsonrpc</artifactId>
     </dependency>
@@ -93,4 +87,20 @@
       <version>${project.version}</version>
     </dependency>
   </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-source-plugin</artifactId>
+        <executions>
+          <execution>
+            <goals>
+              <goal>jar</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
 </project>
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
new file mode 100644
index 0000000..270b476
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CacheBasedWebSession.java
@@ -0,0 +1,220 @@
+// 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 static java.util.concurrent.TimeUnit.HOURS;
+import static java.util.concurrent.TimeUnit.MINUTES;
+
+import com.google.gerrit.httpd.WebSessionManager.Key;
+import com.google.gerrit.httpd.WebSessionManager.Val;
+import com.google.gerrit.reviewdb.Account;
+import com.google.gerrit.reviewdb.AccountExternalId;
+import com.google.gerrit.server.AccessPath;
+import com.google.gerrit.server.AnonymousUser;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AuthResult;
+import com.google.gerrit.server.cache.Cache;
+import com.google.gerrit.server.cache.CacheModule;
+import com.google.gerrit.server.cache.EvictionPolicy;
+import com.google.gerrit.server.config.AuthConfig;
+import com.google.inject.Inject;
+import com.google.inject.Module;
+import com.google.inject.Provider;
+import com.google.inject.TypeLiteral;
+import com.google.inject.servlet.RequestScoped;
+
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+@RequestScoped
+public final class CacheBasedWebSession implements WebSession {
+  private static final String ACCOUNT_COOKIE = "GerritAccount";
+  static final long MAX_AGE_MINUTES = HOURS.toMinutes(12);
+
+  public static Module module() {
+    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
+        ;
+        bind(WebSessionManager.class);
+        bind(WebSession.class)
+          .to(CacheBasedWebSession.class)
+          .in(RequestScoped.class);
+      }
+    };
+  }
+
+  private final HttpServletRequest request;
+  private final HttpServletResponse response;
+  private final WebSessionManager manager;
+  private final AuthConfig authConfig;
+  private final Provider<AnonymousUser> anonymousProvider;
+  private final IdentifiedUser.RequestFactory identified;
+  private AccessPath accessPath = AccessPath.WEB_UI;
+  private Cookie outCookie;
+
+  private Key key;
+  private Val val;
+
+  @Inject
+  CacheBasedWebSession(final HttpServletRequest request,
+      final HttpServletResponse response, final WebSessionManager manager,
+      final AuthConfig authConfig,
+      final Provider<AnonymousUser> anonymousProvider,
+      final IdentifiedUser.RequestFactory identified) {
+    this.request = request;
+    this.response = response;
+    this.manager = manager;
+    this.authConfig = authConfig;
+    this.anonymousProvider = anonymousProvider;
+    this.identified = identified;
+
+    final String cookie = readCookie();
+    if (cookie != null) {
+      key = new Key(cookie);
+      val = manager.get(key);
+    } else {
+      key = null;
+      val = null;
+    }
+
+    if (isSignedIn() && val.needsCookieRefresh()) {
+      // Cookie is more than half old. Send the cookie again to the
+      // client with an updated expiration date. We don't dare to
+      // change the key token here because there may be other RPCs
+      // queued up in the browser whose xsrfKey would not get updated
+      // with the new token, causing them to fail.
+      //
+      val = manager.createVal(key, val);
+      saveCookie();
+    }
+  }
+
+  private String readCookie() {
+    final Cookie[] all = request.getCookies();
+    if (all != null) {
+      for (final Cookie c : all) {
+        if (ACCOUNT_COOKIE.equals(c.getName())) {
+          final String v = c.getValue();
+          return v != null && !"".equals(v) ? v : null;
+        }
+      }
+    }
+    return null;
+  }
+
+  public boolean isSignedIn() {
+    return val != null;
+  }
+
+  public String getToken() {
+    return isSignedIn() ? val.getXsrfToken() : null;
+  }
+
+  public boolean isTokenValid(final String inputToken) {
+    return isSignedIn() //
+        && val.getXsrfToken() != null //
+        && val.getXsrfToken().equals(inputToken);
+  }
+
+  public AccountExternalId.Key getLastLoginExternalId() {
+    return val != null ? val.getExternalId() : null;
+  }
+
+  public CurrentUser getCurrentUser() {
+    if (isSignedIn()) {
+      return identified.create(accessPath, val.getAccountId());
+    }
+    return anonymousProvider.get();
+  }
+
+  public void login(final AuthResult res, final boolean rememberMe) {
+    final Account.Id id = res.getAccountId();
+    final AccountExternalId.Key identity = res.getExternalId();
+
+    if (val != null) {
+      manager.destroy(key);
+    }
+
+    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;
+  }
+
+  /** Set the user account for this current request only. */
+  public void setUserAccountId(Account.Id id) {
+    key = new Key("id:" + id);
+    val = new Val(id, 0, false, null, "");
+  }
+
+  public void logout() {
+    if (val != null) {
+      manager.destroy(key);
+      key = null;
+      val = null;
+      saveCookie();
+    }
+  }
+
+  private void saveCookie() {
+    final String token;
+    final int ageSeconds;
+
+    if (key == null) {
+      token = "";
+      ageSeconds = 0 /* erase at client */;
+    } else {
+      token = key.getToken();
+      ageSeconds = manager.getCookieAge(val);
+    }
+
+    String path = authConfig.getCookiePath();
+    if (path == null || path.isEmpty()) {
+      path = request.getContextPath();
+      if (path == null || path.isEmpty()) {
+        path = "/";
+      }
+    }
+
+    if (outCookie != null) {
+      throw new IllegalStateException("Cookie " + ACCOUNT_COOKIE + " was set");
+    }
+
+    outCookie = new Cookie(ACCOUNT_COOKIE, token);
+    outCookie.setSecure(isSecure(request));
+    outCookie.setPath(path);
+    outCookie.setMaxAge(ageSeconds);
+    outCookie.setSecure(authConfig.getCookieSecure());
+    response.addCookie(outCookie);
+  }
+
+  private static boolean isSecure(final HttpServletRequest req) {
+    return req.isSecure() || "https".equals(req.getScheme());
+  }
+}
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 dabd706..c0e3f42 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
@@ -14,17 +14,22 @@
 
 package com.google.gerrit.httpd;
 
+import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
 import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
 
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
-import com.google.gerrit.server.config.AuthConfig;
+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;
+import java.util.Locale;
 
 import javax.servlet.Filter;
 import javax.servlet.FilterChain;
@@ -44,7 +49,7 @@
  * <p>
  * This filter should only be configured to run, when authentication is
  * configured to trust container authentication. This filter is intended only to
- * protect the {@link ProjectServlet} and its handled URLs, which provide remote
+ * protect the {@link GitOverHttpServlet} and its handled URLs, which provide remote
  * repository access over HTTP.
  */
 @Singleton
@@ -53,12 +58,14 @@
 
   private final Provider<WebSession> session;
   private final AccountCache accountCache;
+  private final Config config;
 
   @Inject
-  ContainerAuthFilter(Provider<WebSession> session, AccountCache accountCache)
-      throws XsrfException {
+  ContainerAuthFilter(Provider<WebSession> session, AccountCache accountCache,
+      @GerritServerConfig Config config) throws XsrfException {
     this.session = session;
     this.accountCache = accountCache;
+    this.config = config;
   }
 
   @Override
@@ -73,6 +80,11 @@
   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);
 
@@ -83,9 +95,15 @@
 
   private boolean verify(HttpServletRequest req, HttpServletResponseWrapper rsp)
       throws IOException {
-    final String username = req.getRemoteUser();
-    final AccountState who =
-        (username == null) ? null : accountCache.getByUsername(username);
+    String username = req.getRemoteUser();
+    if (username == null) {
+      rsp.sendError(SC_FORBIDDEN);
+      return false;
+    }
+    if (config.getBoolean("auth", "userNameToLowerCase", false)) {
+      username = username.toLowerCase(Locale.US);
+    }
+    final AccountState who = accountCache.getByUsername(username);
     if (who == null || !who.getAccount().isActive()) {
       rsp.sendError(SC_UNAUTHORIZED);
       return false;
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 60486ac..469af22 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
@@ -20,6 +20,7 @@
 import com.google.gerrit.reviewdb.Account;
 import com.google.gerrit.server.account.Realm;
 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.GerritServerConfig;
@@ -56,13 +57,14 @@
   private EmailSender emailSender;
   private final ContactStore contactStore;
   private final ServletContext servletContext;
+  private final String anonymousCowardName;
 
   @Inject
   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 AuthConfig ac, final GitWebConfig gwc, final AllProjectsName wp,
+      final SshInfo si, final ApprovalTypes at, final ContactStore cs,
+      final ServletContext sc, final DownloadSchemeConfig dc,
+      final @AnonymousCowardName String acn) {
     realm = r;
     cfg = gsc;
     authConfig = ac;
@@ -73,6 +75,7 @@
     approvalTypes = at;
     contactStore = cs;
     servletContext = sc;
+    anonymousCowardName = acn;
   }
 
   @Inject(optional = true)
@@ -91,10 +94,16 @@
       case LDAP_BIND:
         config.setRegisterUrl(cfg.getString("auth", null, "registerurl"));
         break;
+
+      case CUSTOM_EXTENSION:
+        config.setRegisterUrl(cfg.getString("auth", null, "registerurl"));
+        config.setHttpPasswordUrl(cfg.getString("auth", null, "httpPasswordUrl"));
+        break;
     }
     config.setUseContributorAgreements(cfg.getBoolean("auth",
         "contributoragreements", false));
     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.setAuthType(authConfig.getAuthType());
@@ -104,6 +113,7 @@
         .getResource("/Documentation/index.html") != null);
     config.setTestChangeMerge(cfg.getBoolean("changeMerge",
         "test", false));
+    config.setAnonymousCowardName(anonymousCowardName);
 
     final Set<Account.FieldName> fields = new HashSet<Account.FieldName>();
     for (final Account.FieldName n : Account.FieldName.values()) {
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
new file mode 100644
index 0000000..6fd94c9
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpModule.java
@@ -0,0 +1,45 @@
+// 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.server.config.AuthConfig;
+import com.google.inject.Inject;
+import com.google.inject.servlet.ServletModule;
+
+import javax.servlet.Filter;
+
+/** Configures Git access over HTTP with authentication. */
+public class GitOverHttpModule extends ServletModule {
+  private final AuthConfig authConfig;
+
+  @Inject
+  GitOverHttpModule(AuthConfig authConfig) {
+    this.authConfig = authConfig;
+  }
+
+  @Override
+  protected void configureServlets() {
+    Class<? extends Filter> authFilter;
+    if (authConfig.isTrustContainerAuth()) {
+      authFilter = ContainerAuthFilter.class;
+    } else {
+      authFilter = ProjectDigestFilter.class;
+    }
+
+    String git = GitOverHttpServlet.URL_REGEX;
+    filterRegex(git).through(authFilter);
+    serveRegex(git).with(GitOverHttpServlet.class);
+  }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java
similarity index 65%
rename from gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectServlet.java
rename to gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java
index 6e1366ae..145891f 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java
@@ -14,16 +14,14 @@
 
 package com.google.gerrit.httpd;
 
-import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.data.Capable;
-import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.reviewdb.Project;
 import com.google.gerrit.reviewdb.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.config.CanonicalWebUrl;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.ReceiveCommits;
 import com.google.gerrit.server.git.TagCache;
@@ -40,6 +38,8 @@
 
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.http.server.GitServlet;
+import org.eclipse.jgit.http.server.GitSmartHttpTools;
+import org.eclipse.jgit.http.server.ServletUtils;
 import org.eclipse.jgit.http.server.resolver.AsIsFileService;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.Repository;
@@ -51,8 +51,6 @@
 import org.eclipse.jgit.transport.resolver.ServiceNotAuthorizedException;
 import org.eclipse.jgit.transport.resolver.ServiceNotEnabledException;
 import org.eclipse.jgit.transport.resolver.UploadPackFactory;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
 import java.util.Collections;
@@ -60,35 +58,42 @@
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
 
-import javax.annotation.Nullable;
 import javax.servlet.Filter;
 import javax.servlet.FilterChain;
 import javax.servlet.FilterConfig;
-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.HttpServletResponse;
 
 /** Serves Git repositories over HTTP. */
 @Singleton
-public class ProjectServlet extends GitServlet {
+public class GitOverHttpServlet extends GitServlet {
   private static final long serialVersionUID = 1L;
-  private static final Logger log =
-      LoggerFactory.getLogger(ProjectServlet.class);
 
   private static final String ATT_CONTROL = ProjectControl.class.getName();
   private static final String ATT_RC = ReceiveCommits.class.getName();
   private static final String ID_CACHE = "adv_bases";
 
+  public static final String URL_REGEX;
+  static {
+    StringBuilder url = new StringBuilder();
+    url.append("^(?:/p/|/)(.*/(?:info/refs");
+    for (String name : GitSmartHttpTools.VALID_SERVICES) {
+      url.append('|').append(name);
+    }
+    url.append("))$");
+    URL_REGEX = url.toString();
+  }
+
   static class Module extends AbstractModule {
     @Override
     protected void configure() {
       bind(Resolver.class);
-      bind(Upload.class);
-      bind(Receive.class);
+      bind(UploadFactory.class);
+      bind(UploadFilter.class);
+      bind(ReceiveFactory.class);
       bind(ReceiveFilter.class);
       install(new CacheModule() {
         @Override
@@ -103,61 +108,20 @@
     }
   }
 
-  static ProjectControl getProjectControl(ServletRequest req)
-      throws ServiceNotEnabledException {
-    ProjectControl pc = (ProjectControl) req.getAttribute(ATT_CONTROL);
-    if (pc == null) {
-      log.error("No " + ATT_CONTROL + " in request", new Exception("here"));
-      throw new ServiceNotEnabledException();
-    }
-    return pc;
-  }
-
-  private final Provider<String> urlProvider;
-
   @Inject
-  ProjectServlet(final Resolver resolver, final Upload upload,
-      final Receive receive,
-      final ReceiveFilter receiveFilter,
-      @CanonicalWebUrl @Nullable Provider<String> urlProvider) {
-    this.urlProvider = urlProvider;
-
+  GitOverHttpServlet(Resolver resolver,
+      UploadFactory upload, UploadFilter uploadFilter,
+      ReceiveFactory receive, ReceiveFilter receiveFilter) {
     setRepositoryResolver(resolver);
     setAsIsFileService(AsIsFileService.DISABLED);
+
     setUploadPackFactory(upload);
+    addUploadPackFilter(uploadFilter);
+
     setReceivePackFactory(receive);
     addReceivePackFilter(receiveFilter);
   }
 
-  @Override
-  public void init(ServletConfig config) throws ServletException {
-    super.init(config);
-
-    serveRegex("^/(.*?)/?$").with(new HttpServlet() {
-      private static final long serialVersionUID = 1L;
-
-      @Override
-      protected void doGet(HttpServletRequest req, HttpServletResponse rsp)
-          throws IOException {
-        ProjectControl pc;
-        try {
-          pc = getProjectControl(req);
-        } catch (ServiceNotEnabledException e) {
-          rsp.sendError(HttpServletResponse.SC_NOT_FOUND);
-          return;
-        }
-
-        Project.NameKey dst = pc.getProject().getNameKey();
-        StringBuilder r = new StringBuilder();
-        r.append(urlProvider.get());
-        r.append('#');
-        r.append(PageLinks.toChangeQuery(PageLinks.projectQuery(dst,
-            Change.Status.NEW)));
-        rsp.sendRedirect(r.toString());
-      }
-    });
-  }
-
   static class Resolver implements RepositoryResolver<HttpServletRequest> {
     private final GitRepositoryManager manager;
     private final ProjectControl.Factory projectControlFactory;
@@ -187,19 +151,9 @@
         }
       }
 
-      while (projectName.startsWith("/")) {
-        // Be nice and drop the leading "/" if supplied by an absolute path.
-        // We don't have a file system hierarchy, just a flat namespace in
-        // the database's Project entities. We never encode these with a
-        // leading '/' but users might accidentally include them in Git URLs.
-        //
-        projectName = projectName.substring(1);
-      }
-
       final ProjectControl pc;
       try {
-        final Project.NameKey nameKey = new Project.NameKey(projectName);
-        pc = projectControlFactory.controlFor(nameKey);
+        pc = projectControlFactory.controlFor(new Project.NameKey(projectName));
       } catch (NoSuchProjectException err) {
         throw new RepositoryNotFoundException(projectName);
       }
@@ -216,75 +170,92 @@
     }
   }
 
-  static class Upload implements UploadPackFactory<HttpServletRequest> {
-    private final Provider<ReviewDb> db;
+  static class UploadFactory implements UploadPackFactory<HttpServletRequest> {
     private final PackConfig packConfig;
-    private final TagCache tagCache;
+    private final Provider<WebSession> session;
 
     @Inject
-    Upload(final Provider<ReviewDb> db, final TransferConfig tc,
-        final TagCache tagCache) {
-      this.db = db;
+    UploadFactory(TransferConfig tc, Provider<WebSession> session) {
       this.packConfig = tc.getPackConfig();
-      this.tagCache = tagCache;
+      this.session = session;
     }
 
     @Override
-    public UploadPack create(HttpServletRequest req, Repository repo)
-        throws ServiceNotEnabledException, ServiceNotAuthorizedException {
-      ProjectControl pc = getProjectControl(req);
-      if (!pc.canRunUploadPack()) {
-        throw new ServiceNotAuthorizedException();
-      }
-
-      // The Resolver above already checked READ access for us.
-      //
+    public UploadPack create(HttpServletRequest req, Repository repo) {
       UploadPack up = new UploadPack(repo);
       up.setPackConfig(packConfig);
-      if (!pc.allRefsAreVisible()) {
-        up.setRefFilter(new VisibleRefFilter(tagCache, repo, pc, db.get(), true));
-      }
+      session.get().setAccessPath(AccessPath.GIT);
       return up;
     }
   }
 
-  static class Receive implements ReceivePackFactory<HttpServletRequest> {
-    private final ReceiveCommits.Factory factory;
+  static class UploadFilter implements Filter {
+    private final Provider<ReviewDb> db;
+    private final TagCache tagCache;
 
     @Inject
-    Receive(final ReceiveCommits.Factory factory) {
+    UploadFilter(Provider<ReviewDb> db, TagCache tagCache) {
+      this.db = db;
+      this.tagCache = tagCache;
+    }
+
+    @Override
+    public void doFilter(ServletRequest request, ServletResponse response,
+        FilterChain next) throws IOException, ServletException {
+      // The Resolver above already checked READ access for us.
+      Repository repo = ServletUtils.getRepository(request);
+      ProjectControl pc = (ProjectControl) request.getAttribute(ATT_CONTROL);
+      UploadPack up = (UploadPack) request.getAttribute(ServletUtils.ATTRIBUTE_HANDLER);
+
+      if (!pc.canRunUploadPack()) {
+        GitSmartHttpTools.sendError((HttpServletRequest) request,
+            (HttpServletResponse) response, HttpServletResponse.SC_FORBIDDEN,
+            "upload-pack not permitted on this server");
+        return;
+      }
+
+      if (!pc.allRefsAreVisible()) {
+        up.setRefFilter(new VisibleRefFilter(tagCache, repo, pc, db.get(), true));
+      }
+
+      next.doFilter(request, response);
+    }
+
+    @Override
+    public void init(FilterConfig config) {
+    }
+
+    @Override
+    public void destroy() {
+    }
+  }
+
+  static class ReceiveFactory implements ReceivePackFactory<HttpServletRequest> {
+    private final ReceiveCommits.Factory factory;
+    private final Provider<WebSession> session;
+
+    @Inject
+    ReceiveFactory(ReceiveCommits.Factory factory, Provider<WebSession> session) {
       this.factory = factory;
+      this.session = session;
     }
 
     @Override
     public ReceivePack create(HttpServletRequest req, Repository db)
-        throws ServiceNotEnabledException, ServiceNotAuthorizedException {
-      final ProjectControl pc = getProjectControl(req);
-      if (!pc.canRunReceivePack()) {
+        throws ServiceNotAuthorizedException {
+      final ProjectControl pc = (ProjectControl) req.getAttribute(ATT_CONTROL);
+
+      if (!(pc.getCurrentUser() instanceof IdentifiedUser)) {
+        // Anonymous users are not permitted to push.
         throw new ServiceNotAuthorizedException();
       }
 
-      if (pc.getCurrentUser() instanceof IdentifiedUser) {
-        final IdentifiedUser user = (IdentifiedUser) pc.getCurrentUser();
-        final ReceiveCommits rc = factory.create(pc, db);
-        final Capable s = rc.canUpload();
-        if (s != Capable.OK) {
-          // TODO We should alert the user to this message on the HTTP
-          // response channel, assuming Git will even report it to them.
-          //
-          final String who = user.getUserName();
-          final String why = s.getMessage();
-          log.warn("Rejected push from " + who + ": " + why);
-          throw new ServiceNotEnabledException();
-        }
-
-        rc.getReceivePack().setRefLogIdent(user.newRefLogIdent());
-        req.setAttribute(ATT_RC, rc);
-        return rc.getReceivePack();
-
-      } else {
-        throw new ServiceNotAuthorizedException();
-      }
+      final IdentifiedUser user = (IdentifiedUser) pc.getCurrentUser();
+      final ReceiveCommits rc = factory.create(pc, db);
+      rc.getReceivePack().setRefLogIdent(user.newRefLogIdent());
+      req.setAttribute(ATT_RC, rc);
+      session.get().setAccessPath(AccessPath.GIT);
+      return rc.getReceivePack();
     }
   }
 
@@ -305,15 +276,23 @@
 
       ReceiveCommits rc = (ReceiveCommits) request.getAttribute(ATT_RC);
       ReceivePack rp = rc.getReceivePack();
-      ProjectControl pc;
-      try {
-        pc = getProjectControl(request);
-      } catch (ServiceNotEnabledException e) {
-        // This shouldn't occur, the parent should have stopped processing.
-        throw new ServletException(e);
+      ProjectControl pc = (ProjectControl) request.getAttribute(ATT_CONTROL);
+      Project.NameKey projectName = pc.getProject().getNameKey();
+
+      if (!pc.canRunReceivePack()) {
+        GitSmartHttpTools.sendError((HttpServletRequest) request,
+            (HttpServletResponse) response, HttpServletResponse.SC_FORBIDDEN,
+            "receive-pack not permitted on this server");
+        return;
       }
 
-      Project.NameKey projectName = pc.getProject().getNameKey();
+      final Capable s = rc.canUpload();
+      if (s != Capable.OK) {
+        GitSmartHttpTools.sendError((HttpServletRequest) request,
+            (HttpServletResponse) response, HttpServletResponse.SC_FORBIDDEN,
+            "\n" + s.getMessage());
+        return;
+      }
 
       if (!rp.isCheckReferencedObjectsAreReachable()) {
         if (isGet) {
@@ -352,7 +331,7 @@
     }
 
     @Override
-    public void init(FilterConfig arg0) throws ServletException {
+    public void init(FilterConfig arg0) {
     }
 
     @Override
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitWebConfig.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitWebConfig.java
index b37a152..9f437bf 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitWebConfig.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitWebConfig.java
@@ -41,9 +41,32 @@
     final String cfgCgi = cfg.getString("gitweb", null, "cgi");
 
     type = GitWebType.fromName(cfg.getString("gitweb", null, "type"));
+    if (type == null) {
+      url = null;
+      gitweb_cgi = null;
+      gitweb_css = null;
+      gitweb_js = null;
+      git_logo_png = null;
+      return;
+    }
+
+    type.setLinkName(cfg.getString("gitweb", null, "linkname"));
     type.setBranch(cfg.getString("gitweb", null, "branch"));
     type.setProject(cfg.getString("gitweb", null, "project"));
     type.setRevision(cfg.getString("gitweb", null, "revision"));
+    String pathSeparator = cfg.getString("gitweb", null, "pathSeparator");
+    if (pathSeparator != null) {
+      if (pathSeparator.length() == 1) {
+        char c = pathSeparator.charAt(0);
+        if (isValidPathSeparator(c)) {
+          type.setPathSeparator(c);
+        } else {
+          log.warn("Invalid value specified for gitweb.pathSeparator: " + c);
+        }
+      } else {
+        log.warn("Value specified for gitweb.pathSeparator is not a single character:" + pathSeparator);
+      }
+    }
 
     if (type.getBranch() == null) {
       log.warn("No Pattern specified for gitweb.branch, disabling.");
@@ -57,7 +80,7 @@
     }
 
     if ((cfgUrl != null && cfgUrl.isEmpty())
-        || (cfgCgi != null && cfgCgi.isEmpty()) || type == null) {
+        || (cfgCgi != null && cfgCgi.isEmpty())) {
       // Either setting was explicitly set to the empty string disabling
       // gitweb for this server. Disable the configuration.
       //
@@ -172,4 +195,31 @@
   public File getGitLogoPNG() {
     return git_logo_png;
   }
+
+  /**
+   * Determines if a given character can be used unencoded in an URL as a
+   * replacement for the path separator '/'.
+   *
+   * Reasoning: http://www.ietf.org/rfc/rfc1738.txt § 2.2:
+   *
+   * ... only alphanumerics, the special characters "$-_.+!*'(),", and
+   *  reserved characters used for their reserved purposes may be used
+   * unencoded within a URL.
+   *
+   * The following characters might occur in file names, however:
+   *
+   * alphanumeric characters,
+   *
+   * "$-_.+!',"
+   */
+  static boolean isValidPathSeparator(char c) {
+    switch (c) {
+      case '*':
+      case '(':
+      case ')':
+        return true;
+      default:
+        return false;
+    }
+  }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpCurrentUserProvider.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpCurrentUserProvider.java
index 04de408..c87a143 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpCurrentUserProvider.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpCurrentUserProvider.java
@@ -17,9 +17,7 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
-import com.google.inject.servlet.RequestScoped;
 
-@RequestScoped
 class HttpCurrentUserProvider implements Provider<CurrentUser> {
   private final WebSession session;
 
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
index d611098..ba2d135 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpIdentifiedUserProvider.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpIdentifiedUserProvider.java
@@ -20,19 +20,18 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.ProvisionException;
-import com.google.inject.servlet.RequestScoped;
 
-@RequestScoped
 class HttpIdentifiedUserProvider implements Provider<IdentifiedUser> {
-  private final CurrentUser user;
+  private final WebSession session;
 
   @Inject
-  HttpIdentifiedUserProvider(final CurrentUser u) {
-    user = u;
+  HttpIdentifiedUserProvider(WebSession session) {
+    this.session = session;
   }
 
   @Override
   public IdentifiedUser get() {
+    CurrentUser user = session.getCurrentUser();
     if (user instanceof IdentifiedUser) {
       return (IdentifiedUser) user;
     }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectAccessPathFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectAccessPathFilter.java
deleted file mode 100644
index 934c4a5..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectAccessPathFilter.java
+++ /dev/null
@@ -1,55 +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.httpd;
-
-import com.google.gerrit.server.AccessPath;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.Singleton;
-
-import java.io.IOException;
-
-import javax.servlet.Filter;
-import javax.servlet.FilterChain;
-import javax.servlet.FilterConfig;
-import javax.servlet.ServletException;
-import javax.servlet.ServletRequest;
-import javax.servlet.ServletResponse;
-
-/** Set the WebSession to {@link AccessPath#GIT}. */
-@Singleton
-class ProjectAccessPathFilter implements Filter {
-  private final Provider<WebSession> session;
-
-  @Inject
-  ProjectAccessPathFilter(final Provider<WebSession> session) {
-    this.session = session;
-  }
-
-  @Override
-  public void doFilter(ServletRequest request, ServletResponse response,
-      FilterChain chain) throws IOException, ServletException {
-    session.get().setAccessPath(AccessPath.GIT);
-    chain.doFilter(request, response);
-  }
-
-  @Override
-  public void init(FilterConfig config) {
-  }
-
-  @Override
-  public void destroy() {
-  }
-}
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 929d034..c5b0e90 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
@@ -23,18 +23,23 @@
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gwtjsonrpc.server.SignedToken;
 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;
 import java.io.UnsupportedEncodingException;
 import java.security.MessageDigest;
 import java.security.NoSuchAlgorithmException;
 import java.util.Collections;
 import java.util.HashMap;
+import java.util.Locale;
 import java.util.Map;
 
 import javax.annotation.Nullable;
@@ -54,7 +59,7 @@
  * <p>
  * The current HTTP request is authenticated by looking up the username from the
  * Authorization header and checking the digest response against the stored
- * password. This filter is intended only to protect the {@link ProjectServlet}
+ * password. This filter is intended only to protect the {@link GitOverHttpServlet}
  * and its handled URLs, which provide remote repository access over HTTP.
  *
  * @see <a href="http://www.ietf.org/rfc/rfc2617.txt">RFC 2617</a>
@@ -67,16 +72,18 @@
   private final Provider<String> urlProvider;
   private final Provider<WebSession> session;
   private final AccountCache accountCache;
+  private final Config config;
   private final SignedToken tokens;
   private ServletContext context;
 
   @Inject
   ProjectDigestFilter(@CanonicalWebUrl @Nullable Provider<String> urlProvider,
-      Provider<WebSession> session, AccountCache accountCache)
-      throws XsrfException {
+      Provider<WebSession> session, AccountCache accountCache,
+      @GerritServerConfig Config config) throws XsrfException {
     this.urlProvider = urlProvider;
     this.session = session;
     this.accountCache = accountCache;
+    this.config = config;
     this.tokens = new SignedToken((int) SECONDS.convert(1, HOURS));
   }
 
@@ -93,6 +100,11 @@
   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);
 
     if (verify(req, rsp)) {
@@ -111,7 +123,7 @@
     }
 
     final Map<String, String> p = parseAuthorization(hdr);
-    final String username = p.get("username");
+    final String user = p.get("username");
     final String realm = p.get("realm");
     final String nonce = p.get("nonce");
     final String uri = p.get("uri");
@@ -121,7 +133,7 @@
     final String cnonce = p.get("cnonce");
     final String method = req.getMethod();
 
-    if (username == null //
+    if (user == null //
         || realm == null //
         || nonce == null //
         || uri == null //
@@ -133,6 +145,11 @@
       return false;
     }
 
+    String username = user;
+    if (config.getBoolean("auth", "userNameToLowerCase", false)) {
+      username = username.toLowerCase(Locale.US);
+    }
+
     final AccountState who = accountCache.getByUsername(username);
     if (who == null || ! who.getAccount().isActive()) {
       rsp.sendError(SC_UNAUTHORIZED);
@@ -145,7 +162,7 @@
       return false;
     }
 
-    final String A1 = username + ":" + realm + ":" + passwd;
+    final String A1 = user + ":" + realm + ":" + passwd;
     final String A2 = method + ":" + uri;
     final String expect =
         KD(H(A1), nonce + ":" + nc + ":" + cnonce + ":" + qop + ":" + H(A2));
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 8d6d684..1964c29 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
@@ -24,7 +24,7 @@
 import com.google.gerrit.httpd.raw.StaticServlet;
 import com.google.gerrit.httpd.raw.ToolServlet;
 import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.server.config.AuthConfig;
+import com.google.gerrit.reviewdb.Project;
 import com.google.gwtexpui.server.CacheControlFilter;
 import com.google.inject.Key;
 import com.google.inject.Provider;
@@ -38,12 +38,6 @@
 import javax.servlet.http.HttpServletResponse;
 
 class UrlModule extends ServletModule {
-  private final AuthConfig authConfig;
-
-  public UrlModule(AuthConfig authConfig) {
-    this.authConfig = authConfig;
-  }
-
   @Override
   protected void configureServlets() {
     filter("/*").through(Key.get(CacheControlFilter.class));
@@ -60,14 +54,6 @@
     serve("/static/*").with(StaticServlet.class);
     serve("/tools/*").with(ToolServlet.class);
 
-    filter("/p/*").through(ProjectAccessPathFilter.class);
-    if (authConfig.isTrustContainerAuth()) {
-      filter("/p/*").through(ContainerAuthFilter.class);
-    } else {
-      filter("/p/*").through(ProjectDigestFilter.class);
-    }
-    serve("/p/*").with(ProjectServlet.class);
-
     serve("/Main.class").with(notFound());
     serve("/com/google/gerrit/launcher/*").with(notFound());
     serve("/servlet/*").with(notFound());
@@ -81,6 +67,7 @@
     serveRegex("^/settings/?$").with(screen(PageLinks.SETTINGS));
     serveRegex("^/register/?$").with(screen(PageLinks.REGISTER + "/"));
     serveRegex("^/([1-9][0-9]*)/?$").with(directChangeById());
+    serveRegex("^/p/(.*)$").with(queryProjectNew());
     serveRegex("^/r/(.+)/?$").with(DirectChangeByCommit.class);
   }
 
@@ -138,6 +125,30 @@
     });
   }
 
+  private Key<HttpServlet> queryProjectNew() {
+    return key(new HttpServlet() {
+      private static final long serialVersionUID = 1L;
+
+      @Override
+      protected void doGet(HttpServletRequest req, HttpServletResponse rsp)
+          throws IOException {
+        String name = req.getPathInfo();
+        while (name.endsWith("/")) {
+          name = name.substring(0, name.length() - 1);
+        }
+        if (name.endsWith(".git")) {
+          name = name.substring(0, name.length() - 4);
+        }
+        while (name.endsWith("/")) {
+          name = name.substring(0, name.length() - 1);
+        }
+        Project.NameKey project = new Project.NameKey(name);
+        toGerrit(PageLinks.toChangeQuery(PageLinks.projectQuery(project,
+            Change.Status.NEW)), req, rsp);
+      }
+    });
+  }
+
   private Key<HttpServlet> query(final String query) {
     return key(new HttpServlet() {
       private static final long serialVersionUID = 1L;
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 96ca017..eb71733 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
@@ -21,10 +21,8 @@
 import com.google.gerrit.httpd.auth.container.HttpAuthModule;
 import com.google.gerrit.httpd.auth.container.HttpsClientSslCertModule;
 import com.google.gerrit.httpd.auth.ldap.LdapAuthModule;
-import com.google.gerrit.httpd.auth.openid.OpenIdModule;
 import com.google.gerrit.httpd.gitweb.GitWebModule;
 import com.google.gerrit.httpd.rpc.UiRpcModule;
-import com.google.gerrit.reviewdb.AuthType;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.RemotePeer;
@@ -38,12 +36,9 @@
 import com.google.gerrit.server.config.GerritRequestModule;
 import com.google.gerrit.server.contact.ContactStore;
 import com.google.gerrit.server.contact.ContactStoreProvider;
-import com.google.gerrit.server.ssh.SshInfo;
-import com.google.gerrit.server.ssh.SshKeyCache;
 import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
-import com.google.inject.Provider;
 import com.google.inject.ProvisionException;
 import com.google.inject.servlet.RequestScoped;
 import com.google.inject.servlet.ServletModule;
@@ -53,20 +48,14 @@
 import javax.annotation.Nullable;
 
 public class WebModule extends FactoryModule {
-  private final Provider<SshInfo> sshInfoProvider;
-  private final Provider<SshKeyCache> sshKeyCacheProvider;
   private final AuthConfig authConfig;
   private final boolean wantSSL;
   private final GitWebConfig gitWebConfig;
 
   @Inject
-  WebModule(final Provider<SshInfo> sshInfoProvider,
-      final Provider<SshKeyCache> sshKeyCacheProvider,
-      final AuthConfig authConfig,
+  WebModule(final AuthConfig authConfig,
       @CanonicalWebUrl @Nullable final String canonicalUrl,
       final Injector creatingInjector) {
-    this.sshInfoProvider = sshInfoProvider;
-    this.sshKeyCacheProvider = sshKeyCacheProvider;
     this.authConfig = authConfig;
     this.wantSSL = canonicalUrl != null && canonicalUrl.startsWith("https:");
 
@@ -93,10 +82,6 @@
     }
 
     switch (authConfig.getAuthType()) {
-      case OPENID:
-        install(new OpenIdModule());
-        break;
-
       case HTTP:
       case HTTP_LDAP:
         install(new HttpAuthModule());
@@ -120,17 +105,18 @@
         });
         break;
 
+      case OPENID:
+        // OpenID support is bound in WebAppInitializer and Daemon.
+      case CUSTOM_EXTENSION:
+        break;
       default:
         throw new ProvisionException("Unsupported loginType: " + authConfig.getAuthType());
     }
 
-    install(new UrlModule(authConfig));
+    install(new UrlModule());
     install(new UiRpcModule());
     install(new GerritRequestModule());
-    install(new ProjectServlet.Module());
-
-    bind(SshInfo.class).toProvider(sshInfoProvider);
-    bind(SshKeyCache.class).toProvider(sshKeyCacheProvider);
+    install(new GitOverHttpServlet.Module());
 
     bind(GitWebConfig.class).toInstance(gitWebConfig);
     if (gitWebConfig.getGitwebCGI() != null) {
@@ -151,11 +137,7 @@
     bind(SocketAddress.class).annotatedWith(RemotePeer.class).toProvider(
         HttpRemotePeerProvider.class).in(RequestScoped.class);
 
-    install(WebSession.module());
-
-    bind(CurrentUser.class).toProvider(HttpCurrentUserProvider.class).in(
-        RequestScoped.class);
-    bind(IdentifiedUser.class).toProvider(HttpIdentifiedUserProvider.class).in(
-        RequestScoped.class);
+    bind(CurrentUser.class).toProvider(HttpCurrentUserProvider.class);
+    bind(IdentifiedUser.class).toProvider(HttpIdentifiedUserProvider.class);
   }
 }
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 a2b7fc2..869b8e7 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
@@ -14,194 +14,30 @@
 
 package com.google.gerrit.httpd;
 
-import static java.util.concurrent.TimeUnit.HOURS;
-
-import com.google.gerrit.httpd.WebSessionManager.Key;
-import com.google.gerrit.httpd.WebSessionManager.Val;
 import com.google.gerrit.reviewdb.Account;
 import com.google.gerrit.reviewdb.AccountExternalId;
 import com.google.gerrit.server.AccessPath;
-import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AuthResult;
-import com.google.gerrit.server.cache.Cache;
-import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.cache.EvictionPolicy;
-import com.google.gerrit.server.config.AuthConfig;
-import com.google.inject.Inject;
-import com.google.inject.Module;
-import com.google.inject.Provider;
-import com.google.inject.TypeLiteral;
-import com.google.inject.servlet.RequestScoped;
 
-import javax.servlet.http.Cookie;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
+public interface WebSession {
+  public boolean isSignedIn();
 
-@RequestScoped
-public final class WebSession {
-  private static final String ACCOUNT_COOKIE = "GerritAccount";
+  public String getToken();
 
-  static Module module() {
-    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(12, HOURS) // expire sessions if they are inactive
-            .evictionPolicy(EvictionPolicy.LRU) // keep most recently used
-        ;
-        bind(WebSessionManager.class);
-        bind(WebSession.class).in(RequestScoped.class);
-      }
-    };
-  }
+  public boolean isTokenValid(String inputToken);
 
-  private final HttpServletRequest request;
-  private final HttpServletResponse response;
-  private final WebSessionManager manager;
-  private final AuthConfig authConfig;
-  private final Provider<AnonymousUser> anonymousProvider;
-  private final IdentifiedUser.RequestFactory identified;
-  private AccessPath accessPath = AccessPath.WEB_UI;
-  private Cookie outCookie;
+  public AccountExternalId.Key getLastLoginExternalId();
 
-  private Key key;
-  private Val val;
+  public CurrentUser getCurrentUser();
 
-  @Inject
-  WebSession(final HttpServletRequest request,
-      final HttpServletResponse response, final WebSessionManager manager,
-      final AuthConfig authConfig,
-      final Provider<AnonymousUser> anonymousProvider,
-      final IdentifiedUser.RequestFactory identified) {
-    this.request = request;
-    this.response = response;
-    this.manager = manager;
-    this.authConfig = authConfig;
-    this.anonymousProvider = anonymousProvider;
-    this.identified = identified;
-
-    final String cookie = readCookie();
-    if (cookie != null) {
-      key = new Key(cookie);
-      val = manager.get(key);
-    } else {
-      key = null;
-      val = null;
-    }
-
-    if (isSignedIn() && val.needsCookieRefresh()) {
-      // Cookie is more than half old. Send the cookie again to the
-      // client with an updated expiration date. We don't dare to
-      // change the key token here because there may be other RPCs
-      // queued up in the browser whose xsrfKey would not get updated
-      // with the new token, causing them to fail.
-      //
-      val = manager.createVal(key, val);
-      saveCookie();
-    }
-  }
-
-  private String readCookie() {
-    final Cookie[] all = request.getCookies();
-    if (all != null) {
-      for (final Cookie c : all) {
-        if (ACCOUNT_COOKIE.equals(c.getName())) {
-          final String v = c.getValue();
-          return v != null && !"".equals(v) ? v : null;
-        }
-      }
-    }
-    return null;
-  }
-
-  public boolean isSignedIn() {
-    return val != null;
-  }
-
-  String getToken() {
-    return isSignedIn() ? key.getToken() : null;
-  }
-
-  public boolean isTokenValid(final String inputToken) {
-    return isSignedIn() && key.getToken().equals(inputToken);
-  }
-
-  public AccountExternalId.Key getLastLoginExternalId() {
-    return val != null ? val.getExternalId() : null;
-  }
-
-  CurrentUser getCurrentUser() {
-    if (isSignedIn()) {
-      return identified.create(accessPath, val.getAccountId());
-    }
-    return anonymousProvider.get();
-  }
-
-  public void login(final AuthResult res, final boolean rememberMe) {
-    final Account.Id id = res.getAccountId();
-    final AccountExternalId.Key identity = res.getExternalId();
-
-    logout();
-
-    key = manager.createKey(id);
-    val = manager.createVal(key, id, rememberMe, identity);
-    saveCookie();
-  }
+  public void login(AuthResult res, boolean rememberMe);
 
   /** Change the access path from the default of {@link AccessPath#WEB_UI}. */
-  void setAccessPath(AccessPath path) {
-    accessPath = path;
-  }
+  public void setAccessPath(AccessPath path);
 
   /** Set the user account for this current request only. */
-  void setUserAccountId(Account.Id id) {
-    key = new Key("id:" + id);
-    val = new Val(id, 0, false, null);
-  }
+  public void setUserAccountId(Account.Id id);
 
-  public void logout() {
-    if (val != null) {
-      manager.destroy(key);
-      key = null;
-      val = null;
-      saveCookie();
-    }
-  }
-
-  private void saveCookie() {
-    final String token;
-    final int ageSeconds;
-
-    if (key == null) {
-      token = "";
-      ageSeconds = 0 /* erase at client */;
-    } else {
-      token = key.getToken();
-      ageSeconds = manager.getCookieAge(val);
-    }
-
-    if (outCookie == null) {
-      String path = authConfig.getCookiePath();
-      if (path == null || path.isEmpty()) {
-        path = request.getContextPath();
-        if (path.isEmpty()) {
-          path = "/";
-        }
-      }
-      outCookie = new Cookie(ACCOUNT_COOKIE, token);
-      outCookie.setPath(path);
-      outCookie.setMaxAge(ageSeconds);
-      outCookie.setSecure(authConfig.getCookieSecure());
-      response.addCookie(outCookie);
-    } else {
-      outCookie.setValue(token);
-      outCookie.setMaxAge(ageSeconds);
-    }
-  }
+  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 43c99ad..186d5da 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
@@ -21,17 +21,22 @@
 import static com.google.gerrit.server.ioutil.BasicSerialization.writeFixInt64;
 import static com.google.gerrit.server.ioutil.BasicSerialization.writeString;
 import static com.google.gerrit.server.ioutil.BasicSerialization.writeVarInt32;
+import static com.google.gerrit.httpd.CacheBasedWebSession.MAX_AGE_MINUTES;
 import static java.util.concurrent.TimeUnit.HOURS;
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
-import static java.util.concurrent.TimeUnit.SECONDS;
+import static java.util.concurrent.TimeUnit.MINUTES;
 
 import com.google.gerrit.reviewdb.Account;
 import com.google.gerrit.reviewdb.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;
 import com.google.inject.Singleton;
 import com.google.inject.name.Named;
 
+import org.eclipse.jgit.lib.Config;
+
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.ObjectInputStream;
@@ -47,13 +52,19 @@
     return System.currentTimeMillis();
   }
 
+  private final long sessionMaxAgeMillis;
   private final SecureRandom prng;
   private final Cache<Key, Val> self;
 
   @Inject
-  WebSessionManager(@Named(CACHE_NAME) final Cache<Key, Val> cache) {
+  WebSessionManager(@GerritServerConfig Config cfg,
+      @Named(CACHE_NAME) final Cache<Key, Val> cache) {
     prng = new SecureRandom();
     self = cache;
+
+    sessionMaxAgeMillis = MINUTES.toMillis(ConfigUtil.getTimeUnit(cfg,
+        "cache", CACHE_NAME, "maxAge",
+        MAX_AGE_MINUTES, MINUTES));
   }
 
   Key createKey(final Account.Id who) {
@@ -78,23 +89,33 @@
     final Account.Id who = val.getAccountId();
     final boolean remember = val.isPersistentCookie();
     final AccountExternalId.Key lastLogin = val.getExternalId();
+    final String xsrfToken = val.getXsrfToken();
 
-    return createVal(key, who, remember, lastLogin);
+    return createVal(key, who, remember, lastLogin, xsrfToken);
   }
 
   Val createVal(final Key key, final Account.Id who, final boolean remember,
-      final AccountExternalId.Key lastLogin) {
+      final AccountExternalId.Key lastLogin, String xsrfToken) {
     // Refresh the cookie every hour or when it is half-expired.
     // This reduces the odds that the user session will be kicked
     // early but also avoids us needing to refresh the cookie on
     // every single request.
     //
-    final long halfAgeRefresh = self.getTimeToLive(MILLISECONDS) >>> 1;
+    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 Val val = new Val(who, refreshCookieAt, remember, lastLogin);
+    if (xsrfToken == null) {
+      // If we don't yet have a token for this session, establish one.
+      //
+      final int nonceLen = 20;
+      final byte[] rnd = new byte[nonceLen];
+      prng.nextBytes(rnd);
+      xsrfToken = CookieBase64.encode(rnd);
+    }
+
+    Val val = new Val(who, refreshCookieAt, remember, lastLogin, xsrfToken);
     self.put(key, val);
     return val;
   }
@@ -104,7 +125,7 @@
       // Client may store the cookie until we would remove it from our
       // own cache, after which it will certainly be invalid.
       //
-      return (int) self.getTimeToLive(SECONDS);
+      return (int) MILLISECONDS.toSeconds(sessionMaxAgeMillis);
     } else {
       // Client should not store the cookie, as the user asked for us
       // to not remember them long-term. Sending -1 as the age will
@@ -162,13 +183,16 @@
     private transient long refreshCookieAt;
     private transient boolean persistentCookie;
     private transient AccountExternalId.Key externalId;
+    private transient String xsrfToken;
 
     Val(final Account.Id accountId, final long refreshCookieAt,
-        final boolean persistentCookie, final AccountExternalId.Key externalId) {
+        final boolean persistentCookie, final AccountExternalId.Key externalId,
+        final String xsrfToken) {
       this.accountId = accountId;
       this.refreshCookieAt = refreshCookieAt;
       this.persistentCookie = persistentCookie;
       this.externalId = externalId;
+      this.xsrfToken = xsrfToken;
     }
 
     Account.Id getAccountId() {
@@ -187,6 +211,10 @@
       return persistentCookie;
     }
 
+    String getXsrfToken() {
+      return xsrfToken;
+    }
+
     private void writeObject(final ObjectOutputStream out) throws IOException {
       writeVarInt32(out, 1);
       writeVarInt32(out, accountId.get());
@@ -202,6 +230,9 @@
         writeString(out, externalId.get());
       }
 
+      writeVarInt32(out, 5);
+      writeString(out, xsrfToken);
+
       writeVarInt32(out, 0);
     }
 
@@ -223,6 +254,9 @@
           case 4:
             externalId = new AccountExternalId.Key(readString(in));
             continue;
+          case 5:
+            xsrfToken = readString(in);
+            continue;
           default:
             throw new IOException("Unknown tag found in object: " + tag);
         }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSshGlueModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSshGlueModule.java
new file mode 100644
index 0000000..82e9da7
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSshGlueModule.java
@@ -0,0 +1,46 @@
+// 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.httpd;
+
+import com.google.gerrit.server.ssh.SshInfo;
+import com.google.gerrit.server.ssh.SshKeyCache;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+/**
+ * Pulls objects from the SSH injector over the HTTP injector.
+ * <p>
+ * This mess is only necessary because we build up two different injectors, in
+ * order to have different request scopes. But some HTTP RPCs can cause changes
+ * to the SSH side of the house, and thus needs access to it.
+ */
+public class WebSshGlueModule extends AbstractModule {
+  private final Provider<SshInfo> sshInfoProvider;
+  private final Provider<SshKeyCache> sshKeyCacheProvider;
+
+  @Inject
+  WebSshGlueModule(Provider<SshInfo> sshInfoProvider,
+      Provider<SshKeyCache> sshKeyCacheProvider) {
+    this.sshInfoProvider = sshInfoProvider;
+    this.sshKeyCacheProvider = sshKeyCacheProvider;
+  }
+
+  @Override
+  protected void configure() {
+    bind(SshInfo.class).toProvider(sshInfoProvider);
+    bind(SshKeyCache.class).toProvider(sshKeyCacheProvider);
+  }
+}
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 8e80a0c..5df004e 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
@@ -80,7 +80,7 @@
   protected void doGet(final HttpServletRequest req,
       final HttpServletResponse rsp) throws ServletException, IOException {
     final String token = getToken(req);
-    if ("logout".equals(token) || "signout".equals(token)) {
+    if ("/logout".equals(token) || "/signout".equals(token)) {
       req.getRequestDispatcher("/logout").forward(req, rsp);
       return;
     }
@@ -166,6 +166,8 @@
     String token = req.getPathInfo();
     if (token == null || token.isEmpty()) {
       token = PageLinks.MINE;
+    } else if (!token.startsWith("/")) {
+      token = "/" + token;
     }
     return token;
   }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/ldap/LoginRedirectServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/ldap/LoginRedirectServlet.java
index 2726a78..da6e227 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/ldap/LoginRedirectServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/ldap/LoginRedirectServlet.java
@@ -77,6 +77,8 @@
     String token = req.getPathInfo();
     if (token == null || token.isEmpty()) {
       token = PageLinks.MINE;
+    } else if (!token.startsWith("/")) {
+      token = "/" + token;
     }
     return token;
   }
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 e4577c1..a77dfc9 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
@@ -17,11 +17,14 @@
 import com.google.gerrit.common.auth.userpass.LoginResult;
 import com.google.gerrit.common.auth.userpass.UserPassAuthService;
 import com.google.gerrit.httpd.WebSession;
+import com.google.gerrit.reviewdb.AuthType;
 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.AuthRequest;
 import com.google.gerrit.server.account.AuthResult;
+import com.google.gerrit.server.auth.AuthenticationUnavailableException;
+import com.google.gerrit.server.config.AuthConfig;
 import com.google.gwt.user.client.rpc.AsyncCallback;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -29,21 +32,23 @@
 class UserPassAuthServiceImpl implements UserPassAuthService {
   private final Provider<WebSession> webSession;
   private final AccountManager accountManager;
+  private final AuthType authType;
 
   @Inject
   UserPassAuthServiceImpl(final Provider<WebSession> webSession,
-      final AccountManager accountManager) {
+      final AccountManager accountManager, final AuthConfig authConfig) {
     this.webSession = webSession;
     this.accountManager = accountManager;
+    this.authType = authConfig.getAuthType();
   }
 
   @Override
   public void authenticate(String username, final String password,
       final AsyncCallback<LoginResult> callback) {
-    LoginResult result = new LoginResult();
+    LoginResult result = new LoginResult(authType);
     if (username == null || "".equals(username.trim()) //
         || password == null || "".equals(password)) {
-      result.success = false;
+      result.setError(LoginResult.Error.INVALID_LOGIN);
       callback.onSuccess(result);
       return;
     }
@@ -62,8 +67,12 @@
       // error screen with error message should be shown to the user
       callback.onFailure(e);
       return;
+    } catch (AuthenticationUnavailableException e) {
+      result.setError(LoginResult.Error.AUTHENTICATION_UNAVAILABLE);
+      callback.onSuccess(result);
+      return;
     } catch (AccountException e) {
-      result.success = false;
+      result.setError(LoginResult.Error.INVALID_LOGIN);
       callback.onSuccess(result);
       return;
     }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java
index 1c2e5b8..f105df0 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java
@@ -17,8 +17,10 @@
 import com.google.gerrit.common.data.GerritConfig;
 import com.google.gerrit.common.data.HostPageData;
 import com.google.gerrit.httpd.HtmlDomUtil;
+import com.google.gerrit.httpd.WebSession;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gwt.user.server.rpc.RPCServletUtils;
 import com.google.gwtexpui.linker.server.Permutation;
@@ -28,6 +30,7 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
+import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 import org.slf4j.Logger;
@@ -61,6 +64,7 @@
   private static final String HPD_ID = "gerrit_hostpagedata";
 
   private final Provider<CurrentUser> currentUser;
+  private final Provider<WebSession> session;
   private final GerritConfig config;
   private final HostPageData.Theme signedOutTheme;
   private final HostPageData.Theme signedInTheme;
@@ -68,17 +72,22 @@
   private final Document template;
   private final String noCacheName;
   private final PermutationSelector selector;
+  private final boolean refreshHeaderFooter;
   private volatile Page page;
 
   @Inject
-  HostPageServlet(final Provider<CurrentUser> cu, final SitePaths sp,
-      final ThemeFactory themeFactory, final GerritConfig gc,
-      final ServletContext servletContext) throws IOException, ServletException {
+  HostPageServlet(final Provider<CurrentUser> cu, final Provider<WebSession> w,
+      final SitePaths sp, final ThemeFactory themeFactory,
+      final GerritConfig gc, final ServletContext servletContext,
+      @GerritServerConfig final Config cfg)
+      throws IOException, ServletException {
     currentUser = cu;
+    session = w;
     config = gc;
     signedOutTheme = themeFactory.getSignedOutTheme();
     signedInTheme = themeFactory.getSignedInTheme();
     site = sp;
+    refreshHeaderFooter = cfg.getBoolean("site", "refreshHeaderFooter", true);
 
     final String pageName = "HostPage.html";
     template = HtmlDomUtil.parseFile(getClass(), pageName);
@@ -95,7 +104,7 @@
 
     final String src = "gerrit/gerrit.nocache.js";
     selector = new PermutationSelector("gerrit");
-    if (IS_DEV) {
+    if (IS_DEV || !cfg.getBoolean("site", "checkUserAgent", true)) {
       noCacheName = src;
     } else {
       final Element devmode = HtmlDomUtil.find(template, "gerrit_gwtdevmode");
@@ -136,7 +145,7 @@
 
   private Page get() {
     Page p = page;
-    if (p.isStale()) {
+    if (refreshHeaderFooter && p.isStale()) {
       final Page newPage;
       try {
         newPage = new Page();
@@ -163,6 +172,10 @@
       json(((IdentifiedUser) user).getAccount(), w);
       w.write(";");
 
+      w.write(HPD_ID + ".xsrfToken=");
+      json(session.get().getToken(), w);
+      w.write(";");
+
       w.write(HPD_ID + ".accountDiffPref=");
       json(((IdentifiedUser) user).getAccountDiffPreference(), w);
       w.write(";");
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 28a0d03..777d63e 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
@@ -102,9 +102,9 @@
     this.queryRewriter = queryRewriter;
   }
 
-  private boolean canRead(final Change c) {
+  private boolean canRead(final Change c, final ReviewDb db) throws OrmException {
     try {
-      return changeControlFactory.controlFor(c).isVisible();
+      return changeControlFactory.controlFor(c).isVisible(db);
     } catch (NoSuchChangeException e) {
       return false;
     }
@@ -187,7 +187,7 @@
         //
         if (!want.isEmpty()) {
           for (Change c : db.changes().get(want)) {
-            if (canRead(c)) {
+            if (canRead(c, db)) {
               r.add(c);
             }
           }
@@ -236,20 +236,20 @@
         }
 
         d = new AccountDashboardInfo(target);
-        d.setByOwner(filter(changes.byOwnerOpen(target), stars, ac));
-        d.setClosed(filter(changes.byOwnerClosed(target), stars, ac));
+        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));
+        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));
+          d.getClosed().addAll(filter(changes.get(closedReviews), stars, ac, db));
           Collections.sort(d.getClosed(), SORT_KEY_COMP);
         }
 
@@ -304,10 +304,11 @@
   }
 
   private List<ChangeInfo> filter(final ResultSet<Change> rs,
-      final Set<Change.Id> starred, final AccountInfoCacheFactory accts) {
+      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)) {
+      if (canRead(c, db)) {
         final ChangeInfo ci = new ChangeInfo(c);
         accts.want(ci.getOwner());
         ci.setStarred(starred.contains(ci.getId()));
@@ -337,6 +338,9 @@
       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()));
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 273bd9d..bf77c6a 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
@@ -30,8 +30,7 @@
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.account.GroupControl;
-import com.google.gerrit.server.account.GroupMembersFactory;
-import com.google.gerrit.server.account.GroupMembersFactory.Factory;
+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.NoSuchProjectException;
@@ -60,7 +59,7 @@
   private final ProjectCache projectCache;
   private final AccountCache accountCache;
   private final GroupControl.Factory groupControlFactory;
-  private final Factory groupMembersFactory;
+  private final GroupMembers.Factory groupMembersFactory;
   private final IdentifiedUser.GenericFactory userFactory;
   private final Provider<CurrentUser> currentUser;
   private final SuggestAccountsEnum suggestAccounts;
@@ -73,7 +72,7 @@
       final ProjectControl.Factory projectControlFactory,
       final ProjectCache projectCache, final AccountCache accountCache,
       final GroupControl.Factory groupControlFactory,
-      final GroupMembersFactory.Factory groupMembersFactory,
+      final GroupMembers.Factory groupMembersFactory,
       final IdentifiedUser.GenericFactory userFactory,
       final Provider<CurrentUser> currentUser,
       @GerritServerConfig final Config cfg, final GroupCache groupCache) {
@@ -281,7 +280,7 @@
 
     try {
       final Set<Account> members =
-          groupMembersFactory.create(project, group.getUUID()).call();
+          groupMembersFactory.create().listAccounts(group.getUUID(), project);
 
       if (members.isEmpty()) {
         return false;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountModule.java
index 91c8aa9..957f339 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountModule.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.httpd.rpc.RpcServletModule;
 import com.google.gerrit.httpd.rpc.UiRpcModule;
 import com.google.gerrit.server.config.FactoryModule;
+import com.google.gerrit.server.mail.RegisterNewEmailSender;
 
 public class AccountModule extends RpcServletModule {
   public AccountModule() {
@@ -34,7 +35,9 @@
         factory(ExternalIdDetailFactory.Factory.class);
         factory(GroupDetailHandler.Factory.class);
         factory(MyGroupsFactory.Factory.class);
+        factory(RegisterNewEmailSender.Factory.class);
         factory(RenameGroup.Factory.class);
+        factory(VisibleGroupsHandler.Factory.class);
       }
     });
     rpc(AccountSecurityImpl.class);
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 f69a0d6..be28840 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,7 +14,7 @@
 
 package com.google.gerrit.httpd.rpc.account;
 
-import com.google.gerrit.common.ChangeHookRunner;
+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.errors.ContactInformationStoreException;
@@ -27,13 +27,11 @@
 import com.google.gerrit.reviewdb.Account;
 import com.google.gerrit.reviewdb.AccountAgreement;
 import com.google.gerrit.reviewdb.AccountExternalId;
-import com.google.gerrit.reviewdb.AccountGroup;
 import com.google.gerrit.reviewdb.AccountSshKey;
 import com.google.gerrit.reviewdb.AuthType;
 import com.google.gerrit.reviewdb.ContactInformation;
 import com.google.gerrit.reviewdb.ContributorAgreement;
 import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gerrit.reviewdb.Project;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountByEmailCache;
@@ -44,27 +42,22 @@
 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.GroupDetailFactory;
 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.ssh.SshKeyCache;
 import com.google.gwt.user.client.rpc.AsyncCallback;
 import com.google.gwtjsonrpc.client.VoidResult;
-import com.google.gwtjsonrpc.server.ValidToken;
-import com.google.gwtjsonrpc.server.XsrfException;
 import com.google.gwtorm.client.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-import org.eclipse.jgit.util.Base64;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.io.UnsupportedEncodingException;
-import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
 import java.util.Set;
@@ -76,6 +69,7 @@
   private final AuthConfig authConfig;
   private final Realm realm;
   private final Provider<IdentifiedUser> user;
+  private final EmailTokenVerifier emailTokenVerifier;
   private final RegisterNewEmailSender.Factory registerNewEmailFactory;
   private final SshKeyCache sshKeyCache;
   private final AccountByEmailCache byEmailCache;
@@ -89,14 +83,14 @@
   private final DeleteExternalIds.Factory deleteExternalIdsFactory;
   private final ExternalIdDetailFactory.Factory externalIdDetailFactory;
   private final MyGroupsFactory.Factory myGroupsFactory;
-  private final GroupDetailFactory.Factory groupDetailFactory;
 
-  private final ChangeHookRunner hooks;
+  private final ChangeHooks hooks;
 
   @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 RegisterNewEmailSender.Factory esf, final SshKeyCache skc,
       final AccountByEmailCache abec, final AccountCache uac,
       final AccountManager am,
@@ -106,13 +100,13 @@
       final DeleteExternalIds.Factory deleteExternalIdsFactory,
       final ExternalIdDetailFactory.Factory externalIdDetailFactory,
       final MyGroupsFactory.Factory myGroupsFactory,
-      final GroupDetailFactory.Factory groupDetailFactory,
-      final ChangeHookRunner hooks) {
+      final ChangeHooks hooks) {
     super(schema, currentUser);
     contactStore = cs;
     authConfig = ac;
     realm = r;
     user = u;
+    emailTokenVerifier = etv;
     registerNewEmailFactory = esf;
     sshKeyCache = skc;
     byEmailCache = abec;
@@ -127,7 +121,6 @@
     this.deleteExternalIdsFactory = deleteExternalIdsFactory;
     this.externalIdDetailFactory = externalIdDetailFactory;
     this.myGroupsFactory = myGroupsFactory;
-    this.groupDetailFactory = groupDetailFactory;
     this.hooks = hooks;
   }
 
@@ -216,11 +209,7 @@
     run(callback, new Action<List<GroupDetail>>() {
       public List<GroupDetail> run(final ReviewDb db) throws OrmException,
           NoSuchGroupException, Failure {
-        List<GroupDetail> groupDetails = new ArrayList<GroupDetail>();
-        for(AccountGroup group : myGroupsFactory.create().call()) {
-          groupDetails.add(groupDetailFactory.create(group.getId()).call());
-        }
-        return groupDetails;
+        return myGroupsFactory.create().call();
       }
     });
   }
@@ -319,26 +308,18 @@
     }
   }
 
-  public void validateEmail(final String token,
+  public void validateEmail(final String tokenString,
       final AsyncCallback<VoidResult> callback) {
     try {
-      final ValidToken t =
-          authConfig.getEmailRegistrationToken().checkToken(token, null);
-      if (t == null || t.getData() == null || "".equals(t.getData())) {
-        callback.onFailure(new IllegalStateException("Invalid token"));
-        return;
+      EmailTokenVerifier.ParsedToken token = emailTokenVerifier.decode(tokenString);
+      Account.Id currentUser = user.get().getAccountId();
+      if (currentUser.equals(token.getAccountId())) {
+        accountManager.link(currentUser, token.toAuthRequest());
+        callback.onSuccess(VoidResult.INSTANCE);
+      } else {
+        throw new EmailTokenVerifier.InvalidTokenException();
       }
-      final String newEmail = new String(Base64.decode(t.getData()), "UTF-8");
-      if (!newEmail.contains("@")) {
-        callback.onFailure(new IllegalStateException("Invalid token"));
-        return;
-      }
-      accountManager.link(user.get().getAccountId(), AuthRequest
-          .forEmail(newEmail));
-      callback.onSuccess(VoidResult.INSTANCE);
-    } catch (XsrfException e) {
-      callback.onFailure(e);
-    } catch (UnsupportedEncodingException e) {
+    } catch (EmailTokenVerifier.InvalidTokenException e) {
       callback.onFailure(e);
     } catch (AccountException e) {
       callback.onFailure(e);
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 caae4be..448baf9 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
@@ -53,7 +53,6 @@
 
 class GroupAdminServiceImpl extends BaseServiceImplementation implements
     GroupAdminService {
-  private final Provider<IdentifiedUser> identifiedUser;
   private final AccountCache accountCache;
   private final AccountResolver accountResolver;
   private final Realm accountRealm;
@@ -64,6 +63,7 @@
   private final CreateGroup.Factory createGroupFactory;
   private final RenameGroup.Factory renameGroupFactory;
   private final GroupDetailHandler.Factory groupDetailFactory;
+  private final VisibleGroupsHandler.Factory visibleGroupsFactory;
 
   @Inject
   GroupAdminServiceImpl(final Provider<ReviewDb> schema,
@@ -75,9 +75,9 @@
       final GroupControl.Factory groupControlFactory,
       final CreateGroup.Factory createGroupFactory,
       final RenameGroup.Factory renameGroupFactory,
-      final GroupDetailHandler.Factory groupDetailFactory) {
+      final GroupDetailHandler.Factory groupDetailFactory,
+      final VisibleGroupsHandler.Factory visibleGroupsFactory) {
     super(schema, currentUser);
-    this.identifiedUser = currentUser;
     this.accountCache = accountCache;
     this.groupIncludeCache = groupIncludeCache;
     this.accountResolver = accountResolver;
@@ -87,41 +87,11 @@
     this.createGroupFactory = createGroupFactory;
     this.renameGroupFactory = renameGroupFactory;
     this.groupDetailFactory = groupDetailFactory;
+    this.visibleGroupsFactory = visibleGroupsFactory;
   }
 
   public void visibleGroups(final AsyncCallback<GroupList> callback) {
-    run(callback, new Action<GroupList>() {
-      public GroupList run(ReviewDb db) throws OrmException,
-          NoSuchGroupException {
-        final IdentifiedUser user = identifiedUser.get();
-        final List<AccountGroup> list;
-        if (user.getCapabilities().canAdministrateServer()) {
-          list = db.accountGroups().all().toList();
-        } else {
-          list = new ArrayList<AccountGroup>();
-          for(final AccountGroup group : db.accountGroups().all().toList()) {
-            final GroupControl c = groupControlFactory.controlFor(group);
-            if (c.isVisible()) {
-              list.add(c.getAccountGroup());
-            }
-          }
-        }
-        Collections.sort(list, new Comparator<AccountGroup>() {
-          public int compare(final AccountGroup a, final AccountGroup b) {
-            return a.getName().compareTo(b.getName());
-          }
-        });
-
-        List<GroupDetail> l = new ArrayList<GroupDetail>();
-        for(AccountGroup group : list) {
-          l.add(groupDetailFactory.create(group.getId()).call());
-        }
-        GroupList res = new GroupList();
-        res.setGroups(l);
-        res.setCanCreateGroup(user.getCapabilities().canCreateGroup());
-        return res;
-      }
-    });
+    visibleGroupsFactory.create().to(callback);
   }
 
   public void createGroup(final String newName,
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 a2277bc..d0557ae 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,46 +14,33 @@
 
 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.AccountGroup;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.account.VisibleGroups;
+import com.google.gwtorm.client.OrmException;
 import com.google.inject.Inject;
 
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
 import java.util.List;
-import java.util.Set;
 
-class MyGroupsFactory extends Handler<List<AccountGroup>> {
+class MyGroupsFactory extends Handler<List<GroupDetail>> {
   interface Factory {
     MyGroupsFactory create();
   }
 
-  private final GroupCache groupCache;
+  private final VisibleGroups.Factory visibleGroupsFactory;
   private final IdentifiedUser user;
 
   @Inject
-  MyGroupsFactory(final GroupCache groupCache, final IdentifiedUser user) {
-    this.groupCache = groupCache;
+  MyGroupsFactory(final VisibleGroups.Factory visibleGroupsFactory, final IdentifiedUser user) {
+    this.visibleGroupsFactory = visibleGroupsFactory;
     this.user = user;
   }
 
   @Override
-  public List<AccountGroup> call() {
-    final Set<AccountGroup.UUID> effective = user.getEffectiveGroups();
-    final int cnt = effective.size();
-    final List<AccountGroup> groupList = new ArrayList<AccountGroup>(cnt);
-    for (final AccountGroup.UUID groupId : effective) {
-      groupList.add(groupCache.get(groupId));
-    }
-    Collections.sort(groupList, new Comparator<AccountGroup>() {
-      @Override
-      public int compare(AccountGroup a, AccountGroup b) {
-        return a.getName().compareTo(b.getName());
-      }
-    });
-    return groupList;
+  public List<GroupDetail> 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 d7cd195..2d7770e 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
@@ -19,51 +19,25 @@
 import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.httpd.rpc.Handler;
 import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.AccountGroupName;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.GroupCache;
-import com.google.gerrit.server.account.GroupControl;
-import com.google.gerrit.server.account.GroupDetailFactory;
-import com.google.gerrit.server.git.RenameGroupOp;
-import com.google.gwtorm.client.OrmDuplicateKeyException;
+import com.google.gerrit.server.account.PerformRenameGroup;
 import com.google.gwtorm.client.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
-import java.util.Collections;
-import java.util.Date;
-import java.util.TimeZone;
-import java.util.concurrent.TimeUnit;
-
 class RenameGroup extends Handler<GroupDetail> {
   interface Factory {
     RenameGroup create(AccountGroup.Id id, String newName);
   }
 
-  private final ReviewDb db;
-  private final GroupCache groupCache;
-  private final GroupControl.Factory groupControlFactory;
-  private final GroupDetailFactory.Factory groupDetailFactory;
-  private final RenameGroupOp.Factory renameGroupOpFactory;
-  private final IdentifiedUser currentUser;
+  private final PerformRenameGroup.Factory performRenameGroupFactory;
 
   private final AccountGroup.Id groupId;
   private final String newName;
 
   @Inject
-  RenameGroup(final ReviewDb db, final GroupCache groupCache,
-      final GroupControl.Factory groupControlFactory,
-      final GroupDetailFactory.Factory groupDetailFactory,
-      final RenameGroupOp.Factory renameGroupOpFactory,
-      final IdentifiedUser currentUser,
+  RenameGroup(final PerformRenameGroup.Factory performRenameGroupFactory,
       @Assisted final AccountGroup.Id groupId, @Assisted final String newName) {
-    this.db = db;
-    this.groupCache = groupCache;
-    this.groupControlFactory = groupControlFactory;
-    this.groupDetailFactory = groupDetailFactory;
-    this.renameGroupOpFactory = renameGroupOpFactory;
-    this.currentUser = currentUser;
+    this.performRenameGroupFactory = performRenameGroupFactory;
     this.groupId = groupId;
     this.newName = newName;
   }
@@ -71,46 +45,6 @@
   @Override
   public GroupDetail call() throws OrmException, NameAlreadyUsedException,
       NoSuchGroupException {
-    final GroupControl ctl = groupControlFactory.validateFor(groupId);
-    final AccountGroup group = db.accountGroups().get(groupId);
-    if (group == null || !ctl.isOwner()) {
-      throw new NoSuchGroupException(groupId);
-    }
-
-    final AccountGroup.NameKey old = group.getNameKey();
-    final AccountGroup.NameKey key = new AccountGroup.NameKey(newName);
-
-    try {
-      final AccountGroupName id = new AccountGroupName(key, groupId);
-      db.accountGroupNames().insert(Collections.singleton(id));
-    } catch (OrmDuplicateKeyException dupeErr) {
-      // If we are using this identity, don't report the exception.
-      //
-      AccountGroupName other = db.accountGroupNames().get(key);
-      if (other != null && other.getId().equals(groupId)) {
-        return groupDetailFactory.create(groupId).call();
-      }
-
-      // Otherwise, someone else has this identity.
-      //
-      throw new NameAlreadyUsedException();
-    }
-
-    group.setNameKey(key);
-    db.accountGroups().update(Collections.singleton(group));
-
-    AccountGroupName priorName = db.accountGroupNames().get(old);
-    if (priorName != null) {
-      db.accountGroupNames().delete(Collections.singleton(priorName));
-    }
-
-    groupCache.evict(group);
-    groupCache.evictAfterRename(old);
-    renameGroupOpFactory.create( //
-        currentUser.newCommitterIdent(new Date(), TimeZone.getDefault()), //
-        group.getGroupUUID(), //
-        old.get(), newName).start(0, TimeUnit.MILLISECONDS);
-
-    return groupDetailFactory.create(groupId).call();
+    return performRenameGroupFactory.create().renameGroup(groupId, newName);
   }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/VisibleGroupsHandler.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/VisibleGroupsHandler.java
new file mode 100644
index 0000000..54f91f7
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/VisibleGroupsHandler.java
@@ -0,0 +1,39 @@
+// 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.account;
+
+import com.google.gerrit.common.data.GroupList;
+import com.google.gerrit.httpd.rpc.Handler;
+import com.google.gerrit.server.account.VisibleGroups;
+import com.google.inject.Inject;
+
+public class VisibleGroupsHandler extends Handler<GroupList> {
+
+  interface Factory {
+    VisibleGroupsHandler create();
+  }
+
+  private final VisibleGroups.Factory visibleGroupsFactory;
+
+  @Inject
+  VisibleGroupsHandler(final VisibleGroups.Factory visibleGroupsFactory) {
+    this.visibleGroupsFactory = visibleGroupsFactory;
+  }
+
+  @Override
+  public GroupList call() throws Exception {
+    return visibleGroupsFactory.create().get();
+  }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/AbandonChange.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/AbandonChange.java
deleted file mode 100644
index b4f595c..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/AbandonChange.java
+++ /dev/null
@@ -1,89 +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.changedetail;
-
-import com.google.gerrit.common.ChangeHookRunner;
-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.Change;
-import com.google.gerrit.reviewdb.PatchSet;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gerrit.server.ChangeUtil;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.mail.AbandonedSender;
-import com.google.gerrit.server.mail.EmailException;
-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;
-import com.google.gwtorm.client.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
-
-import javax.annotation.Nullable;
-
-class AbandonChange extends Handler<ChangeDetail> {
-  interface Factory {
-    AbandonChange create(PatchSet.Id patchSetId, String message);
-  }
-
-  private final ChangeControl.Factory changeControlFactory;
-  private final ReviewDb db;
-  private final IdentifiedUser currentUser;
-  private final AbandonedSender.Factory senderFactory;
-  private final ChangeDetailFactory.Factory changeDetailFactory;
-
-  private final PatchSet.Id patchSetId;
-  @Nullable
-  private final String message;
-
-  private final ChangeHookRunner hooks;
-
-  @Inject
-  AbandonChange(final ChangeControl.Factory changeControlFactory,
-      final ReviewDb db, final IdentifiedUser currentUser,
-      final AbandonedSender.Factory senderFactory,
-      final ChangeDetailFactory.Factory changeDetailFactory,
-      @Assisted final PatchSet.Id patchSetId,
-      @Assisted @Nullable final String message, final ChangeHookRunner hooks) {
-    this.changeControlFactory = changeControlFactory;
-    this.db = db;
-    this.currentUser = currentUser;
-    this.senderFactory = senderFactory;
-    this.changeDetailFactory = changeDetailFactory;
-
-    this.patchSetId = patchSetId;
-    this.message = message;
-    this.hooks = hooks;
-  }
-
-  @Override
-  public ChangeDetail call() throws NoSuchChangeException, OrmException,
-      EmailException, NoSuchEntityException, InvalidChangeOperationException,
-      PatchSetInfoNotAvailableException {
-
-    final Change.Id changeId = patchSetId.getParentKey();
-    final ChangeControl control = changeControlFactory.validateFor(changeId);
-    if (!control.canAbandon()) {
-      throw new NoSuchChangeException(changeId);
-    }
-
-    ChangeUtil.abandon(patchSetId, currentUser, message, db, senderFactory,
-        hooks);
-
-    return changeDetailFactory.create(changeId).call();
-  }
-}
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
new file mode 100644
index 0000000..c8bb0f7
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/AbandonChangeHandler.java
@@ -0,0 +1,69 @@
+// 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.changedetail;
+
+import com.google.gerrit.common.data.ChangeDetail;
+import com.google.gerrit.common.data.ReviewResult;
+import com.google.gerrit.common.errors.NoSuchEntityException;
+import com.google.gerrit.httpd.rpc.Handler;
+import com.google.gerrit.reviewdb.PatchSet;
+import com.google.gerrit.server.changedetail.AbandonChange;
+import com.google.gerrit.server.mail.EmailException;
+import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gwtorm.client.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import javax.annotation.Nullable;
+
+class AbandonChangeHandler extends Handler<ChangeDetail> {
+
+  interface Factory {
+    AbandonChangeHandler create(PatchSet.Id patchSetId, String message);
+  }
+
+  private final AbandonChange.Factory abandonChangeFactory;
+  private final ChangeDetailFactory.Factory changeDetailFactory;
+
+  private final PatchSet.Id patchSetId;
+  @Nullable
+  private final String message;
+
+  @Inject
+  AbandonChangeHandler(final AbandonChange.Factory abandonChangeFactory,
+      final ChangeDetailFactory.Factory changeDetailFactory,
+      @Assisted final PatchSet.Id patchSetId,
+      @Assisted @Nullable final String message) {
+    this.abandonChangeFactory = abandonChangeFactory;
+    this.changeDetailFactory = changeDetailFactory;
+
+    this.patchSetId = patchSetId;
+    this.message = message;
+  }
+
+  @Override
+  public ChangeDetail call() throws NoSuchChangeException, OrmException,
+      EmailException, NoSuchEntityException, InvalidChangeOperationException,
+      PatchSetInfoNotAvailableException {
+    final ReviewResult result =
+        abandonChangeFactory.create(patchSetId, message).call();
+    if (result.getErrors().size() > 0) {
+      throw new NoSuchChangeException(result.getChangeId());
+    }
+    return changeDetailFactory.create(result.getChangeId()).call();
+  }
+}
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 248515d..d226fd0 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,6 +32,7 @@
 import com.google.gerrit.reviewdb.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.account.AccountInfoCacheFactory;
 import com.google.gerrit.server.config.GerritServerConfig;
@@ -42,6 +43,7 @@
 import com.google.gerrit.server.workflow.CategoryFunction;
 import com.google.gerrit.server.workflow.FunctionState;
 import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.client.ResultSet;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -74,6 +76,7 @@
 
   private ChangeDetail detail;
   private ChangeControl control;
+  private Map<PatchSet.Id, PatchSet> patchsetsById;
 
   private final MergeOp.Factory opFactory;
   private boolean testMerge;
@@ -116,15 +119,19 @@
 
     detail = new ChangeDetail();
     detail.setChange(change);
-    detail.setAllowsAnonymous(control.forUser(anonymousUser).isVisible());
+    detail.setAllowsAnonymous(control.forUser(anonymousUser).isVisible(db));
 
-    detail.setCanAbandon(change.getStatus().isOpen() && control.canAbandon());
+    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.setCanDeleteDraft(control.canDeleteDraft(db));
     detail.setStarred(control.getCurrentUser().getStarredChanges().contains(
         changeId));
 
     detail.setCanRevert(change.getStatus() == Change.Status.MERGED && control.canAddPatchSet());
 
+    detail.setCanEdit(control.getRefControl().canWrite());
+
     if (detail.getChange().getStatus().isOpen()) {
       List<SubmitRecord> submitRecords = control.canSubmit(db, patch.getId());
       for (SubmitRecord rec : submitRecords) {
@@ -140,6 +147,7 @@
       detail.setSubmitRecords(submitRecords);
     }
 
+    patchsetsById = new HashMap<PatchSet.Id, PatchSet>();
     loadPatchSets();
     loadMessages();
     if (change.currentPatchSetId() != null) {
@@ -151,11 +159,34 @@
   }
 
   private void loadPatchSets() throws OrmException {
-    detail.setPatchSets(db.patchSets().byChange(changeId).toList());
+    ResultSet<PatchSet> source = db.patchSets().byChange(changeId);
+    List<PatchSet> patches = new ArrayList<PatchSet>();
+    CurrentUser user = control.getCurrentUser();
+    for (PatchSet ps : source) {
+      if (control.isPatchVisible(ps, db)) {
+        patches.add(ps);
+      }
+      patchsetsById.put(ps.getId(), ps);
+    }
+    detail.setPatchSets(patches);
   }
 
   private void loadMessages() throws OrmException {
-    detail.setMessages(db.changeMessages().byChange(changeId).toList());
+    ResultSet<ChangeMessage> source = db.changeMessages().byChange(changeId);
+    List<ChangeMessage> msgList = new ArrayList<ChangeMessage>();
+    for (ChangeMessage msg : source) {
+      PatchSet.Id id = msg.getPatchSetId();
+      if (id != null) {
+        PatchSet ps = patchsetsById.get(msg.getPatchSetId());
+        if (control.isPatchVisible(ps, db)) {
+          msgList.add(msg);
+        }
+      } else {
+        // Not guaranteed to have a non-null patchset id, so just display it.
+        msgList.add(msg);
+      }
+    }
+    detail.setMessages(msgList);
     for (final ChangeMessage m : detail.getMessages()) {
       aic.want(m.getAuthor());
     }
@@ -211,11 +242,13 @@
   private void loadCurrentPatchSet() throws OrmException,
       NoSuchEntityException, PatchSetInfoNotAvailableException,
       NoSuchChangeException {
-    final PatchSet.Id psId = detail.getChange().currentPatchSetId();
+    final PatchSet currentPatch = findCurrentOrLatestPatchSet();
+    final PatchSet.Id psId = currentPatch.getId();
     final PatchSetDetailFactory loader = patchSetDetail.create(null, psId, null);
-    loader.patchSet = detail.getCurrentPatchSet();
+    loader.patchSet = currentPatch;
     loader.control = control;
     detail.setCurrentPatchSetDetail(loader.call());
+    detail.setCurrentPatchSetId(psId);
 
     final HashSet<Change.Id> changesToGet = new HashSet<Change.Id>();
     final HashMap<Change.Id,PatchSet.Id> ancestorPatchIds =
@@ -270,6 +303,22 @@
     detail.setNeededBy(neededBy);
   }
 
+  private PatchSet findCurrentOrLatestPatchSet() {
+    PatchSet currentPatch = detail.getCurrentPatchSet();
+    // If the current patch set is a draft and user can't see it, set the
+    // current patch set to whatever the latest one is
+    if (currentPatch == null) {
+      List<PatchSet> patchSets = detail.getPatchSets();
+      if (!detail.getPatchSets().isEmpty()) {
+        currentPatch = patchSets.get(patchSets.size() - 1);
+      } else {
+        // Shouldn't happen, change shouldn't be visible if all the patchsets
+        // are drafts
+      }
+    }
+    return currentPatch;
+  }
+
   private ChangeInfo newChangeInfo(final Change ac,
       Map<Change.Id,PatchSet.Id> ancestorPatchIds) {
     aic.want(ac.getOwner());
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeManageServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeManageServiceImpl.java
index 9e04756..f055498 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeManageServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeManageServiceImpl.java
@@ -18,23 +18,30 @@
 import com.google.gerrit.common.data.ChangeManageService;
 import com.google.gerrit.reviewdb.PatchSet;
 import com.google.gwt.user.client.rpc.AsyncCallback;
+import com.google.gwtjsonrpc.client.VoidResult;
 import com.google.inject.Inject;
 
 class ChangeManageServiceImpl implements ChangeManageService {
   private final SubmitAction.Factory submitAction;
-  private final AbandonChange.Factory abandonChangeFactory;
-  private final RestoreChange.Factory restoreChangeFactory;
+  private final AbandonChangeHandler.Factory abandonChangeHandlerFactory;
+  private final RestoreChangeHandler.Factory restoreChangeHandlerFactory;
   private final RevertChange.Factory revertChangeFactory;
+  private final PublishAction.Factory publishAction;
+  private final DeleteDraftChange.Factory deleteDraftChangeFactory;
 
   @Inject
   ChangeManageServiceImpl(final SubmitAction.Factory patchSetAction,
-      final AbandonChange.Factory abandonChangeFactory,
-      final RestoreChange.Factory restoreChangeFactory,
-      final RevertChange.Factory revertChangeFactory) {
+      final AbandonChangeHandler.Factory abandonChangeHandlerFactory,
+      final RestoreChangeHandler.Factory restoreChangeHandlerFactory,
+      final RevertChange.Factory revertChangeFactory,
+      final PublishAction.Factory publishAction,
+      final DeleteDraftChange.Factory deleteDraftChangeFactory) {
     this.submitAction = patchSetAction;
-    this.abandonChangeFactory = abandonChangeFactory;
-    this.restoreChangeFactory = restoreChangeFactory;
+    this.abandonChangeHandlerFactory = abandonChangeHandlerFactory;
+    this.restoreChangeHandlerFactory = restoreChangeHandlerFactory;
     this.revertChangeFactory = revertChangeFactory;
+    this.publishAction = publishAction;
+    this.deleteDraftChangeFactory = deleteDraftChangeFactory;
   }
 
   public void submit(final PatchSet.Id patchSetId,
@@ -44,7 +51,7 @@
 
   public void abandonChange(final PatchSet.Id patchSetId, final String message,
       final AsyncCallback<ChangeDetail> callback) {
-    abandonChangeFactory.create(patchSetId, message).to(callback);
+    abandonChangeHandlerFactory.create(patchSetId, message).to(callback);
   }
 
   public void revertChange(final PatchSet.Id patchSetId, final String message,
@@ -54,6 +61,16 @@
 
   public void restoreChange(final PatchSet.Id patchSetId, final String message,
       final AsyncCallback<ChangeDetail> callback) {
-    restoreChangeFactory.create(patchSetId, message).to(callback);
+    restoreChangeHandlerFactory.create(patchSetId, message).to(callback);
+  }
+
+  public void publish(final PatchSet.Id patchSetId,
+      final AsyncCallback<ChangeDetail> callback) {
+    publishAction.create(patchSetId).to(callback);
+  }
+
+  public void deleteDraftChange(final PatchSet.Id patchSetId,
+      final AsyncCallback<VoidResult> callback) {
+    deleteDraftChangeFactory.create(patchSetId).to(callback);
   }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeModule.java
index 95c438c..4224293 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeModule.java
@@ -28,14 +28,16 @@
     install(new FactoryModule() {
       @Override
       protected void configure() {
-        factory(AbandonChange.Factory.class);
-        factory(RestoreChange.Factory.class);
+        factory(AbandonChangeHandler.Factory.class);
+        factory(RestoreChangeHandler.Factory.class);
         factory(RevertChange.Factory.class);
         factory(ChangeDetailFactory.Factory.class);
         factory(IncludedInDetailFactory.Factory.class);
         factory(PatchSetDetailFactory.Factory.class);
         factory(PatchSetPublishDetailFactory.Factory.class);
         factory(SubmitAction.Factory.class);
+        factory(PublishAction.Factory.class);
+        factory(DeleteDraftChange.Factory.class);
       }
     });
     rpc(ChangeDetailServiceImpl.class);
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
new file mode 100644
index 0000000..ab809ce
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/DeleteDraftChange.java
@@ -0,0 +1,71 @@
+// 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.changedetail;
+
+import com.google.gerrit.httpd.rpc.Handler;
+import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.reviewdb.PatchSet;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.server.ChangeUtil;
+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.client.VoidResult;
+import com.google.gwtorm.client.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import java.io.IOException;
+
+class DeleteDraftChange extends Handler<VoidResult> {
+  interface Factory {
+    DeleteDraftChange create(PatchSet.Id patchSetId);
+  }
+
+  private final ChangeControl.Factory changeControlFactory;
+  private final ReviewDb db;
+  private final GitRepositoryManager gitManager;
+  private final ReplicationQueue replication;
+
+  private final PatchSet.Id patchSetId;
+
+  @Inject
+  DeleteDraftChange(final ReviewDb db,
+      final ChangeControl.Factory changeControlFactory,
+      final GitRepositoryManager gitManager,
+      final ReplicationQueue replication,
+      @Assisted final PatchSet.Id patchSetId) {
+    this.changeControlFactory = changeControlFactory;
+    this.db = db;
+    this.gitManager = gitManager;
+    this.replication = replication;
+
+    this.patchSetId = patchSetId;
+  }
+
+  @Override
+  public VoidResult call() throws NoSuchChangeException, OrmException, IOException {
+
+    final Change.Id changeId = patchSetId.getParentKey();
+    final ChangeControl control = changeControlFactory.validateFor(changeId);
+    if (!control.canDeleteDraft(db)) {
+      throw new NoSuchChangeException(changeId);
+    }
+
+    ChangeUtil.deleteDraftChange(patchSetId, gitManager, replication, db);
+    return VoidResult.INSTANCE;
+  }
+}
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 f478d66..9ef715f 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
@@ -128,7 +128,7 @@
       byKey.put(p.getKey(), p);
     }
 
-    for (final PatchLineComment c : db.patchComments().published(psIdNew)) {
+    for (final PatchLineComment c : db.patchComments().publishedByPatchSet(psIdNew)) {
       final Patch p = byKey.get(c.getKey().getParentKey());
       if (p != null) {
         p.setCommentCount(p.getCommentCount() + 1);
@@ -138,7 +138,7 @@
     detail = new PatchSetDetail();
     detail.setPatchSet(patchSet);
 
-    detail.setInfo(infoFactory.get(psIdNew));
+    detail.setInfo(infoFactory.get(db, psIdNew));
     detail.setPatches(patches);
 
     final CurrentUser user = control.getCurrentUser();
@@ -148,7 +148,7 @@
       // quickly locate where they have pending drafts, and review them.
       //
       final Account.Id me = ((IdentifiedUser) user).getAccountId();
-      for (final PatchLineComment c : db.patchComments().draft(psIdNew, me)) {
+      for (final PatchLineComment c : db.patchComments().draftByPatchSetAuthor(psIdNew, me)) {
         final Patch p = byKey.get(c.getKey().getParentKey());
         if (p != null) {
           p.setDraftCount(p.getDraftCount() + 1);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetPublishDetailFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetPublishDetailFactory.java
index cbddad1..32a58d0 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
@@ -83,8 +83,8 @@
     final Change.Id changeId = patchSetId.getParentKey();
     final ChangeControl control = changeControlFactory.validateFor(changeId);
     change = control.getChange();
-    patchSetInfo = infoFactory.get(patchSetId);
-    drafts = db.patchComments().draft(patchSetId, user.getAccountId()).toList();
+    patchSetInfo = infoFactory.get(db, patchSetId);
+    drafts = db.patchComments().draftByPatchSetAuthor(patchSetId, user.getAccountId()).toList();
 
     aic.want(change.getOwner());
 
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
new file mode 100644
index 0000000..f6241fa
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PublishAction.java
@@ -0,0 +1,59 @@
+// 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.changedetail;
+
+import com.google.gerrit.common.data.ChangeDetail;
+import com.google.gerrit.common.data.ReviewResult;
+import com.google.gerrit.common.errors.NoSuchEntityException;
+import com.google.gerrit.httpd.rpc.Handler;
+import com.google.gerrit.reviewdb.PatchSet;
+import com.google.gerrit.server.changedetail.PublishDraft;
+import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gwtorm.client.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+class PublishAction extends Handler<ChangeDetail> {
+  interface Factory {
+    PublishAction create(PatchSet.Id patchSetId);
+  }
+
+  private final PublishDraft.Factory publishFactory;
+  private final ChangeDetailFactory.Factory changeDetailFactory;
+
+  private final PatchSet.Id patchSetId;
+
+  @Inject
+  PublishAction(final PublishDraft.Factory publishFactory,
+      final ChangeDetailFactory.Factory changeDetailFactory,
+      @Assisted final PatchSet.Id patchSetId) {
+    this.publishFactory = publishFactory;
+    this.changeDetailFactory = changeDetailFactory;
+
+    this.patchSetId = patchSetId;
+  }
+
+  @Override
+  public ChangeDetail call() throws OrmException, NoSuchEntityException,
+      IllegalStateException, PatchSetInfoNotAvailableException,
+      NoSuchChangeException {
+    final ReviewResult result = publishFactory.create(patchSetId).call();
+    if (result.getErrors().size() > 0) {
+      throw new IllegalStateException("Cannot publish patchset");
+    }
+    return changeDetailFactory.create(result.getChangeId()).call();
+  }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RestoreChange.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RestoreChange.java
deleted file mode 100644
index 22fa568..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RestoreChange.java
+++ /dev/null
@@ -1,89 +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.changedetail;
-
-import com.google.gerrit.common.ChangeHookRunner;
-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.Change;
-import com.google.gerrit.reviewdb.PatchSet;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gerrit.server.ChangeUtil;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.mail.EmailException;
-import com.google.gerrit.server.mail.RestoredSender;
-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;
-import com.google.gwtorm.client.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
-
-import javax.annotation.Nullable;
-
-class RestoreChange extends Handler<ChangeDetail> {
-  interface Factory {
-    RestoreChange create(PatchSet.Id patchSetId, String message);
-  }
-
-  private final ChangeControl.Factory changeControlFactory;
-  private final ReviewDb db;
-  private final IdentifiedUser currentUser;
-  private final RestoredSender.Factory senderFactory;
-  private final ChangeDetailFactory.Factory changeDetailFactory;
-
-  private final PatchSet.Id patchSetId;
-  @Nullable
-  private final String message;
-
-  private final ChangeHookRunner hooks;
-
-  @Inject
-  RestoreChange(final ChangeControl.Factory changeControlFactory,
-      final ReviewDb db, final IdentifiedUser currentUser,
-      final RestoredSender.Factory senderFactory,
-      final ChangeDetailFactory.Factory changeDetailFactory,
-      @Assisted final PatchSet.Id patchSetId,
-      @Assisted @Nullable final String message, final ChangeHookRunner hooks) {
-    this.changeControlFactory = changeControlFactory;
-    this.db = db;
-    this.currentUser = currentUser;
-    this.senderFactory = senderFactory;
-    this.changeDetailFactory = changeDetailFactory;
-
-    this.patchSetId = patchSetId;
-    this.message = message;
-    this.hooks = hooks;
-  }
-
-  @Override
-  public ChangeDetail call() throws NoSuchChangeException, OrmException,
-      EmailException, NoSuchEntityException, InvalidChangeOperationException,
-      PatchSetInfoNotAvailableException {
-
-    final Change.Id changeId = patchSetId.getParentKey();
-    final ChangeControl control = changeControlFactory.validateFor(changeId);
-    if (!control.canRestore()) {
-      throw new NoSuchChangeException(changeId);
-    }
-
-    ChangeUtil.restore(patchSetId, currentUser, message, db, senderFactory,
-        hooks);
-
-    return changeDetailFactory.create(changeId).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
new file mode 100644
index 0000000..667861a
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RestoreChangeHandler.java
@@ -0,0 +1,68 @@
+// 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.changedetail;
+
+import com.google.gerrit.common.data.ChangeDetail;
+import com.google.gerrit.common.data.ReviewResult;
+import com.google.gerrit.common.errors.NoSuchEntityException;
+import com.google.gerrit.httpd.rpc.Handler;
+import com.google.gerrit.reviewdb.PatchSet;
+import com.google.gerrit.server.changedetail.RestoreChange;
+import com.google.gerrit.server.mail.EmailException;
+import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gwtorm.client.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import javax.annotation.Nullable;
+
+class RestoreChangeHandler extends Handler<ChangeDetail> {
+  interface Factory {
+    RestoreChangeHandler create(PatchSet.Id patchSetId, String message);
+  }
+
+  private final RestoreChange.Factory restoreChangeFactory;
+  private final ChangeDetailFactory.Factory changeDetailFactory;
+
+  private final PatchSet.Id patchSetId;
+  @Nullable
+  private final String message;
+
+  @Inject
+  RestoreChangeHandler(final RestoreChange.Factory restoreChangeFactory,
+      final ChangeDetailFactory.Factory changeDetailFactory,
+      @Assisted final PatchSet.Id patchSetId,
+      @Assisted @Nullable final String message) {
+    this.restoreChangeFactory = restoreChangeFactory;
+    this.changeDetailFactory = changeDetailFactory;
+
+    this.patchSetId = patchSetId;
+    this.message = message;
+  }
+
+  @Override
+  public ChangeDetail call() throws NoSuchChangeException, OrmException,
+      EmailException, NoSuchEntityException, InvalidChangeOperationException,
+      PatchSetInfoNotAvailableException {
+    final ReviewResult result =
+        restoreChangeFactory.create(patchSetId, message).call();
+    if (result.getErrors().size() > 0) {
+      throw new NoSuchChangeException(result.getChangeId());
+    }
+    return changeDetailFactory.create(result.getChangeId()).call();
+  }
+}
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 3dfa0e3..da74941 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
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.httpd.rpc.changedetail;
 
-import com.google.gerrit.common.ChangeHookRunner;
+import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.common.data.ChangeDetail;
 import com.google.gerrit.common.errors.NoSuchEntityException;
 import com.google.gerrit.httpd.rpc.Handler;
@@ -60,7 +60,7 @@
   @Nullable
   private final String message;
 
-  private final ChangeHookRunner hooks;
+  private final ChangeHooks hooks;
 
   private final GitRepositoryManager gitManager;
   private final PatchSetInfoFactory patchSetInfoFactory;
@@ -73,7 +73,7 @@
       final RevertedSender.Factory revertedSenderFactory,
       final ChangeDetailFactory.Factory changeDetailFactory,
       @Assisted final PatchSet.Id patchSetId,
-      @Assisted @Nullable final String message, final ChangeHookRunner hooks,
+      @Assisted @Nullable final String message, final ChangeHooks hooks,
       final GitRepositoryManager gitManager,
       final PatchSetInfoFactory patchSetInfoFactory,
       final ReplicationQueue replication,
@@ -105,10 +105,10 @@
       throw new NoSuchChangeException(changeId);
     }
 
-    ChangeUtil.revert(patchSetId, currentUser, message, db,
+    Change.Id revertedChangeId = ChangeUtil.revert(patchSetId, currentUser, message, db,
         revertedSenderFactory, hooks, gitManager, patchSetInfoFactory,
         replication, myIdent);
 
-    return changeDetailFactory.create(changeId).call();
+    return changeDetailFactory.create(revertedChangeId).call();
   }
 }
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 ec003f1..38574bc 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
@@ -15,109 +15,48 @@
 package com.google.gerrit.httpd.rpc.changedetail;
 
 import com.google.gerrit.common.data.ChangeDetail;
-import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.common.data.ReviewResult;
 import com.google.gerrit.common.errors.NoSuchEntityException;
 import com.google.gerrit.httpd.rpc.Handler;
-import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.reviewdb.PatchSet;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gerrit.server.ChangeUtil;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.git.MergeOp;
-import com.google.gerrit.server.git.MergeQueue;
+import com.google.gerrit.server.changedetail.Submit;
 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;
 import com.google.gwtorm.client.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
-import java.util.List;
-
 class SubmitAction extends Handler<ChangeDetail> {
   interface Factory {
     SubmitAction create(PatchSet.Id patchSetId);
   }
 
-  private final ReviewDb db;
-  private final MergeQueue merger;
-  private final IdentifiedUser user;
+  private final Submit.Factory submitFactory;
   private final ChangeDetailFactory.Factory changeDetailFactory;
-  private final ChangeControl.Factory changeControlFactory;
-  private final MergeOp.Factory opFactory;
 
   private final PatchSet.Id patchSetId;
 
   @Inject
-  SubmitAction(final ReviewDb db, final MergeQueue mq,
-      final IdentifiedUser user,
+  SubmitAction(final Submit.Factory submitFactory,
       final ChangeDetailFactory.Factory changeDetailFactory,
-      final ChangeControl.Factory changeControlFactory,
-      final MergeOp.Factory opFactory,
       @Assisted final PatchSet.Id patchSetId) {
-    this.db = db;
-    this.merger = mq;
-    this.user = user;
-    this.changeControlFactory = changeControlFactory;
+    this.submitFactory = submitFactory;
     this.changeDetailFactory = changeDetailFactory;
-    this.opFactory = opFactory;
 
     this.patchSetId = patchSetId;
   }
 
   @Override
   public ChangeDetail call() throws OrmException, NoSuchEntityException,
-      IllegalStateException, PatchSetInfoNotAvailableException,
-      NoSuchChangeException {
-
-    final Change.Id changeId = patchSetId.getParentKey();
-    final ChangeControl changeControl =
-        changeControlFactory.validateFor(changeId);
-
-    List<SubmitRecord> result = changeControl.canSubmit(db, patchSetId);
-    if (result.isEmpty()) {
-      throw new IllegalStateException("Cannot submit");
+      IllegalStateException, InvalidChangeOperationException,
+      PatchSetInfoNotAvailableException, NoSuchChangeException {
+    final ReviewResult result =
+        submitFactory.create(patchSetId).call();
+    if (result.getErrors().size() > 0) {
+      throw new IllegalStateException(
+          "Cannot submit " + result.getErrors().get(0).getMessageOrType());
     }
-
-    switch (result.get(0).status) {
-      case OK:
-        ChangeUtil.submit(patchSetId, user, db, opFactory, merger);
-        return changeDetailFactory.create(changeId).call();
-
-      case NOT_READY: {
-        for (SubmitRecord.Label lbl : result.get(0).labels) {
-          switch (lbl.status) {
-            case OK:
-              break;
-
-            case REJECT:
-              throw new IllegalStateException("Blocked by " + lbl.label);
-
-            case NEED:
-              throw new IllegalStateException("Needs " + lbl.label);
-
-            case IMPOSSIBLE:
-              throw new IllegalStateException("Cannnot submit, check project access");
-
-            default:
-              throw new IllegalArgumentException("Unknown status " + lbl.status);
-          }
-        }
-        throw new IllegalStateException("Cannot submit");
-      }
-
-      case CLOSED:
-        throw new IllegalStateException("Change is closed");
-
-      case RULE_ERROR:
-        if (result.get(0).errorMessage != null) {
-          throw new IllegalStateException(result.get(0).errorMessage);
-        } else {
-          throw  new IllegalStateException("Internal rule error");
-        }
-
-      default:
-        throw new IllegalStateException("Uknown status " + result.get(0).status);
-    }
+    return changeDetailFactory.create(result.getChangeId()).call();
   }
 }
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 0f3b4f8..50d6cea 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
@@ -14,7 +14,9 @@
 
 package com.google.gerrit.httpd.rpc.patch;
 
+import com.google.gerrit.common.data.ChangeDetail;
 import com.google.gerrit.common.data.ReviewerResult;
+import com.google.gerrit.common.data.ReviewResult;
 import com.google.gerrit.common.data.ApprovalSummary;
 import com.google.gerrit.common.data.ApprovalSummarySet;
 import com.google.gerrit.common.data.ApprovalTypes;
@@ -23,6 +25,7 @@
 import com.google.gerrit.common.errors.NoSuchEntityException;
 import com.google.gerrit.httpd.rpc.BaseServiceImplementation;
 import com.google.gerrit.httpd.rpc.Handler;
+import com.google.gerrit.httpd.rpc.changedetail.ChangeDetailFactory;
 import com.google.gerrit.reviewdb.Account;
 import com.google.gerrit.reviewdb.AccountDiffPreference;
 import com.google.gerrit.reviewdb.AccountPatchReview;
@@ -35,8 +38,14 @@
 import com.google.gerrit.reviewdb.PatchSetApproval;
 import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.reviewdb.Patch.Key;
+import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.AccountInfoCacheFactory;
+import com.google.gerrit.server.changedetail.DeleteDraftPatchSet;
+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.patch.PublishComments;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
@@ -60,11 +69,16 @@
   private final AccountInfoCacheFactory.Factory accountInfoCacheFactory;
   private final AddReviewerHandler.Factory addReviewerHandlerFactory;
   private final ChangeControl.Factory changeControlFactory;
+  private final DeleteDraftPatchSet.Factory deleteDraftPatchSetFactory;
   private final RemoveReviewerHandler.Factory removeReviewerHandlerFactory;
   private final FunctionState.Factory functionStateFactory;
   private final PublishComments.Factory publishCommentsFactory;
   private final PatchScriptFactory.Factory patchScriptFactoryFactory;
   private final SaveDraft.Factory saveDraftFactory;
+  private final PatchSetInfoFactory patchSetInfoFactory;
+  private final GitRepositoryManager gitManager;
+  private final ReplicationQueue replication;
+  private final ChangeDetailFactory.Factory changeDetailFactory;
 
   @Inject
   PatchDetailServiceImpl(final Provider<ReviewDb> schema,
@@ -74,10 +88,15 @@
       final AddReviewerHandler.Factory addReviewerHandlerFactory,
       final RemoveReviewerHandler.Factory removeReviewerHandlerFactory,
       final ChangeControl.Factory changeControlFactory,
+      final DeleteDraftPatchSet.Factory deleteDraftPatchSetFactory,
       final FunctionState.Factory functionStateFactory,
       final PatchScriptFactory.Factory patchScriptFactoryFactory,
       final PublishComments.Factory publishCommentsFactory,
-      final SaveDraft.Factory saveDraftFactory) {
+      final SaveDraft.Factory saveDraftFactory,
+      final PatchSetInfoFactory patchSetInfoFactory,
+      final GitRepositoryManager gitManager,
+      final ReplicationQueue replication,
+      final ChangeDetailFactory.Factory changeDetailFactory) {
     super(schema, currentUser);
     this.approvalTypes = approvalTypes;
 
@@ -85,10 +104,15 @@
     this.addReviewerHandlerFactory = addReviewerHandlerFactory;
     this.removeReviewerHandlerFactory = removeReviewerHandlerFactory;
     this.changeControlFactory = changeControlFactory;
+    this.deleteDraftPatchSetFactory = deleteDraftPatchSetFactory;
     this.functionStateFactory = functionStateFactory;
     this.patchScriptFactoryFactory = patchScriptFactoryFactory;
     this.publishCommentsFactory = publishCommentsFactory;
     this.saveDraftFactory = saveDraftFactory;
+    this.patchSetInfoFactory = patchSetInfoFactory;
+    this.gitManager = gitManager;
+    this.replication = replication;
+    this.changeDetailFactory = changeDetailFactory;
   }
 
   public void patchScript(final Patch.Key patchKey, final PatchSet.Id psa,
@@ -110,18 +134,52 @@
       final AsyncCallback<VoidResult> callback) {
     run(callback, new Action<VoidResult>() {
       public VoidResult run(ReviewDb db) throws OrmException, Failure {
-        final PatchLineComment comment = db.patchComments().get(commentKey);
-        if (comment == null) {
-          throw new Failure(new NoSuchEntityException());
+        Change.Id id = commentKey.getParentKey().getParentKey().getParentKey();
+        db.changes().beginTransaction(id);
+        try {
+          final PatchLineComment comment = db.patchComments().get(commentKey);
+          if (comment == null) {
+            throw new Failure(new NoSuchEntityException());
+          }
+          if (!getAccountId().equals(comment.getAuthor())) {
+            throw new Failure(new NoSuchEntityException());
+          }
+          if (comment.getStatus() != PatchLineComment.Status.DRAFT) {
+            throw new Failure(new IllegalStateException("Comment published"));
+          }
+          db.patchComments().delete(Collections.singleton(comment));
+          db.commit();
+          return VoidResult.INSTANCE;
+        } finally {
+          db.rollback();
         }
-        if (!getAccountId().equals(comment.getAuthor())) {
-          throw new Failure(new NoSuchEntityException());
+      }
+    });
+  }
+
+  public void deleteDraftPatchSet(final PatchSet.Id psid,
+      final AsyncCallback<ChangeDetail> callback) {
+    run(callback, new Action<ChangeDetail>() {
+      public ChangeDetail run(ReviewDb db) throws OrmException, Failure {
+        ReviewResult result = null;
+        try {
+          result = deleteDraftPatchSetFactory.create(psid).call();
+          if (result.getErrors().size() > 0) {
+            throw new Failure(new NoSuchEntityException());
+          }
+          if (result.getChangeId() == null) {
+            // the change was deleted because the draft patch set that was
+            // deleted was the only patch set in the change
+            return null;
+          }
+          return changeDetailFactory.create(result.getChangeId()).call();
+        } catch (NoSuchChangeException e) {
+          throw new Failure(new NoSuchChangeException(result.getChangeId()));
+        } catch (NoSuchEntityException e) {
+          throw new Failure(e);
+        } catch (PatchSetInfoNotAvailableException e) {
+          throw new Failure(e);
         }
-        if (comment.getStatus() != PatchLineComment.Status.DRAFT) {
-          throw new Failure(new IllegalStateException("Comment published"));
-        }
-        db.patchComments().delete(Collections.singleton(comment));
-        return VoidResult.INSTANCE;
       }
     });
   }
@@ -129,7 +187,7 @@
   public void publishComments(final PatchSet.Id psid, final String msg,
       final Set<ApprovalCategoryValue.Id> tags,
       final AsyncCallback<VoidResult> cb) {
-    Handler.wrap(publishCommentsFactory.create(psid, msg, tags)).to(cb);
+    Handler.wrap(publishCommentsFactory.create(psid, msg, tags, false)).to(cb);
   }
 
   /**
@@ -142,14 +200,20 @@
         Account.Id account = getAccountId();
         AccountPatchReview.Key key =
             new AccountPatchReview.Key(patchKey, account);
-        AccountPatchReview apr = db.accountPatchReviews().get(key);
-        if (apr == null && reviewed) {
-          db.accountPatchReviews().insert(
-              Collections.singleton(new AccountPatchReview(patchKey, account)));
-        } else if (apr != null && !reviewed) {
-          db.accountPatchReviews().delete(Collections.singleton(apr));
+        db.accounts().beginTransaction(account);
+        try {
+          AccountPatchReview apr = db.accountPatchReviews().get(key);
+          if (apr == null && reviewed) {
+            db.accountPatchReviews().insert(
+                Collections.singleton(new AccountPatchReview(patchKey, account)));
+          } else if (apr != null && !reviewed) {
+            db.accountPatchReviews().delete(Collections.singleton(apr));
+          }
+          db.commit();
+          return VoidResult.INSTANCE;
+        } finally {
+          db.rollback();
         }
-        return VoidResult.INSTANCE;
       }
     });
   }
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 9ad8e3a..5a60623 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
@@ -287,7 +287,7 @@
 
   private void loadPublished(final Map<Patch.Key, Patch> byKey,
       final AccountInfoCacheFactory aic, final String file) throws OrmException {
-    for (PatchLineComment c : db.patchComments().published(changeId, file)) {
+    for (PatchLineComment c : db.patchComments().publishedByChangeFile(changeId, file)) {
       if (comments.include(c)) {
         aic.want(c.getAuthor());
       }
@@ -303,7 +303,7 @@
   private void loadDrafts(final Map<Patch.Key, Patch> byKey,
       final AccountInfoCacheFactory aic, final Account.Id me, final String file)
       throws OrmException {
-    for (PatchLineComment c : db.patchComments().draft(changeId, file, me)) {
+    for (PatchLineComment c : db.patchComments().draftByChangeFileAuthor(changeId, file, me)) {
       if (comments.include(c)) {
         aic.want(me);
       }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/SaveDraft.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/SaveDraft.java
index 1fbc559..0f14635 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/SaveDraft.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/SaveDraft.java
@@ -49,7 +49,6 @@
     this.changeControlFactory = changeControlFactory;
     this.db = db;
     this.currentUser = currentUser;
-
     this.comment = comment;
   }
 
@@ -62,42 +61,50 @@
     final Patch.Key patchKey = comment.getKey().getParentKey();
     final PatchSet.Id patchSetId = patchKey.getParentKey();
     final Change.Id changeId = patchKey.getParentKey().getParentKey();
-    changeControlFactory.validateFor(changeId);
-    if (db.patchSets().get(patchSetId) == null) {
-      throw new NoSuchChangeException(changeId);
-    }
 
-    final Account.Id me = currentUser.getAccountId();
-    if (comment.getKey().get() == null) {
-      if (comment.getLine() < 1) {
-        throw new IllegalStateException("Comment line must be >= 1, not "
-            + comment.getLine());
-      }
-
-      if (comment.getParentUuid() != null) {
-        final PatchLineComment parent =
-            db.patchComments().get(
-                new PatchLineComment.Key(patchKey, comment.getParentUuid()));
-        if (parent == null || parent.getSide() != comment.getSide()) {
-          throw new IllegalStateException("Parent comment must be on same side");
-        }
-      }
-
-      final PatchLineComment nc =
-          new PatchLineComment(new PatchLineComment.Key(patchKey, ChangeUtil
-              .messageUUID(db)), comment.getLine(), me, comment.getParentUuid());
-      nc.setSide(comment.getSide());
-      nc.setMessage(comment.getMessage());
-      db.patchComments().insert(Collections.singleton(nc));
-      return nc;
-
-    } else {
-      if (!me.equals(comment.getAuthor())) {
+    db.changes().beginTransaction(changeId);
+    try {
+      changeControlFactory.validateFor(changeId);
+      if (db.patchSets().get(patchSetId) == null) {
         throw new NoSuchChangeException(changeId);
       }
-      comment.updated();
-      db.patchComments().update(Collections.singleton(comment));
-      return comment;
+
+      final Account.Id me = currentUser.getAccountId();
+      if (comment.getKey().get() == null) {
+        if (comment.getLine() < 1) {
+          throw new IllegalStateException("Comment line must be >= 1, not "
+              + comment.getLine());
+        }
+
+        if (comment.getParentUuid() != null) {
+          final PatchLineComment parent =
+              db.patchComments().get(
+                  new PatchLineComment.Key(patchKey, comment.getParentUuid()));
+          if (parent == null || parent.getSide() != comment.getSide()) {
+            throw new IllegalStateException("Parent comment must be on same side");
+          }
+        }
+
+        final PatchLineComment nc =
+            new PatchLineComment(new PatchLineComment.Key(patchKey, ChangeUtil
+                .messageUUID(db)), comment.getLine(), me, comment.getParentUuid());
+        nc.setSide(comment.getSide());
+        nc.setMessage(comment.getMessage());
+        db.patchComments().insert(Collections.singleton(nc));
+        db.commit();
+        return nc;
+
+      } else {
+        if (!me.equals(comment.getAuthor())) {
+          throw new NoSuchChangeException(changeId);
+        }
+        comment.updated();
+        db.patchComments().update(Collections.singleton(comment));
+        db.commit();
+        return comment;
+      }
+    } finally {
+      db.rollback();
     }
   }
 }
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 7cef016..db03f16 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
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.httpd.rpc.project;
 
-import com.google.gerrit.common.ChangeHookRunner;
+import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.common.data.ListBranchesResult;
 import com.google.gerrit.common.errors.InvalidNameException;
 import com.google.gerrit.common.errors.InvalidRevisionException;
@@ -23,11 +23,11 @@
 import com.google.gerrit.reviewdb.Project;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.ReceiveCommits;
 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;
+import com.google.gerrit.server.util.MagicBranch;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -60,7 +60,7 @@
   private final IdentifiedUser identifiedUser;
   private final GitRepositoryManager repoManager;
   private final ReplicationQueue replication;
-  private final ChangeHookRunner hooks;
+  private final ChangeHooks hooks;
 
   private final Project.NameKey projectName;
   private final String branchName;
@@ -72,7 +72,7 @@
       final IdentifiedUser identifiedUser,
       final GitRepositoryManager repoManager,
       final ReplicationQueue replication,
-      final ChangeHookRunner hooks,
+      final ChangeHooks hooks,
 
       @Assisted Project.NameKey projectName,
       @Assisted("branchName") String branchName,
@@ -106,8 +106,8 @@
     if (!Repository.isValidRefName(refname)) {
       throw new InvalidNameException();
     }
-    if (refname.startsWith(ReceiveCommits.NEW_CHANGE)) {
-      throw new BranchCreationNotAllowedException(ReceiveCommits.NEW_CHANGE);
+    if (MagicBranch.isMagicBranch(refname)) {
+      throw new BranchCreationNotAllowedException(refname);
     }
 
     final Branch.NameKey name = new Branch.NameKey(projectName, refname);
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 2b0f856..7d1ad51 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
@@ -21,11 +21,12 @@
 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.PerRequestProjectControlCache;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gwtorm.client.OrmConcurrencyException;
 import com.google.gwtorm.client.OrmException;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
@@ -40,9 +41,9 @@
 
   private final ProjectDetailFactory.Factory projectDetailFactory;
   private final ProjectControl.Factory projectControlFactory;
-  private final ProjectCache projectCache;
   private final GitRepositoryManager mgr;
   private final MetaDataUpdate.User metaDataUpdateFactory;
+  private final Provider<PerRequestProjectControlCache> userCache;
 
   private final Project update;
 
@@ -50,13 +51,14 @@
   ChangeProjectSettings(
       final ProjectDetailFactory.Factory projectDetailFactory,
       final ProjectControl.Factory projectControlFactory,
-      final ProjectCache projectCache, final GitRepositoryManager mgr,
+      final GitRepositoryManager mgr,
       final MetaDataUpdate.User metaDataUpdateFactory,
+      final Provider<PerRequestProjectControlCache> uc,
       @Assisted final Project update) {
     this.projectDetailFactory = projectDetailFactory;
     this.projectControlFactory = projectControlFactory;
-    this.projectCache = projectCache;
     this.mgr = mgr;
+    this.userCache = uc;
     this.metaDataUpdateFactory = metaDataUpdateFactory;
 
     this.update = update;
@@ -83,7 +85,7 @@
       md.setMessage("Modified project settings\n");
       if (config.commit(md)) {
         mgr.setProjectDescription(projectName, update.getDescription());
-        projectCache.evict(config.getProject());
+        userCache.get().evict(config.getProject());
       } else {
         throw new OrmConcurrencyException("Cannot update " + projectName);
       }
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
new file mode 100644
index 0000000..8360615
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/CreateProjectHandler.java
@@ -0,0 +1,85 @@
+// 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.common.errors.ProjectCreationFailedException;
+import com.google.gerrit.httpd.rpc.Handler;
+import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.reviewdb.Project.SubmitType;
+import com.google.gerrit.server.project.CreateProject;
+import com.google.gerrit.server.project.CreateProjectArgs;
+import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gwtjsonrpc.client.VoidResult;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import org.eclipse.jgit.lib.Constants;
+
+public class CreateProjectHandler extends Handler<VoidResult> {
+
+  interface Factory {
+    CreateProjectHandler create(@Assisted("projectName") String projectName,
+        @Assisted("parentName") String parentName,
+        @Assisted("emptyCommit") boolean emptyCommit,
+        @Assisted("permissionsOnly") boolean permissionsOnly);
+  }
+
+  private final CreateProject.Factory createProjectFactory;
+  private final ProjectControl.Factory projectControlFactory;
+  private final String projectName;
+  private final String parentName;
+  private final boolean emptyCommit;
+  private final boolean permissionsOnly;
+
+  @Inject
+  public CreateProjectHandler(final CreateProject.Factory createProjectFactory,
+      final ProjectControl.Factory projectControlFactory,
+      @Assisted("projectName") final String projectName,
+      @Assisted("parentName") final String parentName,
+      @Assisted("emptyCommit") final boolean emptyCommit,
+      @Assisted("permissionsOnly") final boolean permissionsOnly) {
+    this.createProjectFactory = createProjectFactory;
+    this.projectControlFactory = projectControlFactory;
+    this.projectName = projectName;
+    this.parentName = parentName;
+    this.emptyCommit = emptyCommit;
+    this.permissionsOnly = permissionsOnly;
+  }
+
+  @Override
+  public VoidResult call() throws ProjectCreationFailedException {
+    final CreateProjectArgs args = new CreateProjectArgs();
+    args.setProjectName(projectName);
+    if (!parentName.equals("")) {
+      final Project.NameKey nameKey = new Project.NameKey(parentName);
+      try {
+        args.newParent = projectControlFactory.validateFor(nameKey);
+      } catch (NoSuchProjectException e) {
+        throw new ProjectCreationFailedException("Parent project \""
+            + parentName + "\" does not exist.", e);
+      }
+    }
+    args.projectDescription = "";
+    args.submitType = SubmitType.MERGE_IF_NECESSARY;
+    args.branch = Constants.MASTER;
+    args.createEmptyCommit = emptyCommit;
+    args.permissionsOnly = permissionsOnly;
+
+    final CreateProject createProject = createProjectFactory.create(args);
+    createProject.createProject();
+    return VoidResult.INSTANCE;
+  }
+}
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 d3eae24..0f19bbc 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
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.httpd.rpc.project;
 
-import com.google.gerrit.common.ChangeHookRunner;
+import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.httpd.rpc.Handler;
 import com.google.gerrit.reviewdb.Branch;
 import com.google.gerrit.reviewdb.Project;
@@ -49,7 +49,7 @@
   private final GitRepositoryManager repoManager;
   private final ReplicationQueue replication;
   private final IdentifiedUser identifiedUser;
-  private final ChangeHookRunner hooks;
+  private final ChangeHooks hooks;
 
   private final Project.NameKey projectName;
   private final Set<Branch.NameKey> toRemove;
@@ -59,7 +59,7 @@
       final GitRepositoryManager repoManager,
       final ReplicationQueue replication,
       final IdentifiedUser identifiedUser,
-      final ChangeHookRunner hooks,
+      final ChangeHooks hooks,
 
       @Assisted Project.NameKey name, @Assisted Set<Branch.NameKey> toRemove) {
     this.projectControlFactory = projectControlFactory;
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 76e1f2b..d2a2900 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
@@ -25,6 +25,7 @@
 import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.project.NoSuchProjectException;
@@ -177,6 +178,7 @@
     }
 
     final ProjectAccess detail = new ProjectAccess();
+    detail.setProjectName(projectName);
 
     if (config.getRevision() != null) {
       detail.setRevision(config.getRevision().name());
@@ -197,6 +199,8 @@
 
     detail.setLocal(local);
     detail.setOwnerOf(ownerOf);
+    detail.setConfigVisible(pc.isOwner()
+        || pc.controlForRef(GitRepositoryManager.REF_CONFIG).isVisible());
     return detail;
   }
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAdminServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAdminServiceImpl.java
index 47b3e3e..bf7eeae 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,9 +19,11 @@
 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.Branch;
 import com.google.gerrit.reviewdb.Project;
 import com.google.gwt.user.client.rpc.AsyncCallback;
+import com.google.gwtjsonrpc.client.VoidResult;
 import com.google.inject.Inject;
 
 import org.eclipse.jgit.lib.ObjectId;
@@ -36,8 +38,11 @@
   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,
@@ -46,24 +51,40 @@
       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 ProjectDetailFactory.Factory projectDetailFactory,
+      final SuggestParentCandidatesHandler.Factory parentCandidatesFactory,
+      final CreateProjectHandler.Factory createNewProjectFactory) {
     this.addBranchFactory = addBranchFactory;
     this.changeProjectAccessFactory = changeProjectAccessFactory;
     this.changeProjectSettingsFactory = changeProjectSettingsFactory;
     this.deleteBranchesFactory = deleteBranchesFactory;
     this.listBranchesFactory = listBranchesFactory;
     this.visibleProjectsFactory = visibleProjectsFactory;
+    this.visibleProjectDetailsFactory = visibleProjectDetailsFactory;
     this.projectAccessFactory = projectAccessFactory;
     this.projectDetailFactory = projectDetailFactory;
+    this.createProjectHandlerFactory = createNewProjectFactory;
+    this.suggestParentCandidatesHandlerFactory = parentCandidatesFactory;
   }
 
   @Override
-  public void visibleProjects(final AsyncCallback<List<Project>> callback) {
+  public void visibleProjects(final AsyncCallback<ProjectList> callback) {
     visibleProjectsFactory.create().to(callback);
   }
 
   @Override
+  public void visibleProjectDetails(final AsyncCallback<List<ProjectDetail>> callback) {
+    visibleProjectDetailsFactory.create().to(callback);
+  }
+
+  @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);
@@ -114,4 +135,12 @@
     addBranchFactory.create(projectName, branchName, startingRevision).to(
         callback);
   }
+
+  @Override
+  public void createNewProject(String projectName, String parentName,
+      boolean emptyCommit, boolean permissionsOnly,
+      AsyncCallback<VoidResult> callback) {
+    createProjectHandlerFactory.create(projectName, parentName, emptyCommit,
+        permissionsOnly).to(callback);
+  }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectDetailFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectDetailFactory.java
index 1eb940b..05f66e9 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
@@ -17,27 +17,36 @@
 import com.google.gerrit.common.data.ProjectDetail;
 import com.google.gerrit.httpd.rpc.Handler;
 import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+
+import java.io.IOException;
+
 class ProjectDetailFactory extends Handler<ProjectDetail> {
   interface Factory {
     ProjectDetailFactory create(@Assisted Project.NameKey name);
   }
 
   private final ProjectControl.Factory projectControlFactory;
+  private final GitRepositoryManager gitRepositoryManager;
 
   private final Project.NameKey projectName;
 
   @Inject
   ProjectDetailFactory(final ProjectControl.Factory projectControlFactory,
-
+      final GitRepositoryManager gitRepositoryManager,
       @Assisted final Project.NameKey name) {
     this.projectControlFactory = projectControlFactory;
-
+    this.gitRepositoryManager = gitRepositoryManager;
     this.projectName = name;
   }
 
@@ -57,6 +66,27 @@
     detail.setCanModifyAgreements(userIsOwner);
     detail.setCanModifyDescription(userIsOwner);
     detail.setCanModifyMergeType(userIsOwner);
+    detail.setCanModifyState(userIsOwner);
+
+    final Project.NameKey projectName = projectState.getProject().getNameKey();
+    Repository git;
+    try {
+      git = gitRepositoryManager.openRepository(projectName);
+    } catch (RepositoryNotFoundException err) {
+      throw new NoSuchProjectException(projectName);
+    }
+    try {
+      Ref head = git.getRef(Constants.HEAD);
+      if (head != null && head.isSymbolic()
+          && GitRepositoryManager.REF_CONFIG.equals(head.getLeaf().getName())) {
+        detail.setPermissionOnly(true);
+      }
+    } catch (IOException err) {
+      throw new NoSuchProjectException(projectName);
+    } finally {
+      git.close();
+    }
+
     return detail;
   }
 }
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 68b3625..2eb55b3 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,12 +30,15 @@
       protected void configure() {
         factory(AddBranch.Factory.class);
         factory(ChangeProjectAccess.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/SuggestParentCandidatesHandler.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/SuggestParentCandidatesHandler.java
new file mode 100644
index 0000000..a6442e3
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/SuggestParentCandidatesHandler.java
@@ -0,0 +1,40 @@
+// 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.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
new file mode 100644
index 0000000..9899147
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/VisibleProjectDetails.java
@@ -0,0 +1,62 @@
+// 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.common.data.ProjectDetail;
+import com.google.gerrit.httpd.rpc.Handler;
+import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.inject.Inject;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+class VisibleProjectDetails extends Handler<List<ProjectDetail>> {
+
+  interface Factory {
+    VisibleProjectDetails create();
+  }
+
+  private final ProjectCache projectCache;
+  private final ProjectDetailFactory.Factory projectDetailFactory;
+
+  @Inject
+  VisibleProjectDetails(final ProjectCache projectCache,
+      final ProjectDetailFactory.Factory projectDetailFactory) {
+    this.projectCache = projectCache;
+    this.projectDetailFactory = projectDetailFactory;
+  }
+
+  @Override
+  public List<ProjectDetail> call() {
+    List<ProjectDetail> result = new ArrayList<ProjectDetail>();
+    for (Project.NameKey projectName : projectCache.all()) {
+      try {
+        result.add(projectDetailFactory.create(projectName).call());
+      } catch (NoSuchProjectException e) {
+      }
+    }
+    Collections.sort(result, new Comparator<ProjectDetail>() {
+      public int compare(final ProjectDetail a, final ProjectDetail b) {
+        return a.project.getName().compareTo(b.project.getName());
+      }
+    });
+    return result;
+  }
+}
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
index 31ba77e..8d4596f 100644
--- 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
@@ -14,9 +14,10 @@
 
 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.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;
@@ -27,23 +28,32 @@
 import java.util.Comparator;
 import java.util.List;
 
-class VisibleProjects extends Handler<List<Project>> {
+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 ProjectCache projectCache, final CurrentUser user) {
     this.projectControlFactory = projectControlFactory;
     this.projectCache = projectCache;
+    this.user = user;
   }
 
   @Override
-  public List<Project> call() {
+  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 {
diff --git a/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/auth/container/LoginRedirect.html b/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/auth/container/LoginRedirect.html
index 856ee71..72b589f 100644
--- a/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/auth/container/LoginRedirect.html
+++ b/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/auth/container/LoginRedirect.html
@@ -8,14 +8,17 @@
       var token;
       if (p >= 0) {
         token = href.substring(p + 1);
+        if (token.length != 0 && token.charAt(0) == '/') {
+          token = token.substring(1);
+        }
         href = href.substring(0, p);
       } else {
-        token = 'mine';
+        token = '';
       }
       window.location.replace(href + 'login/' + token);
     </script>
   </head>
   <body>
-    <p>Redirecting to <a href="login/mine">Gerrit Code Review</a>.</p>
+    <p>Redirecting to <a href="login/">Gerrit Code Review</a>.</p>
   </body>
 </html>
diff --git a/gerrit-httpd/src/test/java/com/google/gerrit/httpd/GitWebConfigTest.java b/gerrit-httpd/src/test/java/com/google/gerrit/httpd/GitWebConfigTest.java
new file mode 100644
index 0000000..c834b94
--- /dev/null
+++ b/gerrit-httpd/src/test/java/com/google/gerrit/httpd/GitWebConfigTest.java
@@ -0,0 +1,35 @@
+// 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;
+
+import junit.framework.TestCase;
+
+public class GitWebConfigTest extends TestCase {
+
+  private static final String VALID_CHARACTERS = "*()";
+  private static final String SOME_INVALID_CHARACTERS = "09AZaz$-_.+!',";
+
+  public void testValidPathSeparator() {
+    for(char c : VALID_CHARACTERS.toCharArray()) {
+      assertTrue("valid character rejected: " + c, GitWebConfig.isValidPathSeparator(c));
+    }
+  }
+
+  public void testInalidPathSeparator() {
+    for(char c : SOME_INVALID_CHARACTERS.toCharArray()) {
+      assertFalse("invalid character accepted: " + c, GitWebConfig.isValidPathSeparator(c));
+    }
+  }
+}
diff --git a/gerrit-launcher/.gitignore b/gerrit-launcher/.gitignore
index 903c6c8..194bedc 100644
--- a/gerrit-launcher/.gitignore
+++ b/gerrit-launcher/.gitignore
@@ -2,3 +2,4 @@
 /.classpath
 /.project
 /.settings/org.maven.ide.eclipse.prefs
+/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
diff --git a/gerrit-launcher/pom.xml b/gerrit-launcher/pom.xml
index a5b0159..5707a22 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.2-SNAPSHOT</version>
+    <version>2.3-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 d9ea7d3..7f2007e 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,6 +31,8 @@
 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.jar.Attributes;
 import java.util.jar.JarFile;
@@ -235,6 +237,12 @@
     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()]));
   }
 
diff --git a/gerrit-main/.gitignore b/gerrit-main/.gitignore
index 903c6c8..194bedc 100644
--- a/gerrit-main/.gitignore
+++ b/gerrit-main/.gitignore
@@ -2,3 +2,4 @@
 /.classpath
 /.project
 /.settings/org.maven.ide.eclipse.prefs
+/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
diff --git a/gerrit-main/pom.xml b/gerrit-main/pom.xml
index f630be64..96f1886 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.2-SNAPSHOT</version>
+    <version>2.3-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-main</artifactId>
diff --git a/gerrit-openid/.gitignore b/gerrit-openid/.gitignore
new file mode 100644
index 0000000..194bedc
--- /dev/null
+++ b/gerrit-openid/.gitignore
@@ -0,0 +1,5 @@
+/target
+/.classpath
+/.project
+/.settings/org.maven.ide.eclipse.prefs
+/.settings/org.eclipse.m2e.core.prefs
\ 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
new file mode 100644
index 0000000..fc11c3f
--- /dev/null
+++ b/gerrit-openid/.settings/org.eclipse.core.resources.prefs
@@ -0,0 +1,5 @@
+#Thu Jul 28 11:02:36 PDT 2011
+eclipse.preferences.version=1
+encoding//src/main/java=UTF-8
+encoding//src/test/java=UTF-8
+encoding/<project>=UTF-8
diff --git a/gerrit-openid/.settings/org.eclipse.core.runtime.prefs b/gerrit-openid/.settings/org.eclipse.core.runtime.prefs
new file mode 100644
index 0000000..8667cfd
--- /dev/null
+++ b/gerrit-openid/.settings/org.eclipse.core.runtime.prefs
@@ -0,0 +1,3 @@
+#Tue Sep 02 16:59:24 PDT 2008
+eclipse.preferences.version=1
+line.separator=\n
diff --git a/gerrit-openid/.settings/org.eclipse.jdt.core.prefs b/gerrit-openid/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 0000000..470942d
--- /dev/null
+++ b/gerrit-openid/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,269 @@
+#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
+org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve
+org.eclipse.jdt.core.compiler.compliance=1.6
+org.eclipse.jdt.core.compiler.debug.lineNumber=generate
+org.eclipse.jdt.core.compiler.debug.localVariable=generate
+org.eclipse.jdt.core.compiler.debug.sourceFile=generate
+org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
+org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
+org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
+org.eclipse.jdt.core.compiler.source=1.6
+org.eclipse.jdt.core.formatter.align_type_members_on_columns=false
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_enum_constant=16
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_explicit_constructor_call=16
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_method_invocation=16
+org.eclipse.jdt.core.formatter.alignment_for_arguments_in_qualified_allocation_expression=16
+org.eclipse.jdt.core.formatter.alignment_for_assignment=16
+org.eclipse.jdt.core.formatter.alignment_for_binary_expression=16
+org.eclipse.jdt.core.formatter.alignment_for_compact_if=16
+org.eclipse.jdt.core.formatter.alignment_for_conditional_expression=16
+org.eclipse.jdt.core.formatter.alignment_for_enum_constants=16
+org.eclipse.jdt.core.formatter.alignment_for_expressions_in_array_initializer=16
+org.eclipse.jdt.core.formatter.alignment_for_multiple_fields=16
+org.eclipse.jdt.core.formatter.alignment_for_parameters_in_constructor_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_parameters_in_method_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_selector_in_method_invocation=16
+org.eclipse.jdt.core.formatter.alignment_for_superclass_in_type_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_enum_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_superinterfaces_in_type_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_constructor_declaration=16
+org.eclipse.jdt.core.formatter.alignment_for_throws_clause_in_method_declaration=16
+org.eclipse.jdt.core.formatter.blank_lines_after_imports=1
+org.eclipse.jdt.core.formatter.blank_lines_after_package=1
+org.eclipse.jdt.core.formatter.blank_lines_before_field=0
+org.eclipse.jdt.core.formatter.blank_lines_before_first_class_body_declaration=0
+org.eclipse.jdt.core.formatter.blank_lines_before_imports=0
+org.eclipse.jdt.core.formatter.blank_lines_before_member_type=0
+org.eclipse.jdt.core.formatter.blank_lines_before_method=1
+org.eclipse.jdt.core.formatter.blank_lines_before_new_chunk=1
+org.eclipse.jdt.core.formatter.blank_lines_before_package=0
+org.eclipse.jdt.core.formatter.blank_lines_between_import_groups=1
+org.eclipse.jdt.core.formatter.blank_lines_between_type_declarations=2
+org.eclipse.jdt.core.formatter.brace_position_for_annotation_type_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_anonymous_type_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_array_initializer=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_block=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_block_in_case=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_constructor_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_enum_constant=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_enum_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_method_declaration=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_switch=end_of_line
+org.eclipse.jdt.core.formatter.brace_position_for_type_declaration=end_of_line
+org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_block_comment=false
+org.eclipse.jdt.core.formatter.comment.clear_blank_lines_in_javadoc_comment=false
+org.eclipse.jdt.core.formatter.comment.format_block_comments=true
+org.eclipse.jdt.core.formatter.comment.format_header=true
+org.eclipse.jdt.core.formatter.comment.format_html=true
+org.eclipse.jdt.core.formatter.comment.format_javadoc_comments=true
+org.eclipse.jdt.core.formatter.comment.format_line_comments=true
+org.eclipse.jdt.core.formatter.comment.format_source_code=true
+org.eclipse.jdt.core.formatter.comment.indent_parameter_description=false
+org.eclipse.jdt.core.formatter.comment.indent_root_tags=true
+org.eclipse.jdt.core.formatter.comment.insert_new_line_before_root_tags=insert
+org.eclipse.jdt.core.formatter.comment.insert_new_line_for_parameter=do not insert
+org.eclipse.jdt.core.formatter.comment.line_length=80
+org.eclipse.jdt.core.formatter.compact_else_if=true
+org.eclipse.jdt.core.formatter.continuation_indentation=2
+org.eclipse.jdt.core.formatter.continuation_indentation_for_array_initializer=2
+org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line=false
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_annotation_declaration_header=true
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_constant_header=true
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_enum_declaration_header=true
+org.eclipse.jdt.core.formatter.indent_body_declarations_compare_to_type_header=true
+org.eclipse.jdt.core.formatter.indent_breaks_compare_to_cases=true
+org.eclipse.jdt.core.formatter.indent_empty_lines=false
+org.eclipse.jdt.core.formatter.indent_statements_compare_to_block=true
+org.eclipse.jdt.core.formatter.indent_statements_compare_to_body=true
+org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_cases=true
+org.eclipse.jdt.core.formatter.indent_switchstatements_compare_to_switch=true
+org.eclipse.jdt.core.formatter.indentation.size=4
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_local_variable=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_member=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_parameter=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_opening_brace_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_at_end_of_file_if_missing=insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_catch_in_try_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_closing_brace_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_else_in_if_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_finally_in_try_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_before_while_in_do_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_annotation_declaration=insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_anonymous_type_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_block=insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_enum_declaration=insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_method_body=insert
+org.eclipse.jdt.core.formatter.insert_new_line_in_empty_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_after_and_in_type_parameter=insert
+org.eclipse.jdt.core.formatter.insert_space_after_assignment_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_at_in_annotation_type_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_binary_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_closing_angle_bracket_in_type_parameters=insert
+org.eclipse.jdt.core.formatter.insert_space_after_closing_brace_in_block=insert
+org.eclipse.jdt.core.formatter.insert_space_after_closing_paren_in_cast=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_assert=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_case=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_conditional=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_after_colon_in_labeled_statement=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_allocation_expression=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_annotation=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_array_initializer=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_parameters=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_constructor_declaration_throws=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_constant_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_enum_declarations=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_explicitconstructorcall_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_increments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_for_inits=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_parameters=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_declaration_throws=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_method_invocation_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_field_declarations=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_multiple_local_declarations=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_parameterized_type_reference=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_superinterfaces=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_arguments=insert
+org.eclipse.jdt.core.formatter.insert_space_after_comma_in_type_parameters=insert
+org.eclipse.jdt.core.formatter.insert_space_after_ellipsis=insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_parameterized_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_angle_bracket_in_type_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_brace_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_bracket_in_array_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_cast=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_catch=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_constructor_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_for=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_if=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_method_invocation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_parenthesized_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_switch=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_synchronized=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_opening_paren_in_while=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_postfix_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_prefix_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_question_in_conditional=insert
+org.eclipse.jdt.core.formatter.insert_space_after_question_in_wildcard=do not insert
+org.eclipse.jdt.core.formatter.insert_space_after_semicolon_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_after_unary_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_and_in_type_parameter=insert
+org.eclipse.jdt.core.formatter.insert_space_before_assignment_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_before_at_in_annotation_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_binary_operator=insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_parameterized_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_angle_bracket_in_type_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_brace_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_bracket_in_array_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_cast=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_catch=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_constructor_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_for=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_if=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_method_invocation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_parenthesized_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_switch=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_synchronized=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_closing_paren_in_while=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_assert=insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_case=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_conditional=insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_default=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_before_colon_in_labeled_statement=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_constructor_declaration_throws=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_constant_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_enum_declarations=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_explicitconstructorcall_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_increments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_for_inits=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_declaration_throws=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_method_invocation_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_field_declarations=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_multiple_local_declarations=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_parameterized_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_superinterfaces=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_comma_in_type_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_ellipsis=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_parameterized_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_arguments=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_angle_bracket_in_type_parameters=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_annotation_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_anonymous_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_array_initializer=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_block=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_constructor_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_constant=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_enum_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_method_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_switch=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_brace_in_type_declaration=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_bracket_in_array_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_annotation_type_member_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_catch=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_constructor_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_for=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_if=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_method_invocation=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_parenthesized_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_switch=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_synchronized=insert
+org.eclipse.jdt.core.formatter.insert_space_before_opening_paren_in_while=insert
+org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_return=insert
+org.eclipse.jdt.core.formatter.insert_space_before_parenthesized_expression_in_throw=insert
+org.eclipse.jdt.core.formatter.insert_space_before_postfix_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_prefix_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_question_in_conditional=insert
+org.eclipse.jdt.core.formatter.insert_space_before_question_in_wildcard=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_semicolon=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_semicolon_in_for=do not insert
+org.eclipse.jdt.core.formatter.insert_space_before_unary_operator=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_brackets_in_array_type_reference=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_braces_in_array_initializer=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_brackets_in_array_allocation_expression=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_annotation_type_member_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_constructor_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_enum_constant=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_declaration=do not insert
+org.eclipse.jdt.core.formatter.insert_space_between_empty_parens_in_method_invocation=do not insert
+org.eclipse.jdt.core.formatter.keep_else_statement_on_same_line=false
+org.eclipse.jdt.core.formatter.keep_empty_array_initializer_on_one_line=false
+org.eclipse.jdt.core.formatter.keep_imple_if_on_one_line=true
+org.eclipse.jdt.core.formatter.keep_then_statement_on_same_line=false
+org.eclipse.jdt.core.formatter.lineSplit=80
+org.eclipse.jdt.core.formatter.never_indent_block_comments_on_first_column=false
+org.eclipse.jdt.core.formatter.never_indent_line_comments_on_first_column=false
+org.eclipse.jdt.core.formatter.number_of_blank_lines_at_beginning_of_method_body=0
+org.eclipse.jdt.core.formatter.number_of_empty_lines_to_preserve=3
+org.eclipse.jdt.core.formatter.put_empty_statement_on_new_line=false
+org.eclipse.jdt.core.formatter.tabulation.char=space
+org.eclipse.jdt.core.formatter.tabulation.size=2
+org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations=false
+org.eclipse.jdt.core.formatter.wrap_before_binary_operator=true
diff --git a/gerrit-openid/.settings/org.eclipse.jdt.ui.prefs b/gerrit-openid/.settings/org.eclipse.jdt.ui.prefs
new file mode 100644
index 0000000..d4218a5
--- /dev/null
+++ b/gerrit-openid/.settings/org.eclipse.jdt.ui.prefs
@@ -0,0 +1,61 @@
+#Wed Jul 29 11:31:38 PDT 2009
+eclipse.preferences.version=1
+editor_save_participant_org.eclipse.jdt.ui.postsavelistener.cleanup=true
+formatter_profile=_Google Format
+formatter_settings_version=11
+org.eclipse.jdt.ui.ignorelowercasenames=true
+org.eclipse.jdt.ui.importorder=com.google;com;junit;net;org;java;javax;
+org.eclipse.jdt.ui.ondemandthreshold=99
+org.eclipse.jdt.ui.staticondemandthreshold=99
+org.eclipse.jdt.ui.text.custom_code_templates=<?xml version\="1.0" encoding\="UTF-8" standalone\="no"?><templates/>
+sp_cleanup.add_default_serial_version_id=true
+sp_cleanup.add_generated_serial_version_id=false
+sp_cleanup.add_missing_annotations=false
+sp_cleanup.add_missing_deprecated_annotations=true
+sp_cleanup.add_missing_methods=false
+sp_cleanup.add_missing_nls_tags=false
+sp_cleanup.add_missing_override_annotations=true
+sp_cleanup.add_serial_version_id=false
+sp_cleanup.always_use_blocks=true
+sp_cleanup.always_use_parentheses_in_expressions=false
+sp_cleanup.always_use_this_for_non_static_field_access=false
+sp_cleanup.always_use_this_for_non_static_method_access=false
+sp_cleanup.convert_to_enhanced_for_loop=false
+sp_cleanup.correct_indentation=false
+sp_cleanup.format_source_code=false
+sp_cleanup.format_source_code_changes_only=false
+sp_cleanup.make_local_variable_final=true
+sp_cleanup.make_parameters_final=true
+sp_cleanup.make_private_fields_final=true
+sp_cleanup.make_type_abstract_if_missing_method=false
+sp_cleanup.make_variable_declarations_final=false
+sp_cleanup.never_use_blocks=false
+sp_cleanup.never_use_parentheses_in_expressions=true
+sp_cleanup.on_save_use_additional_actions=true
+sp_cleanup.organize_imports=false
+sp_cleanup.qualify_static_field_accesses_with_declaring_class=false
+sp_cleanup.qualify_static_member_accesses_through_instances_with_declaring_class=true
+sp_cleanup.qualify_static_member_accesses_through_subtypes_with_declaring_class=true
+sp_cleanup.qualify_static_member_accesses_with_declaring_class=false
+sp_cleanup.qualify_static_method_accesses_with_declaring_class=false
+sp_cleanup.remove_private_constructors=true
+sp_cleanup.remove_trailing_whitespaces=true
+sp_cleanup.remove_trailing_whitespaces_all=true
+sp_cleanup.remove_trailing_whitespaces_ignore_empty=false
+sp_cleanup.remove_unnecessary_casts=false
+sp_cleanup.remove_unnecessary_nls_tags=false
+sp_cleanup.remove_unused_imports=false
+sp_cleanup.remove_unused_local_variables=false
+sp_cleanup.remove_unused_private_fields=true
+sp_cleanup.remove_unused_private_members=false
+sp_cleanup.remove_unused_private_methods=true
+sp_cleanup.remove_unused_private_types=true
+sp_cleanup.sort_members=false
+sp_cleanup.sort_members_all=false
+sp_cleanup.use_blocks=false
+sp_cleanup.use_blocks_only_for_return_and_throw=false
+sp_cleanup.use_parentheses_in_expressions=false
+sp_cleanup.use_this_for_non_static_field_access=false
+sp_cleanup.use_this_for_non_static_field_access_only_if_necessary=true
+sp_cleanup.use_this_for_non_static_method_access=false
+sp_cleanup.use_this_for_non_static_method_access_only_if_necessary=true
diff --git a/gerrit-openid/pom.xml b/gerrit-openid/pom.xml
new file mode 100644
index 0000000..74328cc
--- /dev/null
+++ b/gerrit-openid/pom.xml
@@ -0,0 +1,91 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<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.3-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>gerrit-openid</artifactId>
+  <name>Gerrit Code Review - OpenID servlet and RPC</name>
+
+  <description>
+    OpenID
+  </description>
+
+  <dependencies>
+    <dependency>
+      <groupId>org.apache.tomcat</groupId>
+      <artifactId>servlet-api</artifactId>
+      <scope>provided</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>com.google.code.findbugs</groupId>
+      <artifactId>jsr305</artifactId>
+    </dependency>
+
+    <dependency>
+      <groupId>org.eclipse.jgit</groupId>
+      <artifactId>org.eclipse.jgit.http.server</artifactId>
+    </dependency>
+
+    <dependency>
+      <groupId>com.google.gwt</groupId>
+      <artifactId>gwt-servlet</artifactId>
+    </dependency>
+
+    <dependency>
+      <groupId>org.openid4java</groupId>
+      <artifactId>openid4java-consumer</artifactId>
+      <type>pom</type>
+    </dependency>
+
+    <dependency>
+      <groupId>com.google.gerrit</groupId>
+      <artifactId>gerrit-httpd</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>com.google.gerrit</groupId>
+      <artifactId>gerrit-server</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+  </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-source-plugin</artifactId>
+        <executions>
+          <execution>
+            <goals>
+              <goal>jar</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+</project>
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdLoginServlet.java b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdLoginServlet.java
similarity index 100%
rename from gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdLoginServlet.java
rename to gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdLoginServlet.java
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdModule.java b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdModule.java
similarity index 100%
rename from gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdModule.java
rename to gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdModule.java
diff --git a/gerrit-httpd/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
similarity index 100%
rename from gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java
rename to gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/openid/XrdsFilter.java b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/XrdsFilter.java
similarity index 100%
rename from gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/openid/XrdsFilter.java
rename to gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/XrdsFilter.java
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/openid/XrdsServlet.java b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/XrdsServlet.java
similarity index 100%
rename from gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/openid/XrdsServlet.java
rename to gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/XrdsServlet.java
diff --git a/gerrit-patch-commonsnet/.gitignore b/gerrit-patch-commonsnet/.gitignore
index 903c6c8..194bedc 100644
--- a/gerrit-patch-commonsnet/.gitignore
+++ b/gerrit-patch-commonsnet/.gitignore
@@ -2,3 +2,4 @@
 /.classpath
 /.project
 /.settings/org.maven.ide.eclipse.prefs
+/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
diff --git a/gerrit-patch-commonsnet/pom.xml b/gerrit-patch-commonsnet/pom.xml
index 738d6ce..f267920 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.2-SNAPSHOT</version>
+    <version>2.3-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-patch-commonsnet</artifactId>
diff --git a/gerrit-patch-jgit/.gitignore b/gerrit-patch-jgit/.gitignore
index 903c6c8..194bedc 100644
--- a/gerrit-patch-jgit/.gitignore
+++ b/gerrit-patch-jgit/.gitignore
@@ -2,3 +2,4 @@
 /.classpath
 /.project
 /.settings/org.maven.ide.eclipse.prefs
+/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
diff --git a/gerrit-patch-jgit/pom.xml b/gerrit-patch-jgit/pom.xml
index f2d1cfd..4c2aa0b 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.2-SNAPSHOT</version>
+    <version>2.3-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-patch-jgit</artifactId>
diff --git a/gerrit-pgm/.gitignore b/gerrit-pgm/.gitignore
index 903c6c8..194bedc 100644
--- a/gerrit-pgm/.gitignore
+++ b/gerrit-pgm/.gitignore
@@ -2,3 +2,4 @@
 /.classpath
 /.project
 /.settings/org.maven.ide.eclipse.prefs
+/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
diff --git a/gerrit-pgm/pom.xml b/gerrit-pgm/pom.xml
index 54f16a7..e816797 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.2-SNAPSHOT</version>
+    <version>2.3-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-pgm</artifactId>
@@ -69,6 +69,12 @@
 
     <dependency>
       <groupId>com.google.gerrit</groupId>
+      <artifactId>gerrit-openid</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>com.google.gerrit</groupId>
       <artifactId>gerrit-sshd</artifactId>
       <version>${project.version}</version>
     </dependency>
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 96c97b0..7382096 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
@@ -16,8 +16,14 @@
 
 import static com.google.gerrit.server.schema.DataSourceProvider.Context.MULTI_USER;
 
+import com.google.gerrit.common.ChangeHookRunner;
+import com.google.gerrit.ehcache.EhcachePoolImpl;
+import com.google.gerrit.httpd.CacheBasedWebSession;
+import com.google.gerrit.httpd.GitOverHttpModule;
 import com.google.gerrit.httpd.HttpCanonicalWebUrlProvider;
 import com.google.gerrit.httpd.WebModule;
+import com.google.gerrit.httpd.WebSshGlueModule;
+import com.google.gerrit.httpd.auth.openid.OpenIdModule;
 import com.google.gerrit.lifecycle.LifecycleManager;
 import com.google.gerrit.pgm.http.jetty.JettyEnv;
 import com.google.gerrit.pgm.http.jetty.JettyModule;
@@ -26,12 +32,20 @@
 import com.google.gerrit.pgm.util.LogFileCompressor;
 import com.google.gerrit.pgm.util.RuntimeShutdown;
 import com.google.gerrit.pgm.util.SiteProgram;
+import com.google.gerrit.reviewdb.AuthType;
+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.MasterNodeStartup;
+import com.google.gerrit.server.contact.HttpContactStoreConnection;
+import com.google.gerrit.server.git.PushReplication;
+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.schema.SchemaVersionCheck;
+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;
@@ -114,10 +128,6 @@
     if (slave && httpd) {
       throw die("Cannot combine --slave and --enable-httpd");
     }
-    if (httpd && !sshd) {
-      // TODO Support HTTP without SSH.
-      throw die("--enable-httpd currently requires --enable-sshd");
-    }
 
     if (consoleLog) {
     } else {
@@ -188,7 +198,13 @@
     final List<Module> modules = new ArrayList<Module>();
     modules.add(SchemaVersionCheck.module());
     modules.add(new LogFileCompressor.Module());
+    modules.add(new WorkQueue.Module());
+    modules.add(new ChangeHookRunner.Module());
     modules.add(cfgInjector.getInstance(GerritGlobalModule.class));
+    modules.add(new EhcachePoolImpl.Module());
+    modules.add(new SmtpEmailSender.Module());
+    modules.add(new SignedTokenEmailTokenVerifier.Module());
+    modules.add(new PushReplication.Module());
     if (httpd) {
       modules.add(new CanonicalWebUrlModule() {
         @Override
@@ -217,11 +233,15 @@
 
   private Injector createSshInjector() {
     final List<Module> modules = new ArrayList<Module>();
-    modules.add(new SshModule());
-    if (slave) {
-      modules.add(new SlaveCommandModule());
+    if (sshd) {
+      modules.add(new SshModule());
+      if (slave) {
+        modules.add(new SlaveCommandModule());
+      } else {
+        modules.add(new MasterCommandModule());
+      }
     } else {
-      modules.add(new MasterCommandModule());
+      modules.add(new NoSshModule());
     }
     return sysInjector.createChildInjector(modules);
   }
@@ -239,8 +259,22 @@
 
   private Injector createWebInjector() {
     final List<Module> modules = new ArrayList<Module>();
-    modules.add(sshInjector.getInstance(WebModule.class));
-    modules.add(sshInjector.getInstance(ProjectQoSFilter.Module.class));
+    modules.add(CacheBasedWebSession.module());
+    modules.add(HttpContactStoreConnection.module());
+    modules.add(sysInjector.getInstance(GitOverHttpModule.class));
+    modules.add(sysInjector.getInstance(WebModule.class));
+    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) {
+      modules.add(new OpenIdModule());
+    }
+
     return sysInjector.createChildInjector(modules);
   }
 
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 7b8c286..7842b6c 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,6 +17,7 @@
 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;
@@ -100,6 +101,7 @@
 
         install(AccountCacheImpl.module());
         install(GroupCacheImpl.module());
+        install(new EhcachePoolImpl.Module());
         install(new FactoryModule() {
           @Override
           protected void configure() {
@@ -109,7 +111,6 @@
         install(new LifecycleModule() {
           @Override
           protected void configure() {
-            listener().to(CachePool.Lifecycle.class);
             listener().to(LocalDiskRepositoryManager.Lifecycle.class);
           }
         });
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/LocalUsernamesToLowerCase.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/LocalUsernamesToLowerCase.java
new file mode 100644
index 0000000..082279c
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/LocalUsernamesToLowerCase.java
@@ -0,0 +1,145 @@
+// 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.pgm;
+
+import static com.google.gerrit.server.schema.DataSourceProvider.Context.MULTI_USER;
+
+import com.google.gerrit.lifecycle.LifecycleManager;
+import com.google.gerrit.pgm.util.SiteProgram;
+import com.google.gerrit.reviewdb.AccountExternalId;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.server.schema.SchemaVersionCheck;
+import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.client.SchemaFactory;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+
+import org.eclipse.jgit.lib.TextProgressMonitor;
+import org.kohsuke.args4j.Option;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+
+/** Converts the local username for all accounts to lower case */
+public class LocalUsernamesToLowerCase extends SiteProgram {
+  @Option(name = "--threads", usage = "Number of concurrent threads to run")
+  private int threads = 2;
+
+  private final LifecycleManager manager = new LifecycleManager();
+  private final TextProgressMonitor monitor = new TextProgressMonitor();
+  private List<AccountExternalId> todo;
+
+  private Injector dbInjector;
+
+  @Inject
+  private SchemaFactory<ReviewDb> database;
+
+  @Override
+  public int run() throws Exception {
+    if (threads <= 0) {
+      threads = 1;
+    }
+
+    dbInjector = createDbInjector(MULTI_USER);
+    manager.add(dbInjector,
+        dbInjector.createChildInjector(SchemaVersionCheck.module()));
+    manager.start();
+    dbInjector.injectMembers(this);
+
+    final ReviewDb db = database.open();
+    try {
+      todo = db.accountExternalIds().all().toList();
+      synchronized (monitor) {
+        monitor.beginTask("Converting local username", todo.size());
+      }
+    } finally {
+      db.close();
+    }
+
+    final List<Worker> workers = new ArrayList<Worker>(threads);
+    for (int tid = 0; tid < threads; tid++) {
+      Worker t = new Worker();
+      t.start();
+      workers.add(t);
+    }
+    for (Worker t : workers) {
+      t.join();
+    }
+    synchronized (monitor) {
+      monitor.endTask();
+    }
+    manager.stop();
+    return 0;
+  }
+
+  private void convertLocalUserToLowerCase(final ReviewDb db,
+      final AccountExternalId extId) {
+    if (extId.isScheme(AccountExternalId.SCHEME_GERRIT)) {
+      final String localUser = extId.getSchemeRest();
+      final String localUserLowerCase = localUser.toLowerCase(Locale.US);
+      if (!localUser.equals(localUserLowerCase)) {
+        final AccountExternalId.Key extIdKeyLowerCase =
+            new AccountExternalId.Key(AccountExternalId.SCHEME_GERRIT,
+                localUserLowerCase);
+        final AccountExternalId extIdLowerCase =
+            new AccountExternalId(extId.getAccountId(), extIdKeyLowerCase);
+        try {
+          db.accountExternalIds().insert(Collections.singleton(extIdLowerCase));
+          db.accountExternalIds().delete(Collections.singleton(extId));
+        } catch (OrmException error) {
+          System.err.println("ERR " + error.getMessage());
+        }
+      }
+    }
+  }
+
+  private AccountExternalId next() {
+    synchronized (todo) {
+      if (todo.isEmpty()) {
+        return null;
+      }
+      return todo.remove(todo.size() - 1);
+    }
+  }
+
+  private class Worker extends Thread {
+    @Override
+    public void run() {
+      final ReviewDb db;
+      try {
+        db = database.open();
+      } catch (OrmException e) {
+        e.printStackTrace();
+        return;
+      }
+      try {
+        for (;;) {
+          final AccountExternalId extId = next();
+          if (extId == null) {
+            break;
+          }
+          convertLocalUserToLowerCase(db, extId);
+          synchronized (monitor) {
+            monitor.update(1);
+          }
+        }
+      } finally {
+        db.close();
+      }
+    }
+  }
+}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ProtoGen.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ProtoGen.java
new file mode 100644
index 0000000..bc7b254
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ProtoGen.java
@@ -0,0 +1,75 @@
+// 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.pgm;
+
+import com.google.gerrit.pgm.util.AbstractProgram;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gwtorm.schema.java.JavaSchemaModel;
+
+import org.eclipse.jgit.storage.file.LockFile;
+import org.eclipse.jgit.util.FS;
+import org.eclipse.jgit.util.IO;
+import org.kohsuke.args4j.Option;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.InputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.nio.ByteBuffer;
+
+public class ProtoGen extends AbstractProgram {
+  @Option(name = "--output", aliases = {"-o"}, required = true, metaVar = "FILE", usage = "File to write .proto into")
+  private File file;
+
+  @Override
+  public int run() throws Exception {
+    LockFile lock = new LockFile(file.getAbsoluteFile(), FS.DETECTED);
+    if (!lock.lock()) {
+      throw die("Cannot lock " + file);
+    }
+    try {
+      JavaSchemaModel jsm = new JavaSchemaModel(ReviewDb.class);
+      PrintWriter out = new PrintWriter(new BufferedWriter(
+          new OutputStreamWriter(lock.getOutputStream(), "UTF-8")));
+      try {
+        String header;
+        InputStream in = getClass().getResourceAsStream("ProtoGenHeader.txt");
+        try {
+          ByteBuffer buf = IO.readWholeStream(in, 1024);
+          int ptr = buf.arrayOffset() + buf.position();
+          int len = buf.remaining();
+          header = new String(buf.array(), ptr, len, "UTF-8");
+        } finally {
+          in.close();
+        }
+
+        String version = com.google.gerrit.common.Version.getVersion();
+        out.write(header.replace("@@VERSION@@", version));
+        jsm.generateProto(out);
+        out.flush();
+      } finally {
+        out.close();
+      }
+      if (!lock.commit()) {
+        throw die("Could not write to " + file);
+      }
+    } finally {
+      lock.unlock();
+    }
+    System.out.println("Created " + file.getPath());
+    return 0;
+  }
+}
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 7ab8d30..a309e4e 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
@@ -29,6 +29,7 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import com.google.inject.servlet.GuiceFilter;
+import com.google.inject.servlet.GuiceHelper;
 import com.google.inject.servlet.GuiceServletContextListener;
 
 import org.eclipse.jetty.io.EndPoint;
@@ -67,6 +68,10 @@
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipFile;
 
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
 @Singleton
 public class JettyServer {
   static class Lifecycle implements LifecycleListener {
@@ -117,7 +122,23 @@
 
     Handler app = makeContext(env, cfg);
     if (cfg.getBoolean("httpd", "requestlog", !reverseProxy)) {
-      RequestLogHandler handler = new RequestLogHandler();
+      RequestLogHandler handler = new RequestLogHandler() {
+        @Override
+        public void handle(String target, Request baseRequest,
+            final HttpServletRequest req, final HttpServletResponse rsp)
+            throws IOException, ServletException {
+          // Force the user to construct, so it's available to our HttpLog
+          // later on when the request gets logged out to the access file.
+          //
+          GuiceHelper.runInContext(req, rsp, new Runnable() {
+            @Override
+            public void run() {
+              userProvider.get();
+            }
+          });
+          super.handle(target, baseRequest, req, rsp);
+        }
+      };
       handler.setRequestLog(new HttpLog(site, userProvider));
       handler.setHandler(app);
       app = handler;
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitSshd.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitSshd.java
index 482405e..bfc0eaf 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitSshd.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitSshd.java
@@ -54,13 +54,20 @@
     String hostname = "*";
     int port = 29418;
     String listenAddress = sshd.get("listenAddress");
-    if (listenAddress != null && !listenAddress.isEmpty()) {
+    if (isOff(listenAddress)) {
+      hostname = "off";
+    } else if (listenAddress != null && !listenAddress.isEmpty()) {
       final InetSocketAddress addr = SocketUtil.parse(listenAddress, port);
       hostname = SocketUtil.hostname(addr);
       port = addr.getPort();
     }
 
     hostname = ui.readString(hostname, "Listen on address");
+    if (isOff(hostname)) {
+      sshd.set("listenAddress", "off");
+      return;
+    }
+
     port = ui.readInt(port, "Listen on port");
     sshd.set("listenAddress", SocketUtil.format(hostname, port));
 
@@ -73,6 +80,12 @@
     generateSshHostKeys();
   }
 
+  private static boolean isOff(String listenHostname) {
+    return "off".equalsIgnoreCase(listenHostname)
+        || "none".equalsIgnoreCase(listenHostname)
+        || "no".equalsIgnoreCase(listenHostname);
+  }
+
   private void generateSshHostKeys() throws InterruptedException, IOException {
     if (!site.ssh_key.exists() //
         && !site.ssh_rsa.exists() //
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteProgram.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteProgram.java
index 340168c..7bc6c0a 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteProgram.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteProgram.java
@@ -20,6 +20,7 @@
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.server.config.GerritServerConfigModule;
 import com.google.gerrit.server.config.SitePath;
+import com.google.gerrit.server.git.LocalDiskRepositoryManager;
 import com.google.gerrit.server.schema.DataSourceProvider;
 import com.google.gerrit.server.schema.DatabaseModule;
 import com.google.gerrit.server.schema.SchemaModule;
@@ -164,6 +165,7 @@
     modules.add(new GerritServerConfigModule());
     modules.add(new DatabaseModule());
     modules.add(new SchemaModule());
+    modules.add(new LocalDiskRepositoryManager.Module());
 
     try {
       return Guice.createInjector(PRODUCTION, modules);
diff --git a/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/ProtoGenHeader.txt b/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/ProtoGenHeader.txt
new file mode 100644
index 0000000..757e8e2
--- /dev/null
+++ b/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/ProtoGenHeader.txt
@@ -0,0 +1,22 @@
+// 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.
+//
+// Gerrit Code Review (version @@VERSION@@)
+
+syntax = "proto2";
+
+option java_api_version = 2;
+option java_package = "com.google.gerrit.proto.reviewdb";
+
+package devtools.gerritcodereview;
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 4538bce..4148847 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
@@ -380,14 +380,14 @@
         chown $GERRIT_USER "$GERRIT_PID"
         su - $GERRIT_USER -c "
           JAVA='$JAVA' ; export JAVA ;
-          $RUN_EXEC $RUN_Arg1 '$RUN_Arg2' $RUN_Arg3 $RUN_ARGS &
+          $RUN_EXEC $RUN_Arg1 '$RUN_Arg2' $RUN_Arg3 $RUN_ARGS </dev/null >/dev/null 2>&1 &
           PID=\$! ;
-          disown \$PID ;
+          disown ;
           echo \$PID >\"$GERRIT_PID\""
       else
-        $RUN_EXEC $RUN_Arg1 "$RUN_Arg2" $RUN_Arg3 $RUN_ARGS &
+        $RUN_EXEC $RUN_Arg1 "$RUN_Arg2" $RUN_Arg3 $RUN_ARGS </dev/null >/dev/null 2>&1 &
         PID=$!
-        type disown >/dev/null 2>&1 && disown $PID
+        type disown >/dev/null 2>&1 && disown
         echo $PID >"$GERRIT_PID"
       fi
     fi
diff --git a/gerrit-prettify/.gitignore b/gerrit-prettify/.gitignore
index 903c6c8..194bedc 100644
--- a/gerrit-prettify/.gitignore
+++ b/gerrit-prettify/.gitignore
@@ -2,3 +2,4 @@
 /.classpath
 /.project
 /.settings/org.maven.ide.eclipse.prefs
+/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
diff --git a/gerrit-prettify/pom.xml b/gerrit-prettify/pom.xml
index 3ac4deb..4f05573 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.2-SNAPSHOT</version>
+    <version>2.3-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-prettify</artifactId>
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/EditList.java b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/EditList.java
index d41865a..1d23009 100644
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/EditList.java
+++ b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/EditList.java
@@ -72,13 +72,15 @@
   private boolean combineA(final int i) {
     final Edit s = edits.get(i);
     final Edit e = edits.get(i - 1);
-    return s.getBeginA() - e.getEndA() <= 2 * context;
+    // + 1 to prevent '... skipping 1 common line ...' messages.
+    return s.getBeginA() - e.getEndA() <= 2 * context + 1;
   }
 
   private boolean combineB(final int i) {
     final int s = edits.get(i).getBeginB();
     final int e = edits.get(i - 1).getEndB();
-    return s - e <= 2 * context;
+    // + 1 to prevent '... skipping 1 common line ...' messages.
+    return s - e <= 2 * context + 1;
   }
 
   public class Hunk {
diff --git a/gerrit-reviewdb/.gitignore b/gerrit-reviewdb/.gitignore
index 903c6c8..194bedc 100644
--- a/gerrit-reviewdb/.gitignore
+++ b/gerrit-reviewdb/.gitignore
@@ -2,3 +2,4 @@
 /.classpath
 /.project
 /.settings/org.maven.ide.eclipse.prefs
+/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
diff --git a/gerrit-reviewdb/pom.xml b/gerrit-reviewdb/pom.xml
index 8c43204..5e8a37c 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.2-SNAPSHOT</version>
+    <version>2.3-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-reviewdb</artifactId>
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountAgreement.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountAgreement.java
index 39ed66d..57b9340 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountAgreement.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountAgreement.java
@@ -45,6 +45,10 @@
       return accountId;
     }
 
+    public ContributorAgreement.Id getContributorAgreementId() {
+      return claId;
+    }
+
     @Override
     public com.google.gwtorm.client.Key<?>[] members() {
       return new com.google.gwtorm.client.Key<?>[] {claId};
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountExternalIdAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountExternalIdAccess.java
index 0719035..4c78139 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountExternalIdAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountExternalIdAccess.java
@@ -42,4 +42,7 @@
   @Query("WHERE emailAddress >= ? AND emailAddress <= ? ORDER BY emailAddress LIMIT ?")
   ResultSet<AccountExternalId> suggestByEmailAddress(String emailA,
       String emailB, int limit) throws OrmException;
+
+  @Query
+  ResultSet<AccountExternalId> all() throws OrmException;
 }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGeneralPreferences.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGeneralPreferences.java
index a551ebb..ebaf0c8 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGeneralPreferences.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGeneralPreferences.java
@@ -43,7 +43,10 @@
     US("MM/dd", "MM/dd/yy"),
 
     /** ISO style dates: 2010-02-14 */
-    ISO("MM-dd", "yyyy-MM-dd");
+    ISO("MM-dd", "yyyy-MM-dd"),
+
+    /** European style dates: 27. Apr, 27.04.2010 */
+    EURO("d. MMM", "dd.MM.yyyy");
 
     private final String shortFormat;
     private final String longFormat;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGroupAgreement.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGroupAgreement.java
index e7c4ad0..fd5d58b 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGroupAgreement.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGroupAgreement.java
@@ -47,6 +47,10 @@
       return groupId;
     }
 
+    public ContributorAgreement.Id getContributorAgreementId() {
+      return claId;
+    }
+
     @Override
     public com.google.gwtorm.client.Key<?>[] members() {
       return new com.google.gwtorm.client.Key<?>[] {claId};
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGroupIncludeAudit.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGroupIncludeAudit.java
index e99e480..326dfd3 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGroupIncludeAudit.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGroupIncludeAudit.java
@@ -49,6 +49,14 @@
       return groupId;
     }
 
+    public AccountGroup.Id getIncludedId() {
+      return includeId;
+    }
+
+    public Timestamp getAddedOn() {
+      return addedOn;
+    }
+
     @Override
     public com.google.gwtorm.client.Key<?>[] members() {
       return new com.google.gwtorm.client.Key<?>[] {includeId};
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGroupMemberAudit.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGroupMemberAudit.java
index e2ce939..7547efb 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGroupMemberAudit.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGroupMemberAudit.java
@@ -49,6 +49,14 @@
       return accountId;
     }
 
+    public AccountGroup.Id getGroupId() {
+      return groupId;
+    }
+
+    public Timestamp getAddedOn() {
+      return addedOn;
+    }
+
     @Override
     public com.google.gwtorm.client.Key<?>[] members() {
       return new com.google.gwtorm.client.Key<?>[] {groupId};
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGroupNameAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGroupNameAccess.java
index 8a2fb6b..12db5ce 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGroupNameAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGroupNameAccess.java
@@ -27,7 +27,7 @@
   AccountGroupName get(AccountGroup.NameKey name) throws OrmException;
 
   @Query("ORDER BY name")
-  ResultSet<AccountGroupName> all();
+  ResultSet<AccountGroupName> all() throws OrmException;
 
   @Query("WHERE name.name >= ? AND name.name <= ? ORDER BY name LIMIT ?")
   ResultSet<AccountGroupName> suggestByName(String nameA, String nameB,
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountPatchReviewAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountPatchReviewAccess.java
index 91e8837..e1fa4d4 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountPatchReviewAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountPatchReviewAccess.java
@@ -28,4 +28,6 @@
   @Query("WHERE key.accountId = ? AND key.patchKey.patchSetId = ?")
   ResultSet<AccountPatchReview> byReviewer(Account.Id who, PatchSet.Id ps) throws OrmException;
 
+  @Query("WHERE key.patchKey.patchSetId = ?")
+  ResultSet<AccountPatchReview> byPatchSet(PatchSet.Id ps) throws OrmException;
 }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountProjectWatch.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountProjectWatch.java
index c18ae82..6c62e12 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountProjectWatch.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountProjectWatch.java
@@ -56,6 +56,14 @@
       return accountId;
     }
 
+    public Project.NameKey getProjectName() {
+      return projectName;
+    }
+
+    public Filter getFilter() {
+      return filter;
+    }
+
     @Override
     public com.google.gwtorm.client.Key<?>[] members() {
       return new com.google.gwtorm.client.Key<?>[] {projectName, filter};
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AuthType.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AuthType.java
index 5d69e21..843f41b 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AuthType.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AuthType.java
@@ -73,6 +73,9 @@
    */
   LDAP_BIND,
 
+  /** Login is managed by additional, unspecified code. */
+  CUSTOM_EXTENSION,
+
   /** Development mode to enable becoming anyone you want. */
   DEVELOPMENT_BECOME_ANY_ACCOUNT;
 }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Change.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Change.java
index 753ac7e..fc544fa 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Change.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Change.java
@@ -185,6 +185,8 @@
   protected static final char STATUS_NEW = 'n';
   /** Database constant for {@link Status#SUBMITTED}. */
   protected static final char STATUS_SUBMITTED = 's';
+  /** Database constant for {@link Status#DRAFT}. */
+  protected static final char STATUS_DRAFT = 'd';
   /** Maximum database status constant for an open change. */
   private static final char MAX_OPEN = 'z';
 
@@ -246,6 +248,24 @@
     SUBMITTED(STATUS_SUBMITTED),
 
     /**
+     * Change is a draft change that only consists of draft patchsets.
+     *
+     * <p>
+     * This is a change that is not meant to be submitted or reviewed yet. If
+     * the uploader publishes the change, it becomes a NEW change.
+     * Publishing is a one-way action, a change cannot return to DRAFT status.
+     * Draft changes are only visible to the uploader and those explicitly
+     * added as reviewers.
+     *
+     * <p>
+     * Changes in the DRAFT state can be moved to:
+     * <ul>
+     * <li>{@link #NEW} - when the change is published, it becomes a new change;
+     * </ul>
+     */
+    DRAFT(STATUS_DRAFT),
+
+    /**
      * Change is closed, and submitted to its destination branch.
      *
      * <p>
@@ -458,6 +478,15 @@
     ++nbrPatchSets;
   }
 
+  /**
+   * Reverts to an older PatchSet id within this change.
+   * <p>
+   * <b>Note: This makes the change dirty. Call update() after.</b>
+   */
+  public void removeLastPatchSetId() {
+    --nbrPatchSets;
+  }
+
   public PatchSet.Id currPatchSetId() {
     return new PatchSet.Id(changeId, nbrPatchSets);
   }
@@ -494,4 +523,4 @@
   public void setMergeable(boolean mergeable) {
     this.mergeable = mergeable;
   }
-}
\ No newline at end of file
+}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ChangeMessage.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ChangeMessage.java
index 56c36c8..d8f1dd4 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ChangeMessage.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ChangeMessage.java
@@ -70,18 +70,24 @@
   @Column(id = 4, notNull = false, length = Integer.MAX_VALUE)
   protected String message;
 
+  /** Which patchset (if any) was this message generated from? */
+  @Column(id = 5, notNull = false)
+  protected PatchSet.Id patchset;
+
   protected ChangeMessage() {
   }
 
-  public ChangeMessage(final ChangeMessage.Key k, final Account.Id a) {
-    this(k, a, new Timestamp(System.currentTimeMillis()));
+  public ChangeMessage(final ChangeMessage.Key k, final Account.Id a,
+      final PatchSet.Id psid) {
+    this(k, a, new Timestamp(System.currentTimeMillis()), psid);
   }
 
   public ChangeMessage(final ChangeMessage.Key k, final Account.Id a,
-      final Timestamp wo) {
+      final Timestamp wo, final PatchSet.Id psid) {
     key = k;
     author = a;
     writtenOn = wo;
+    patchset = psid;
   }
 
   public ChangeMessage.Key getKey() {
@@ -111,4 +117,12 @@
   public void setMessage(final String s) {
     message = s;
   }
-}
+
+  public PatchSet.Id getPatchSetId() {
+    return patchset;
+  }
+
+  public void setPatchSetId(PatchSet.Id id) {
+    patchset = id;
+  }
+}
\ No newline at end of file
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ChangeMessageAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ChangeMessageAccess.java
index 377aa59..725c49f 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ChangeMessageAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ChangeMessageAccess.java
@@ -27,4 +27,10 @@
 
   @Query("WHERE key.changeId = ? ORDER BY writtenOn")
   ResultSet<ChangeMessage> byChange(Change.Id id) throws OrmException;
+
+  @Query("WHERE patchset = ?")
+  ResultSet<ChangeMessage> byPatchSet(PatchSet.Id id) throws OrmException;
+
+  @Query
+  ResultSet<ChangeMessage> all() throws OrmException;
 }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/PatchLineCommentAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/PatchLineCommentAccess.java
index 26785a8..29588e1 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/PatchLineCommentAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/PatchLineCommentAccess.java
@@ -28,37 +28,32 @@
   @Query("WHERE key.patchKey.patchSetId.changeId = ?")
   ResultSet<PatchLineComment> byChange(Change.Id id) throws OrmException;
 
-  @Query("WHERE key.patchKey = ? AND status = '"
-      + PatchLineComment.STATUS_PUBLISHED + "' ORDER BY lineNbr,writtenOn")
-  ResultSet<PatchLineComment> published(Patch.Key patch) throws OrmException;
+  @Query("WHERE key.patchKey.patchSetId = ?")
+  ResultSet<PatchLineComment> byPatchSet(PatchSet.Id id) throws OrmException;
 
   @Query("WHERE key.patchKey.patchSetId.changeId = ?"
       + " AND key.patchKey.fileName = ? AND status = '"
       + PatchLineComment.STATUS_PUBLISHED + "' ORDER BY lineNbr,writtenOn")
-  ResultSet<PatchLineComment> published(Change.Id id, String file)
+  ResultSet<PatchLineComment> publishedByChangeFile(Change.Id id, String file)
       throws OrmException;
 
   @Query("WHERE key.patchKey.patchSetId = ? AND status = '"
       + PatchLineComment.STATUS_PUBLISHED + "'")
-  ResultSet<PatchLineComment> published(PatchSet.Id patchset)
+  ResultSet<PatchLineComment> publishedByPatchSet(PatchSet.Id patchset)
       throws OrmException;
 
   @Query("WHERE key.patchKey.patchSetId = ? AND status = '"
       + PatchLineComment.STATUS_DRAFT
       + "' AND author = ? ORDER BY key.patchKey,lineNbr,writtenOn")
-  ResultSet<PatchLineComment> draft(PatchSet.Id patchset, Account.Id author)
-      throws OrmException;
-
-  @Query("WHERE key.patchKey = ? AND status = '"
-      + PatchLineComment.STATUS_DRAFT
-      + "' AND author = ? ORDER BY lineNbr,writtenOn")
-  ResultSet<PatchLineComment> draft(Patch.Key patch, Account.Id author)
+  ResultSet<PatchLineComment> draftByPatchSetAuthor
+      (PatchSet.Id patchset, Account.Id author)
       throws OrmException;
 
   @Query("WHERE key.patchKey.patchSetId.changeId = ?"
       + " AND key.patchKey.fileName = ? AND author = ? AND status = '"
       + PatchLineComment.STATUS_DRAFT + "' ORDER BY lineNbr,writtenOn")
-  ResultSet<PatchLineComment> draft(Change.Id id, String file, Account.Id author)
+  ResultSet<PatchLineComment> draftByChangeFileAuthor
+      (Change.Id id, String file, Account.Id author)
       throws OrmException;
 
   @Query("WHERE status = '" + PatchLineComment.STATUS_DRAFT
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/PatchSet.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/PatchSet.java
index c46a849..1e7643f 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/PatchSet.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/PatchSet.java
@@ -97,6 +97,9 @@
   @Column(id = 4)
   protected Timestamp createdOn;
 
+  @Column(id = 5)
+  protected boolean draft;
+
   protected PatchSet() {
   }
 
@@ -136,6 +139,14 @@
     createdOn = ts;
   }
 
+  public boolean isDraft() {
+    return draft;
+  }
+
+  public void setDraft(boolean draftStatus) {
+    draft = draftStatus;
+  }
+
   public String getRefName() {
     final StringBuilder r = new StringBuilder();
     r.append(REFS_CHANGES);
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/PatchSetAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/PatchSetAccess.java
index fa594f7..c9d4b4f 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/PatchSetAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/PatchSetAccess.java
@@ -27,10 +27,6 @@
   @Query("WHERE id.changeId = ? ORDER BY id.patchSetId")
   ResultSet<PatchSet> byChange(Change.Id id) throws OrmException;
 
-  @Query("WHERE id.changeId = ? AND revision = ?")
-  ResultSet<PatchSet> byChangeRevision(Change.Id id, RevId rev)
-      throws OrmException;
-
   @Query("WHERE revision = ? LIMIT 2")
   ResultSet<PatchSet> byRevision(RevId rev) throws OrmException;
 
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/PatchSetAncestorAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/PatchSetAncestorAccess.java
index eeea372..838af5d 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/PatchSetAncestorAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/PatchSetAncestorAccess.java
@@ -28,6 +28,9 @@
   @Query("WHERE key.patchSetId = ? ORDER BY key.position")
   ResultSet<PatchSetAncestor> ancestorsOf(PatchSet.Id id) throws OrmException;
 
+  @Query("WHERE key.patchSetId = ?")
+  ResultSet<PatchSetAncestor> byPatchSet(PatchSet.Id id) throws OrmException;
+
   @Query("WHERE ancestorRevision = ?")
   ResultSet<PatchSetAncestor> descendantsOf(RevId revision)
       throws OrmException;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/PatchSetApproval.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/PatchSetApproval.java
index 341d085..a3698f4 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/PatchSetApproval.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/PatchSetApproval.java
@@ -51,6 +51,14 @@
       return patchSetId;
     }
 
+    public Account.Id getAccountId() {
+      return accountId;
+    }
+
+    public ApprovalCategory.Id getCategoryId() {
+      return categoryId;
+    }
+
     @Override
     public com.google.gwtorm.client.Key<?>[] members() {
       return new com.google.gwtorm.client.Key<?>[] {accountId, categoryId};
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Project.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Project.java
index b9a6967..33c6d31 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Project.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Project.java
@@ -21,7 +21,7 @@
 public final class Project {
   /** Project name key */
   public static class NameKey extends
-      StringKey<com.google.gwtorm.client.Key<?>> implements Comparable<NameKey> {
+      StringKey<com.google.gwtorm.client.Key<?>>{
     private static final long serialVersionUID = 1L;
 
     @Column(id = 1)
@@ -45,11 +45,6 @@
     }
 
     @Override
-    public int compareTo(NameKey other) {
-      return get().compareTo(other.get());
-    }
-
-    @Override
     public int hashCode() {
       return get().hashCode();
     }
@@ -80,6 +75,14 @@
     CHERRY_PICK;
   }
 
+  public static enum State {
+    ACTIVE,
+
+    READ_ONLY,
+
+    HIDDEN;
+  }
+
   protected NameKey name;
 
   protected String description;
@@ -90,6 +93,8 @@
 
   protected SubmitType submitType;
 
+  protected State state;
+
   protected NameKey parent;
 
   protected boolean requireChangeID;
@@ -102,6 +107,7 @@
   public Project(Project.NameKey nameKey) {
     name = nameKey;
     submitType = SubmitType.MERGE_IF_NECESSARY;
+    state = State.ACTIVE;
   }
 
   public Project.NameKey getNameKey() {
@@ -160,6 +166,14 @@
     submitType = type;
   }
 
+  public State getState() {
+    return state;
+  }
+
+  public void setState(final State newState) {
+    state = newState;
+  }
+
   public void copySettingsFrom(final Project update) {
     description = update.description;
     useContributorAgreements = update.useContributorAgreements;
@@ -167,6 +181,7 @@
     useContentMerge = update.useContentMerge;
     requireChangeID = update.requireChangeID;
     submitType = update.submitType;
+    state = update.state;
   }
 
   public Project.NameKey getParent() {
@@ -180,4 +195,8 @@
   public void setParentName(String n) {
     parent = n != null ? new NameKey(n) : null;
   }
+
+  public void setParentName(NameKey n) {
+    parent = n;
+  }
 }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ReviewDb.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ReviewDb.java
index b75b91b..cd4414e 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ReviewDb.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ReviewDb.java
@@ -32,95 +32,94 @@
 public interface ReviewDb extends Schema {
   /* If you change anything, update SchemaVersion.C to use a new version. */
 
-  @Relation
+  @Relation(id = 1)
   SchemaVersionAccess schemaVersion();
 
-  @Relation
+  @Relation(id = 2)
   SystemConfigAccess systemConfig();
 
-  @Relation
+  @Relation(id = 3)
   ApprovalCategoryAccess approvalCategories();
 
-  @Relation
+  @Relation(id = 4)
   ApprovalCategoryValueAccess approvalCategoryValues();
 
-  @Relation
+  @Relation(id = 5)
   ContributorAgreementAccess contributorAgreements();
 
-  @Relation
+  @Relation(id = 6)
   AccountAccess accounts();
 
-  @Relation
+  @Relation(id = 7)
   AccountExternalIdAccess accountExternalIds();
 
-  @Relation
+  @Relation(id = 8)
   AccountSshKeyAccess accountSshKeys();
 
-  @Relation
+  @Relation(id = 9)
   AccountAgreementAccess accountAgreements();
 
-  @Relation
+  @Relation(id = 10)
   AccountGroupAccess accountGroups();
 
-  @Relation
+  @Relation(id = 11)
   AccountGroupNameAccess accountGroupNames();
 
-  @Relation
+  @Relation(id = 12)
   AccountGroupMemberAccess accountGroupMembers();
 
-  @Relation
+  @Relation(id = 13)
   AccountGroupMemberAuditAccess accountGroupMembersAudit();
 
-  @Relation
+  @Relation(id = 14)
   AccountGroupIncludeAccess accountGroupIncludes();
 
-  @Relation
+  @Relation(id = 15)
   AccountGroupIncludeAuditAccess accountGroupIncludesAudit();
 
-  @Relation
+  @Relation(id = 16)
   AccountGroupAgreementAccess accountGroupAgreements();
 
-  @Relation
+  @Relation(id = 17)
   AccountDiffPreferenceAccess accountDiffPreferences();
 
-  @Relation
+  @Relation(id = 18)
   StarredChangeAccess starredChanges();
 
-  @Relation
+  @Relation(id = 19)
   AccountProjectWatchAccess accountProjectWatches();
 
-  @Relation
+  @Relation(id = 20)
   AccountPatchReviewAccess accountPatchReviews();
 
-  @Relation
+  @Relation(id = 21)
   ChangeAccess changes();
 
-  @Relation
+  @Relation(id = 22)
   PatchSetApprovalAccess patchSetApprovals();
 
-  @Relation
+  @Relation(id = 23)
   ChangeMessageAccess changeMessages();
 
-  @Relation
+  @Relation(id = 24)
   PatchSetAccess patchSets();
 
-  @Relation
+  @Relation(id = 25)
   PatchSetAncestorAccess patchSetAncestors();
 
-  @Relation
+  @Relation(id = 26)
   PatchLineCommentAccess patchComments();
 
-  @Relation
+  @Relation(id = 27)
   TrackingIdAccess trackingIds();
 
+  @Relation(id = 28)
+  SubmoduleSubscriptionAccess submoduleSubscriptions();
+
   /** Create the next unique id for an {@link Account}. */
   @Sequence(startWith = 1000000)
   int nextAccountId() throws OrmException;
 
-  /** Create the next unique id for a {@link ContributorAgreement}. */
-  @Sequence
-  int nextContributorAgreementId() throws OrmException;
-
   /** Next unique id for a {@link AccountGroup}. */
   @Sequence
   int nextAccountGroupId() throws OrmException;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/StarredChange.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/StarredChange.java
index 7e4359b..426bd11 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/StarredChange.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/StarredChange.java
@@ -43,6 +43,10 @@
       return accountId;
     }
 
+    public Change.Id getChangeId() {
+      return changeId;
+    }
+
     @Override
     public com.google.gwtorm.client.Key<?>[] members() {
       return new com.google.gwtorm.client.Key<?>[] {changeId};
@@ -59,6 +63,10 @@
     key = k;
   }
 
+  public StarredChange.Key getKey() {
+    return key;
+  }
+
   public Account.Id getAccountId() {
     return key.accountId;
   }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/SubmoduleSubscription.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/SubmoduleSubscription.java
new file mode 100644
index 0000000..026348a
--- /dev/null
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/SubmoduleSubscription.java
@@ -0,0 +1,122 @@
+// 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.reviewdb;
+
+import com.google.gwtorm.client.Column;
+import com.google.gwtorm.client.StringKey;
+
+/**
+ * Defining a project/branch subscription to a project/branch project.
+ * <p>
+ * This means a class instance represents a repo/branch subscription to a
+ * project/branch (the subscriber).
+ * <p>
+ * A subscriber operates a submodule in defined path.
+ */
+public final class SubmoduleSubscription {
+  /** Subscription key */
+  public static class Key extends StringKey<Branch.NameKey> {
+    private static final long serialVersionUID = 1L;
+
+    /**
+     * Indicates the super project, aka subscriber: the project owner of the
+     * gitlinks to the submodules.
+     */
+    @Column(id = 1)
+    protected Branch.NameKey superProject;
+
+    @Column(id = 2)
+    protected String submodulePath;
+
+    protected Key() {
+      superProject = new Branch.NameKey();
+    }
+
+    protected Key(Branch.NameKey superProject, String path) {
+      this.superProject = superProject;
+      this.submodulePath = path;
+    }
+
+    @Override
+    public Branch.NameKey getParentKey() {
+      return superProject;
+    }
+
+    @Override
+    public String get() {
+      return submodulePath;
+    }
+
+    @Override
+    protected void set(String newValue) {
+      this.submodulePath = newValue;
+    }
+  }
+
+  @Column(id = 1, name = Column.NONE)
+  protected Key key;
+
+  @Column(id = 2)
+  protected Branch.NameKey submodule;
+
+  protected SubmoduleSubscription() {
+  }
+
+  public SubmoduleSubscription(Branch.NameKey superProject,
+      Branch.NameKey submodule,
+      String path) {
+    this.key = new Key(superProject, path);
+    this.submodule = submodule;
+  }
+
+  public Key getKey() {
+    return key;
+  }
+
+  public Branch.NameKey getSuperProject() {
+    return key.superProject;
+  }
+
+  public String getPath() {
+    return key.get();
+  }
+
+  public Branch.NameKey getSubmodule() {
+    return submodule;
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    if (o instanceof SubmoduleSubscription) {
+      return key.equals(((SubmoduleSubscription) o).key)
+          && submodule.equals(((SubmoduleSubscription) o).submodule);
+    }
+    return false;
+  }
+
+  @Override
+  public int hashCode() {
+    return key.hashCode();
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder sb = new StringBuilder();
+    sb.append(getSuperProject()).append(':').append(getPath());
+    sb.append(" follows ");
+    sb.append(getSubmodule());
+    return sb.toString();
+  }
+}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/SubmoduleSubscriptionAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/SubmoduleSubscriptionAccess.java
new file mode 100644
index 0000000..909bd57
--- /dev/null
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/SubmoduleSubscriptionAccess.java
@@ -0,0 +1,35 @@
+// 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.reviewdb;
+
+import com.google.gwtorm.client.Access;
+import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.client.PrimaryKey;
+import com.google.gwtorm.client.Query;
+import com.google.gwtorm.client.ResultSet;
+
+public interface SubmoduleSubscriptionAccess extends
+    Access<SubmoduleSubscription, SubmoduleSubscription.Key> {
+  @PrimaryKey("key")
+  SubmoduleSubscription get(SubmoduleSubscription.Key key) throws OrmException;
+
+  @Query("WHERE key.superProject = ?")
+  ResultSet<SubmoduleSubscription> bySuperProject(Branch.NameKey superProject)
+      throws OrmException;
+
+  @Query("WHERE submodule = ?")
+  ResultSet<SubmoduleSubscription> bySubmodule(Branch.NameKey submodule)
+      throws OrmException;
+}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/TrackingId.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/TrackingId.java
index d59e492..1548807 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/TrackingId.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/TrackingId.java
@@ -102,6 +102,14 @@
       return changeId;
     }
 
+    public TrackingId.Id getTrackingId() {
+      return trackingId;
+    }
+
+    public TrackingId.System getTrackingSystem() {
+      return trackingSystem;
+    }
+
     @Override
     public com.google.gwtorm.client.Key<?>[] members() {
       return new com.google.gwtorm.client.Key<?>[] {trackingId, trackingSystem};
@@ -123,6 +131,10 @@
     key = new Key(ch, new TrackingId.Id(id), new TrackingId.System(s));
   }
 
+  public TrackingId.Key getKey() {
+    return key;
+  }
+
   public Change.Id getChangeId() {
     return key.changeId;
   }
diff --git a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/index_generic.sql b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/index_generic.sql
index 9784a4d..83f7d15 100644
--- a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/index_generic.sql
+++ b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/index_generic.sql
@@ -170,3 +170,9 @@
 
 CREATE INDEX starred_changes_byChange
 ON starred_changes (change_id);
+
+-- *********************************************************************
+-- SubmoduleSubscriptionAccess
+
+CREATE INDEX submodule_subscription_access_bySubscription
+ON submodule_subscriptions (submodule_project_name, submodule_branch_name);
diff --git a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/index_postgres.sql b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/index_postgres.sql
index db6894d..3afa7ef 100644
--- a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/index_postgres.sql
+++ b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/index_postgres.sql
@@ -252,3 +252,9 @@
 
 CREATE INDEX starred_changes_byChange
 ON starred_changes (change_id);
+
+-- *********************************************************************
+-- SubmoduleSubscriptionAccess
+
+CREATE INDEX submodule_subscription_access_bySubscription
+ON submodule_subscriptions (submodule_project_name, submodule_branch_name);
diff --git a/gerrit-server/.gitignore b/gerrit-server/.gitignore
index 903c6c8..194bedc 100644
--- a/gerrit-server/.gitignore
+++ b/gerrit-server/.gitignore
@@ -2,3 +2,4 @@
 /.classpath
 /.project
 /.settings/org.maven.ide.eclipse.prefs
+/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
diff --git a/gerrit-server/pom.xml b/gerrit-server/pom.xml
index 3302e36..20a7286 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.2-SNAPSHOT</version>
+    <version>2.3-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-server</artifactId>
@@ -54,11 +54,6 @@
     </dependency>
 
     <dependency>
-      <groupId>net.sf.ehcache</groupId>
-      <artifactId>ehcache-core</artifactId>
-    </dependency>
-
-    <dependency>
       <groupId>commons-dbcp</groupId>
       <artifactId>commons-dbcp</artifactId>
     </dependency>
@@ -215,6 +210,18 @@
           </execution>
         </executions>
       </plugin>
+
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-source-plugin</artifactId>
+        <executions>
+          <execution>
+            <goals>
+              <goal>jar</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
     </plugins>
   </build>
 </project>
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 0a7337a..6ecf19b 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
@@ -24,9 +24,11 @@
 import com.google.gerrit.reviewdb.ContributorAgreement;
 import com.google.gerrit.reviewdb.PatchSet;
 import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.events.ApprovalAttribute;
@@ -43,6 +45,8 @@
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.project.ProjectState;
+import com.google.gwtorm.client.OrmException;
+import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -64,14 +68,20 @@
 import java.util.Map.Entry;
 import java.util.concurrent.ConcurrentHashMap;
 
-/**
- * This class implements hooks for certain gerrit events.
- */
+/** Spawns local executables when a hook action occurs. */
 @Singleton
-public class ChangeHookRunner {
+public class ChangeHookRunner implements ChangeHooks {
     /** A logger for this class. */
     private static final Logger log = LoggerFactory.getLogger(ChangeHookRunner.class);
 
+    public static class Module extends AbstractModule {
+      @Override
+      protected void configure() {
+        bind(ChangeHookRunner.class);
+        bind(ChangeHooks.class).to(ChangeHookRunner.class);
+      }
+    }
+
     private static class ChangeListenerHolder {
         final ChangeListener listener;
         final IdentifiedUser user;
@@ -107,6 +117,8 @@
     /** Filename of the cla signed hook. */
     private final File claSignedHook;
 
+    private final String anonymousCowardName;
+
     /** Repository Manager. */
     private final GitRepositoryManager repoManager;
 
@@ -133,11 +145,12 @@
     @Inject
     public ChangeHookRunner(final WorkQueue queue,
       final GitRepositoryManager repoManager,
-      @GerritServerConfig final Config config, final SitePaths sitePath,
-      final ProjectCache projectCache,
-      final AccountCache accountCache,
-      final ApprovalTypes approvalTypes,
+      final @GerritServerConfig Config config,
+      final @AnonymousCowardName String anonymousCowardName,
+      final SitePaths sitePath, final ProjectCache projectCache,
+      final AccountCache accountCache, final ApprovalTypes approvalTypes,
       final EventFactory eventFactory) {
+        this.anonymousCowardName = anonymousCowardName;
         this.repoManager = repoManager;
         this.hookQueue = queue.createQueue(1, "hook");
         this.projectCache = projectCache;
@@ -210,20 +223,15 @@
         }
     }
 
-    /**
-     * Fire the Patchset Created Hook.
-     *
-     * @param change The change itself.
-     * @param patchSet The Patchset that was created.
-     */
-    public void doPatchsetCreatedHook(final Change change, final PatchSet patchSet) {
+    public void doPatchsetCreatedHook(final Change change, final PatchSet patchSet,
+          final ReviewDb db) throws OrmException {
         final PatchSetCreatedEvent event = new PatchSetCreatedEvent();
         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);
+        fireEvent(change, event, db);
 
         final List<String> args = new ArrayList<String>();
         addArg(args, "--change", event.change.id);
@@ -237,16 +245,9 @@
         runHook(openRepository(change), patchsetCreatedHook, args);
     }
 
-    /**
-     * 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 comment The comment given.
-     * @param approvals Map of Approval Categories and Scores
-     */
-    public void doCommentAddedHook(final Change change, final Account account, final PatchSet patchSet, final String comment, final Map<ApprovalCategory.Id, ApprovalCategoryValue.Id> approvals) {
+    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 {
         final CommentAddedEvent event = new CommentAddedEvent();
 
         event.change = eventFactory.asChangeAttribute(change);
@@ -262,7 +263,7 @@
             }
         }
 
-        fireEvent(change, event);
+        fireEvent(change, event, db);
 
         final List<String> args = new ArrayList<String>();
         addArg(args, "--change", event.change.id);
@@ -279,20 +280,14 @@
         runHook(openRepository(change), commentAddedHook, args);
     }
 
-    /**
-     * Fire the Change Merged Hook.
-     *
-     * @param change The change itself.
-     * @param account The gerrit user who commited the change.
-     * @param patchSet The patchset that was merged.
-     */
-    public void doChangeMergedHook(final Change change, final Account account, final PatchSet patchSet) {
+    public void doChangeMergedHook(final Change change, final Account account,
+          final PatchSet patchSet, final ReviewDb db) throws OrmException {
         final ChangeMergedEvent event = new ChangeMergedEvent();
 
         event.change = eventFactory.asChangeAttribute(change);
         event.submitter = eventFactory.asAccountAttribute(account);
         event.patchSet = eventFactory.asPatchSetAttribute(patchSet);
-        fireEvent(change, event);
+        fireEvent(change, event, db);
 
         final List<String> args = new ArrayList<String>();
         addArg(args, "--change", event.change.id);
@@ -305,20 +300,14 @@
         runHook(openRepository(change), changeMergedHook, args);
     }
 
-    /**
-     * Fire the Change Abandoned Hook.
-     *
-     * @param change The change itself.
-     * @param account The gerrit user who abandoned the change.
-     * @param reason Reason for abandoning the change.
-     */
-    public void doChangeAbandonedHook(final Change change, final Account account, final String reason) {
+    public void doChangeAbandonedHook(final Change change, final Account account,
+          final String reason, final ReviewDb db) throws OrmException {
         final ChangeAbandonedEvent event = new ChangeAbandonedEvent();
 
         event.change = eventFactory.asChangeAttribute(change);
         event.abandoner = eventFactory.asAccountAttribute(account);
         event.reason = reason;
-        fireEvent(change, event);
+        fireEvent(change, event, db);
 
         final List<String> args = new ArrayList<String>();
         addArg(args, "--change", event.change.id);
@@ -331,20 +320,14 @@
         runHook(openRepository(change), changeAbandonedHook, args);
     }
 
-    /**
-     * Fire the Change Restored Hook.
-     *
-     * @param change The change itself.
-     * @param account The gerrit user who restored the change.
-     * @param reason Reason for restoring the change.
-     */
-    public void doChangeRestoreHook(final Change change, final Account account, final String reason) {
+    public void doChangeRestoreHook(final Change change, final Account account,
+          final String reason, final ReviewDb db) throws OrmException {
         final ChangeRestoreEvent event = new ChangeRestoreEvent();
 
         event.change = eventFactory.asChangeAttribute(change);
         event.restorer = eventFactory.asAccountAttribute(account);
         event.reason = reason;
-        fireEvent(change, event);
+        fireEvent(change, event, db);
 
         final List<String> args = new ArrayList<String>();
         addArg(args, "--change", event.change.id);
@@ -357,23 +340,10 @@
         runHook(openRepository(change), changeRestoredHook, args);
     }
 
-    /**
-     * Fire the Ref Updated Hook
-     * @param project The project the ref update occured on
-     * @param refUpdate An actual RefUpdate object
-     * @param account The gerrit user who moved the ref
-     */
     public void doRefUpdatedHook(final Branch.NameKey refName, final RefUpdate refUpdate, final Account account) {
       doRefUpdatedHook(refName, refUpdate.getOldObjectId(), refUpdate.getNewObjectId(), account);
     }
 
-    /**
-     * Fire the Ref Updated Hook
-     * @param refName The Branch.NameKey of the ref that was updated
-     * @param oldId The ref's old id
-     * @param newId The ref's new id
-     * @param account The gerrit user who moved the ref
-     */
     public void doRefUpdatedHook(final Branch.NameKey refName, final ObjectId oldId, final ObjectId newId, final Account account) {
       final RefUpdatedEvent event = new RefUpdatedEvent();
 
@@ -406,9 +376,9 @@
       }
     }
 
-    private void fireEvent(final Change change, final ChangeEvent event) {
+    private void fireEvent(final Change change, final ChangeEvent event, final ReviewDb db) throws OrmException {
       for (ChangeListenerHolder holder : listeners.values()) {
-          if (isVisibleTo(change, holder.user)) {
+          if (isVisibleTo(change, holder.user, db)) {
               holder.listener.onChangeEvent(event);
           }
       }
@@ -422,13 +392,13 @@
       }
     }
 
-    private boolean isVisibleTo(Change change, IdentifiedUser user) {
+    private boolean isVisibleTo(Change change, IdentifiedUser user, ReviewDb db) throws OrmException {
         final ProjectState pe = projectCache.get(change.getProject());
         if (pe == null) {
           return false;
         }
         final ProjectControl pc = pe.controlFor(user);
-        return pc.controlFor(change).isVisible();
+        return pc.controlFor(change).isVisible(db);
     }
 
     private boolean isVisibleTo(Branch.NameKey branchName, IdentifiedUser user) {
@@ -465,14 +435,14 @@
      */
     private String getDisplayName(final Account account) {
         if (account != null) {
-            String result = (account.getFullName() == null) ? "Anonymous Coward" : account.getFullName();
+            String result = (account.getFullName() == null) ? anonymousCowardName : account.getFullName();
             if (account.getPreferredEmail() != null) {
                 result += " (" + account.getPreferredEmail() + ")";
             }
             return result;
         }
 
-        return "Anonymous Coward";
+        return anonymousCowardName;
     }
 
   /**
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
new file mode 100644
index 0000000..3447c1f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHooks.java
@@ -0,0 +1,119 @@
+// 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;
+
+import com.google.gerrit.reviewdb.Account;
+import com.google.gerrit.reviewdb.ApprovalCategory;
+import com.google.gerrit.reviewdb.ApprovalCategoryValue;
+import com.google.gerrit.reviewdb.Branch;
+import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.reviewdb.ContributorAgreement;
+import com.google.gerrit.reviewdb.PatchSet;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gwtorm.client.OrmException;
+
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.RefUpdate;
+
+import java.util.Map;
+
+/** Invokes hooks on server actions. */
+public interface ChangeHooks {
+  public void addChangeListener(ChangeListener listener, IdentifiedUser user);
+
+  public void removeChangeListener(ChangeListener listener);
+
+  /**
+   * Fire the Patchset Created Hook.
+   *
+   * @param change The change itself.
+   * @param patchSet The Patchset that was created.
+   * @throws OrmException
+   */
+  public void doPatchsetCreatedHook(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 comment The comment given.
+   * @param approvals Map of Approval Categories and Scores
+   * @throws OrmException
+   */
+  public void doCommentAddedHook(Change change, Account account,
+      PatchSet patchSet, String comment,
+      Map<ApprovalCategory.Id, ApprovalCategoryValue.Id> approvals, ReviewDb db)
+      throws OrmException;
+
+  /**
+   * Fire the Change Merged Hook.
+   *
+   * @param change The change itself.
+   * @param account The gerrit user who commited the change.
+   * @param patchSet The patchset that was merged.
+   * @throws OrmException
+   */
+  public void doChangeMergedHook(Change change, Account account,
+      PatchSet patchSet, ReviewDb db) throws OrmException;
+
+  /**
+   * Fire the Change Abandoned Hook.
+   *
+   * @param change The change itself.
+   * @param account The gerrit user who abandoned the change.
+   * @param reason Reason for abandoning the change.
+   * @throws OrmException
+   */
+  public void doChangeAbandonedHook(Change change, Account account,
+      String reason, ReviewDb db) throws OrmException;
+
+  /**
+   * Fire the Change Restored Hook.
+   *
+   * @param change The change itself.
+   * @param account The gerrit user who restored the change.
+   * @param reason Reason for restoring the change.
+   * @throws OrmException
+   */
+  public void doChangeRestoreHook(Change change, Account account,
+      String reason, ReviewDb db) throws OrmException;
+
+  /**
+   * Fire the Ref Updated Hook
+   *
+   * @param refName The updated project and branch.
+   * @param refUpdate An actual RefUpdate object
+   * @param account The gerrit user who moved the ref
+   */
+  public void doRefUpdatedHook(Branch.NameKey refName, RefUpdate refUpdate,
+      Account account);
+
+  /**
+   * Fire the Ref Updated Hook
+   *
+   * @param refName The Branch.NameKey of the ref that was updated
+   * @param oldId The ref's old id
+   * @param newId The ref's new id
+   * @param account The gerrit user who moved the ref
+   */
+  public void doRefUpdatedHook(Branch.NameKey refName, ObjectId oldId,
+      ObjectId newId, Account account);
+
+  public void doClaSignupHook(Account account, ContributorAgreement cla);
+}
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
new file mode 100644
index 0000000..7f61f62
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java
@@ -0,0 +1,81 @@
+// 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;
+
+import com.google.gerrit.reviewdb.Account;
+import com.google.gerrit.reviewdb.ApprovalCategory;
+import com.google.gerrit.reviewdb.ApprovalCategoryValue;
+import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.reviewdb.ContributorAgreement;
+import com.google.gerrit.reviewdb.PatchSet;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.Branch.NameKey;
+import com.google.gerrit.server.IdentifiedUser;
+
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.RefUpdate;
+
+import java.util.Map;
+
+/** Does not invoke hooks. */
+public final class DisabledChangeHooks implements ChangeHooks {
+  @Override
+  public void addChangeListener(ChangeListener listener, IdentifiedUser user) {
+  }
+
+  @Override
+  public void doChangeAbandonedHook(Change change, Account account,
+      String reason, ReviewDb db) {
+  }
+
+  @Override
+  public void doChangeMergedHook(Change change, Account account,
+      PatchSet patchSet, ReviewDb db) {
+  }
+
+  @Override
+  public void doChangeRestoreHook(Change change, Account account,
+      String reason, ReviewDb db) {
+  }
+
+  @Override
+  public void doClaSignupHook(Account account, ContributorAgreement cla) {
+  }
+
+  @Override
+  public void doCommentAddedHook(Change change, Account account,
+      PatchSet patchSet, String comment,
+      Map<ApprovalCategory.Id, ApprovalCategoryValue.Id> approvals, ReviewDb db) {
+  }
+
+  @Override
+  public void doPatchsetCreatedHook(Change change, PatchSet patchSet,
+      ReviewDb db) {
+  }
+
+  @Override
+  public void doRefUpdatedHook(NameKey refName, RefUpdate refUpdate,
+      Account account) {
+  }
+
+  @Override
+  public void doRefUpdatedHook(NameKey refName, ObjectId oldId, ObjectId newId,
+      Account account) {
+  }
+
+  @Override
+  public void removeChangeListener(ChangeListener listener) {
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/rules/PrologEnvironment.java b/gerrit-server/src/main/java/com/google/gerrit/rules/PrologEnvironment.java
index 5d86954..310b401 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/rules/PrologEnvironment.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/rules/PrologEnvironment.java
@@ -82,6 +82,7 @@
    * @param sv unique key.
    * @return the value; null if not stored.
    */
+  @SuppressWarnings("unchecked")
   public <T> T get(StoredValue<T> sv) {
     return (T) storedValues.get(sv);
   }
@@ -93,6 +94,7 @@
    * @param sv unique key.
    * @param obj the value to store under {@code sv}.
    */
+  @SuppressWarnings("unchecked")
   public <T> void set(StoredValue<T> sv, T obj) {
     storedValues.put((StoredValue<Object>) sv, obj);
   }
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 690d5ca..8d9633f 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
@@ -90,6 +90,7 @@
     }
   }
 
+  private final boolean enableProjectRules;
   private final File cacheDir;
   private final File rulesDir;
   private final GitRepositoryManager gitMgr;
@@ -99,6 +100,7 @@
   @Inject
   protected RulesCache(@GerritServerConfig Config config, SitePaths site,
       GitRepositoryManager gm) {
+    enableProjectRules = config.getBoolean("rules", null, "enable", true);
     cacheDir = site.resolve(config.getString("cache", null, "directory"));
     rulesDir = cacheDir != null ? new File(cacheDir, "rules") : null;
     gitMgr = gm;
@@ -117,7 +119,7 @@
       Project.NameKey project,
       ObjectId rulesId)
       throws CompileException {
-    if (project == null || rulesId == null) {
+    if (!enableProjectRules || project == null || rulesId == null) {
       return defaultMachine;
     }
 
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 cdcbb41..de85742 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
@@ -51,7 +51,7 @@
       PatchSetInfoFactory patchInfoFactory =
           env.getInjector().getInstance(PatchSetInfoFactory.class);
       try {
-        return patchInfoFactory.get(patchSetId);
+        return patchInfoFactory.get(REVIEW_DB.get(engine), patchSetId);
       } catch (PatchSetInfoNotAvailableException e) {
         throw new SystemException(e.getMessage());
       }
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
new file mode 100644
index 0000000..6997d47
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
@@ -0,0 +1,36 @@
+// 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.Change;
+import com.google.gerrit.reviewdb.PatchSetApproval;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gwtorm.client.OrmException;
+
+import java.util.List;
+
+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)
+      throws OrmException {
+    final List<PatchSetApproval> approvals =
+        db.patchSetApprovals().byChange(change.getId()).toList();
+    for (PatchSetApproval a : approvals) {
+      a.cache(change);
+    }
+    db.patchSetApprovals().update(approvals);
+  }
+}
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 36ac938..55de507 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,7 @@
 
 package com.google.gerrit.server;
 
-import static com.google.gerrit.reviewdb.ApprovalCategory.SUBMIT;
-
-import com.google.gerrit.common.ChangeHookRunner;
+import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.reviewdb.ChangeMessage;
 import com.google.gerrit.reviewdb.PatchSet;
@@ -29,18 +27,14 @@
 import com.google.gerrit.server.config.TrackingFooters;
 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.git.ReplicationQueue;
-import com.google.gerrit.server.mail.AbandonedSender;
 import com.google.gerrit.server.mail.EmailException;
 import com.google.gerrit.server.mail.ReplyToChangeSender;
-import com.google.gerrit.server.mail.RestoredSender;
 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.InvalidChangeOperationException;
 import com.google.gerrit.server.project.NoSuchChangeException;
-import com.google.gwtorm.client.AtomicUpdate;
 import com.google.gwtorm.client.OrmConcurrencyException;
 import com.google.gwtorm.client.OrmException;
 
@@ -52,6 +46,7 @@
 import org.eclipse.jgit.lib.ObjectInserter;
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.RefUpdate.Result;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.FooterLine;
 import org.eclipse.jgit.revwalk.RevCommit;
@@ -60,7 +55,6 @@
 import org.eclipse.jgit.util.NB;
 
 import java.io.IOException;
-import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
@@ -167,99 +161,10 @@
     opFactory.create(change.getDest()).verifyMergeability(change);
   }
 
-  public static void submit(final PatchSet.Id patchSetId,
-      final IdentifiedUser user, final ReviewDb db,
-      final MergeOp.Factory opFactory, final MergeQueue merger)
-      throws OrmException {
-    final Change.Id changeId = patchSetId.getParentKey();
-    final PatchSetApproval approval = createSubmitApproval(patchSetId, user, db);
-
-    db.patchSetApprovals().upsert(Collections.singleton(approval));
-
-    final Change updatedChange = db.changes().atomicUpdate(changeId,
-        new AtomicUpdate<Change>() {
-      @Override
-      public Change update(Change change) {
-        if (change.getStatus() == Change.Status.NEW) {
-          change.setStatus(Change.Status.SUBMITTED);
-          ChangeUtil.updated(change);
-        }
-        return change;
-      }
-    });
-
-    if (updatedChange.getStatus() == Change.Status.SUBMITTED) {
-      merger.merge(opFactory, updatedChange.getDest());
-    }
-  }
-
-  public static PatchSetApproval createSubmitApproval(
-      final PatchSet.Id patchSetId, final IdentifiedUser user, final ReviewDb db
-      ) throws OrmException {
-    final List<PatchSetApproval> allApprovals =
-        new ArrayList<PatchSetApproval>(db.patchSetApprovals().byPatchSet(
-            patchSetId).toList());
-
-    final PatchSetApproval.Key akey =
-        new PatchSetApproval.Key(patchSetId, user.getAccountId(), SUBMIT);
-
-    for (final PatchSetApproval approval : allApprovals) {
-      if (akey.equals(approval.getKey())) {
-        approval.setValue((short) 1);
-        approval.setGranted();
-        return approval;
-      }
-    }
-    return new PatchSetApproval(akey, (short) 1);
-  }
-
-  public static void abandon(final PatchSet.Id patchSetId,
-      final IdentifiedUser user, final String message, final ReviewDb db,
-      final AbandonedSender.Factory senderFactory,
-      final ChangeHookRunner hooks) throws NoSuchChangeException,
-      InvalidChangeOperationException, EmailException, OrmException {
-    final Change.Id changeId = patchSetId.getParentKey();
-    final PatchSet patch = db.patchSets().get(patchSetId);
-    if (patch == null) {
-      throw new NoSuchChangeException(changeId);
-    }
-
-    final ChangeMessage cmsg =
-        new ChangeMessage(new ChangeMessage.Key(changeId, ChangeUtil
-            .messageUUID(db)), user.getAccountId());
-    final StringBuilder msgBuf =
-        new StringBuilder("Patch Set " + patchSetId.get() + ": Abandoned");
-    if (message != null && message.length() > 0) {
-      msgBuf.append("\n\n");
-      msgBuf.append(message);
-    }
-    cmsg.setMessage(msgBuf.toString());
-
-    final Change updatedChange = db.changes().atomicUpdate(changeId,
-        new AtomicUpdate<Change>() {
-      @Override
-      public Change update(Change change) {
-        if (change.getStatus().isOpen()
-            && change.currentPatchSetId().equals(patchSetId)) {
-          change.setStatus(Change.Status.ABANDONED);
-          ChangeUtil.updated(change);
-          return change;
-        } else {
-          return null;
-        }
-      }
-    });
-
-    updatedChange(db, user, updatedChange, cmsg, senderFactory,
-        "Change is no longer open or patchset is not latest");
-
-    hooks.doChangeAbandonedHook(updatedChange, user.getAccount(), message);
-  }
-
-  public static void revert(final PatchSet.Id patchSetId,
+  public static Change.Id revert(final PatchSet.Id patchSetId,
       final IdentifiedUser user, final String message, final ReviewDb db,
       final RevertedSender.Factory revertedSenderFactory,
-      final ChangeHookRunner hooks, GitRepositoryManager gitManager,
+      final ChangeHooks hooks, GitRepositoryManager gitManager,
       final PatchSetInfoFactory patchSetInfoFactory,
       final ReplicationQueue replication, PersonIdent myIdent)
       throws NoSuchChangeException, EmailException, OrmException,
@@ -337,7 +242,7 @@
 
       final ChangeMessage cmsg =
           new ChangeMessage(new ChangeMessage.Key(changeId,
-              ChangeUtil.messageUUID(db)), user.getAccountId());
+              ChangeUtil.messageUUID(db)), user.getAccountId(), patchSetId);
       final StringBuilder msgBuf =
           new StringBuilder("Patch Set " + patchSetId.get() + ": Reverted");
       msgBuf.append("\n\n");
@@ -351,72 +256,86 @@
       cm.setChangeMessage(cmsg);
       cm.send();
 
-      hooks.doPatchsetCreatedHook(change, ps);
+      hooks.doPatchsetCreatedHook(change, ps, db);
+
+      return change.getId();
     } finally {
       revWalk.release();
       git.close();
     }
   }
 
-  public static void restore(final PatchSet.Id patchSetId,
-      final IdentifiedUser user, final String message, final ReviewDb db,
-      final RestoredSender.Factory senderFactory,
-      final ChangeHookRunner hooks) throws NoSuchChangeException,
-      InvalidChangeOperationException, EmailException, OrmException {
+  public static void deleteDraftChange(final PatchSet.Id patchSetId,
+      GitRepositoryManager gitManager,
+      final ReplicationQueue replication, final ReviewDb db)
+      throws NoSuchChangeException, OrmException, IOException {
     final Change.Id changeId = patchSetId.getParentKey();
-    final PatchSet patch = db.patchSets().get(patchSetId);
-    if (patch == null) {
+    final Change change = db.changes().get(changeId);
+    if (change == null || change.getStatus() != Change.Status.DRAFT) {
       throw new NoSuchChangeException(changeId);
     }
 
-    final ChangeMessage cmsg =
-        new ChangeMessage(new ChangeMessage.Key(changeId, ChangeUtil
-            .messageUUID(db)), user.getAccountId());
-    final StringBuilder msgBuf =
-        new StringBuilder("Patch Set " + patchSetId.get() + ": Restored");
-    if (message != null && message.length() > 0) {
-      msgBuf.append("\n\n");
-      msgBuf.append(message);
+    for (PatchSet ps : db.patchSets().byChange(changeId)) {
+      // These should all be draft patch sets.
+      deleteOnlyDraftPatchSet(ps, change, gitManager, replication, db);
     }
-    cmsg.setMessage(msgBuf.toString());
 
-    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;
-        }
-      }
-    });
-
-    updatedChange(db, user, updatedChange, cmsg, senderFactory,
-       "Change is not abandoned or patchset is not latest");
-
-    hooks.doChangeRestoreHook(updatedChange, user.getAccount(), message);
+    db.changeMessages().delete(db.changeMessages().byChange(changeId));
+    db.starredChanges().delete(db.starredChanges().byChange(changeId));
+    db.trackingIds().delete(db.trackingIds().byChange(changeId));
+    db.changes().delete(Collections.singleton(change));
   }
 
-  private static void updatedChange(final ReviewDb db, final IdentifiedUser user,
-      final Change change, final ChangeMessage cmsg,
-      ReplyToChangeSender.Factory senderFactory, final String err)
-      throws NoSuchChangeException, InvalidChangeOperationException,
-      EmailException, OrmException {
+  public static void deleteOnlyDraftPatchSet(final PatchSet patch,
+      final Change change, GitRepositoryManager gitManager,
+      final ReplicationQueue replication, final ReviewDb db)
+      throws NoSuchChangeException, OrmException, IOException {
+    final PatchSet.Id patchSetId = patch.getId();
+    if (patch == null || !patch.isDraft()) {
+      throw new NoSuchChangeException(patchSetId.getParentKey());
+    }
+
+    Repository repo = gitManager.openRepository(change.getProject());
+    try {
+      RefUpdate update = repo.updateRef(patch.getRefName());
+      update.setForceUpdate(true);
+      update.disableRefLog();
+      switch (update.delete()) {
+        case NEW:
+        case FAST_FORWARD:
+        case FORCED:
+        case NO_CHANGE:
+          // Successful deletion.
+          break;
+        default:
+          throw new IOException("Failed to delete ref " + patch.getRefName() +
+              " in " + repo.getDirectory() + ": " + update.getResult());
+      }
+      replication.scheduleUpdate(change.getProject(), update.getName());
+    } finally {
+      repo.close();
+    }
+
+    db.accountPatchReviews().delete(db.accountPatchReviews().byPatchSet(patchSetId));
+    db.changeMessages().delete(db.changeMessages().byPatchSet(patchSetId));
+    db.patchComments().delete(db.patchComments().byPatchSet(patchSetId));
+    db.patchSetApprovals().delete(db.patchSetApprovals().byPatchSet(patchSetId));
+    db.patchSetAncestors().delete(db.patchSetAncestors().byPatchSet(patchSetId));
+
+    db.patchSets().delete(Collections.singleton(patch));
+  }
+
+  public 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);
     }
     db.changeMessages().insert(Collections.singleton(cmsg));
 
-    final List<PatchSetApproval> approvals =
-        db.patchSetApprovals().byChange(change.getId()).toList();
-    for (PatchSetApproval a : approvals) {
-      a.cache(change);
-    }
-    db.patchSetApprovals().update(approvals);
+    ApprovalsUtil.syncChangeStatus(db, change);
 
     // Email the reviewers
     final ReplyToChangeSender cm = senderFactory.create(change);
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 722e3d7..6828e18 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
@@ -26,6 +26,7 @@
 import com.google.gerrit.server.account.CapabilityControl;
 import com.google.gerrit.server.account.GroupIncludeCache;
 import com.google.gerrit.server.account.Realm;
+import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gwtorm.client.OrmException;
@@ -66,6 +67,7 @@
   public static class GenericFactory {
     private final CapabilityControl.Factory capabilityControlFactory;
     private final AuthConfig authConfig;
+    private final String anonymousCowardName;
     private final Provider<String> canonicalUrl;
     private final Realm realm;
     private final AccountCache accountCache;
@@ -75,11 +77,13 @@
     GenericFactory(
         CapabilityControl.Factory capabilityControlFactory,
         final AuthConfig authConfig,
+        final @AnonymousCowardName String anonymousCowardName,
         final @CanonicalWebUrl Provider<String> canonicalUrl,
         final Realm realm, final AccountCache accountCache,
         final GroupIncludeCache groupIncludeCache) {
       this.capabilityControlFactory = capabilityControlFactory;
       this.authConfig = authConfig;
+      this.anonymousCowardName = anonymousCowardName;
       this.canonicalUrl = canonicalUrl;
       this.realm = realm;
       this.accountCache = accountCache;
@@ -92,15 +96,15 @@
 
     public IdentifiedUser create(Provider<ReviewDb> db, Account.Id id) {
       return new IdentifiedUser(capabilityControlFactory, AccessPath.UNKNOWN,
-          authConfig, canonicalUrl, realm, accountCache, groupIncludeCache,
-          null, db, id);
+          authConfig, anonymousCowardName, canonicalUrl, realm, accountCache,
+          groupIncludeCache, null, db, id);
     }
 
     public IdentifiedUser create(AccessPath accessPath,
         Provider<SocketAddress> remotePeerProvider, Account.Id id) {
       return new IdentifiedUser(capabilityControlFactory, accessPath,
-          authConfig, canonicalUrl, realm, accountCache, groupIncludeCache,
-          remotePeerProvider, null, id);
+          authConfig, anonymousCowardName, canonicalUrl, realm, accountCache,
+          groupIncludeCache, remotePeerProvider, null, id);
     }
   }
 
@@ -114,6 +118,7 @@
   public static class RequestFactory {
     private final CapabilityControl.Factory capabilityControlFactory;
     private final AuthConfig authConfig;
+    private final String anonymousCowardName;
     private final Provider<String> canonicalUrl;
     private final Realm realm;
     private final AccountCache accountCache;
@@ -126,6 +131,7 @@
     RequestFactory(
         CapabilityControl.Factory capabilityControlFactory,
         final AuthConfig authConfig,
+        final @AnonymousCowardName String anonymousCowardName,
         final @CanonicalWebUrl Provider<String> canonicalUrl,
         final Realm realm, final AccountCache accountCache,
         final GroupIncludeCache groupIncludeCache,
@@ -134,6 +140,7 @@
         final Provider<ReviewDb> dbProvider) {
       this.capabilityControlFactory = capabilityControlFactory;
       this.authConfig = authConfig;
+      this.anonymousCowardName = anonymousCowardName;
       this.canonicalUrl = canonicalUrl;
       this.realm = realm;
       this.accountCache = accountCache;
@@ -146,8 +153,8 @@
     public IdentifiedUser create(final AccessPath accessPath,
         final Account.Id id) {
       return new IdentifiedUser(capabilityControlFactory, accessPath,
-          authConfig, canonicalUrl, realm, accountCache, groupIncludeCache,
-          remotePeerProvider, dbProvider, id);
+          authConfig, anonymousCowardName, canonicalUrl, realm, accountCache,
+          groupIncludeCache, remotePeerProvider, dbProvider, id);
     }
   }
 
@@ -181,6 +188,7 @@
   private final AccountCache accountCache;
   private final GroupIncludeCache groupIncludeCache;
   private final AuthConfig authConfig;
+  private final String anonymousCowardName;
 
   @Nullable
   private final Provider<SocketAddress> remotePeerProvider;
@@ -199,7 +207,9 @@
   private IdentifiedUser(
       CapabilityControl.Factory capabilityControlFactory,
       final AccessPath accessPath,
-      final AuthConfig authConfig, final Provider<String> canonicalUrl,
+      final AuthConfig authConfig,
+      final String anonymousCowardName,
+      final Provider<String> canonicalUrl,
       final Realm realm, final AccountCache accountCache,
       final GroupIncludeCache groupIncludeCache,
       @Nullable final Provider<SocketAddress> remotePeerProvider,
@@ -210,6 +220,7 @@
     this.accountCache = accountCache;
     this.groupIncludeCache = groupIncludeCache;
     this.authConfig = authConfig;
+    this.anonymousCowardName = anonymousCowardName;
     this.remotePeerProvider = remotePeerProvider;
     this.dbProvider = dbProvider;
     this.accountId = id;
@@ -343,7 +354,7 @@
       name = ua.getPreferredEmail();
     }
     if (name == null || name.isEmpty()) {
-      name = "Anonymous Coward";
+      name = anonymousCowardName;
     }
 
     String user = getUserName();
@@ -403,7 +414,7 @@
       if (0 < at) {
         name = email.substring(0, at);
       } else {
-        name = "Anonymous Coward";
+        name = anonymousCowardName;
       }
     }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/RequestCleanup.java b/gerrit-server/src/main/java/com/google/gerrit/server/RequestCleanup.java
index 23c0a6f..d836646 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/RequestCleanup.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/RequestCleanup.java
@@ -33,16 +33,21 @@
       LoggerFactory.getLogger(RequestCleanup.class);
 
   private final List<Runnable> cleanup = new LinkedList<Runnable>();
+  private boolean ran;
 
   /** Register a task to be completed after the request ends. */
   public void add(final Runnable task) {
     synchronized (cleanup) {
+      if (ran) {
+        throw new IllegalStateException("Request has already been cleaned up");
+      }
       cleanup.add(task);
     }
   }
 
   public void run() {
     synchronized (cleanup) {
+      ran = true;
       for (final Iterator<Runnable> i = cleanup.iterator(); i.hasNext();) {
         try {
           i.next().run();
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 9af8171..3f4d0d1 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
@@ -382,11 +382,13 @@
    * @throws AccountException the identity belongs to a different account, or it
    *         cannot be linked at this time.
    */
-  public AuthResult link(final Account.Id to, final AuthRequest who)
+  public AuthResult link(final Account.Id to, AuthRequest who)
       throws AccountException {
     try {
       final ReviewDb db = schema.open();
       try {
+        who = realm.link(db, to, who);
+
         final AccountExternalId.Key key = id(who);
         AccountExternalId extId = db.accountExternalIds().get(key);
         if (extId != null) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthRequest.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthRequest.java
index 2a54029..034b176 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthRequest.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthRequest.java
@@ -52,7 +52,7 @@
     return r;
   }
 
-  private final String externalId;
+  private String externalId;
   private String password;
   private String displayName;
   private String emailAddress;
@@ -78,6 +78,14 @@
     return null;
   }
 
+  public void setLocalUser(final String localUser) {
+    if (isScheme(SCHEME_GERRIT)) {
+      final AccountExternalId.Key key =
+          new AccountExternalId.Key(SCHEME_GERRIT, localUser);
+      externalId = key.get();
+    }
+  }
+
   public String getPassword() {
     return password;
   }
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 77afb13..c03aee9 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
@@ -16,12 +16,13 @@
 
 import com.google.gerrit.reviewdb.Account;
 import com.google.gerrit.reviewdb.AccountGroup;
+import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.inject.Inject;
 
 import java.util.Collections;
 import java.util.Set;
 
-public final class DefaultRealm implements Realm {
+public class DefaultRealm implements Realm {
   private final EmailExpander emailExpander;
   private final AccountByEmailCache byEmail;
 
@@ -47,6 +48,11 @@
   }
 
   @Override
+  public AuthRequest link(ReviewDb db, Account.Id to, AuthRequest who) {
+    return who;
+  }
+
+  @Override
   public void onCreateAccount(final AuthRequest who, final Account account) {
   }
 
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 6dce197..788e08e 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
@@ -18,17 +18,32 @@
 
 import java.util.Collection;
 
+import javax.annotation.Nullable;
+
 /** Tracks group objects in memory for efficient access. */
 public interface GroupCache {
   public AccountGroup get(AccountGroup.Id groupId);
 
   public AccountGroup get(AccountGroup.NameKey name);
 
+  /**
+   * Lookup a group definition by its UUID. The returned definition may be null
+   * if the group has been deleted and the UUID reference is stale, or was
+   * copied from another server.
+   */
+  @Nullable
   public AccountGroup get(AccountGroup.UUID uuid);
 
   public Collection<AccountGroup> get(AccountGroup.ExternalNameKey externalName);
 
+  /** @return sorted iteration of groups. */
+  public abstract Iterable<AccountGroup> all();
+
+  /** Notify the cache that a new group was constructed. */
+  public void onCreateGroup(AccountGroup.NameKey newGroupName);
+
   public void evict(AccountGroup group);
 
-  public void evictAfterRename(AccountGroup.NameKey oldName);
+  public void evictAfterRename(final AccountGroup.NameKey oldName,
+      final AccountGroup.NameKey newName);
 }
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 4d6dbc1..0e11d6b 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
@@ -27,8 +27,14 @@
 import com.google.inject.TypeLiteral;
 import com.google.inject.name.Named;
 
+import java.util.ArrayList;
 import java.util.Collection;
+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;
 
 /** Tracks group objects in memory for efficient access. */
 @Singleton
@@ -37,6 +43,7 @@
   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() {
@@ -59,6 +66,10 @@
         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);
+
         bind(GroupCacheImpl.class);
         bind(GroupCache.class).to(GroupCacheImpl.class);
       }
@@ -69,17 +80,22 @@
   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;
 
   @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(BYEXT_NAME) Cache<AccountGroup.ExternalNameKey, Collection<AccountGroup>> byExternalName,
+      @Named(BYNAME_LIST) final Cache<ListKey, SortedSet<AccountGroup.NameKey>> list) {
     this.byId = byId;
     this.byName = byName;
     this.byUUID = byUUID;
     this.byExternalName = byExternalName;
+    this.list = list;
+    this.listLock = new ReentrantLock(true /* fair */);
   }
 
   public AccountGroup get(final AccountGroup.Id groupId) {
@@ -93,8 +109,10 @@
     byExternalName.remove(group.getExternalNameKey());
   }
 
-  public void evictAfterRename(final AccountGroup.NameKey oldName) {
+  public void evictAfterRename(final AccountGroup.NameKey oldName,
+      final AccountGroup.NameKey newName) {
     byName.remove(oldName);
+    updateGroupList(oldName, newName);
   }
 
   public AccountGroup get(final AccountGroup.NameKey name) {
@@ -110,6 +128,41 @@
     return byExternalName.get(externalName);
   }
 
+  @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);
+      }
+    }
+    return Collections.unmodifiableList(groups);
+  }
+
+  @Override
+  public void onCreateGroup(final AccountGroup.NameKey newGroupName) {
+    updateGroupList(null, newGroupName);
+  }
+
+  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();
+    }
+  }
+
   static class ByIdLoader extends EntryCreator<AccountGroup.Id, AccountGroup> {
     private final SchemaFactory<ReviewDb> schema;
 
@@ -216,4 +269,38 @@
       }
     }
   }
+
+  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/GroupComparator.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupComparator.java
new file mode 100644
index 0000000..9348a0c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupComparator.java
@@ -0,0 +1,27 @@
+// 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.account;
+
+import com.google.gerrit.reviewdb.AccountGroup;
+
+import java.util.Comparator;
+
+public class GroupComparator implements Comparator<AccountGroup> {
+
+  @Override
+  public int compare(final AccountGroup group1, final AccountGroup group2) {
+    return group1.getName().compareTo(group2.getName());
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembersFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembers.java
similarity index 62%
rename from gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembersFactory.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembers.java
index aae3a909..bf5329a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembersFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembers.java
@@ -26,60 +26,60 @@
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gwtorm.client.OrmException;
 import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
 
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.Set;
-import java.util.concurrent.Callable;
 
-public class GroupMembersFactory implements Callable<Set<Account>> {
+public class GroupMembers {
   public interface Factory {
-    GroupMembersFactory create(Project.NameKey project,
-        AccountGroup.UUID groupUUID);
+    GroupMembers create();
   }
 
-  private GroupCache groupCache;
+  private final GroupCache groupCache;
   private final GroupDetailFactory.Factory groupDetailFactory;
   private final AccountCache accountCache;
   private final ProjectControl.GenericFactory projectControl;
   private final IdentifiedUser currentUser;
 
-  private final Project.NameKey project;
-  private final AccountGroup.UUID groupUUID;
-
-
   @Inject
-  GroupMembersFactory(final GroupCache groupCache,
+  GroupMembers(final GroupCache groupCache,
       final GroupDetailFactory.Factory groupDetailFactory,
       final AccountCache accountCache,
       final ProjectControl.GenericFactory projectControl,
-      final IdentifiedUser currentUser,
-      @Assisted final Project.NameKey project,
-      @Assisted final AccountGroup.UUID groupUUID) {
+      final IdentifiedUser currentUser) {
     this.groupCache = groupCache;
     this.groupDetailFactory = groupDetailFactory;
     this.accountCache = accountCache;
     this.projectControl = projectControl;
     this.currentUser = currentUser;
-
-    this.project = project;
-    this.groupUUID = groupUUID;
   }
 
-  @Override
-  public Set<Account> call() throws NoSuchGroupException,
+  public Set<Account> listAccounts(final AccountGroup.UUID groupUUID,
+      final Project.NameKey project) throws NoSuchGroupException,
       NoSuchProjectException, OrmException {
-    if (AccountGroup.PROJECT_OWNERS.equals(groupUUID)) {
-      return getProjectOwners();
-    }
-
-    return getAllGroupMembers(groupCache.get(groupUUID),
-        new HashSet<AccountGroup.Id>());
+    return listAccounts(groupUUID, project, new HashSet<AccountGroup.UUID>());
   }
 
-  private Set<Account> getProjectOwners() throws NoSuchProjectException,
+  private Set<Account> listAccounts(final AccountGroup.UUID groupUUID,
+      final Project.NameKey project, final Set<AccountGroup.UUID> seen)
+      throws NoSuchGroupException, OrmException, NoSuchProjectException {
+    if (AccountGroup.PROJECT_OWNERS.equals(groupUUID)) {
+      return getProjectOwners(project, seen);
+    } else {
+      AccountGroup group = groupCache.get(groupUUID);
+      if (group != null) {
+        return getGroupMembers(group, project, seen);
+      } else {
+        return Collections.emptySet();
+      }
+    }
+  }
+
+  private Set<Account> getProjectOwners(final Project.NameKey project,
+      final Set<AccountGroup.UUID> seen) throws NoSuchProjectException,
       NoSuchGroupException, OrmException {
+    seen.add(AccountGroup.PROJECT_OWNERS);
     if (project == null) {
       return Collections.emptySet();
     }
@@ -90,16 +90,17 @@
 
     final HashSet<Account> projectOwners = new HashSet<Account>();
     for (final AccountGroup.UUID ownerGroup : ownerGroups) {
-      projectOwners.addAll(getAllGroupMembers(groupCache.get(ownerGroup),
-          new HashSet<AccountGroup.Id>()));
+      if (!seen.contains(ownerGroup)) {
+        projectOwners.addAll(listAccounts(ownerGroup, project, seen));
+      }
     }
     return projectOwners;
   }
 
-  private Set<Account> getAllGroupMembers(final AccountGroup group,
-      final Set<AccountGroup.Id> seen) throws NoSuchGroupException,
-      OrmException {
-    seen.add(group.getId());
+  private Set<Account> getGroupMembers(final AccountGroup group,
+      final Project.NameKey project, final Set<AccountGroup.UUID> seen)
+      throws NoSuchGroupException, OrmException, NoSuchProjectException {
+    seen.add(group.getGroupUUID());
     final GroupDetail groupDetail =
         groupDetailFactory.create(group.getId()).call();
 
@@ -110,10 +111,11 @@
       }
     }
     if (groupDetail.includes != null) {
-      for (AccountGroupInclude groupInclude : groupDetail.includes) {
-        if (!seen.contains(groupInclude.getIncludeId())) {
-          members.addAll(getAllGroupMembers(
-              groupCache.get(groupInclude.getIncludeId()), seen));
+      for (final AccountGroupInclude groupInclude : groupDetail.includes) {
+        final AccountGroup includedGroup =
+            groupCache.get(groupInclude.getIncludeId());
+        if (!seen.contains(includedGroup.getGroupUUID())) {
+          members.addAll(listAccounts(includedGroup.getGroupUUID(), project, seen));
         }
       }
     }
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 0a6b800..0b3555c 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
@@ -49,17 +49,20 @@
   private final GroupIncludeCache groupIncludeCache;
   private final IdentifiedUser currentUser;
   private final PersonIdent serverIdent;
+  private final GroupCache groupCache;
 
   @Inject
   PerformCreateGroup(final ReviewDb db, final AccountCache accountCache,
       final GroupIncludeCache groupIncludeCache,
       final IdentifiedUser currentUser,
-      @GerritPersonIdent final PersonIdent serverIdent) {
+      @GerritPersonIdent final PersonIdent serverIdent,
+      final GroupCache groupCache) {
     this.db = db;
     this.accountCache = accountCache;
     this.groupIncludeCache = groupIncludeCache;
     this.currentUser = currentUser;
     this.serverIdent = serverIdent;
+    this.groupCache = groupCache;
   }
 
   /**
@@ -125,6 +128,8 @@
       addGroups(groupId, initialGroups);
     }
 
+    groupCache.onCreateGroup(nameKey);
+
     return groupId;
   }
 
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
new file mode 100644
index 0000000..a57b708
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PerformRenameGroup.java
@@ -0,0 +1,118 @@
+// 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.account;
+
+import com.google.gerrit.common.data.GroupDetail;
+import com.google.gerrit.common.errors.NameAlreadyUsedException;
+import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.reviewdb.AccountGroup;
+import com.google.gerrit.reviewdb.AccountGroupName;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.git.RenameGroupOp;
+import com.google.gwtorm.client.OrmDuplicateKeyException;
+import com.google.gwtorm.client.OrmException;
+import com.google.inject.Inject;
+
+import java.util.Collections;
+import java.util.Date;
+import java.util.TimeZone;
+import java.util.concurrent.TimeUnit;
+
+public class PerformRenameGroup {
+
+  public interface Factory {
+    PerformRenameGroup create();
+  }
+
+  private final ReviewDb db;
+  private final GroupCache groupCache;
+  private final GroupControl.Factory groupControlFactory;
+  private final GroupDetailFactory.Factory groupDetailFactory;
+  private final RenameGroupOp.Factory renameGroupOpFactory;
+  private final IdentifiedUser currentUser;
+
+  @Inject
+  PerformRenameGroup(final ReviewDb db, final GroupCache groupCache,
+      final GroupControl.Factory groupControlFactory,
+      final GroupDetailFactory.Factory groupDetailFactory,
+      final RenameGroupOp.Factory renameGroupOpFactory,
+      final IdentifiedUser currentUser) {
+    this.db = db;
+    this.groupCache = groupCache;
+    this.groupControlFactory = groupControlFactory;
+    this.groupDetailFactory = groupDetailFactory;
+    this.renameGroupOpFactory = renameGroupOpFactory;
+    this.currentUser = currentUser;
+  }
+
+  public GroupDetail renameGroup(final String groupName,
+      final String newGroupName) throws OrmException, NameAlreadyUsedException,
+      NoSuchGroupException {
+    final AccountGroup.NameKey groupNameKey =
+        new AccountGroup.NameKey(groupName);
+    final AccountGroup group = groupCache.get(groupNameKey);
+    if (group == null) {
+      throw new NoSuchGroupException(groupNameKey);
+    }
+    return renameGroup(group.getId(), newGroupName);
+  }
+
+  public GroupDetail renameGroup(final AccountGroup.Id groupId,
+      final String newName) throws OrmException, NameAlreadyUsedException,
+      NoSuchGroupException {
+    final GroupControl ctl = groupControlFactory.validateFor(groupId);
+    final AccountGroup group = db.accountGroups().get(groupId);
+    if (group == null || !ctl.isOwner()) {
+      throw new NoSuchGroupException(groupId);
+    }
+
+    final AccountGroup.NameKey old = group.getNameKey();
+    final AccountGroup.NameKey key = new AccountGroup.NameKey(newName);
+
+    try {
+      final AccountGroupName id = new AccountGroupName(key, groupId);
+      db.accountGroupNames().insert(Collections.singleton(id));
+    } catch (OrmDuplicateKeyException dupeErr) {
+      // If we are using this identity, don't report the exception.
+      //
+      AccountGroupName other = db.accountGroupNames().get(key);
+      if (other != null && other.getId().equals(groupId)) {
+        return groupDetailFactory.create(groupId).call();
+      }
+
+      // Otherwise, someone else has this identity.
+      //
+      throw new NameAlreadyUsedException();
+    }
+
+    group.setNameKey(key);
+    db.accountGroups().update(Collections.singleton(group));
+
+    AccountGroupName priorName = db.accountGroupNames().get(old);
+    if (priorName != null) {
+      db.accountGroupNames().delete(Collections.singleton(priorName));
+    }
+
+    groupCache.evict(group);
+    groupCache.evictAfterRename(old, key);
+    renameGroupOpFactory.create( //
+        currentUser.newCommitterIdent(new Date(), TimeZone.getDefault()), //
+        group.getGroupUUID(), //
+        old.get(), newName).start(0, TimeUnit.MILLISECONDS);
+
+    return groupDetailFactory.create(groupId).call();
+  }
+}
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 072f796..58527ac 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
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.reviewdb.Account;
 import com.google.gerrit.reviewdb.AccountGroup;
+import com.google.gerrit.reviewdb.ReviewDb;
 
 import java.util.Set;
 
@@ -25,6 +26,9 @@
 
   public AuthRequest authenticate(AuthRequest who) throws AccountException;
 
+  public AuthRequest link(ReviewDb db, Account.Id to, AuthRequest who)
+      throws AccountException;
+
   public void onCreateAccount(AuthRequest who, Account account);
 
   public Set<AccountGroup.UUID> groups(AccountState who);
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
new file mode 100644
index 0000000..44d360e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/VisibleGroups.java
@@ -0,0 +1,139 @@
+// 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.account;
+
+import com.google.gerrit.common.data.GroupDetail;
+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.AccountGroup;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gwtorm.client.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.List;
+import java.util.Set;
+import java.util.TreeSet;
+
+public class VisibleGroups {
+
+  public interface Factory {
+    VisibleGroups create();
+  }
+
+  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;
+
+  @Inject
+  VisibleGroups(final Provider<IdentifiedUser> currentUser,
+      final GroupCache groupCache,
+      final GroupControl.Factory groupControlFactory,
+      final GroupDetailFactory.Factory groupDetailFactory) {
+    this.identifiedUser = currentUser;
+    this.groupCache = groupCache;
+    this.groupControlFactory = groupControlFactory;
+    this.groupDetailFactory = groupDetailFactory;
+  }
+
+  public void setOnlyVisibleToAll(final boolean onlyVisibleToAll) {
+    this.onlyVisibleToAll = onlyVisibleToAll;
+  }
+
+  public void setGroupType(final AccountGroup.Type groupType) {
+    this.groupType = groupType;
+  }
+
+  public GroupList get() throws OrmException, NoSuchGroupException {
+    final Iterable<AccountGroup> groups = groupCache.all();
+    return createGroupList(filterGroups(groups));
+  }
+
+  public GroupList get(final Collection<ProjectControl> projects)
+      throws OrmException, NoSuchGroupException {
+    final Set<AccountGroup> groups =
+        new TreeSet<AccountGroup>(new GroupComparator());
+    for (final ProjectControl projectControl : projects) {
+      final Set<GroupReference> groupsRefs = projectControl.getAllGroups();
+      for (final GroupReference groupRef : groupsRefs) {
+        final AccountGroup group = groupCache.get(groupRef.getUUID());
+        if (group == null) {
+          throw new NoSuchGroupException(groupRef.getUUID());
+        }
+        groups.add(group);
+      }
+    }
+    return createGroupList(filterGroups(groups));
+  }
+
+  public GroupList get(final IdentifiedUser user) throws OrmException,
+      NoSuchGroupException {
+    if (identifiedUser.get().getAccountId().equals(user.getAccountId())
+        || identifiedUser.get().getCapabilities().canAdministrateServer()) {
+      final Set<AccountGroup.UUID> effective = user.getEffectiveGroups();
+      final Set<AccountGroup> groups =
+          new TreeSet<AccountGroup>(new GroupComparator());
+      for (final AccountGroup.UUID groupId : effective) {
+        AccountGroup group = groupCache.get(groupId);
+        if (group != null) {
+          groups.add(group);
+        }
+      }
+      return createGroupList(filterGroups(groups));
+    } else {
+      throw new NoSuchGroupException("Groups of user '" + user.getAccountId()
+          + "' are not visible.");
+    }
+  }
+
+  private List<AccountGroup> filterGroups(final Iterable<AccountGroup> groups) {
+    final List<AccountGroup> filteredGroups = new LinkedList<AccountGroup>();
+    final boolean isAdmin =
+        identifiedUser.get().getCapabilities().canAdministrateServer();
+    for (final AccountGroup group : groups) {
+      if (!isAdmin) {
+        final GroupControl c = groupControlFactory.controlFor(group);
+        if (!c.isVisible()) {
+          continue;
+        }
+      }
+      if ((onlyVisibleToAll && !group.isVisibleToAll())
+          || (groupType != null && !groupType.equals(group.getType()))) {
+        continue;
+      }
+      filteredGroups.add(group);
+    }
+    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()
+        .getCapabilities().canCreateGroup());
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/AuthenticationUnavailableException.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/AuthenticationUnavailableException.java
new file mode 100644
index 0000000..b0b6142
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/AuthenticationUnavailableException.java
@@ -0,0 +1,26 @@
+// 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.auth;
+
+import com.google.gerrit.server.account.AccountException;
+
+/** A query to the authentication server failed */
+public class AuthenticationUnavailableException extends AccountException {
+  private static final long serialVersionUID = 1L;
+
+  public AuthenticationUnavailableException(final String message, final Throwable why) {
+    super(message, why);
+  }
+}
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 e11e9bb..4dd4742 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
@@ -18,6 +18,7 @@
 import com.google.gerrit.reviewdb.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;
@@ -32,6 +33,7 @@
 import java.util.List;
 import java.util.Properties;
 import java.util.Set;
+import java.util.concurrent.TimeUnit;
 
 import javax.naming.CompositeName;
 import javax.naming.Context;
@@ -53,6 +55,7 @@
   private final String referral;
   private final boolean sslVerify;
   private volatile LdapSchema ldapSchema;
+  private final String readTimeOutMillis;
 
   @Inject
   Helper(@GerritServerConfig final Config config, final GroupCache groupCache) {
@@ -63,6 +66,14 @@
     this.password = LdapRealm.optional(config, "password");
     this.referral = LdapRealm.optional(config, "referral");
     this.sslVerify = config.getBoolean("ldap", "sslverify", true);
+    String timeout = LdapRealm.optional(config, "readTimeout");
+    if (timeout != null) {
+      readTimeOutMillis =
+          Long.toString(ConfigUtil.getTimeUnit(timeout, 0,
+              TimeUnit.MILLISECONDS));
+    } else {
+      readTimeOutMillis = null;
+    }
   }
 
   private Properties createContextProperties() {
@@ -73,6 +84,9 @@
       Class<? extends SSLSocketFactory> factory = BlindSSLSocketFactory.class;
       env.put("java.naming.ldap.factory.socket", factory.getName());
     }
+    if (readTimeOutMillis != null) {
+      env.put("com.sun.jndi.ldap.read.timeout", readTimeOutMillis);
+    }
     return env;
   }
 
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 e804ce6..5b19f81 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
@@ -27,6 +27,7 @@
 import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.account.EmailExpander;
 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;
@@ -49,6 +50,7 @@
 import java.util.HashMap;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 
@@ -67,6 +69,7 @@
   private final EmailExpander emailExpander;
   private final Cache<String, Account.Id> usernameCache;
   private final Set<Account.FieldName> readOnlyAccountFields;
+  private final Config config;
 
   private final Cache<String, Set<AccountGroup.UUID>> membershipCache;
 
@@ -83,6 +86,7 @@
     this.emailExpander = emailExpander;
     this.usernameCache = usernameCache;
     this.membershipCache = membershipCache;
+    this.config = config;
 
     this.readOnlyAccountFields = new HashSet<Account.FieldName>();
 
@@ -181,6 +185,10 @@
 
   public AuthRequest authenticate(final AuthRequest who)
       throws AccountException {
+    if (config.getBoolean("ldap", "localUsernameToLowerCase", false)) {
+      who.setLocalUser(who.getLocalUser().toLowerCase(Locale.US));
+    }
+
     final String username = who.getLocalUser();
     try {
       final DirContext ctx;
@@ -231,11 +239,16 @@
       }
     } catch (NamingException e) {
       log.error("Cannot query LDAP to autenticate user", e);
-      throw new AccountException("Cannot query LDAP for account", e);
+      throw new AuthenticationUnavailableException("Cannot query LDAP for account", e);
     }
   }
 
   @Override
+  public AuthRequest link(ReviewDb db, Account.Id to, AuthRequest who) {
+    return who;
+  }
+
+  @Override
   public void onCreateAccount(final AuthRequest who, final Account account) {
     usernameCache.put(who.getLocalUser(), account.getId());
   }
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
index 7159501..7892ea1 100644
--- 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
@@ -14,9 +14,6 @@
 
 package com.google.gerrit.server.cache;
 
-import java.util.concurrent.TimeUnit;
-
-
 /**
  * A fast in-memory and/or on-disk based cache.
  *
@@ -35,12 +32,4 @@
 
   /** Remove all cached items. */
   public void removeAll();
-
-  /**
-   * Get the time an element will survive in the cache.
-   *
-   * @param unit desired units of the return value.
-   * @return time an item can live before being purged.
-   */
-  public long getTimeToLive(TimeUnit unit);
 }
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/cache/CachePool.java
index 5230ff6..3370b08 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/CachePool.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/cache/CachePool.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2009 The Android Open Source Project
+// 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.
@@ -14,237 +14,6 @@
 
 package com.google.gerrit.server.cache;
 
-import static java.util.concurrent.TimeUnit.MINUTES;
-import static java.util.concurrent.TimeUnit.SECONDS;
-
-import com.google.gerrit.lifecycle.LifecycleListener;
-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.ProvisionException;
-import com.google.inject.Singleton;
-
-import net.sf.ehcache.CacheManager;
-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 CachePool {
-  private static final Logger log = LoggerFactory.getLogger(CachePool.class);
-
-  public static class Lifecycle implements LifecycleListener {
-    private final CachePool cachePool;
-
-    @Inject
-    Lifecycle(final CachePool 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
-  CachePool(@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()) {
-        p.bind(manager.getEhcache(p.getName()));
-      }
-    }
-  }
-
-  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;
-    }
-  }
-
-  <K, V> ProxyEhcache 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 ProxyEhcache(n);
-    }
-  }
-
-  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 void configureDefaultCache() {
-      final 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);
-      }
-
-      mgr.setDefaultCacheConfiguration(c);
-    }
-
-    private CacheConfiguration newCache(final String name) {
-      try {
-        final CacheConfiguration c;
-        c = mgr.getDefaultCacheConfiguration().clone();
-        c.setName(name);
-        return c;
-      } catch (CloneNotSupportedException e) {
-        throw new ProvisionException("Cannot configure cache " + name, e);
-      }
-    }
-  }
+public interface CachePool {
+  public <K, V> ProxyCache<K, V> register(CacheProvider<K, V> provider);
 }
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 0ba424f2..1fa047b 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
@@ -22,11 +22,9 @@
 import com.google.inject.Provider;
 import com.google.inject.ProvisionException;
 
-import net.sf.ehcache.Ehcache;
-
 import java.util.concurrent.TimeUnit;
 
-final class CacheProvider<K, V> implements Provider<Cache<K, V>>,
+public final class CacheProvider<K, V> implements Provider<Cache<K, V>>,
     NamedCacheBinding<K, V>, UnnamedCacheBinding<K, V> {
   private final CacheModule module;
   private final boolean disk;
@@ -35,7 +33,7 @@
   private long maxAge;
   private EvictionPolicy evictionPolicy;
   private String cacheName;
-  private ProxyEhcache cache;
+  private ProxyCache<K, V> cache;
   private Provider<EntryCreator<K, V>> entryCreator;
 
   CacheProvider(final boolean disk, CacheModule module) {
@@ -56,34 +54,41 @@
     this.cache = pool.register(this);
   }
 
-  void bind(final Ehcache ehcache) {
-    cache.bind(ehcache);
+  public void bind(Cache<K, V> impl) {
+    if (cache == null) {
+      throw new ProvisionException("Cache was never registered");
+    }
+    cache.bind(impl);
   }
 
-  String getName() {
+  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;
   }
 
-  boolean disk() {
+  public boolean disk() {
     return disk;
   }
 
-  int memoryLimit() {
+  public int memoryLimit() {
     return memoryLimit;
   }
 
-  int diskLimit() {
+  public int diskLimit() {
     return diskLimit;
   }
 
-  long maxAge() {
+  public long maxAge() {
     return maxAge;
   }
 
-  EvictionPolicy evictionPolicy() {
+  public EvictionPolicy evictionPolicy() {
     return evictionPolicy;
   }
 
@@ -133,9 +138,6 @@
     if (cache == null) {
       throw new ProvisionException("Cache \"" + cacheName + "\" not available");
     }
-    if (entryCreator != null) {
-      return new PopulatingCache<K, V>(cache, entryCreator.get());
-    }
-    return new SimpleCache<K, V>(cache);
+    return cache;
   }
 }
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
index 268fe2b..bafdc49 100644
--- 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
@@ -15,7 +15,6 @@
 package com.google.gerrit.server.cache;
 
 import java.util.concurrent.ConcurrentHashMap;
-import java.util.concurrent.TimeUnit;
 
 /**
  * An infinitely sized cache backed by java.util.ConcurrentHashMap.
@@ -46,9 +45,4 @@
   public void removeAll() {
     map.clear();
   }
-
-  @Override
-  public long getTimeToLive(TimeUnit unit) {
-    return 0;
-  }
 }
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
new file mode 100644
index 0000000..c1b0292
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/cache/ProxyCache.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2010 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.cache;
+
+/** Proxy around a cache which has not yet been created. */
+public final class ProxyCache<K, V> implements Cache<K, V> {
+  private volatile Cache<K, V> self;
+
+  public void bind(Cache<K, V> self) {
+    this.self = self;
+  }
+
+  public V get(K key) {
+    return self.get(key);
+  }
+
+  public void put(K key, V value) {
+    self.put(key, value);
+  }
+
+  public void remove(K key) {
+    self.remove(key);
+  }
+
+  public void removeAll() {
+    self.removeAll();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/ProxyEhcache.java b/gerrit-server/src/main/java/com/google/gerrit/server/cache/ProxyEhcache.java
deleted file mode 100644
index bcead63..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/cache/ProxyEhcache.java
+++ /dev/null
@@ -1,393 +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.server.cache;
-
-import net.sf.ehcache.CacheException;
-import net.sf.ehcache.CacheManager;
-import net.sf.ehcache.Ehcache;
-import net.sf.ehcache.Element;
-import net.sf.ehcache.Statistics;
-import net.sf.ehcache.Status;
-import net.sf.ehcache.bootstrap.BootstrapCacheLoader;
-import net.sf.ehcache.config.CacheConfiguration;
-import net.sf.ehcache.event.RegisteredEventListeners;
-import net.sf.ehcache.exceptionhandler.CacheExceptionHandler;
-import net.sf.ehcache.extension.CacheExtension;
-import net.sf.ehcache.loader.CacheLoader;
-import net.sf.ehcache.statistics.CacheUsageListener;
-import net.sf.ehcache.statistics.LiveCacheStatistics;
-import net.sf.ehcache.statistics.sampled.SampledCacheStatistics;
-
-import java.io.Serializable;
-import java.util.Collection;
-import java.util.List;
-import java.util.Map;
-
-/** Proxy around a cache which has not yet been created. */
-final class ProxyEhcache implements Ehcache {
-  private final String cacheName;
-  private volatile Ehcache self;
-
-  ProxyEhcache(final String cacheName) {
-    this.cacheName = cacheName;
-  }
-
-  void bind(final Ehcache self) {
-    this.self = self;
-  }
-
-  private Ehcache self() {
-    return self;
-  }
-
-  @Override
-  public Object clone() throws CloneNotSupportedException {
-    throw new CloneNotSupportedException();
-  }
-
-  @Override
-  public String getName() {
-    return cacheName;
-  }
-
-  @Override
-  public void setName(String name) {
-    throw new UnsupportedOperationException();
-  }
-
-  //
-  // Everything else delegates through self.
-  //
-
-  public void bootstrap() {
-    self().bootstrap();
-  }
-
-  public long calculateInMemorySize() throws IllegalStateException,
-      CacheException {
-    return self().calculateInMemorySize();
-  }
-
-  public void clearStatistics() {
-    self().clearStatistics();
-  }
-
-  public void dispose() throws IllegalStateException {
-    self().dispose();
-  }
-
-  public void evictExpiredElements() {
-    self().evictExpiredElements();
-  }
-
-  public void flush() throws IllegalStateException, CacheException {
-    self().flush();
-  }
-
-  public Element get(Object key) throws IllegalStateException, CacheException {
-    return self().get(key);
-  }
-
-  public Element get(Serializable key) throws IllegalStateException,
-      CacheException {
-    return self().get(key);
-  }
-
-  @SuppressWarnings("rawtypes")
-  public Map getAllWithLoader(Collection keys, Object loaderArgument)
-      throws CacheException {
-    return self().getAllWithLoader(keys, loaderArgument);
-  }
-
-  public float getAverageGetTime() {
-    return self().getAverageGetTime();
-  }
-
-  public BootstrapCacheLoader getBootstrapCacheLoader() {
-    return self().getBootstrapCacheLoader();
-  }
-
-  public CacheConfiguration getCacheConfiguration() {
-    if (self == null) {
-      // In Ehcache 1.7, BlockingCache wants to ask us if we are
-      // clustered using Terracotta. Unfortunately it is too early
-      // to know for certain as the caches have not actually been
-      // created or configured.
-      //
-      return new CacheConfiguration();
-    }
-    return self().getCacheConfiguration();
-  }
-
-  public RegisteredEventListeners getCacheEventNotificationService() {
-    return self().getCacheEventNotificationService();
-  }
-
-  public CacheExceptionHandler getCacheExceptionHandler() {
-    return self().getCacheExceptionHandler();
-  }
-
-  public CacheManager getCacheManager() {
-    return self().getCacheManager();
-  }
-
-  public int getDiskStoreSize() throws IllegalStateException {
-    return self().getDiskStoreSize();
-  }
-
-  public String getGuid() {
-    return self().getGuid();
-  }
-
-  @SuppressWarnings("rawtypes")
-  public List getKeys() throws IllegalStateException, CacheException {
-    return self().getKeys();
-  }
-
-  @SuppressWarnings("rawtypes")
-  public List getKeysNoDuplicateCheck() throws IllegalStateException {
-    return self().getKeysNoDuplicateCheck();
-  }
-
-  @SuppressWarnings("rawtypes")
-  public List getKeysWithExpiryCheck() throws IllegalStateException,
-      CacheException {
-    return self().getKeysWithExpiryCheck();
-  }
-
-  public long getMemoryStoreSize() throws IllegalStateException {
-    return self().getMemoryStoreSize();
-  }
-
-  public Element getQuiet(Object key) throws IllegalStateException,
-      CacheException {
-    return self().getQuiet(key);
-  }
-
-  public Element getQuiet(Serializable key) throws IllegalStateException,
-      CacheException {
-    return self().getQuiet(key);
-  }
-
-  public List<CacheExtension> getRegisteredCacheExtensions() {
-    return self().getRegisteredCacheExtensions();
-  }
-
-  public List<CacheLoader> getRegisteredCacheLoaders() {
-    return self().getRegisteredCacheLoaders();
-  }
-
-  public int getSize() throws IllegalStateException, CacheException {
-    return self().getSize();
-  }
-
-  public Statistics getStatistics() throws IllegalStateException {
-    return self().getStatistics();
-  }
-
-  public int getStatisticsAccuracy() {
-    return self().getStatisticsAccuracy();
-  }
-
-  public Status getStatus() {
-    return self().getStatus();
-  }
-
-  public Element getWithLoader(Object key, CacheLoader loader,
-      Object loaderArgument) throws CacheException {
-    return self().getWithLoader(key, loader, loaderArgument);
-  }
-
-  public void initialise() {
-    self().initialise();
-  }
-
-  public boolean isDisabled() {
-    return self().isDisabled();
-  }
-
-  public boolean isElementInMemory(Object key) {
-    return self().isElementInMemory(key);
-  }
-
-  public boolean isElementInMemory(Serializable key) {
-    return self().isElementInMemory(key);
-  }
-
-  public boolean isElementOnDisk(Object key) {
-    return self().isElementOnDisk(key);
-  }
-
-  public boolean isElementOnDisk(Serializable key) {
-    return self().isElementOnDisk(key);
-  }
-
-  public boolean isExpired(Element element) throws IllegalStateException,
-      NullPointerException {
-    return self().isExpired(element);
-  }
-
-  public boolean isKeyInCache(Object key) {
-    return self().isKeyInCache(key);
-  }
-
-  public boolean isValueInCache(Object value) {
-    return self().isValueInCache(value);
-  }
-
-  public void load(Object key) throws CacheException {
-    self().load(key);
-  }
-
-  @SuppressWarnings("rawtypes")
-  public void loadAll(Collection keys, Object argument) throws CacheException {
-    self().loadAll(keys, argument);
-  }
-
-  public void put(Element element, boolean doNotNotifyCacheReplicators)
-      throws IllegalArgumentException, IllegalStateException, CacheException {
-    self().put(element, doNotNotifyCacheReplicators);
-  }
-
-  public void put(Element element) throws IllegalArgumentException,
-      IllegalStateException, CacheException {
-    self().put(element);
-  }
-
-  public void putQuiet(Element element) throws IllegalArgumentException,
-      IllegalStateException, CacheException {
-    self().putQuiet(element);
-  }
-
-  public void registerCacheExtension(CacheExtension cacheExtension) {
-    self().registerCacheExtension(cacheExtension);
-  }
-
-  public void registerCacheLoader(CacheLoader cacheLoader) {
-    self().registerCacheLoader(cacheLoader);
-  }
-
-  public boolean remove(Object key, boolean doNotNotifyCacheReplicators)
-      throws IllegalStateException {
-    return self().remove(key, doNotNotifyCacheReplicators);
-  }
-
-  public boolean remove(Object key) throws IllegalStateException {
-    return self().remove(key);
-  }
-
-  public boolean remove(Serializable key, boolean doNotNotifyCacheReplicators)
-      throws IllegalStateException {
-    return self().remove(key, doNotNotifyCacheReplicators);
-  }
-
-  public boolean remove(Serializable key) throws IllegalStateException {
-    return self().remove(key);
-  }
-
-  public void removeAll() throws IllegalStateException, CacheException {
-    self().removeAll();
-  }
-
-  public void removeAll(boolean doNotNotifyCacheReplicators)
-      throws IllegalStateException, CacheException {
-    self().removeAll(doNotNotifyCacheReplicators);
-  }
-
-  public boolean removeQuiet(Object key) throws IllegalStateException {
-    return self().removeQuiet(key);
-  }
-
-  public boolean removeQuiet(Serializable key) throws IllegalStateException {
-    return self().removeQuiet(key);
-  }
-
-  public void setBootstrapCacheLoader(BootstrapCacheLoader bootstrapCacheLoader)
-      throws CacheException {
-    self().setBootstrapCacheLoader(bootstrapCacheLoader);
-  }
-
-  public void setCacheExceptionHandler(
-      CacheExceptionHandler cacheExceptionHandler) {
-    self().setCacheExceptionHandler(cacheExceptionHandler);
-  }
-
-  public void setCacheManager(CacheManager cacheManager) {
-    self().setCacheManager(cacheManager);
-  }
-
-  public void setDisabled(boolean disabled) {
-    self().setDisabled(disabled);
-  }
-
-  public void setDiskStorePath(String diskStorePath) throws CacheException {
-    self().setDiskStorePath(diskStorePath);
-  }
-
-  public void setStatisticsAccuracy(int statisticsAccuracy) {
-    self().setStatisticsAccuracy(statisticsAccuracy);
-  }
-
-  public void unregisterCacheExtension(CacheExtension cacheExtension) {
-    self().unregisterCacheExtension(cacheExtension);
-  }
-
-  public void unregisterCacheLoader(CacheLoader cacheLoader) {
-    self().unregisterCacheLoader(cacheLoader);
-  }
-
-  public Object getInternalContext() {
-    return self.getInternalContext();
-  }
-
-  public LiveCacheStatistics getLiveCacheStatistics() throws IllegalStateException {
-    return self.getLiveCacheStatistics();
-  }
-
-  public SampledCacheStatistics getSampledCacheStatistics() {
-    return self.getSampledCacheStatistics();
-  }
-
-  public int getSizeBasedOnAccuracy(int statisticsAccuracy) throws IllegalArgumentException,
-      IllegalStateException, CacheException {
-    return self.getSizeBasedOnAccuracy(statisticsAccuracy);
-  }
-
-  public boolean isSampledStatisticsEnabled() {
-    return self.isSampledStatisticsEnabled();
-  }
-
-  public boolean isStatisticsEnabled() {
-    return self.isStatisticsEnabled();
-  }
-
-  public void registerCacheUsageListener(CacheUsageListener cacheUsageListener)
-      throws IllegalStateException {
-    self.registerCacheUsageListener(cacheUsageListener);
-  }
-
-  public void removeCacheUsageListener(CacheUsageListener cacheUsageListener)
-      throws IllegalStateException {
-    self.removeCacheUsageListener(cacheUsageListener);
-  }
-
-  public void setSampledStatisticsEnabled(boolean enableStatistics) {
-    self.setSampledStatisticsEnabled(enableStatistics);
-  }
-
-  public void setStatisticsEnabled(boolean enableStatistics) {
-    self.setStatisticsEnabled(enableStatistics);
-  }
-}
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
new file mode 100644
index 0000000..cc111e6
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/AbandonChange.java
@@ -0,0 +1,121 @@
+// 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.changedetail;
+
+import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.common.data.ReviewResult;
+import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.reviewdb.ChangeMessage;
+import com.google.gerrit.reviewdb.PatchSet;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.mail.AbandonedSender;
+import com.google.gerrit.server.mail.EmailException;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gwtorm.client.AtomicUpdate;
+import com.google.gwtorm.client.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import java.util.concurrent.Callable;
+
+public class AbandonChange implements Callable<ReviewResult> {
+
+  public interface Factory {
+    AbandonChange create(PatchSet.Id patchSetId, String changeComment);
+  }
+
+  private final AbandonedSender.Factory abandonedSenderFactory;
+  private final ChangeControl.Factory changeControlFactory;
+  private final ReviewDb db;
+  private final IdentifiedUser currentUser;
+  private final ChangeHooks hooks;
+
+  private final PatchSet.Id patchSetId;
+  private final String changeComment;
+
+  @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) {
+    this.abandonedSenderFactory = abandonedSenderFactory;
+    this.changeControlFactory = changeControlFactory;
+    this.db = db;
+    this.currentUser = currentUser;
+    this.hooks = hooks;
+
+    this.patchSetId = patchSetId;
+    this.changeComment = changeComment;
+  }
+
+  @Override
+  public ReviewResult call() throws EmailException,
+      InvalidChangeOperationException, NoSuchChangeException, OrmException {
+    final ReviewResult result = new ReviewResult();
+
+    final Change.Id changeId = patchSetId.getParentKey();
+    result.setChangeId(changeId);
+    final ChangeControl control = changeControlFactory.validateFor(changeId);
+    final PatchSet patch = db.patchSets().get(patchSetId);
+    if (!control.canAbandon()) {
+      result.addError(new ReviewResult.Error(
+          ReviewResult.Error.Type.ABANDON_NOT_PERMITTED));
+    } else if (patch == null) {
+      throw new NoSuchChangeException(changeId);
+    } else {
+
+      // Create a message to accompany the abandoned 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() + ": Abandoned");
+      if (changeComment != null && changeComment.length() > 0) {
+        msgBuf.append("\n\n");
+        msgBuf.append(changeComment);
+      }
+      cmsg.setMessage(msgBuf.toString());
+
+      // Abandon the change
+      final Change updatedChange = db.changes().atomicUpdate(changeId,
+          new AtomicUpdate<Change>() {
+        @Override
+        public Change update(Change change) {
+          if (change.getStatus().isOpen()
+              && change.currentPatchSetId().equals(patchSetId)) {
+            change.setStatus(Change.Status.ABANDONED);
+            ChangeUtil.updated(change);
+            return change;
+          } else {
+            return null;
+          }
+        }
+      });
+      ChangeUtil.updatedChange(
+          db, currentUser, updatedChange, cmsg, abandonedSenderFactory,
+          "Change is no longer open or patchset is not latest");
+      hooks.doChangeAbandonedHook(updatedChange, currentUser.getAccount(),
+                                  changeComment, 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
new file mode 100644
index 0000000..761b765
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/DeleteDraftPatchSet.java
@@ -0,0 +1,125 @@
+// 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.changedetail;
+
+import com.google.gerrit.common.data.ReviewResult;
+import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.reviewdb.PatchSet;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.server.ChangeUtil;
+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.NoSuchChangeException;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gwtorm.client.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.Callable;
+
+public class DeleteDraftPatchSet implements Callable<ReviewResult> {
+
+  public interface Factory {
+    DeleteDraftPatchSet create(PatchSet.Id patchSetId);
+  }
+
+  private final ChangeControl.Factory changeControlFactory;
+  private final ReviewDb db;
+  private final GitRepositoryManager gitManager;
+  private final ReplicationQueue replication;
+  private final PatchSetInfoFactory patchSetInfoFactory;
+
+  private final PatchSet.Id patchSetId;
+
+  @Inject
+  DeleteDraftPatchSet(ChangeControl.Factory changeControlFactory,
+      ReviewDb db, GitRepositoryManager gitManager,
+      ReplicationQueue replication, PatchSetInfoFactory patchSetInfoFactory,
+      @Assisted final PatchSet.Id patchSetId) {
+    this.changeControlFactory = changeControlFactory;
+    this.db = db;
+    this.gitManager = gitManager;
+    this.replication = replication;
+    this.patchSetInfoFactory = patchSetInfoFactory;
+
+    this.patchSetId = patchSetId;
+  }
+
+  @Override
+  public ReviewResult call() throws NoSuchChangeException, OrmException {
+    final ReviewResult result = new ReviewResult();
+
+    final Change.Id changeId = patchSetId.getParentKey();
+    result.setChangeId(changeId);
+    final ChangeControl control = changeControlFactory.validateFor(changeId);
+    final PatchSet patch = db.patchSets().get(patchSetId);
+    if (patch == null) {
+      throw new NoSuchChangeException(changeId);
+    }
+    if (!patch.isDraft()) {
+      result.addError(new ReviewResult.Error(
+          ReviewResult.Error.Type.NOT_A_DRAFT));
+      return result;
+    }
+
+    if (!control.canDeleteDraft(db)) {
+      result.addError(new ReviewResult.Error(
+          ReviewResult.Error.Type.DELETE_NOT_PERMITTED));
+      return result;
+    }
+    final Change change = control.getChange();
+
+    try {
+      ChangeUtil.deleteOnlyDraftPatchSet(patch, change, gitManager, replication, db);
+    } catch (IOException e) {
+      result.addError(new ReviewResult.Error(
+          ReviewResult.Error.Type.GIT_ERROR, e.getMessage()));
+    }
+
+    List<PatchSet> restOfPatches = db.patchSets().byChange(changeId).toList();
+    if (restOfPatches.size() == 0) {
+      try {
+        ChangeUtil.deleteDraftChange(patchSetId, gitManager, replication, db);
+        result.setChangeId(null);
+      } catch (IOException e) {
+        result.addError(new ReviewResult.Error(
+            ReviewResult.Error.Type.GIT_ERROR, e.getMessage()));
+      }
+    } else {
+      PatchSet.Id highestId = null;
+      for (PatchSet ps : restOfPatches) {
+        if (highestId == null || ps.getPatchSetId() > highestId.get()) {
+          highestId = ps.getId();
+        }
+      }
+      if (change.currentPatchSetId().equals(patchSetId)) {
+        change.removeLastPatchSetId();
+        try {
+          change.setCurrentPatchSet(patchSetInfoFactory.get(db, change.currPatchSetId()));
+        } catch (PatchSetInfoNotAvailableException e) {
+          throw new NoSuchChangeException(changeId);
+        }
+        db.changes().update(Collections.singleton(change));
+      }
+    }
+    return result;
+  }
+}
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
new file mode 100644
index 0000000..c24c73c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/PublishDraft.java
@@ -0,0 +1,103 @@
+// 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.changedetail;
+
+import com.google.gerrit.common.data.ReviewResult;
+import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.reviewdb.PatchSet;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gwtorm.client.AtomicUpdate;
+import com.google.gwtorm.client.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import java.util.concurrent.Callable;
+
+public class PublishDraft implements Callable<ReviewResult> {
+
+  public interface Factory {
+    PublishDraft create(PatchSet.Id patchSetId);
+  }
+
+  private final ChangeControl.Factory changeControlFactory;
+  private final ReviewDb db;
+
+  private final PatchSet.Id patchSetId;
+
+  @Inject
+  PublishDraft(ChangeControl.Factory changeControlFactory,
+      ReviewDb db, @Assisted final PatchSet.Id patchSetId) {
+    this.changeControlFactory = changeControlFactory;
+    this.db = db;
+
+    this.patchSetId = patchSetId;
+  }
+
+  @Override
+  public ReviewResult call() throws NoSuchChangeException, OrmException {
+    final ReviewResult result = new ReviewResult();
+
+    final Change.Id changeId = patchSetId.getParentKey();
+    result.setChangeId(changeId);
+    final ChangeControl control = changeControlFactory.validateFor(changeId);
+    final PatchSet patch = db.patchSets().get(patchSetId);
+    if (patch == null) {
+      throw new NoSuchChangeException(changeId);
+    }
+    if (!patch.isDraft()) {
+      result.addError(new ReviewResult.Error(
+          ReviewResult.Error.Type.NOT_A_DRAFT));
+      return result;
+    }
+
+    if (!control.canPublish(db)) {
+      result.addError(new ReviewResult.Error(
+          ReviewResult.Error.Type.PUBLISH_NOT_PERMITTED));
+    } else {
+      db.patchSets().atomicUpdate(patchSetId, new AtomicUpdate<PatchSet>() {
+        @Override
+        public PatchSet update(PatchSet patchset) {
+          if (patchset.isDraft()) {
+            patchset.setDraft(false);
+          }
+          return null;
+        }
+      });
+
+      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;
+            }
+          }
+        });
+      }
+    }
+
+    return result;
+  }
+}
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
new file mode 100644
index 0000000..5df973b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RestoreChange.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.server.changedetail;
+
+import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.common.data.ReviewResult;
+import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.reviewdb.ChangeMessage;
+import com.google.gerrit.reviewdb.PatchSet;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.mail.RestoredSender;
+import com.google.gerrit.server.mail.EmailException;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gwtorm.client.AtomicUpdate;
+import com.google.gwtorm.client.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import java.util.concurrent.Callable;
+
+public class RestoreChange implements Callable<ReviewResult> {
+
+  public interface Factory {
+    RestoreChange create(PatchSet.Id patchSetId, String changeComment);
+  }
+
+  private final RestoredSender.Factory restoredSenderFactory;
+  private final ChangeControl.Factory changeControlFactory;
+  private final ReviewDb db;
+  private final IdentifiedUser currentUser;
+  private final ChangeHooks hooks;
+
+  private final PatchSet.Id patchSetId;
+  private final String changeComment;
+
+  @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) {
+    this.restoredSenderFactory = restoredSenderFactory;
+    this.changeControlFactory = changeControlFactory;
+    this.db = db;
+    this.currentUser = currentUser;
+    this.hooks = hooks;
+
+    this.patchSetId = patchSetId;
+    this.changeComment = changeComment;
+  }
+
+  @Override
+  public ReviewResult call() throws EmailException,
+      InvalidChangeOperationException, NoSuchChangeException, OrmException {
+    final ReviewResult result = new ReviewResult();
+
+    final Change.Id changeId = patchSetId.getParentKey();
+    result.setChangeId(changeId);
+    final ChangeControl control = changeControlFactory.validateFor(changeId);
+    final PatchSet patch = db.patchSets().get(patchSetId);
+    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;
+  }
+}
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
new file mode 100644
index 0000000..813b92a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/Submit.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.server.changedetail;
+
+import static com.google.gerrit.reviewdb.ApprovalCategory.SUBMIT;
+
+import com.google.gerrit.common.data.ReviewResult;
+import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.reviewdb.PatchSet;
+import com.google.gerrit.reviewdb.PatchSetApproval;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.git.MergeOp;
+import com.google.gerrit.server.git.MergeQueue;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gwtorm.client.AtomicUpdate;
+import com.google.gwtorm.client.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import java.util.concurrent.Callable;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class Submit implements Callable<ReviewResult> {
+
+  public interface Factory {
+    Submit create(PatchSet.Id patchSetId);
+  }
+
+  private final ChangeControl.Factory changeControlFactory;
+  private final MergeOp.Factory opFactory;
+  private final MergeQueue merger;
+  private final ReviewDb db;
+  private final IdentifiedUser currentUser;
+
+  private final PatchSet.Id patchSetId;
+
+  @Inject
+  Submit(final ChangeControl.Factory changeControlFactory,
+      final MergeOp.Factory opFactory, final MergeQueue merger,
+      final ReviewDb db, final IdentifiedUser currentUser,
+      @Assisted final PatchSet.Id patchSetId) {
+    this.changeControlFactory = changeControlFactory;
+    this.opFactory = opFactory;
+    this.merger = merger;
+    this.db = db;
+    this.currentUser = currentUser;
+
+    this.patchSetId = patchSetId;
+  }
+
+  @Override
+  public ReviewResult call() throws IllegalStateException,
+      InvalidChangeOperationException, NoSuchChangeException, OrmException {
+    final ReviewResult result = new ReviewResult();
+
+    final PatchSet patch = db.patchSets().get(patchSetId);
+    final Change.Id changeId = patchSetId.getParentKey();
+    final ChangeControl control = changeControlFactory.validateFor(changeId);
+    result.setChangeId(changeId);
+    if (patch == null) {
+      throw new NoSuchChangeException(changeId);
+    }
+
+    List<SubmitRecord> submitResult = control.canSubmit(db, patchSetId);
+    if (submitResult.isEmpty()) {
+      throw new IllegalStateException(
+          "ChangeControl.canSubmit returned empty list");
+    }
+
+    for (SubmitRecord submitRecord : submitResult) {
+      switch (submitRecord.status) {
+        case OK:
+          if (!control.getRefControl().canSubmit()) {
+            result.addError(new ReviewResult.Error(
+              ReviewResult.Error.Type.SUBMIT_NOT_PERMITTED));
+          }
+          break;
+
+        case NOT_READY:
+          StringBuilder errMsg = new StringBuilder();
+          for (SubmitRecord.Label lbl : submitRecord.labels) {
+            switch (lbl.status) {
+              case OK:
+                break;
+
+              case REJECT:
+                if (errMsg.length() > 0) errMsg.append("; ");
+                errMsg.append("change " + changeId + ": blocked by "
+                              + lbl.label);
+                break;
+
+              case NEED:
+                if (errMsg.length() > 0) errMsg.append("; ");
+                errMsg.append("change " + changeId + ": needs " + lbl.label);
+                break;
+
+              case IMPOSSIBLE:
+                if (errMsg.length() > 0) errMsg.append("; ");
+                errMsg.append("change " + changeId + ": needs " + lbl.label
+                    + " (check project access)");
+                break;
+
+              default:
+                throw new IllegalArgumentException(
+                    "Unsupported SubmitRecord.Label.status (" + lbl.status
+                    + ")");
+            }
+          }
+          result.addError(new ReviewResult.Error(
+            ReviewResult.Error.Type.SUBMIT_NOT_READY, errMsg.toString()));
+          break;
+
+        case CLOSED:
+          result.addError(new ReviewResult.Error(
+            ReviewResult.Error.Type.CHANGE_IS_CLOSED));
+          break;
+
+        case RULE_ERROR:
+          result.addError(new ReviewResult.Error(
+            ReviewResult.Error.Type.RULE_ERROR,
+            submitResult.get(0).errorMessage));
+          break;
+
+        default:
+          throw new IllegalStateException(
+              "Unsupported SubmitRecord.status + (" + submitRecord.status
+              + ")");
+      }
+    }
+
+    // Submit the change if we can
+    if (result.getErrors().isEmpty()) {
+      final List<PatchSetApproval> allApprovals =
+          new ArrayList<PatchSetApproval>(db.patchSetApprovals().byPatchSet(
+              patchSetId).toList());
+
+      final PatchSetApproval.Key akey =
+          new PatchSetApproval.Key(patchSetId, currentUser.getAccountId(),
+                                   SUBMIT);
+
+      PatchSetApproval approval = new PatchSetApproval(akey, (short) 1);
+      for (final PatchSetApproval candidateApproval : allApprovals) {
+        if (akey.equals(candidateApproval.getKey())) {
+          candidateApproval.setValue((short) 1);
+          candidateApproval.setGranted();
+          approval = candidateApproval;
+          break;
+        }
+      }
+      db.patchSetApprovals().upsert(Collections.singleton(approval));
+
+      final Change updatedChange = db.changes().atomicUpdate(changeId,
+          new AtomicUpdate<Change>() {
+        @Override
+        public Change update(Change change) {
+          if (change.getStatus() == Change.Status.NEW) {
+            change.setStatus(Change.Status.SUBMITTED);
+            ChangeUtil.updated(change);
+          }
+          return change;
+        }
+      });
+
+      if (updatedChange.getStatus() == Change.Status.SUBMITTED) {
+        merger.merge(opFactory, updatedChange.getDest());
+      }
+    }
+    return result;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/AnonymousCowardName.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/AnonymousCowardName.java
new file mode 100644
index 0000000..197d64e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/AnonymousCowardName.java
@@ -0,0 +1,27 @@
+// 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.config;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.inject.BindingAnnotation;
+
+import java.lang.annotation.Retention;
+
+/** Special name for a user that hasn't set a name. */
+@Retention(RUNTIME)
+@BindingAnnotation
+public @interface AnonymousCowardName {
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/AnonymousCowardNameProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/AnonymousCowardNameProvider.java
new file mode 100644
index 0000000..ffa90b6
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/AnonymousCowardNameProvider.java
@@ -0,0 +1,39 @@
+// 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.config;
+
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.lib.Config;
+
+public class AnonymousCowardNameProvider implements Provider<String> {
+
+  private final String anonymousCoward;
+
+  @Inject
+  public AnonymousCowardNameProvider(@GerritServerConfig final Config cfg) {
+    String anonymousCoward = cfg.getString("user", null, "anonymousCoward");
+    if (anonymousCoward == null) {
+      anonymousCoward = "Anonymous Coward";
+    }
+    this.anonymousCoward = anonymousCoward;
+  }
+
+  @Override
+  public String get() {
+    return anonymousCoward;
+  }
+}
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 f1a9328..fc189dd 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
@@ -61,7 +61,7 @@
     if (key != null && !key.isEmpty()) {
       int age = (int) ConfigUtil.getTimeUnit(cfg,
           "auth", null, "maxRegisterEmailTokenAge",
-          TimeUnit.SECONDS.convert(5, TimeUnit.DAYS),
+          TimeUnit.SECONDS.convert(12, TimeUnit.HOURS),
           TimeUnit.SECONDS);
       emailReg = new SignedToken(age, key);
     } else {
@@ -140,6 +140,7 @@
       case LDAP:
       case LDAP_BIND:
       case CLIENT_SSL_CERT_LDAP:
+      case CUSTOM_EXTENSION:
         // Its safe to assume yes for an HTTP authentication type, as the
         // only way in is through some external system that the admin trusts
         //
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 7bca406..171d719 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
@@ -17,8 +17,6 @@
 import static com.google.inject.Scopes.SINGLETON;
 
 import com.google.gerrit.common.data.ApprovalTypes;
-import com.google.gerrit.lifecycle.LifecycleListener;
-import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.reviewdb.AuthType;
 import com.google.gerrit.rules.PrologModule;
 import com.google.gerrit.rules.RulesCache;
@@ -37,23 +35,18 @@
 import com.google.gerrit.server.account.GroupInfoCacheFactory;
 import com.google.gerrit.server.account.Realm;
 import com.google.gerrit.server.auth.ldap.LdapModule;
-import com.google.gerrit.server.cache.CachePool;
 import com.google.gerrit.server.events.EventFactory;
 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.PushReplication;
 import com.google.gerrit.server.git.ReloadSubmitQueueOp;
-import com.google.gerrit.server.git.ReplicationQueue;
 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.git.WorkQueue;
-import com.google.gerrit.server.mail.EmailSender;
 import com.google.gerrit.server.mail.FromAddressGenerator;
 import com.google.gerrit.server.mail.FromAddressGeneratorProvider;
-import com.google.gerrit.server.mail.SmtpEmailSender;
+import com.google.gerrit.server.mail.VelocityRuntimeProvider;
 import com.google.gerrit.server.patch.PatchListCacheImpl;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.project.AccessControlModule;
@@ -68,51 +61,14 @@
 import com.google.gerrit.server.workflow.FunctionState;
 import com.google.inject.Inject;
 
-import org.apache.velocity.app.Velocity;
-import org.apache.velocity.runtime.RuntimeConstants;
+import org.apache.velocity.runtime.RuntimeInstance;
 import org.eclipse.jgit.lib.Config;
 
-import java.util.Properties;
-
 
 /** Starts global state with standard dependencies. */
 public class GerritGlobalModule extends FactoryModule {
   private final AuthType loginType;
 
-  public static class VelocityLifecycle implements LifecycleListener {
-    private final SitePaths site;
-
-    @Inject
-    VelocityLifecycle(final SitePaths site) {
-      this.site = site;
-    }
-
-    @Override
-    public void start() {
-      String rl = "resource.loader";
-      String pkg = "org.apache.velocity.runtime.resource.loader";
-      Properties p = new Properties();
-
-      p.setProperty(rl, "file, class");
-      p.setProperty("file." + rl + ".class", pkg + ".FileResourceLoader");
-      p.setProperty("file." + rl + ".path", site.mail_dir.getAbsolutePath());
-      p.setProperty("class." + rl + ".class", pkg + ".ClasspathResourceLoader");
-      p.setProperty(RuntimeConstants.RUNTIME_LOG_LOGSYSTEM_CLASS,
-              "org.apache.velocity.runtime.log.SimpleLog4JLogSystem" );
-      p.setProperty("runtime.log.logsystem.log4j.category", "velocity");
-
-      try {
-        Velocity.init(p);
-      } catch(Exception e) {
-        throw new RuntimeException(e);
-      }
-    }
-
-    @Override
-    public void stop() {
-    }
-  }
-
   @Inject
   GerritGlobalModule(final AuthConfig authConfig,
       @GerritServerConfig final Config config) {
@@ -129,6 +85,9 @@
         install(new LdapModule());
         break;
 
+      case CUSTOM_EXTENSION:
+        break;
+
       default:
         bind(Realm.class).to(DefaultRealm.class);
         break;
@@ -140,7 +99,6 @@
         SINGLETON);
 
     bind(IdGenerator.class);
-    bind(CachePool.class);
     bind(RulesCache.class);
     install(AccountByEmailCacheImpl.module());
     install(AccountCacheImpl.module());
@@ -161,21 +119,22 @@
     bind(PermissionCollection.Factory.class);
 
     bind(FileTypeRegistry.class).to(MimeUtilFileTypeRegistry.class);
-    bind(WorkQueue.class);
     bind(ToolsCatalog.class);
     bind(EventFactory.class);
     bind(TransferConfig.class);
 
-    bind(ReplicationQueue.class).to(PushReplication.class).in(SINGLETON);
     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);
 
+    bind(RuntimeInstance.class)
+        .toProvider(VelocityRuntimeProvider.class)
+        .in(SINGLETON);
     bind(FromAddressGenerator.class).toProvider(
         FromAddressGeneratorProvider.class).in(SINGLETON);
-    bind(EmailSender.class).to(SmtpEmailSender.class).in(SINGLETON);
 
     bind(PatchSetInfoFactory.class);
     bind(IdentifiedUser.GenericFactory.class).in(SINGLETON);
@@ -183,14 +142,5 @@
     bind(ProjectControl.GenericFactory.class);
     factory(FunctionState.Factory.class);
     factory(ReplicationUser.Factory.class);
-
-    install(new LifecycleModule() {
-      @Override
-      protected void configure() {
-        listener().to(CachePool.Lifecycle.class);
-        listener().to(WorkQueue.Lifecycle.class);
-        listener().to(VelocityLifecycle.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 3ba9aab..54445c5 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
@@ -23,19 +23,26 @@
 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.GroupMembersFactory;
+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.CreateCodeReviewNotes;
 import com.google.gerrit.server.git.MergeOp;
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ReceiveCommits;
+import com.google.gerrit.server.git.SubmoduleOp;
 import com.google.gerrit.server.mail.AbandonedSender;
 import com.google.gerrit.server.mail.AddReviewerSender;
 import com.google.gerrit.server.mail.CommentSender;
 import com.google.gerrit.server.mail.CreateChangeSender;
 import com.google.gerrit.server.mail.MergeFailSender;
 import com.google.gerrit.server.mail.MergedSender;
-import com.google.gerrit.server.mail.RegisterNewEmailSender;
 import com.google.gerrit.server.mail.ReplacePatchSetSender;
 import com.google.gerrit.server.mail.RestoredSender;
 import com.google.gerrit.server.mail.RevertedSender;
@@ -43,8 +50,10 @@
 import com.google.gerrit.server.patch.PublishComments;
 import com.google.gerrit.server.patch.RemoveReviewer;
 import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.CreateProject;
 import com.google.gerrit.server.project.PerRequestProjectControlCache;
 import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.project.SuggestParentCandidates;
 import com.google.gerrit.server.query.change.ChangeQueryBuilder;
 import com.google.gerrit.server.query.change.ChangeQueryRewriter;
 import com.google.inject.servlet.RequestScoped;
@@ -69,27 +78,36 @@
 
     factory(ChangeQueryBuilder.Factory.class);
     factory(ReceiveCommits.Factory.class);
+    factory(SubmoduleOp.Factory.class);
     factory(MergeOp.Factory.class);
     factory(CreateCodeReviewNotes.Factory.class);
 
     // 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);
+    factory(DeleteDraftPatchSet.Factory.class);
     factory(PublishComments.Factory.class);
+    factory(PublishDraft.Factory.class);
     factory(ReplacePatchSetSender.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);
     factory(MergedSender.Factory.class);
     factory(MergeFailSender.Factory.class);
-    factory(RegisterNewEmailSender.Factory.class);
     factory(PerformCreateGroup.Factory.class);
+    factory(PerformRenameGroup.Factory.class);
+    factory(VisibleGroups.Factory.class);
     factory(GroupDetailFactory.Factory.class);
-    factory(GroupMembersFactory.Factory.class);
+    factory(GroupMembers.Factory.class);
+    factory(CreateProject.Factory.class);
+    factory(Submit.Factory.class);
+    factory(SuggestParentCandidates.Factory.class);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/RequestScopedReviewDbProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/RequestScopedReviewDbProvider.java
index 5aa78cb..9c3da74 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/RequestScopedReviewDbProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/RequestScopedReviewDbProvider.java
@@ -17,7 +17,7 @@
 import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.server.RequestCleanup;
 import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.jdbc.Database;
+import com.google.gwtorm.client.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.ProvisionException;
@@ -26,11 +26,11 @@
 /** Provides {@link ReviewDb} database handle live only for this request. */
 @Singleton
 final class RequestScopedReviewDbProvider implements Provider<ReviewDb> {
-  private final Database<ReviewDb> schema;
+  private final SchemaFactory<ReviewDb> schema;
   private final Provider<RequestCleanup> cleanup;
 
   @Inject
-  RequestScopedReviewDbProvider(final Database<ReviewDb> schema,
+  RequestScopedReviewDbProvider(final SchemaFactory<ReviewDb> schema,
       final Provider<RequestCleanup> cleanup) {
     this.schema = schema;
     this.cleanup = cleanup;
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 c3a5fb7..ab52a9d 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
@@ -19,6 +19,7 @@
 
 import java.io.File;
 import java.io.FileNotFoundException;
+import java.io.IOException;
 
 /** Important paths within a {@link SitePath}. */
 @Singleton
@@ -112,7 +113,11 @@
       if (!loc.isAbsolute()) {
         loc = new File(site_path, path);
       }
-      return loc;
+      try {
+        return loc.getCanonicalFile();
+      } catch (IOException e) {
+        return loc.getAbsoluteFile();
+      }
     }
     return null;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/contact/ContactStoreConnection.java b/gerrit-server/src/main/java/com/google/gerrit/server/contact/ContactStoreConnection.java
new file mode 100644
index 0000000..24c9e66
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/contact/ContactStoreConnection.java
@@ -0,0 +1,42 @@
+// 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.contact;
+
+import java.io.IOException;
+import java.net.URL;
+
+/** Single connection to a {@link ContactStore}. */
+public interface ContactStoreConnection {
+  public static interface Factory {
+    /**
+     * Open a new connection to a {@link ContactStore}.
+     *
+     * @param url contact store URL.
+     * @return a new connection to the store.
+     *
+     * @throws IOException the URL couldn't be opened.
+     */
+    ContactStoreConnection open(URL url) throws IOException;
+  }
+
+  /**
+   * Store a blob of contact data in the store.
+   *
+   * @param body protocol-specific body data.
+   *
+   * @throws IOException an error occurred storing the contact data.
+   */
+  public void store(byte[] body) throws IOException;
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/contact/ContactStoreProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/contact/ContactStoreProvider.java
index da17c08..73dd46e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/contact/ContactStoreProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/contact/ContactStoreProvider.java
@@ -22,31 +22,39 @@
 import com.google.inject.Provider;
 import com.google.inject.ProvisionException;
 
+import org.bouncycastle.jce.provider.BouncyCastleProvider;
 import org.bouncycastle.openpgp.PGPPublicKey;
 import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.util.StringUtils;
 
 import java.io.File;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
 import java.net.MalformedURLException;
 import java.net.URL;
+import java.security.Security;
 
 /** Creates the {@link ContactStore} based on the configuration. */
 public class ContactStoreProvider implements Provider<ContactStore> {
   private final Config config;
   private final SitePaths site;
   private final SchemaFactory<ReviewDb> schema;
+  private final ContactStoreConnection.Factory connFactory;
 
   @Inject
   ContactStoreProvider(@GerritServerConfig final Config config,
-      final SitePaths site, final SchemaFactory<ReviewDb> schema) {
+      final SitePaths site, final SchemaFactory<ReviewDb> schema,
+      final ContactStoreConnection.Factory connFactory) {
     this.config = config;
     this.site = site;
     this.schema = schema;
+    this.connFactory = connFactory;
   }
 
   @Override
   public ContactStore get() {
     final String url = config.getString("contactstore", null, "url");
-    if (url == null) {
+    if (StringUtils.isEmptyOrNull(url)) {
       return new NoContactStore();
     }
 
@@ -68,17 +76,39 @@
       throw new ProvisionException("PGP public key file \""
           + pubkey.getAbsolutePath() + "\" not found");
     }
-    return new EncryptedContactStore(storeUrl, storeAPPSEC, pubkey, schema);
+    return new EncryptedContactStore(storeUrl, storeAPPSEC, pubkey, schema,
+        connFactory);
   }
 
   private static boolean havePGP() {
     try {
       Class.forName(PGPPublicKey.class.getName());
+      addBouncyCastleProvider();
       return true;
     } catch (NoClassDefFoundError noBouncyCastle) {
       return false;
     } catch (ClassNotFoundException noBouncyCastle) {
       return false;
+    } catch (SecurityException noBouncyCastle) {
+      return false;
+    } catch (NoSuchMethodException noBouncyCastle) {
+      return false;
+    } catch (InstantiationException noBouncyCastle) {
+      return false;
+    } catch (IllegalAccessException noBouncyCastle) {
+      return false;
+    } catch (InvocationTargetException noBouncyCastle) {
+      return false;
+    } catch (ClassCastException noBouncyCastle) {
+      return false;
     }
   }
+
+  private static void addBouncyCastleProvider() throws ClassNotFoundException,
+          SecurityException, NoSuchMethodException, InstantiationException,
+          IllegalAccessException, InvocationTargetException {
+    Class<?> clazz = Class.forName(BouncyCastleProvider.class.getName());
+    Constructor<?> constructor = clazz.getConstructor();
+    Security.addProvider((java.security.Provider) constructor.newInstance());
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/contact/EncryptedContactStore.java b/gerrit-server/src/main/java/com/google/gerrit/server/contact/EncryptedContactStore.java
index 05d4e7a..d391694 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/contact/EncryptedContactStore.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/contact/EncryptedContactStore.java
@@ -37,7 +37,6 @@
 import org.bouncycastle.openpgp.PGPPublicKeyRing;
 import org.bouncycastle.openpgp.PGPPublicKeyRingCollection;
 import org.bouncycastle.openpgp.PGPUtil;
-import org.eclipse.jgit.util.IO;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -47,7 +46,6 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
-import java.net.HttpURLConnection;
 import java.net.URL;
 import java.security.NoSuchAlgorithmException;
 import java.security.NoSuchProviderException;
@@ -70,13 +68,16 @@
   private final SecureRandom prng;
   private final URL storeUrl;
   private final String storeAPPSEC;
+  private final ContactStoreConnection.Factory connFactory;
 
   EncryptedContactStore(final URL storeUrl, final String storeAPPSEC,
-      final File pubKey, final SchemaFactory<ReviewDb> schema) {
+      final File pubKey, final SchemaFactory<ReviewDb> schema,
+      final ContactStoreConnection.Factory connFactory) {
     this.storeUrl = storeUrl;
     this.storeAPPSEC = storeAPPSEC;
     this.schema = schema;
     this.dest = selectKey(readPubRing(pubKey));
+    this.connFactory = connFactory;
 
     final String prngName = "SHA1PRNG";
     try {
@@ -157,33 +158,7 @@
       }
       u.put("account_id", String.valueOf(account.getId().get()));
       u.put("data", encStr);
-      final byte[] body = u.toString().getBytes("UTF-8");
-
-      final HttpURLConnection c = (HttpURLConnection) storeUrl.openConnection();
-      c.setRequestMethod("POST");
-      c.setRequestProperty("Content-Type",
-          "application/x-www-form-urlencoded; charset=UTF-8");
-      c.setDoOutput(true);
-      c.setFixedLengthStreamingMode(body.length);
-      final OutputStream out = c.getOutputStream();
-      out.write(body);
-      out.close();
-
-      if (c.getResponseCode() == 200) {
-        final byte[] dst = new byte[2];
-        final InputStream in = c.getInputStream();
-        try {
-          IO.readFully(in, dst, 0, 2);
-        } finally {
-          in.close();
-        }
-        if (dst[0] != 'O' || dst[1] != 'K') {
-          throw new IOException("Store failed: " + c.getResponseCode());
-        }
-      } else {
-        throw new IOException("Store failed: " + c.getResponseCode());
-      }
-
+      connFactory.open(storeUrl).store(u.toString().getBytes("UTF-8"));
     } catch (IOException e) {
       log.error("Cannot store encrypted contact information", e);
       throw new ContactInformationStoreException(e);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/contact/HttpContactStoreConnection.java b/gerrit-server/src/main/java/com/google/gerrit/server/contact/HttpContactStoreConnection.java
new file mode 100644
index 0000000..781f401
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/contact/HttpContactStoreConnection.java
@@ -0,0 +1,74 @@
+// Copyright 2011 Google Inc. All Rights Reserved.
+
+package com.google.gerrit.server.contact;
+
+import static com.google.inject.Scopes.SINGLETON;
+
+import com.google.gerrit.server.contact.ContactStoreConnection;
+import com.google.gerrit.server.contact.HttpContactStoreConnection;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import com.google.inject.Module;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.FactoryProvider;
+
+import org.eclipse.jgit.util.IO;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.net.HttpURLConnection;
+import java.net.URL;
+import java.net.URLConnection;
+
+/** {@link ContactStoreConnection} with an underlying {@HttpURLConnection}. */
+public class HttpContactStoreConnection implements ContactStoreConnection {
+  public static Module module() {
+    return new AbstractModule() {
+      @Override
+      protected void configure() {
+        bind(ContactStoreConnection.Factory.class)
+            .toProvider(FactoryProvider.newFactory(
+                ContactStoreConnection.Factory.class,
+                HttpContactStoreConnection.class))
+            .in(SINGLETON);
+      }
+    };
+  }
+
+  private final HttpURLConnection conn;
+
+  @Inject
+  HttpContactStoreConnection(@Assisted final URL url) throws IOException {
+    final URLConnection urlConn = url.openConnection();
+    if (!(urlConn instanceof HttpURLConnection)) {
+      throw new IllegalArgumentException("Non-HTTP URL not supported: " + urlConn);
+    }
+    conn = (HttpURLConnection) urlConn;
+  }
+
+  @Override
+  public void store(final byte[] body) throws IOException {
+    conn.setRequestMethod("POST");
+    conn.setRequestProperty("Content-Type",
+        "application/x-www-form-urlencoded; charset=UTF-8");
+    conn.setDoOutput(true);
+    conn.setFixedLengthStreamingMode(body.length);
+    final OutputStream out = conn.getOutputStream();
+    out.write(body);
+    out.close();
+    if (conn.getResponseCode() != HttpURLConnection.HTTP_OK) {
+      throw new IOException("Connection failed: " + conn.getResponseCode());
+    }
+    final byte[] dst = new byte[2];
+    final InputStream in = conn.getInputStream();
+    try {
+      IO.readFully(in, dst, 0, 2);
+    } finally {
+      in.close();
+    }
+    if (dst[0] != 'O' || dst[1] != 'K') {
+      throw new IOException("Store failed: " + dst[0] + dst[1]);
+    }
+  }
+}
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 e9a977e..9050641 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
@@ -27,6 +27,7 @@
     public String subject;
     public AccountAttribute owner;
     public String url;
+    public String commitMessage;
 
     public Long createdOn;
     public Long lastUpdated;
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 6b60ee1..cf035c8 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
@@ -26,6 +26,9 @@
 import com.google.gerrit.reviewdb.TrackingId;
 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.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -43,14 +46,17 @@
   private final AccountCache accountCache;
   private final Provider<String> urlProvider;
   private final ApprovalTypes approvalTypes;
+  private final PatchListCache patchListCache;
 
   @Inject
   EventFactory(AccountCache accountCache,
       @CanonicalWebUrl @Nullable Provider<String> urlProvider,
-      ApprovalTypes approvalTypes) {
+      ApprovalTypes approvalTypes,
+      PatchListCache patchListCache) {
     this.accountCache = accountCache;
     this.urlProvider = urlProvider;
     this.approvalTypes = approvalTypes;
+    this.patchListCache = patchListCache;
   }
 
   /**
@@ -113,12 +119,23 @@
     }
   }
 
+  public void addCommitMessage(ChangeAttribute a, String commitMessage) {
+    a.commitMessage = commitMessage;
+  }
+
   public void addPatchSets(ChangeAttribute a, Collection<PatchSet> ps) {
-    addPatchSets(a, ps, null);
+    addPatchSets(a, ps, null, false, null);
   }
 
   public void addPatchSets(ChangeAttribute ca, Collection<PatchSet> ps,
       Map<PatchSet.Id,Collection<PatchSetApproval>> approvals) {
+    addPatchSets(ca, ps, approvals, false, null);
+  }
+
+  public void addPatchSets(ChangeAttribute ca, Collection<PatchSet> ps,
+      Map<PatchSet.Id,Collection<PatchSetApproval>> approvals,
+      boolean includeFiles, Change change) {
+
     if (!ps.isEmpty()) {
       ca.patchSets = new ArrayList<PatchSetAttribute>(ps.size());
       for (PatchSet p : ps) {
@@ -127,6 +144,9 @@
           addApprovals(psa, p.getId(), approvals);
         }
         ca.patchSets.add(psa);
+        if (includeFiles && change != null) {
+          addPatchSetFileNames(psa, change, p);
+        }
       }
     }
   }
@@ -145,6 +165,21 @@
     }
   }
 
+  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>();
+      }
+
+      PatchAttribute p = new PatchAttribute();
+      p.file = patch.getNewName();
+      p.type = patch.getChangeType();
+      patchSetAttribute.files.add(p);
+    }
+  }
+
   public void addComments(ChangeAttribute ca,
       Collection<ChangeMessage> messages) {
     if (!messages.isEmpty()) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/PatchAttribute.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/PatchAttribute.java
new file mode 100644
index 0000000..2b08fa7
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/PatchAttribute.java
@@ -0,0 +1,22 @@
+// 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.events;
+
+import com.google.gerrit.reviewdb.Patch.ChangeType;
+
+public class PatchAttribute {
+    public String file;
+    public ChangeType type;
+}
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 ee29314..dca4438 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
@@ -25,4 +25,5 @@
 
     public List<ApprovalAttribute> approvals;
     public List<PatchSetCommentAttribute> comments;
+    public List<PatchAttribute> files;
 }
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 cbe6594..0bcd37b 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
@@ -29,6 +29,7 @@
 import com.google.inject.Injector;
 import com.google.inject.OutOfScopeException;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 import com.google.inject.servlet.RequestScoped;
 
 import com.jcraft.jsch.HostKey;
@@ -43,6 +44,7 @@
 import java.util.Map;
 import java.util.concurrent.TimeUnit;
 
+@Singleton
 public class ChangeMergeQueue implements MergeQueue {
   private static final Logger log =
       LoggerFactory.getLogger(ChangeMergeQueue.class);
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 fb88ad6..129dfa9 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
@@ -24,6 +24,7 @@
 import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gwtorm.client.OrmException;
 import com.google.gwtorm.client.ResultSet;
@@ -76,6 +77,7 @@
   private final AccountCache accountCache;
   private final ApprovalTypes approvalTypes;
   private final String canonicalWebUrl;
+  private final String anonymousCowardName;
   private final Repository db;
   private final RevWalk revWalk;
   private final ObjectInserter inserter;
@@ -95,15 +97,17 @@
       @GerritPersonIdent final PersonIdent gerritIdent,
       final AccountCache accountCache,
       final ApprovalTypes approvalTypes,
-      @Nullable @CanonicalWebUrl final String canonicalWebUrl,
-      @Assisted  ReviewDb reviewDb,
-      @Assisted final Repository db) {
+      final @Nullable @CanonicalWebUrl String canonicalWebUrl,
+      final @AnonymousCowardName String anonymousCowardName,
+      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.db = db;
 
     revWalk = new RevWalk(db);
@@ -185,7 +189,8 @@
       throws CodeReviewNoteCreationException, IOException {
     try {
       ReviewNoteHeaderFormatter formatter =
-        new ReviewNoteHeaderFormatter(author.getTimeZone());
+          new ReviewNoteHeaderFormatter(author.getTimeZone(),
+              anonymousCowardName);
       final List<String> idList = commit.getFooterLines(CHANGE_ID);
       if (idList.isEmpty())
         formatter.appendChangeId(change.getKey());
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 268eed3..be444f8 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
@@ -15,9 +15,11 @@
 package com.google.gerrit.server.git;
 
 import com.google.gerrit.lifecycle.LifecycleListener;
+import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.reviewdb.Project;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
+import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -54,6 +56,20 @@
   private static final String UNNAMED =
       "Unnamed repository; edit this file to name it for gitweb.";
 
+  public static class Module extends AbstractModule {
+    @Override
+    protected void configure() {
+      bind(GitRepositoryManager.class).to(LocalDiskRepositoryManager.class);
+
+      install(new LifecycleModule() {
+        @Override
+        protected void configure() {
+          listener().to(LocalDiskRepositoryManager.Lifecycle.class);
+        }
+      });
+    }
+  }
+
   public static class Lifecycle implements LifecycleListener {
     private final Config cfg;
 
@@ -100,8 +116,13 @@
       throw new RepositoryNotFoundException("Invalid name: " + name);
     }
 
+    final FileKey loc = FileKey.lenient(gitDirOf(name), FS.DETECTED);
+
+    if (!getProjectName(loc.getFile()).equals(name)) {
+      throw new RepositoryNotFoundException(gitDirOf(name));
+    }
+
     try {
-      final FileKey loc = FileKey.lenient(gitDirOf(name), FS.DETECTED);
       return RepositoryCache.open(loc);
     } catch (IOException e1) {
       final RepositoryNotFoundException e2;
@@ -117,24 +138,30 @@
       throw new RepositoryNotFoundException("Invalid name: " + name);
     }
 
-    try {
-      File dir = FileKey.resolve(gitDirOf(name), FS.DETECTED);
-      FileKey loc;
-      if (dir != null) {
-        // Already exists on disk, use the repository we found.
-        //
-        loc = FileKey.exact(dir, FS.DETECTED);
-      } else {
-        // It doesn't exist under any of the standard permutations
-        // of the repository name, so prefer the standard bare name.
-        //
-        String n = name.get();
-        if (!n.endsWith(Constants.DOT_GIT_EXT)) {
-          n = n + Constants.DOT_GIT_EXT;
-        }
-        loc = FileKey.exact(new File(basePath, n), FS.DETECTED);
-      }
+    File dir = FileKey.resolve(gitDirOf(name), FS.DETECTED);
+    FileKey loc;
+    if (dir != null) {
+      // Already exists on disk, use the repository we found.
+      //
+      loc = FileKey.exact(dir, FS.DETECTED);
 
+      final Project.NameKey nameOfExistingProject =
+          getProjectName(loc.getFile());
+      if (!nameOfExistingProject.equals(name)) {
+        throw new RepositoryCaseMismatchException(name, nameOfExistingProject);
+      }
+    } else {
+      // It doesn't exist under any of the standard permutations
+      // of the repository name, so prefer the standard bare name.
+      //
+      String n = name.get();
+      if (!n.endsWith(Constants.DOT_GIT_EXT)) {
+        n = n + Constants.DOT_GIT_EXT;
+      }
+      loc = FileKey.exact(new File(basePath, n), FS.DETECTED);
+    }
+
+    try {
       Repository db = RepositoryCache.open(loc, false);
       db.create(true /* bare */);
 
@@ -256,19 +283,7 @@
     for (File f : ls) {
       String fileName = f.getName();
       if (FileKey.isGitRepository(f, FS.DETECTED)) {
-        String projectName;
-        if (fileName.equals(Constants.DOT_GIT)) {
-          projectName = prefix.substring(0, prefix.length() - 1);
-
-        } else if (fileName.endsWith(Constants.DOT_GIT_EXT)) {
-          int newLen = fileName.length() - Constants.DOT_GIT_EXT.length();
-          projectName = prefix + fileName.substring(0, newLen);
-
-        } else {
-          projectName = prefix + fileName;
-        }
-
-        Project.NameKey nameKey = new Project.NameKey(projectName);
+        Project.NameKey nameKey = getProjectName(prefix, fileName);
         if (isUnreasonableName(nameKey)) {
           log.warn("Ignoring unreasonably named repository " + f.getAbsolutePath());
         } else {
@@ -280,4 +295,33 @@
       }
     }
   }
+
+  private Project.NameKey getProjectName(final String prefix,
+      final String fileName) {
+    final String projectName;
+    if (fileName.equals(Constants.DOT_GIT)) {
+      projectName = prefix.substring(0, prefix.length() - 1);
+
+    } else if (fileName.endsWith(Constants.DOT_GIT_EXT)) {
+      int newLen = fileName.length() - Constants.DOT_GIT_EXT.length();
+      projectName = prefix + fileName.substring(0, newLen);
+
+    } else {
+      projectName = prefix + fileName;
+    }
+
+    return new Project.NameKey(projectName);
+  }
+
+  private Project.NameKey getProjectName(final File gitDir) {
+    String relativeGitPath =
+        getBasePath().toURI().relativize(gitDir.toURI()).getPath();
+    if (!relativeGitPath.endsWith("/")) {
+      relativeGitPath = relativeGitPath + "/";
+    }
+    final String prefix =
+        relativeGitPath.substring(0, relativeGitPath.length() - 1
+            - gitDir.getName().length());
+    return getProjectName(prefix, gitDir.getName());
+  }
 }
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 b7ece9b..c0f5f182 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
@@ -17,7 +17,7 @@
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static java.util.concurrent.TimeUnit.MINUTES;
 
-import com.google.gerrit.common.ChangeHookRunner;
+import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.common.data.ApprovalType;
 import com.google.gerrit.common.data.ApprovalTypes;
 import com.google.gerrit.common.data.Capable;
@@ -27,6 +27,7 @@
 import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.reviewdb.ChangeMessage;
 import com.google.gerrit.reviewdb.PatchSet;
+import com.google.gerrit.reviewdb.PatchSetAncestor;
 import com.google.gerrit.reviewdb.PatchSetApproval;
 import com.google.gerrit.reviewdb.Project;
 import com.google.gerrit.reviewdb.RevId;
@@ -158,10 +159,11 @@
   private Set<RevCommit> alreadyAccepted;
   private RefUpdate branchUpdate;
 
-  private final ChangeHookRunner hooks;
+  private final ChangeHooks hooks;
   private final AccountCache accountCache;
   private final TagCache tagCache;
   private final CreateCodeReviewNotes.Factory codeReviewNotesFactory;
+  private final SubmoduleOp.Factory subOpFactory;
 
   @Inject
   MergeOp(final GitRepositoryManager grm, final SchemaFactory<ReviewDb> sf,
@@ -174,8 +176,9 @@
       final ChangeControl.GenericFactory changeControlFactory,
       @GerritPersonIdent final PersonIdent myIdent,
       final MergeQueue mergeQueue, @Assisted final Branch.NameKey branch,
-      final ChangeHookRunner hooks, final AccountCache accountCache,
-      final TagCache tagCache, final CreateCodeReviewNotes.Factory crnf) {
+      final ChangeHooks hooks, final AccountCache accountCache,
+      final TagCache tagCache, final CreateCodeReviewNotes.Factory crnf,
+      final SubmoduleOp.Factory subOpFactory) {
     repoManager = grm;
     schemaFactory = sf;
     functionState = fs;
@@ -193,6 +196,7 @@
     this.accountCache = accountCache;
     this.tagCache = tagCache;
     codeReviewNotesFactory = crnf;
+    this.subOpFactory = subOpFactory;
 
     this.myIdent = myIdent;
     destBranch = branch;
@@ -268,6 +272,7 @@
       preMerge();
       updateBranch();
       updateChangeStatus();
+      updateSubscriptions();
     } catch (OrmException e) {
       throw new MergeException("Cannot query the database", e);
     } finally {
@@ -282,7 +287,7 @@
     }
   }
 
-  private void preMerge() throws MergeException {
+  private void preMerge() throws MergeException, OrmException {
     openBranch();
     validateChangeList();
     mergeTip = branchTip;
@@ -674,7 +679,7 @@
     }
   }
 
-  private void cherryPickChanges() throws MergeException {
+  private void cherryPickChanges() throws MergeException, OrmException {
     while (!toMerge.isEmpty()) {
       final CodeReviewCommit n = toMerge.remove(0);
       final ThreeWayMerger m;
@@ -758,7 +763,7 @@
   }
 
   private void writeCherryPickCommit(final Merger m, final CodeReviewCommit n)
-      throws IOException {
+      throws IOException, OrmException {
     rw.parseBody(n);
 
     final List<FooterLine> footers = n.getFooterLines();
@@ -799,8 +804,9 @@
     }
 
     PatchSetApproval submitAudit = null;
+    List<PatchSetApproval> approvalList = null;
     try {
-      final List<PatchSetApproval> approvalList =
+      approvalList =
           schema.patchSetApprovals().byPatchSet(n.patchsetId).toList();
       Collections.sort(approvalList, new Comparator<PatchSetApproval>() {
         public int compare(final PatchSetApproval a, final PatchSetApproval b) {
@@ -885,14 +891,63 @@
 
     final ObjectId id = commit(m, mergeCommit);
     final CodeReviewCommit newCommit = (CodeReviewCommit) rw.parseCommit(id);
+
+    n.change =
+        schema.changes().atomicUpdate(n.change.getId(),
+            new AtomicUpdate<Change>() {
+              @Override
+              public Change update(Change change) {
+                change.nextPatchSetId();
+                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);
+    schema.patchSets().insert(Collections.singleton(ps));
+
+    n.change =
+        schema.changes().atomicUpdate(n.change.getId(),
+            new AtomicUpdate<Change>() {
+              @Override
+              public Change update(Change change) {
+                change.setCurrentPatchSet(patchSetInfoFactory.get(newCommit,
+                    ps.getId()));
+                return change;
+              }
+            });
+
+    if (approvalList != null) {
+      for (PatchSetApproval a : approvalList) {
+        schema.patchSetApprovals().insert(
+            Collections.singleton(new PatchSetApproval(ps.getId(), a)));
+      }
+    }
+
     newCommit.copyFrom(n);
     newCommit.statusCode = CommitMergeStatus.CLEAN_PICK;
     commits.put(newCommit.patchsetId.getParentKey(), newCommit);
-
     mergeTip = newCommit;
     setRefLogIdent(submitAudit);
   }
 
+  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).getId().name()));
+      toInsert.add(a);
+    }
+    schema.patchSetAncestors().insert(toInsert);
+  }
+
   private ObjectId commit(final Merger m, final CommitBuilder mergeCommit)
       throws IOException, UnsupportedEncodingException {
     ObjectInserter oi = m.getObjectInserter();
@@ -1066,6 +1121,21 @@
         GitRepositoryManager.REFS_NOTES_REVIEW);
   }
 
+  private void updateSubscriptions() throws MergeException {
+    if (mergeTip != null && (branchTip == null || branchTip != mergeTip)) {
+      SubmoduleOp subOp =
+          subOpFactory.create(destBranch, mergeTip, rw, db, destProject,
+              submitted, commits);
+      try {
+        subOp.update();
+      } catch (SubmoduleException e) {
+        log
+            .error("The gitLinks were not updated according to the subscriptions "
+                + e.getMessage());
+      }
+    }
+  }
+
   private Capable isSubmitStillPossible(final CodeReviewCommit commit) {
     final Capable capable;
     final Change c = commit.change;
@@ -1209,7 +1279,8 @@
       return null;
     }
     final ChangeMessage m =
-        new ChangeMessage(new ChangeMessage.Key(c.getId(), uuid), null);
+        new ChangeMessage(new ChangeMessage.Key(c.getId(), uuid), null,
+            c.currentPatchSetId());
     m.setMessage(body);
     return m;
   }
@@ -1238,7 +1309,10 @@
 
   private void setMerged(Change c, ChangeMessage msg) {
     final Change.Id changeId = c.getId();
-    final PatchSet.Id merged = c.currentPatchSetId();
+    // We must pull the patchset out of commits, because the patchset ID is
+    // modified when using the cherry-pick merge strategy.
+    final CodeReviewCommit commit = commits.get(c.getId());
+    final PatchSet.Id merged = commit.change.currentPatchSetId();
 
     try {
       schema.changes().atomicUpdate(changeId, new AtomicUpdate<Change>() {
@@ -1254,7 +1328,7 @@
             // Go back to the patch set that was actually merged.
             //
             try {
-              c.setCurrentPatchSet(patchSetInfoFactory.get(merged));
+              c.setCurrentPatchSet(patchSetInfoFactory.get(schema, merged));
             } catch (PatchSetInfoNotAvailableException e1) {
               log.error("Cannot read merged patch set " + merged, e1);
             }
@@ -1332,7 +1406,7 @@
     try {
       hooks.doChangeMergedHook(c, //
           accountCache.get(submitter.getAccountId()).getAccount(), //
-          schema.patchSets().get(c.currentPatchSetId()));
+          schema.patchSets().get(c.currentPatchSetId()), schema);
     } catch (OrmException ex) {
       log.error("Cannot run hook for submitted patch set " + c.getId(), ex);
     }
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 164fb05..7131012 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
@@ -24,6 +24,7 @@
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.reviewdb.AccountGroup;
 import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.reviewdb.Project.State;
 import com.google.gerrit.reviewdb.Project.SubmitType;
 import com.google.gerrit.server.account.GroupCache;
 
@@ -66,9 +67,12 @@
   private static final String SUBMIT = "submit";
   private static final String KEY_ACTION = "action";
   private static final String KEY_MERGE_CONTENT = "mergeContent";
+  private static final String KEY_STATE = "state";
 
   private static final SubmitType defaultSubmitAction =
       SubmitType.MERGE_IF_NECESSARY;
+  private static final State defaultStateValue =
+      State.ACTIVE;
 
   private Project.NameKey projectName;
   private Project project;
@@ -217,6 +221,7 @@
 
     p.setSubmitType(getEnum(rc, SUBMIT, null, KEY_ACTION, defaultSubmitAction));
     p.setUseContentMerge(getBoolean(rc, SUBMIT, KEY_MERGE_CONTENT, false));
+    p.setState(getEnum(rc, PROJECT, null, KEY_STATE, defaultStateValue));
 
     accessSections = new HashMap<String, AccessSection>();
     for (String refName : rc.getSubsections(ACCESS)) {
@@ -341,6 +346,8 @@
     set(rc, SUBMIT, null, KEY_ACTION, p.getSubmitType(), defaultSubmitAction);
     set(rc, SUBMIT, null, KEY_MERGE_CONTENT, p.isUseContentMerge());
 
+    set(rc, PROJECT, null, KEY_STATE, p.getState(), null);
+
     Set<AccountGroup.UUID> keepGroups = new HashSet<AccountGroup.UUID>();
     AccessSection capability = accessSections.get(AccessSection.GLOBAL_CAPABILITIES);
     if (capability != null) {
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
index 4bff8eb..5279ae4 100644
--- 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
@@ -314,7 +314,7 @@
         return Collections.emptyList();
       }
       try {
-        local = new VisibleRefFilter(tagCache, db, pc, meta, true).filter(local);
+        local = new VisibleRefFilter(tagCache, db, pc, meta, true).filter(local, true);
       } finally {
         meta.close();
       }
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
index a3d327a..154c609 100644
--- 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
@@ -39,8 +39,10 @@
 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;
@@ -72,6 +74,13 @@
 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.
@@ -263,16 +272,43 @@
   }
 
   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());
 
-    if (!usingSSH(replicateURI)) {
-      log.warn("Cannot create new project on remote site since the connection "
-          + "method is not SSH: " + replicateURI.toString());
-      return;
-    }
-
     OutputStream errStream = createErrStream();
     String cmd =
         "mkdir -p " + projectPath + "&& cd " + projectPath
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 e5d063a..8450dc5 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,11 +14,12 @@
 
 package com.google.gerrit.server.git;
 
-import com.google.gerrit.common.ChangeHookRunner;
+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.Account;
 import com.google.gerrit.reviewdb.ApprovalCategory;
@@ -32,6 +33,7 @@
 import com.google.gerrit.reviewdb.Project;
 import com.google.gerrit.reviewdb.RevId;
 import com.google.gerrit.reviewdb.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;
@@ -48,6 +50,7 @@
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.project.RefControl;
+import com.google.gerrit.server.util.MagicBranch;
 import com.google.gwtorm.client.AtomicUpdate;
 import com.google.gwtorm.client.OrmException;
 import com.google.inject.Inject;
@@ -74,8 +77,8 @@
 import org.eclipse.jgit.transport.PostReceiveHook;
 import org.eclipse.jgit.transport.PreReceiveHook;
 import org.eclipse.jgit.transport.ReceiveCommand;
-import org.eclipse.jgit.transport.ReceivePack;
 import org.eclipse.jgit.transport.ReceiveCommand.Result;
+import org.eclipse.jgit.transport.ReceivePack;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -99,7 +102,6 @@
   private static final Logger log =
       LoggerFactory.getLogger(ReceiveCommits.class);
 
-  public static final String NEW_CHANGE = "refs/for/";
   private static final Pattern NEW_PATCHSET =
       Pattern.compile("^refs/changes/(?:[0-9][0-9]/)?([1-9][0-9]*)(?:/new)?$");
 
@@ -123,7 +125,7 @@
   private final ReplacePatchSetSender.Factory replacePatchSetFactory;
   private final ReplicationQueue replication;
   private final PatchSetInfoFactory patchSetInfoFactory;
-  private final ChangeHookRunner hooks;
+  private final ChangeHooks hooks;
   private final GitRepositoryManager repoManager;
   private final ProjectCache projectCache;
   private final String canonicalWebUrl;
@@ -141,7 +143,7 @@
   private Branch.NameKey destBranch;
   private RefControl destBranchCtl;
 
-  private final List<Change.Id> allNewChanges = new ArrayList<Change.Id>();
+  private final List<Change> allNewChanges = new ArrayList<Change>();
   private final Map<Change.Id, ReplaceRequest> replaceByChange =
       new HashMap<Change.Id, ReplaceRequest>();
   private final Map<RevCommit, ReplaceRequest> replaceByCommit =
@@ -152,6 +154,8 @@
 
   private String destTopicName;
 
+  private final SubmoduleOp.Factory subOpFactory;
+
   @Inject
   ReceiveCommits(final ReviewDb db, final ApprovalTypes approvalTypes,
       final AccountResolver accountResolver,
@@ -160,7 +164,7 @@
       final ReplacePatchSetSender.Factory replacePatchSetFactory,
       final ReplicationQueue replication,
       final PatchSetInfoFactory patchSetInfoFactory,
-      final ChangeHookRunner hooks,
+      final ChangeHooks hooks,
       final ProjectCache projectCache,
       final GitRepositoryManager repoManager,
       final TagCache tagCache,
@@ -169,7 +173,8 @@
       final TrackingFooters trackingFooters,
 
       @Assisted final ProjectControl projectControl,
-      @Assisted final Repository repo) throws IOException {
+      @Assisted final Repository repo,
+      final SubmoduleOp.Factory subOpFactory) throws IOException {
     this.currentUser = (IdentifiedUser) projectControl.getCurrentUser();
     this.db = db;
     this.approvalTypes = approvalTypes;
@@ -193,6 +198,8 @@
     this.rp = new ReceivePack(repo);
     this.rejectCommits = loadRejectCommitsMap();
 
+    this.subOpFactory = subOpFactory;
+
     rp.setAllowCreates(true);
     rp.setAllowDeletes(true);
     rp.setAllowNonFastForwards(true);
@@ -304,29 +311,7 @@
       return result;
     }
 
-    // Don't permit receive-pack to be executed if a refs/for/branch_name
-    // reference exists in the destination repository. These block the
-    // client from being able to even send us a pack file, as it is very
-    // unlikely the user passed the --force flag and the new commit is
-    // probably not going to fast-forward the branch.
-    //
-    Map<String, Ref> blockingFors;
-    try {
-      blockingFors = repo.getRefDatabase().getRefs("refs/for/");
-    } catch (IOException err) {
-      String projName = project.getName();
-      log.warn("Cannot scan refs in '" + projName + "'", err);
-      return new Capable("Server process cannot read '" + projName + "'");
-    }
-    if (!blockingFors.isEmpty()) {
-      String projName = project.getName();
-      log.error("Repository '" + projName
-          + "' needs the following refs removed to receive changes: "
-          + blockingFors.keySet());
-      return new Capable("One or more refs/for/ names blocks change upload");
-    }
-
-    return Capable.OK;
+    return MagicBranch.checkMagicBranchRefs(repo, project);
   }
 
   @Override
@@ -376,7 +361,7 @@
               ps.getProject().getDescription());
         }
 
-        if (!c.getRefName().startsWith(NEW_CHANGE)) {
+        if (!MagicBranch.isMagicBranch(c.getRefName())) {
           // We only schedule direct refs updates for replication.
           // Change refs are scheduled when they are created.
           //
@@ -391,8 +376,13 @@
       final String url = canonicalWebUrl;
       rp.sendMessage("");
       rp.sendMessage("New Changes:");
-      for (final Change.Id c : allNewChanges) {
-        rp.sendMessage("  " + url + c.get());
+      for (final Change c : allNewChanges) {
+        if (c.getStatus() == Change.Status.DRAFT) {
+          rp.sendMessage("  " + url + c.getChangeId() + " [DRAFT]");
+        }
+        else {
+          rp.sendMessage("  " + url + c.getChangeId());
+        }
       }
       rp.sendMessage("");
     }
@@ -422,7 +412,7 @@
         continue;
       }
 
-      if (cmd.getRefName().startsWith(NEW_CHANGE)) {
+      if (MagicBranch.isMagicBranch(cmd.getRefName())) {
         parseNewChangeCommand(cmd);
         continue;
       }
@@ -527,7 +517,7 @@
 
       // Let the core receive process handle it
     } else {
-      reject(cmd);
+      reject(cmd, "can not create new references");
     }
   }
 
@@ -541,7 +531,7 @@
       validateNewCommits(ctl, cmd);
       // Let the core receive process handle it
     } else {
-      reject(cmd);
+      reject(cmd, "can not update the reference as a fast forward");
     }
   }
 
@@ -569,7 +559,7 @@
     if (ctl.canDelete()) {
       // Let the core receive process handle it
     } else {
-      reject(cmd);
+      reject(cmd, "can not delete references");
     }
   }
 
@@ -597,7 +587,8 @@
     if (ctl.canForceUpdate()) {
       // Let the core receive process handle it
     } else {
-      cmd.setResult(ReceiveCommand.Result.REJECTED_NONFASTFORWARD);
+      cmd.setResult(ReceiveCommand.Result.REJECTED_NONFASTFORWARD, " need '"
+          + PermissionRule.FORCE_PUSH + "' privilege.");
     }
   }
 
@@ -610,7 +601,7 @@
     }
 
     newChange = cmd;
-    String destBranchName = cmd.getRefName().substring(NEW_CHANGE.length());
+    String destBranchName = MagicBranch.getDestBranchName(cmd.getRefName());
     if (!destBranchName.startsWith(Constants.R_REFS)) {
       destBranchName = Constants.R_HEADS + destBranchName;
     }
@@ -665,7 +656,7 @@
         destBranchName.substring(0, split));
     destBranchCtl = projectControl.controlForRef(destBranch);
     if (!destBranchCtl.canUpload()) {
-      reject(cmd);
+      reject(cmd, "can not upload a change to this reference");
       return;
     }
 
@@ -941,45 +932,59 @@
     cc.remove(me);
     cc.removeAll(reviewers);
 
-    final Change change =
-        new Change(changeKey, new Change.Id(db.nextChangeId()), me, destBranch);
+    final Change change;
+    final PatchSet ps;
+    final PatchSetInfo info;
+
+    change = new Change(changeKey, new Change.Id(db.nextChangeId()), me, destBranch);
     change.setTopic(destTopicName);
     change.nextPatchSetId();
 
-    final PatchSet ps = new PatchSet(change.currPatchSetId());
-    ps.setCreatedOn(change.getCreatedOn());
-    ps.setUploader(me);
-    ps.setRevision(toRevId(c));
-    insertAncestors(ps.getId(), c);
-    db.patchSets().insert(Collections.singleton(ps));
-
-    final PatchSetInfo info = patchSetInfoFactory.get(c, ps.getId());
-    change.setCurrentPatchSet(info);
-    ChangeUtil.updated(change);
-    db.changes().insert(Collections.singleton(change));
-
-    final Set<Account.Id> haveApprovals = new HashSet<Account.Id>();
-    final List<ApprovalType> allTypes = approvalTypes.getApprovalTypes();
-    haveApprovals.add(me);
-
-    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);
+    db.changes().beginTransaction(change.getId());
+    try {
+      ps = new PatchSet(change.currPatchSetId());
+      ps.setCreatedOn(change.getCreatedOn());
+      ps.setUploader(me);
+      ps.setRevision(toRevId(c));
+      if (MagicBranch.isDraft(newChange.getRefName())) {
+        change.setStatus(Change.Status.DRAFT);
+        ps.setDraft(true);
       }
-      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);
+      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);
+
+      final Set<Account.Id> haveApprovals = new HashSet<Account.Id>();
+      final List<ApprovalType> allTypes = approvalTypes.getApprovalTypes();
+      haveApprovals.add(me);
+
+      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);
+        }
+        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);
+          }
         }
       }
+      db.commit();
+    } finally {
+      db.rollback();
     }
 
     final RefUpdate ru = repo.updateRef(ps.getRefName());
@@ -991,7 +996,7 @@
     }
     replication.scheduleUpdate(project.getNameKey(), ru.getName());
 
-    allNewChanges.add(change.getId());
+    allNewChanges.add(change);
 
     try {
       final CreateChangeSender cm;
@@ -1005,8 +1010,7 @@
       log.error("Cannot send email for new change " + change.getId(), e);
     }
 
-    ChangeUtil.updateTrackingIds(db, change, trackingFooters, footerLines);
-    hooks.doPatchsetCreatedHook(change, ps);
+    hooks.doPatchsetCreatedHook(change, ps, db);
   }
 
   private static boolean isReviewer(final FooterLine candidateFooterLine) {
@@ -1163,7 +1167,11 @@
       }
     }
 
-    change =
+    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) {
@@ -1176,126 +1184,143 @@
             }
           }
         });
-    if (change == null) {
-      reject(request.cmd, "change is closed");
-      return null;
-    }
-
-    final PatchSet ps = new PatchSet(change.currPatchSetId());
-    ps.setCreatedOn(new Timestamp(System.currentTimeMillis()));
-    ps.setUploader(currentUser.getAccountId());
-    ps.setRevision(toRevId(c));
-    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;
-    }
-    result.change = change;
-    result.patchSet = ps;
-    result.info = patchSetInfoFactory.get(c, ps.getId());
-
-    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());
+      if (change == null) {
+        reject(request.cmd, "change is closed");
+        return null;
       }
 
-      // 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)));
+      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());
+        }
+
+        // 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)));
+          }
+        }
+
+        if (!haveAuthor && authorId != null && a.getAccountId().equals(authorId)) {
+          haveAuthor = true;
+        }
+        if (!haveCommitter && committerId != null
+            && a.getAccountId().equals(committerId)) {
+          haveCommitter = true;
         }
       }
 
-      if (!haveAuthor && authorId != null && a.getAccountId().equals(authorId)) {
-        haveAuthor = true;
+      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);
+          }
+        }
       }
-      if (!haveCommitter && committerId != null
-          && a.getAccountId().equals(committerId)) {
-        haveCommitter = true;
-      }
-    }
 
-    final ChangeMessage msg =
-        new ChangeMessage(new ChangeMessage.Key(change.getId(), ChangeUtil
-            .messageUUID(db)), me, ps.getCreatedOn());
-    msg.setMessage("Uploaded patch set " + ps.getPatchSetId() + ".");
-    db.changeMessages().insert(Collections.singleton(msg));
-    result.msg = msg;
+      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.
+        //
+        change =
+            db.changes().atomicUpdate(change.getId(), new AtomicUpdate<Change>() {
+              @Override
+              public Change update(Change change) {
+                if (change.getStatus().isOpen()) {
+                  if (destTopicName != null) {
+                    change.setTopic(destTopicName);
+                  }
+                  if (change.getStatus() == Change.Status.DRAFT && ps.isDraft()) {
+                    // Leave in draft status.
+                  } else {
+                    change.setStatus(Change.Status.NEW);
+                  }
+                  change.setCurrentPatchSet(result.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;
+        }
+      }
+
+      db.commit();
+    } finally {
+      db.rollback();
+    }
 
     if (result.mergedIntoRef != null) {
       // Change was already submitted to a branch, close it.
       //
       markChangeMergedByPush(db, result);
-    } else {
-      // 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().isOpen()) {
-                if (destTopicName != null) {
-                  change.setTopic(destTopicName);
-                }
-                change.setStatus(Change.Status.NEW);
-                change.setCurrentPatchSet(result.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;
-      }
-    }
-
-    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 RefUpdate ru = repo.updateRef(ps.getRefName());
@@ -1306,7 +1331,7 @@
           + repo.getDirectory() + ": " + ru.getResult());
     }
     replication.scheduleUpdate(project.getNameKey(), ru.getName());
-    hooks.doPatchsetCreatedHook(result.change, ps);
+    hooks.doPatchsetCreatedHook(result.change, ps, db);
     request.cmd.setResult(ReceiveCommand.Result.OK);
 
     try {
@@ -1324,7 +1349,6 @@
       log.error("Cannot send email for new patch set " + ps.getId(), e);
     }
 
-    ChangeUtil.updateTrackingIds(db, change, trackingFooters, footerLines);
     sendMergedEmail(result);
     return result != null ? result.info.getKey() : null;
   }
@@ -1533,7 +1557,7 @@
     }
 
     final List<String> idList = c.getFooterLines(CHANGE_ID);
-    if ((cmd.getRefName().startsWith(NEW_CHANGE) || 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";
@@ -1725,10 +1749,24 @@
           closeChange(req.cmd, psi, req.newCommit);
         }
       }
+
+      // It handles gitlinks if required.
+
+      rw.reset();
+      final RevCommit codeReviewCommit = rw.parseCommit(cmd.getNewId());
+
+      final SubmoduleOp subOp =
+          subOpFactory.create(
+              new Branch.NameKey(project.getNameKey(), cmd.getRefName()),
+              codeReviewCommit, rw, repo, project, new ArrayList<Change>(),
+              new HashMap<Change.Id, CodeReviewCommit>());
+      subOp.update();
     } catch (IOException e) {
       log.error("Can't scan for changes to close", e);
     } catch (OrmException e) {
       log.error("Can't scan for changes to close", e);
+    } catch (SubmoduleException e) {
+      log.error("Can't complete git links check", e);
     }
   }
 
@@ -1744,7 +1782,8 @@
       return;
     }
 
-    if (change.getStatus() == Change.Status.MERGED) {
+    if (change.getStatus() == Change.Status.MERGED ||
+        change.getStatus() == Change.Status.ABANDONED) {
       // If its already merged, don't make further updates, it
       // might just be moving from an experimental branch into
       // a more stable branch.
@@ -1792,12 +1831,7 @@
     change.setStatus(Change.Status.MERGED);
     ChangeUtil.updated(change);
 
-    final List<PatchSetApproval> approvals =
-        db.patchSetApprovals().byChange(change.getId()).toList();
-    for (PatchSetApproval a : approvals) {
-      a.cache(change);
-    }
-    db.patchSetApprovals().update(approvals);
+    ApprovalsUtil.syncChangeStatus(db, change);
 
     final StringBuilder msgBuf = new StringBuilder();
     msgBuf.append("Change has been successfully pushed");
@@ -1813,7 +1847,7 @@
     msgBuf.append(".");
     final ChangeMessage msg =
         new ChangeMessage(new ChangeMessage.Key(change.getId(), ChangeUtil
-            .messageUUID(db)), currentUser.getAccountId());
+            .messageUUID(db)), currentUser.getAccountId(), result.info.getKey());
     msg.setMessage(msgBuf.toString());
 
     db.changeMessages().insert(Collections.singleton(msg));
@@ -1843,8 +1877,12 @@
         log.error("Cannot send email for submitted patch set " + psi, e);
       }
 
-      hooks.doChangeMergedHook(result.change, currentUser.getAccount(),
-          result.patchSet);
+      try {
+        hooks.doChangeMergedHook(result.change, currentUser.getAccount(),
+            result.patchSet, db);
+      } catch (OrmException err) {
+        log.error("Cannot open change: " + result.change.getChangeId(), err);
+      }
     }
   }
 
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
new file mode 100644
index 0000000..3c8a1a5
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/RepositoryCaseMismatchException.java
@@ -0,0 +1,51 @@
+// 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.Project;
+import com.google.gerrit.reviewdb.Project.NameKey;
+
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+
+/**
+ * This exception is thrown if a project cannot be created because a project
+ * with the same name in a different case already exists. This can only happen
+ * if the OS has a case insensitive file system (e.g. Windows), because in this
+ * case the name for the git repository in the file system is already occupied
+ * by the existing project.
+ */
+public class RepositoryCaseMismatchException extends
+    RepositoryNotFoundException {
+
+  private static final long serialVersionUID = 1L;
+
+  private final NameKey nameOfExistingProject;
+
+  /**
+   * @param projectName name of the project that cannot be created
+   * @param nameOfExistingProject name of the project that already exists and
+   *        occupies the name for the git repository in the file system
+   */
+  public RepositoryCaseMismatchException(final Project.NameKey projectName,
+      final Project.NameKey nameOfExistingProject) {
+    super("Name occupied in other case: " + projectName.get() + "; project "
+        + nameOfExistingProject.get() + " exists");
+    this.nameOfExistingProject = nameOfExistingProject;
+  }
+
+  public NameKey getNameOfExistingProject() {
+    return nameOfExistingProject;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReviewNoteHeaderFormatter.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReviewNoteHeaderFormatter.java
index 8997e2b..39ae9b3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReviewNoteHeaderFormatter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReviewNoteHeaderFormatter.java
@@ -37,12 +37,14 @@
 class ReviewNoteHeaderFormatter {
 
   private final DateFormat rfc2822DateFormatter;
+  private final String anonymousCowardName;
   private final StringBuilder sb = new StringBuilder();
 
-  ReviewNoteHeaderFormatter(TimeZone tz) {
+  ReviewNoteHeaderFormatter(TimeZone tz, String anonymousCowardName) {
     rfc2822DateFormatter =
         new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z", Locale.US);
     rfc2822DateFormatter.setCalendar(Calendar.getInstance(tz, Locale.US));
+    this.anonymousCowardName = anonymousCowardName;
   }
 
   void appendChangeId(Change.Key changeKey) {
@@ -76,7 +78,7 @@
     }
 
     if (!wroteData) {
-      sb.append("Anonymous Coward #").append(user.getId());
+      sb.append(anonymousCowardName).append(" #").append(user.getId());
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleException.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleException.java
new file mode 100644
index 0000000..d7e8446
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleException.java
@@ -0,0 +1,28 @@
+// 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;
+
+/** Indicates the gitlink's update cannot be processed at this time. */
+class SubmoduleException extends Exception {
+  private static final long serialVersionUID = 1L;
+
+  SubmoduleException(final String msg) {
+    super(msg, null);
+  }
+
+  SubmoduleException(final String msg, final Throwable why) {
+    super(msg, why);
+  }
+}
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
new file mode 100644
index 0000000..a7a43b1
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
@@ -0,0 +1,379 @@
+// 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.Branch;
+import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.SubmoduleSubscription;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.util.SubmoduleSectionParser;
+import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.client.SchemaFactory;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.assistedinject.Assisted;
+
+import org.eclipse.jgit.dircache.DirCache;
+import org.eclipse.jgit.dircache.DirCacheBuilder;
+import org.eclipse.jgit.dircache.DirCacheEditor;
+import org.eclipse.jgit.dircache.DirCacheEntry;
+import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.lib.BlobBasedConfig;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.FileMode;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+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.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+public class SubmoduleOp {
+  public interface Factory {
+    SubmoduleOp create(Branch.NameKey destBranch, RevCommit mergeTip,
+        RevWalk rw, Repository db, Project destProject, List<Change> submitted,
+        Map<Change.Id, CodeReviewCommit> commits);
+  }
+
+  private static final Logger log = LoggerFactory.getLogger(SubmoduleOp.class);
+  private static final String GIT_MODULES = ".gitmodules";
+
+  private final Branch.NameKey destBranch;
+  private RevCommit mergeTip;
+  private RevWalk rw;
+  private final Provider<String> urlProvider;
+  private ReviewDb schema;
+  private Repository db;
+  private Project destProject;
+  private List<Change> submitted;
+  private final Map<Change.Id, CodeReviewCommit> commits;
+  private final PersonIdent myIdent;
+  private final GitRepositoryManager repoManager;
+  private final ReplicationQueue replication;
+  private final SchemaFactory<ReviewDb> schemaFactory;
+  private final Set<Branch.NameKey> updatedSubscribers;
+
+  @Inject
+  public SubmoduleOp(@Assisted final Branch.NameKey destBranch,
+      @Assisted RevCommit mergeTip, @Assisted RevWalk rw,
+      @CanonicalWebUrl @Nullable final Provider<String> urlProvider,
+      final SchemaFactory<ReviewDb> sf, @Assisted Repository db,
+      @Assisted Project destProject, @Assisted List<Change> submitted,
+      @Assisted final Map<Change.Id, CodeReviewCommit> commits,
+      @GerritPersonIdent final PersonIdent myIdent,
+      GitRepositoryManager repoManager, ReplicationQueue replication) {
+    this.destBranch = destBranch;
+    this.mergeTip = mergeTip;
+    this.rw = rw;
+    this.urlProvider = urlProvider;
+    this.schemaFactory = sf;
+    this.db = db;
+    this.destProject = destProject;
+    this.submitted = submitted;
+    this.commits = commits;
+    this.myIdent = myIdent;
+    this.repoManager = repoManager;
+    this.replication = replication;
+
+    updatedSubscribers = new HashSet<Branch.NameKey>();
+  }
+
+  public void update() throws SubmoduleException {
+    try {
+      schema = schemaFactory.open();
+
+      updateSubmoduleSubscriptions();
+      updateSuperProjects(destBranch, mergeTip.getId().toObjectId(), null);
+    } catch (OrmException e) {
+      throw new SubmoduleException("Cannot open database", e);
+    } finally {
+      if (schema != null) {
+        schema.close();
+        schema = null;
+      }
+    }
+  }
+
+  private void updateSubmoduleSubscriptions() throws SubmoduleException {
+    if (urlProvider.get() == null) {
+      logAndThrowSubmoduleException("Cannot establish canonical web url used to access gerrit."
+              + " It should be provided in gerrit.config file.");
+    }
+
+    try {
+      final TreeWalk tw = TreeWalk.forPath(db, GIT_MODULES, mergeTip.getTree());
+      if (tw != null
+          && (FileMode.REGULAR_FILE.equals(tw.getRawMode(0)) || FileMode.EXECUTABLE_FILE
+              .equals(tw.getRawMode(0)))) {
+
+        BlobBasedConfig bbc =
+            new BlobBasedConfig(null, db, mergeTip, GIT_MODULES);
+
+        final String thisServer = new URI(urlProvider.get()).getHost();
+
+        final Branch.NameKey target =
+            new Branch.NameKey(new Project.NameKey(destProject.getName()),
+                destBranch.get());
+
+        final Set<SubmoduleSubscription> oldSubscriptions =
+            new HashSet<SubmoduleSubscription>(schema.submoduleSubscriptions()
+                .bySuperProject(destBranch).toList());
+        final List<SubmoduleSubscription> newSubscriptions =
+            new SubmoduleSectionParser(bbc, thisServer, target, repoManager)
+                .parseAllSections();
+
+        final Set<SubmoduleSubscription> alreadySubscribeds =
+            new HashSet<SubmoduleSubscription>();
+        for (SubmoduleSubscription s : newSubscriptions) {
+          if (oldSubscriptions.contains(s)) {
+            alreadySubscribeds.add(s);
+          }
+        }
+
+        oldSubscriptions.removeAll(newSubscriptions);
+        newSubscriptions.removeAll(alreadySubscribeds);
+
+        if (!oldSubscriptions.isEmpty()) {
+          schema.submoduleSubscriptions().delete(oldSubscriptions);
+        }
+        schema.submoduleSubscriptions().insert(newSubscriptions);
+      }
+    } catch (OrmException e) {
+      logAndThrowSubmoduleException(
+          "Database problem at update of subscriptions table from "
+              + GIT_MODULES + " file.", e);
+    } catch (ConfigInvalidException e) {
+      logAndThrowSubmoduleException(
+          "Problem at update of subscriptions table: " + GIT_MODULES
+              + " config file is invalid.", e);
+    } catch (IOException e) {
+      logAndThrowSubmoduleException(
+          "Problem at update of subscriptions table from " + GIT_MODULES + ".",
+          e);
+    } catch (URISyntaxException e) {
+      logAndThrowSubmoduleException(
+          "Incorrect gerrit canonical web url provided in gerrit.config file.",
+          e);
+    }
+  }
+
+  private void updateSuperProjects(final Branch.NameKey updatedBranch,
+      final ObjectId mergedCommit, final String msg) throws SubmoduleException {
+    try {
+      final List<SubmoduleSubscription> subscribers =
+          schema.submoduleSubscriptions().bySubmodule(updatedBranch).toList();
+
+      if (!subscribers.isEmpty()) {
+        String msgbuf = msg;
+        if (msgbuf == null) {
+          // The first updatedBranch on a cascade event of automatic
+          // updates of repos is added to updatedSubscribers set so
+          // if we face a situation having
+          // submodule-a(master)-->super(master)-->submodule-a(master),
+          // it will be detected we have a circular subscription
+          // when updateSuperProjects is called having as updatedBranch
+          // the super(master) value.
+          updatedSubscribers.add(updatedBranch);
+
+          for (final Change chg : submitted) {
+            final CodeReviewCommit c = commits.get(chg.getId());
+            if (c != null
+                && (c.statusCode == CommitMergeStatus.CLEAN_MERGE || c.statusCode == CommitMergeStatus.CLEAN_PICK)) {
+              msgbuf += "\n";
+              msgbuf += c.getFullMessage();
+            }
+          }
+        }
+
+        // update subscribers of this module
+        for (final SubmoduleSubscription s : subscribers) {
+          if (!updatedSubscribers.add(s.getSuperProject())) {
+            log.error("Possible circular subscription involving "
+                + s.toString());
+          } else {
+
+            Map<Branch.NameKey, ObjectId> modules =
+                new HashMap<Branch.NameKey, ObjectId>(1);
+            modules.put(updatedBranch, mergedCommit);
+
+            Map<Branch.NameKey, String> paths =
+                new HashMap<Branch.NameKey, String>(1);
+            paths.put(updatedBranch, s.getPath());
+
+            try {
+              updateGitlinks(s.getSuperProject(), modules, paths, msgbuf);
+            } catch (SubmoduleException e) {
+              throw e;
+            }
+          }
+        }
+      }
+    } catch (OrmException e) {
+      logAndThrowSubmoduleException("Cannot read subscription records", e);
+    }
+  }
+
+  private void updateGitlinks(final Branch.NameKey subscriber,
+      final Map<Branch.NameKey, ObjectId> modules,
+      final Map<Branch.NameKey, String> paths, final String msg)
+      throws SubmoduleException {
+    PersonIdent author = null;
+
+    final StringBuilder msgbuf = new StringBuilder();
+    msgbuf.append("Updated " + subscriber.getParentKey().get());
+    Repository pdb = null;
+
+    try {
+      boolean sameAuthorForAll = true;
+
+      for (final Map.Entry<Branch.NameKey, ObjectId> me : modules.entrySet()) {
+        RevCommit c = rw.parseCommit(me.getValue());
+
+        msgbuf.append("\nProject: ");
+        msgbuf.append(me.getKey().getParentKey().get());
+        msgbuf.append("  " + me.getValue().getName());
+        msgbuf.append("\n");
+        if (modules.size() == 1 && msg != null) {
+          msgbuf.append(msg);
+        } else {
+          msgbuf.append(c.getShortMessage());
+        }
+        msgbuf.append("\n");
+
+        if (author == null) {
+          author = c.getAuthorIdent();
+        } else if (!author.equals(c.getAuthorIdent())) {
+          sameAuthorForAll = false;
+        }
+      }
+
+      if (!sameAuthorForAll || author == null) {
+        author = myIdent;
+      }
+
+      pdb = repoManager.openRepository(subscriber.getParentKey());
+      if (pdb.getRef(subscriber.get()) == null) {
+        throw new SubmoduleException(
+            "The branch was probably deleted from the subscriber repository");
+      }
+
+      final ObjectId currentCommitId =
+          pdb.getRef(subscriber.get()).getObjectId();
+
+      DirCache dc = readTree(pdb, pdb.getRef(subscriber.get()));
+      DirCacheEditor ed = dc.editor();
+      for (final Map.Entry<Branch.NameKey, ObjectId> me : modules.entrySet()) {
+        ed.add(new PathEdit(paths.get(me.getKey())) {
+          public void apply(DirCacheEntry ent) {
+            ent.setFileMode(FileMode.GITLINK);
+            ent.setObjectId(me.getValue().copy());
+          }
+        });
+      }
+      ed.finish();
+
+      ObjectInserter oi = pdb.newObjectInserter();
+      ObjectId tree = dc.writeTree(oi);
+
+      final CommitBuilder commit = new CommitBuilder();
+      commit.setTreeId(tree);
+      commit.setParentIds(new ObjectId[] {currentCommitId});
+      commit.setAuthor(author);
+      commit.setCommitter(myIdent);
+      commit.setMessage(msgbuf.toString());
+      oi.insert(commit);
+
+      ObjectId commitId = oi.idFor(Constants.OBJ_COMMIT, commit.build());
+
+      final RefUpdate rfu = pdb.updateRef(subscriber.get());
+      rfu.setForceUpdate(false);
+      rfu.setNewObjectId(commitId);
+      rfu.setExpectedOldObjectId(currentCommitId);
+      rfu
+          .setRefLogMessage("Submit to " + subscriber.getParentKey().get(),
+              true);
+
+      switch (rfu.update()) {
+        case NEW:
+        case FAST_FORWARD:
+          replication.scheduleUpdate(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;
+
+        default:
+          throw new IOException(rfu.getResult().name());
+      }
+
+      // Recursive call: update subscribers of the subscriber
+      updateSuperProjects(subscriber, commitId, msgbuf.toString());
+    } catch (IOException e) {
+      logAndThrowSubmoduleException("Cannot update gitlinks for "
+          + subscriber.get(), e);
+    } finally {
+      if (pdb != null) {
+        pdb.close();
+      }
+    }
+  }
+
+  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;
+  }
+
+  private static void logAndThrowSubmoduleException(final String errorMsg,
+      final Exception e) throws SubmoduleException {
+    log.error(errorMsg, e);
+    throw new SubmoduleException(errorMsg, e);
+  }
+
+  private static void logAndThrowSubmoduleException(final String errorMsg)
+      throws SubmoduleException {
+    log.error(errorMsg);
+    throw new SubmoduleException(errorMsg);
+  }
+}
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 ba63847..cfa5420 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
@@ -320,6 +320,11 @@
       int flag = refs.size();
       commit.refFlags.set(flag);
       refs.put(ref.getName(), new CachedRef(ref, flag));
+    } catch (IncorrectObjectTypeException notCommit) {
+      // No need to spam the logs.
+      // Quite many refs will point to non-commits.
+      // For instance, refs from refs/cache-automerge
+      // will often end up here.
     } catch (IOException e) {
       log.warn("Error on " + ref.getName() + " of " + projectName, e);
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/TransferConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/TransferConfig.java
index de4130d..af404b5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/TransferConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/TransferConfig.java
@@ -28,11 +28,13 @@
 public class TransferConfig {
   private final int timeout;
   private final PackConfig packConfig;
+  private final long maxObjectSizeLimit;
 
   @Inject
   TransferConfig(@GerritServerConfig final Config cfg) {
     timeout = (int) ConfigUtil.getTimeUnit(cfg, "transfer", null, "timeout", //
         0, TimeUnit.SECONDS);
+    maxObjectSizeLimit = cfg.getLong("receive", "maxObjectSizeLimit", 0);
 
     packConfig = new PackConfig();
     packConfig.setDeltaCompress(false);
@@ -48,4 +50,8 @@
   public PackConfig getPackConfig() {
     return packConfig;
   }
+
+  public long getMaxObjectSizeLimit() {
+    return maxObjectSizeLimit;
+  }
 }
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 77a346f..feadaf1 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
@@ -60,6 +60,10 @@
 
   @Override
   public Map<String, Ref> filter(Map<String, Ref> refs) {
+    return filter(refs, false);
+  }
+
+  public Map<String, Ref> filter(Map<String, Ref> refs, boolean filterTagsSeperately) {
     final Set<Change.Id> visibleChanges = visibleChanges();
     final Map<String, Ref> result = new HashMap<String, Ref>();
     final List<Ref> deferredTags = new ArrayList<Ref>();
@@ -91,8 +95,9 @@
     // If we have tags that were deferred, we need to do a revision walk
     // to identify what tags we can actually reach, and what we cannot.
     //
-    if (!deferredTags.isEmpty() && !result.isEmpty()) {
-      TagMatcher tags = tagCache.get(projectName).matcher(db, result.values());
+    if (!deferredTags.isEmpty() && (!result.isEmpty() || filterTagsSeperately)) {
+      TagMatcher tags = tagCache.get(projectName).
+          matcher(db, filterTagsSeperately ? filter(db.getAllRefs()).values() : result.values());
       for (Ref tag : deferredTags) {
         if (tags.isReachable(tag)) {
           result.put(tag.getName(), tag);
@@ -112,7 +117,7 @@
     try {
       final Set<Change.Id> visibleChanges = new HashSet<Change.Id>();
       for (Change change : reviewDb.changes().byProject(project.getNameKey())) {
-        if (projectCtl.controlFor(change).isVisible()) {
+        if (projectCtl.controlFor(change).isVisible(reviewDb)) {
           visibleChanges.add(change.getId());
         }
       }
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 14eacca..87e6880 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
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.git;
 
 import com.google.gerrit.lifecycle.LifecycleListener;
+import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.reviewdb.Project.NameKey;
 import com.google.gerrit.server.util.IdGenerator;
 import com.google.inject.Inject;
@@ -61,6 +62,14 @@
     }
   }
 
+  public static class Module extends LifecycleModule {
+    @Override
+    protected void configure() {
+      bind(WorkQueue.class);
+      listener().to(Lifecycle.class);
+    }
+  }
+
   private static final Logger log = LoggerFactory.getLogger(WorkQueue.class);
   private static final UncaughtExceptionHandler LOG_UNCAUGHT_EXCEPTION =
       new UncaughtExceptionHandler() {
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 bb28a84..ac62bd0 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.Change;
+import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -26,8 +27,9 @@
   }
 
   @Inject
-  public AbandonedSender(EmailArguments ea, @Assisted Change c) {
-    super(ea, c, "abandon");
+  public AbandonedSender(EmailArguments ea,
+      @AnonymousCowardName String anonymousCowardName, @Assisted Change c) {
+    super(ea, anonymousCowardName, c, "abandon");
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/AddReviewerSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/AddReviewerSender.java
index e5437cf..02f8fb7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/AddReviewerSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/AddReviewerSender.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.mail;
 
 import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.ssh.SshInfo;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
@@ -26,9 +27,10 @@
   }
 
   @Inject
-  public AddReviewerSender(EmailArguments ea, SshInfo sshInfo,
+  public AddReviewerSender(EmailArguments ea,
+      @AnonymousCowardName String anonymousCowardName, SshInfo sshInfo,
       @Assisted Change c) {
-    super(ea, sshInfo, c);
+    super(ea, anonymousCowardName, sshInfo, c);
   }
 
   @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 1640e05..624e626 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
@@ -16,8 +16,8 @@
 
 import java.io.UnsupportedEncodingException;
 
-class Address {
-  static Address parse(final String in) {
+public class Address {
+  public static Address parse(final String in) {
     final int lt = in.indexOf('<');
     final int gt = in.indexOf('>');
     final int at = in.indexOf("@");
@@ -37,15 +37,23 @@
   final String name;
   final String email;
 
-  Address(String email) {
+  public Address(String email) {
     this(null, email);
   }
 
-  Address(String name, String email) {
+  public Address(String name, String email) {
     this.name = name;
     this.email = email;
   }
 
+  public String getName() {
+    return name;
+  }
+
+  public String getEmail() {
+    return email;
+  }
+
   @Override
   public String toString() {
     try {
@@ -55,7 +63,7 @@
     }
   }
 
-  String toHeaderString() throws UnsupportedEncodingException {
+  public String toHeaderString() throws UnsupportedEncodingException {
     if (name != null) {
       return quotedPhrase(name) + " <" + email + ">";
     } else if (isSimple()) {
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 4b5d279..822bd51 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
@@ -57,8 +57,9 @@
   protected Set<Account.Id> authors;
   protected boolean emailOnlyAuthors;
 
-  protected ChangeEmail(EmailArguments ea, final Change c, final String mc) {
-    super(ea, mc);
+  protected ChangeEmail(EmailArguments ea, final String anonymousCowardName,
+      final Change c, final String mc) {
+    super(ea, anonymousCowardName, mc);
     change = c;
     changeData = change != null ? new ChangeData(change) : null;
     emailOnlyAuthors = false;
@@ -71,7 +72,8 @@
     final IdentifiedUser user =  args.identifiedUserFactory.create(id);
     final Set<AccountGroup.UUID> gids = user.getEffectiveGroups();
     for (final AccountGroup.UUID gid : gids) {
-      if (args.groupCache.get(gid).isEmailOnlyAuthors()) {
+      AccountGroup group = args.groupCache.get(gid);
+      if (group != null && group.isEmailOnlyAuthors()) {
         emailOnlyAuthors = true;
         break;
       }
@@ -135,7 +137,7 @@
 
     if (patchSet != null && patchSetInfo == null) {
       try {
-        patchSetInfo = args.patchSetInfoFactory.get(patchSet.getId());
+        patchSetInfo = args.patchSetInfoFactory.get(args.db.get(), patchSet.getId());
       } catch (PatchSetInfoNotAvailableException err) {
         patchSetInfo = null;
       }
@@ -399,11 +401,11 @@
     }
   }
 
-  protected boolean isVisibleTo(final Account.Id to) {
+  protected boolean isVisibleTo(final Account.Id to) throws OrmException {
     return projectState == null
         || change == null
         || projectState.controlFor(args.identifiedUserFactory.create(to))
-            .controlFor(change).isVisible();
+            .controlFor(change).isVisible(args.db.get());
   }
 
   /** Find all users who are authors of any part of this change. */
@@ -415,8 +417,12 @@
       authors.add(patchSet.getUploader());
     }
     if (patchSetInfo != null) {
-      authors.add(patchSetInfo.getAuthor().getAccount());
-      authors.add(patchSetInfo.getCommitter().getAccount());
+      if (patchSetInfo.getAuthor().getAccount() != null) {
+        authors.add(patchSetInfo.getAuthor().getAccount());
+      }
+      if (patchSetInfo.getCommitter().getAccount() != null) {
+        authors.add(patchSetInfo.getCommitter().getAccount());
+      }
     }
     return authors;
   }
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 062d14b..e887221 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,6 +17,7 @@
 import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.reviewdb.Patch;
 import com.google.gerrit.reviewdb.PatchLineComment;
+import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.patch.PatchFile;
 import com.google.gerrit.server.patch.PatchList;
 import com.google.inject.Inject;
@@ -41,8 +42,9 @@
   private List<PatchLineComment> inlineComments = Collections.emptyList();
 
   @Inject
-  public CommentSender(EmailArguments ea, @Assisted Change c) {
-    super(ea, c, "comment");
+  public CommentSender(EmailArguments ea,
+      @AnonymousCowardName String anonymousCowardName, @Assisted Change c) {
+    super(ea, anonymousCowardName, c, "comment");
   }
 
   public void setPatchLineComments(final List<PatchLineComment> plc) {
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 8bb68df..dabb6f3 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
@@ -21,6 +21,7 @@
 import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.reviewdb.AccountProjectWatch.NotifyType;
 import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.ssh.SshInfo;
 import com.google.gwtorm.client.OrmException;
 import com.google.inject.Inject;
@@ -38,9 +39,10 @@
   private final GroupCache groupCache;
 
   @Inject
-  public CreateChangeSender(EmailArguments ea, SshInfo sshInfo,
+  public CreateChangeSender(EmailArguments ea,
+      @AnonymousCowardName String anonymousCowardName, SshInfo sshInfo,
       GroupCache groupCache, @Assisted Change c) {
-    super(ea, sshInfo, c);
+    super(ea, anonymousCowardName, sshInfo, c);
     this.groupCache = groupCache;
   }
 
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 ae4aff0..15f6846 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
@@ -21,7 +21,6 @@
 import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.CanonicalWebUrl;
-import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
@@ -31,6 +30,8 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
+import org.apache.velocity.runtime.RuntimeInstance;
+
 import javax.annotation.Nullable;
 
 class EmailArguments {
@@ -49,7 +50,7 @@
   final ChangeQueryBuilder.Factory queryBuilder;
   final Provider<ChangeQueryRewriter> queryRewriter;
   final Provider<ReviewDb> db;
-  final SitePaths site;
+  final RuntimeInstance velocityRuntime;
 
   @Inject
   EmailArguments(GitRepositoryManager server, ProjectCache projectCache,
@@ -61,7 +62,7 @@
       AllProjectsName allProjectsName,
       ChangeQueryBuilder.Factory queryBuilder,
       Provider<ChangeQueryRewriter> queryRewriter, Provider<ReviewDb> db,
-      SitePaths site) {
+      RuntimeInstance velocityRuntime) {
     this.server = server;
     this.projectCache = projectCache;
     this.groupCache = groupCache;
@@ -76,6 +77,6 @@
     this.queryBuilder = queryBuilder;
     this.queryRewriter = queryRewriter;
     this.db = db;
-    this.site = site;
+    this.velocityRuntime = velocityRuntime;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailHeader.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailHeader.java
index 10f6510..16f9062 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailHeader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailHeader.java
@@ -20,28 +20,33 @@
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
 import java.util.Iterator;
+import java.util.Collections;
 import java.util.List;
 import java.util.Locale;
 
-abstract class EmailHeader {
-  abstract boolean isEmpty();
+public abstract class EmailHeader {
+  public abstract boolean isEmpty();
 
-  abstract void write(Writer w) throws IOException;
+  public abstract void write(Writer w) throws IOException;
 
-  static class String extends EmailHeader {
+  public static class String extends EmailHeader {
     private java.lang.String value;
 
-    String(java.lang.String v) {
+    public String(java.lang.String v) {
       value = v;
     }
 
+    public java.lang.String getString() {
+      return value;
+    }
+
     @Override
-    boolean isEmpty() {
+    public boolean isEmpty() {
       return value == null || value.length() == 0;
     }
 
     @Override
-    void write(Writer w) throws IOException {
+    public void write(Writer w) throws IOException {
       if (needsQuotedPrintable(value)) {
         w.write(quotedPrintable(value));
       } else {
@@ -84,20 +89,24 @@
     return r.toString();
   }
 
-  static class Date extends EmailHeader {
+  public static class Date extends EmailHeader {
     private java.util.Date value;
 
-    Date(java.util.Date v) {
+    public Date(java.util.Date v) {
       value = v;
     }
 
+    public java.util.Date getDate() {
+      return value;
+    }
+
     @Override
-    boolean isEmpty() {
+    public boolean isEmpty() {
       return value == null;
     }
 
     @Override
-    void write(Writer w) throws IOException {
+    public void write(Writer w) throws IOException {
       final SimpleDateFormat fmt;
       // Mon, 1 Jun 2009 10:49:44 -0700
       fmt = new SimpleDateFormat("EEE, d MMM yyyy HH:mm:ss Z", Locale.ENGLISH);
@@ -105,17 +114,21 @@
     }
   }
 
-  static class AddressList extends EmailHeader {
+  public static class AddressList extends EmailHeader {
     private final List<Address> list = new ArrayList<Address>();
 
-    AddressList() {
+    public AddressList() {
     }
 
-    AddressList(Address addr) {
+    public AddressList(Address addr) {
       add(addr);
     }
 
-    void add(Address addr) {
+    public List<Address> getAddressList() {
+      return Collections.unmodifiableList(list);
+    }
+
+    public void add(Address addr) {
       list.add(addr);
     }
 
@@ -128,12 +141,12 @@
     }
 
     @Override
-    boolean isEmpty() {
+    public boolean isEmpty() {
       return list.isEmpty();
     }
 
     @Override
-    void write(Writer w) throws IOException {
+    public void write(Writer w) throws IOException {
       int len = 8;
       boolean firstAddress = true;
       boolean needComma = false;
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
new file mode 100644
index 0000000..374ae34
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailTokenVerifier.java
@@ -0,0 +1,67 @@
+// Copyright 2012 Google Inc. All Rights Reserved.
+
+package com.google.gerrit.server.mail;
+
+import com.google.gerrit.reviewdb.Account;
+import com.google.gerrit.server.account.AuthRequest;
+
+/** Verifies the token sent by {@link RegisterNewEmailSender}. */
+public interface EmailTokenVerifier {
+  /**
+   * Construct a token to verify an email address for a user.
+   *
+   * @param accountId the caller that wants to add an email to their account.
+   * @param emailAddress the address to add.
+   * @return an unforgeable string to email to {@code emailAddress}. Presenting
+   *         the string provides proof the user has the ability to read messages
+   *         sent to that address.
+   */
+  public String encode(Account.Id accountId, String emailAddress);
+
+  /**
+   * Decode a token previously created.
+   * @param tokenString the string created by encode.
+   * @return a pair of account id and email address.
+   * @throws InvalidTokenException the token is invalid, expired, malformed, etc.
+   */
+  public ParsedToken decode(String tokenString) throws InvalidTokenException;
+
+  /** Exception thrown when a token does not parse correctly. */
+  public static class InvalidTokenException extends Exception {
+    public InvalidTokenException() {
+      super("Invalid token");
+    }
+
+    public InvalidTokenException(Throwable cause) {
+      super("Invalid token", cause);
+    }
+  }
+
+  /** Pair returned from decode to provide the data used during encode. */
+  public static class ParsedToken {
+    private final Account.Id accountId;
+    private final String emailAddress;
+
+    public ParsedToken(Account.Id accountId, String emailAddress) {
+      this.accountId = accountId;
+      this.emailAddress = emailAddress;
+    }
+
+    public Account.Id getAccountId() {
+      return accountId;
+    }
+
+    public String getEmailAddress() {
+      return emailAddress;
+    }
+
+    public AuthRequest toAuthRequest() {
+      return AuthRequest.forEmail(getEmailAddress());
+    }
+
+    @Override
+    public String toString() {
+      return accountId + " adds " + emailAddress;
+    }
+  }
+}
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 05a419e..0abf96f 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
@@ -18,6 +18,7 @@
 import com.google.gerrit.reviewdb.Account;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -34,6 +35,7 @@
 
   @Inject
   FromAddressGeneratorProvider(@GerritServerConfig final Config cfg,
+      final @AnonymousCowardName String anonymousCowardName,
       @GerritPersonIdent final PersonIdent myIdent,
       final AccountCache accountCache) {
 
@@ -42,7 +44,9 @@
 
     if (from == null || "MIXED".equalsIgnoreCase(from)) {
       ParameterizedString name = new ParameterizedString("${user} (Code Review)");
-      generator = new PatternGen(srvAddr, accountCache, name, srvAddr.email);
+      generator =
+          new PatternGen(srvAddr, accountCache, anonymousCowardName, name,
+              srvAddr.email);
 
     } else if ("USER".equalsIgnoreCase(from)) {
       generator = new UserGen(accountCache, srvAddr);
@@ -56,7 +60,9 @@
       if (name == null || name.getParameterNames().isEmpty()) {
         generator = new ServerGen(a);
       } else {
-        generator = new PatternGen(srvAddr, accountCache, name, a.email);
+        generator =
+            new PatternGen(srvAddr, accountCache, anonymousCowardName, name,
+                a.email);
       }
     }
   }
@@ -118,13 +124,16 @@
     private final String senderEmail;
     private final Address serverAddress;
     private final AccountCache accountCache;
+    private final String anonymousCowardName;
     private final ParameterizedString namePattern;
 
     PatternGen(final Address serverAddress, final AccountCache accountCache,
+        final String anonymousCowardName,
         final ParameterizedString namePattern, final String senderEmail) {
       this.senderEmail = senderEmail;
       this.serverAddress = serverAddress;
       this.accountCache = accountCache;
+      this.anonymousCowardName = anonymousCowardName;
       this.namePattern = namePattern;
     }
 
@@ -141,7 +150,7 @@
         final Account account = accountCache.get(fromId).getAccount();
         String fullName = account.getFullName();
         if (fullName == null || "".equals(fullName)) {
-          fullName = "Anonymous Coward";
+          fullName = anonymousCowardName;
         }
         senderName = namePattern.replace("user", fullName).toString();
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergeFailSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergeFailSender.java
index 19fb48b..f11b7c4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergeFailSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MergeFailSender.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.mail;
 
 import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -25,8 +26,9 @@
   }
 
   @Inject
-  public MergeFailSender(EmailArguments ea, @Assisted Change c) {
-    super(ea, c, "merge-failed");
+  public MergeFailSender(EmailArguments ea,
+      @AnonymousCowardName String anonymousCowardName, @Assisted Change c) {
+    super(ea, anonymousCowardName, c, "merge-failed");
   }
 
   @Override
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 9dbabe6..54efb02 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
@@ -23,6 +23,7 @@
 import com.google.gerrit.reviewdb.ApprovalCategoryValue;
 import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.reviewdb.PatchSetApproval;
+import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gwtorm.client.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
@@ -39,8 +40,10 @@
   private final ApprovalTypes approvalTypes;
 
   @Inject
-  public MergedSender(EmailArguments ea, ApprovalTypes at, @Assisted Change c) {
-    super(ea, c, "merged");
+  public MergedSender(EmailArguments ea,
+      @AnonymousCowardName String anonymousCowardName, ApprovalTypes at,
+      @Assisted Change c) {
+    super(ea, anonymousCowardName, c, "merged");
     approvalTypes = at;
   }
 
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 186f57a..fb9ba60 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
@@ -32,8 +32,9 @@
   private final Set<Account.Id> reviewers = new HashSet<Account.Id>();
   private final Set<Account.Id> extraCC = new HashSet<Account.Id>();
 
-  protected NewChangeSender(EmailArguments ea, SshInfo sshInfo, Change c) {
-    super(ea, c, "newchange");
+  protected NewChangeSender(EmailArguments ea, String anonymousCowardName,
+      SshInfo sshInfo, Change c) {
+    super(ea, anonymousCowardName, c, "newchange");
     this.sshInfo = sshInfo;
   }
 
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 e6a1ebc..d208c84 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
@@ -18,14 +18,19 @@
 import com.google.gerrit.reviewdb.UserIdentity;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.mail.EmailHeader.AddressList;
+import com.google.gwtorm.client.OrmException;
 
 import org.apache.commons.lang.StringUtils;
+import org.apache.velocity.Template;
 import org.apache.velocity.VelocityContext;
-import org.apache.velocity.app.Velocity;
+import org.apache.velocity.context.InternalContextAdapterImpl;
+import org.apache.velocity.runtime.RuntimeInstance;
+import org.apache.velocity.runtime.parser.node.SimpleNode;
 import org.eclipse.jgit.util.SystemReader;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.io.StringReader;
 import java.io.StringWriter;
 import java.net.MalformedURLException;
 import java.net.URL;
@@ -54,10 +59,14 @@
   protected VelocityContext velocityContext;
 
   protected final EmailArguments args;
+  private final String anonymousCowardName;
   protected Account.Id fromId;
 
-  protected OutgoingEmail(EmailArguments ea, final String mc) {
+
+  protected OutgoingEmail(EmailArguments ea, final String anonymousCowardName,
+      final String mc) {
     args = ea;
+    this.anonymousCowardName = anonymousCowardName;
     messageClass = mc;
     headers = new LinkedHashMap<String, EmailHeader>();
   }
@@ -226,7 +235,7 @@
   /** Lookup a human readable name for an account, usually the "full name". */
   protected String getNameFor(final Account.Id accountId) {
     if (accountId == null) {
-      return "Anonymous Coward";
+      return anonymousCowardName;
     }
 
     final Account userAccount = args.accountCache.get(accountId).getAccount();
@@ -235,7 +244,7 @@
       name = userAccount.getPreferredEmail();
     }
     if (name == null) {
-      name = "Anonymous Coward #" + accountId;
+      name = anonymousCowardName + " #" + accountId;
     }
     return name;
   }
@@ -254,7 +263,7 @@
       return email;
 
     } else /* (name == null && email == null) */{
-      return "Anonymous Coward #" + accountId;
+      return anonymousCowardName + " #" + accountId;
     }
   }
 
@@ -297,13 +306,17 @@
 
   /** Schedule delivery of this message to the given account. */
   protected void add(final RecipientType rt, final Account.Id to) {
-    if (!rcptTo.contains(to) && isVisibleTo(to)) {
-      rcptTo.add(to);
-      add(rt, toAddress(to));
+    try {
+      if (!rcptTo.contains(to) && isVisibleTo(to)) {
+        rcptTo.add(to);
+        add(rt, toAddress(to));
+      }
+    } catch (OrmException e) {
+      log.error("Error reading database for account: " + to, e);
     }
   }
 
-  protected boolean isVisibleTo(final Account.Id to) {
+  protected boolean isVisibleTo(final Account.Id to) throws OrmException {
     return true;
   }
 
@@ -345,24 +358,64 @@
 
   protected String velocify(String template) throws EmailException {
     try {
-      StringWriter w = new StringWriter();
-      Velocity.evaluate(velocityContext, w, "OutgoingEmail", template);
-      return w.toString();
-    } catch(Exception e) {
-      throw new EmailException("Velocity template " + template, e);
+      RuntimeInstance runtime = args.velocityRuntime;
+      String templateName = "OutgoingEmail";
+      SimpleNode tree = runtime.parse(new StringReader(template), templateName);
+      InternalContextAdapterImpl ica = new InternalContextAdapterImpl(velocityContext);
+      ica.pushCurrentTemplateName(templateName);
+      try {
+        tree.init(ica, runtime);
+        StringWriter w = new StringWriter();
+        tree.render(ica, w);
+        return w.toString();
+      } finally {
+        ica.popCurrentTemplateName();
+      }
+    } catch (Exception e) {
+      throw new EmailException("Cannot format velocity template: " + template, e);
     }
   }
 
   protected String velocifyFile(String name) throws EmailException {
-    if (!Velocity.resourceExists(name)) {
-      name = "com/google/gerrit/server/mail/" + name;
-    }
     try {
+      RuntimeInstance runtime = args.velocityRuntime;
+      if (runtime.getLoaderNameForResource(name) == null) {
+        name = "com/google/gerrit/server/mail/" + name;
+      }
+      Template template = runtime.getTemplate(name, "UTF-8");
       StringWriter w = new StringWriter();
-      Velocity.mergeTemplate(name, "UTF-8", velocityContext, w);
+      template.merge(velocityContext, w);
       return w.toString();
-    } catch(Exception e) {
-      throw new EmailException("Velocity template " + name + ".\n", e);
+    } catch (EmailException e) {
+      throw e;
+    } catch (Exception e) {
+      throw new EmailException("Cannot format velocity template " + name, e);
     }
   }
+
+  public String joinStrings(Iterable<Object> in, String joiner) {
+    return joinStrings(in.iterator(), joiner);
+  }
+
+  public String joinStrings(Iterator<Object> in, String joiner) {
+    if (!in.hasNext()) {
+      return "";
+    }
+
+    Object first = in.next();
+    if (!in.hasNext()) {
+      return safeToString(first);
+    }
+
+    StringBuilder r = new StringBuilder();
+    r.append(safeToString(first));
+    while (in.hasNext()) {
+      r.append(joiner).append(safeToString(in.next()));
+    }
+    return r.toString();
+  }
+
+  private static String safeToString(Object obj) {
+    return obj != null ? obj.toString() : "";
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RegisterNewEmailSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RegisterNewEmailSender.java
index 2c77999..17fe9c6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RegisterNewEmailSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RegisterNewEmailSender.java
@@ -14,28 +14,30 @@
 
 package com.google.gerrit.server.mail;
 
-import com.google.gerrit.server.config.AuthConfig;
-import com.google.gwtjsonrpc.server.XsrfException;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
-import org.eclipse.jgit.util.Base64;
-
-import java.io.UnsupportedEncodingException;
-
 public class RegisterNewEmailSender extends OutgoingEmail {
   public interface Factory {
     public RegisterNewEmailSender create(String address);
   }
 
-  private final AuthConfig authConfig;
+  private final EmailTokenVerifier tokenVerifier;
+  private final IdentifiedUser user;
   private final String addr;
+  private String emailToken;
 
   @Inject
-  public RegisterNewEmailSender(EmailArguments ea, AuthConfig ac,
+  public RegisterNewEmailSender(EmailArguments ea,
+      EmailTokenVerifier etv,
+      @AnonymousCowardName String anonymousCowardName,
+      IdentifiedUser callingUser,
       @Assisted final String address) {
-    super(ea, "registernewemail");
-    authConfig = ac;
+    super(ea, anonymousCowardName, "registernewemail");
+    tokenVerifier = etv;
+    user = callingUser;
     addr = address;
   }
 
@@ -56,14 +58,29 @@
     appendText(velocifyFile("RegisterNewEmail.vm"));
   }
 
-  public String getEmailRegistrationToken() {
-    try {
-      return authConfig.getEmailRegistrationToken().newToken(
-          Base64.encodeBytes(addr.getBytes("UTF-8")));
-    } catch (XsrfException e) {
-      throw new IllegalArgumentException(e);
-    } catch (UnsupportedEncodingException e) {
-      throw new IllegalArgumentException(e);
+  public String getUserNameEmail() {
+    String name = user.getAccount().getFullName();
+    String email = user.getAccount().getPreferredEmail();
+
+    if (name != null && email != null) {
+      return name + " <" + email + ">";
+    } else if (email != null) {
+      return email;
+    } else if (name != null) {
+      return name;
+    } else {
+      String username = user.getUserName();
+      if (username != null) {
+        return username;
+      }
     }
+    return null;
+  }
+
+  public String getEmailRegistrationToken() {
+    if (emailToken == null) {
+      emailToken = tokenVerifier.encode(user.getAccountId(), addr);
+    }
+    return emailToken;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplacePatchSetSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplacePatchSetSender.java
index 2ae27fa..b09cb27 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplacePatchSetSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplacePatchSetSender.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.reviewdb.Account;
 import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.ssh.SshInfo;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
@@ -39,8 +40,10 @@
   private final SshInfo sshInfo;
 
   @Inject
-  public ReplacePatchSetSender(EmailArguments ea, SshInfo si, @Assisted Change c) {
-    super(ea, c, "newpatchset");
+  public ReplacePatchSetSender(EmailArguments ea,
+      @AnonymousCowardName String anonymousCowardName, SshInfo si,
+      @Assisted Change c) {
+    super(ea, anonymousCowardName, c, "newpatchset");
     sshInfo = si;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplyToChangeSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplyToChangeSender.java
index 401a0b8..6b0ed4d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplyToChangeSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ReplyToChangeSender.java
@@ -22,8 +22,9 @@
     public T create(Change change);
   }
 
-  protected ReplyToChangeSender(EmailArguments ea, Change c, String mc) {
-    super(ea, c, mc);
+  protected ReplyToChangeSender(EmailArguments ea, String anonymousCowardName,
+      Change c, String mc) {
+    super(ea, anonymousCowardName, c, mc);
   }
 
   @Override
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 fee73d6..b9df397 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.Change;
+import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -26,8 +27,9 @@
   }
 
   @Inject
-  public RestoredSender(EmailArguments ea, @Assisted Change c) {
-    super(ea, c, "restore");
+  public RestoredSender(EmailArguments ea,
+      @AnonymousCowardName String anonymousCowardName, @Assisted Change c) {
+    super(ea, anonymousCowardName, c, "restore");
   }
 
   @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 8ad993b..e6f88ea 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.Change;
+import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -25,8 +26,9 @@
   }
 
   @Inject
-  public RevertedSender(EmailArguments ea, @Assisted Change c) {
-    super(ea, c, "revert");
+  public RevertedSender(EmailArguments ea,
+      @AnonymousCowardName String anonymousCowardName, @Assisted Change c) {
+    super(ea, anonymousCowardName, c, "revert");
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/SignedTokenEmailTokenVerifier.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/SignedTokenEmailTokenVerifier.java
new file mode 100644
index 0000000..aad2f76
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/SignedTokenEmailTokenVerifier.java
@@ -0,0 +1,81 @@
+// Copyright 2012 Google Inc. All Rights Reserved.
+
+package com.google.gerrit.server.mail;
+
+import com.google.gerrit.reviewdb.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;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+/** Verifies the token sent by {@link RegisterNewEmailSender}. */
+public class SignedTokenEmailTokenVerifier implements EmailTokenVerifier {
+  private final SignedToken emailRegistrationToken;
+
+  public static class Module extends AbstractModule {
+    @Override
+    protected void configure() {
+      bind(EmailTokenVerifier.class).to(SignedTokenEmailTokenVerifier.class);
+    }
+  }
+
+  @Inject
+  SignedTokenEmailTokenVerifier(AuthConfig config) {
+    emailRegistrationToken = config.getEmailRegistrationToken();
+  }
+
+  public String encode(Account.Id accountId, String emailAddress) {
+    try {
+      String payload = String.format("%s:%s", accountId, emailAddress);
+      byte[] utf8 = payload.getBytes("UTF-8");
+      String base64 = Base64.encodeBytes(utf8);
+      return emailRegistrationToken.newToken(base64);
+    } catch (XsrfException e) {
+      throw new IllegalArgumentException(e);
+    } catch (UnsupportedEncodingException e) {
+      throw new IllegalArgumentException(e);
+    }
+  }
+
+  public ParsedToken decode(String tokenString) throws InvalidTokenException {
+    ValidToken token;
+    try {
+      token = emailRegistrationToken.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);
+    }
+
+    Matcher matcher = Pattern.compile("^([0-9]+):(.+@.+)$").matcher(payload);
+    if (!matcher.matches()) {
+      throw new InvalidTokenException();
+    }
+
+    Account.Id id;
+    try {
+      id = Account.Id.parse(matcher.group(1));
+    } catch (IllegalArgumentException err) {
+      throw new InvalidTokenException(err);
+    }
+
+    String newEmail = matcher.group(2);
+    return new ParsedToken(id, newEmail);
+  }
+}
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 3c5e64a..f681710 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
@@ -17,6 +17,7 @@
 import com.google.gerrit.common.Version;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -40,6 +41,13 @@
 /** Sends email via a nearby SMTP server. */
 @Singleton
 public class SmtpEmailSender implements EmailSender {
+  public static class Module extends AbstractModule {
+    @Override
+    protected void configure() {
+      bind(EmailSender.class).to(SmtpEmailSender.class);
+    }
+  }
+
   public static enum Encryption {
     NONE, SSL, TLS;
   }
@@ -148,6 +156,7 @@
         new SimpleDateFormat("EEE, dd MMM yyyy HH:mm:ss Z").format(expiry));
     }
 
+    StringBuffer rejected = new StringBuffer();
     try {
       final SMTPClient client = open();
       try {
@@ -156,11 +165,18 @@
               + " rejected from address " + from.email);
         }
 
+        /* Do not prevent the email from being sent to "good" users simply
+         * because some users get rejected.  If not, a single rejected
+         * project watcher could prevent email for most actions on a project
+         * from being sent to any user!  Instead, queue up the errors, and
+         * throw an exception after sending the email to get the rejected
+         * error(s) logged.
+         */
         for (Address addr : rcpt) {
           if (!client.addRecipient(addr.email)) {
             String error = client.getReplyString();
-            throw new EmailException("Server " + smtpHost
-                + " rejected recipient " + addr + ": " + error);
+            rejected.append("Server " + smtpHost + " rejected recipient "
+                + addr + ": " + error);
           }
         }
 
@@ -189,6 +205,9 @@
         }
 
         client.logout();
+        if (rejected.length() > 0) {
+          throw new EmailException(rejected.toString());
+        }
       } finally {
         client.disconnect();
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/VelocityRuntimeProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/VelocityRuntimeProvider.java
new file mode 100644
index 0000000..144c74d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/VelocityRuntimeProvider.java
@@ -0,0 +1,116 @@
+// 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.mail;
+
+import com.google.gerrit.server.config.SitePaths;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.ProvisionException;
+
+import org.apache.velocity.runtime.RuntimeConstants;
+import org.apache.velocity.runtime.RuntimeInstance;
+import org.apache.velocity.runtime.RuntimeServices;
+import org.apache.velocity.runtime.log.LogChute;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Properties;
+
+/** Configures Velocity template engine for sending email. */
+public class VelocityRuntimeProvider implements Provider<RuntimeInstance> {
+  private final SitePaths site;
+
+  @Inject
+  VelocityRuntimeProvider(SitePaths site) {
+    this.site = site;
+  }
+
+  public RuntimeInstance get() {
+    String rl = "resource.loader";
+    String pkg = "org.apache.velocity.runtime.resource.loader";
+
+    Properties p = new Properties();
+    p.setProperty(RuntimeConstants.VM_PERM_INLINE_LOCAL, "true");
+    p.setProperty(RuntimeConstants.RUNTIME_LOG_LOGSYSTEM_CLASS,
+        Slf4jLogChute.class.getName());
+    p.setProperty("runtime.log.logsystem.log4j.category", "velocity");
+
+    if (site.mail_dir.isDirectory()) {
+      p.setProperty(rl, "file, class");
+      p.setProperty("file." + rl + ".class", pkg + ".FileResourceLoader");
+      p.setProperty("file." + rl + ".path", site.mail_dir.getAbsolutePath());
+      p.setProperty("class." + rl + ".class", pkg + ".ClasspathResourceLoader");
+    } else {
+      p.setProperty(rl, "class");
+      p.setProperty("class." + rl + ".class", pkg + ".ClasspathResourceLoader");
+    }
+
+    RuntimeInstance ri = new RuntimeInstance();
+    try {
+      ri.init(p);
+    } catch (Exception err) {
+      throw new ProvisionException("Cannot configure Velocity templates", err);
+    }
+    return ri;
+  }
+
+  /** Connects Velocity to sfl4j. */
+  public static class Slf4jLogChute implements LogChute {
+    private static final Logger log = LoggerFactory.getLogger("velocity");
+
+    @Override
+    public void init(RuntimeServices rs) {
+    }
+
+    @Override
+    public boolean isLevelEnabled(int level) {
+      switch (level) {
+        default:
+        case DEBUG_ID:
+          return log.isDebugEnabled();
+        case INFO_ID:
+          return log.isInfoEnabled();
+        case WARN_ID:
+          return log.isWarnEnabled();
+        case ERROR_ID:
+          return log.isErrorEnabled();
+      }
+    }
+
+    @Override
+    public void log(int level, String message) {
+      log(level, message, null);
+    }
+
+    @Override
+    public void log(int level, String msg, Throwable err) {
+      switch (level) {
+        default:
+        case DEBUG_ID:
+          log.debug(msg, err);
+          break;
+        case INFO_ID:
+          log.info(msg, err);
+          break;
+        case WARN_ID:
+          log.warn(msg, err);
+          break;
+        case ERROR_ID:
+          log.error(msg, err);
+          break;
+      }
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/AddReviewer.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/AddReviewer.java
index b7f0a81..9fdc899 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/AddReviewer.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/AddReviewer.java
@@ -28,7 +28,7 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountResolver;
 import com.google.gerrit.server.account.GroupCache;
-import com.google.gerrit.server.account.GroupMembersFactory;
+import com.google.gerrit.server.account.GroupMembers;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.mail.AddReviewerSender;
 import com.google.gerrit.server.project.ChangeControl;
@@ -57,7 +57,7 @@
   private final AddReviewerSender.Factory addReviewerSenderFactory;
   private final AccountResolver accountResolver;
   private final GroupCache groupCache;
-  private final GroupMembersFactory.Factory groupMembersFactory;
+  private final GroupMembers.Factory groupMembersFactory;
   private final ChangeControl.Factory changeControlFactory;
   private final ReviewDb db;
   private final IdentifiedUser currentUser;
@@ -72,7 +72,7 @@
   @Inject
   AddReviewer(final AddReviewerSender.Factory addReviewerSenderFactory,
       final AccountResolver accountResolver, final GroupCache groupCache,
-      final GroupMembersFactory.Factory groupMembersFactory,
+      final GroupMembers.Factory groupMembersFactory,
       final ChangeControl.Factory changeControlFactory, final ReviewDb db,
       final IdentifiedUser.GenericFactory identifiedUserFactory,
       final IdentifiedUser currentUser, final ApprovalTypes approvalTypes,
@@ -122,8 +122,8 @@
         }
 
         final Set<Account> members =
-            groupMembersFactory.create(control.getProject().getNameKey(),
-                group.getGroupUUID()).call();
+            groupMembersFactory.create().listAccounts(group.getGroupUUID(),
+                control.getProject().getNameKey());
         if (members == null || members.size() == 0) {
           result.addError(new ReviewerResult.Error(
               ReviewerResult.Error.Type.GROUP_EMPTY, reviewer));
@@ -159,7 +159,9 @@
           if (member.isActive()) {
             final IdentifiedUser user =
                 identifiedUserFactory.create(member.getId());
-            if (control.forUser(user).isVisible()) {
+            // Does not account for draft status as a user might want to let a
+            // reviewer see a draft.
+            if (control.forUser(user).isRefVisible()) {
               reviewerIds.add(member.getId());
             }
           }
@@ -175,7 +177,9 @@
       }
 
       final IdentifiedUser user = identifiedUserFactory.create(account.getId());
-      if (!control.forUser(user).isVisible()) {
+      // Does not account for draft status as a user might want to let a
+      // reviewer see a draft.
+      if (!control.forUser(user).isRefVisible()) {
         result.addError(new ReviewerResult.Error(
             ReviewerResult.Error.Type.CHANGE_NOT_VISIBLE,
             formatUser(account, reviewer)));
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 16a9582..0c70eac 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
@@ -25,7 +25,6 @@
 import com.google.gerrit.server.account.AccountByEmailCache;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.client.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -49,15 +48,12 @@
 @Singleton
 public class PatchSetInfoFactory {
   private final GitRepositoryManager repoManager;
-  private final SchemaFactory<ReviewDb> schemaFactory;
   private final AccountByEmailCache byEmailCache;
 
   @Inject
   public PatchSetInfoFactory(final GitRepositoryManager grm,
-      final SchemaFactory<ReviewDb> schemaFactory,
       final AccountByEmailCache byEmailCache) {
     this.repoManager = grm;
-    this.schemaFactory = schemaFactory;
     this.byEmailCache = byEmailCache;
   }
 
@@ -68,16 +64,13 @@
     info.setAuthor(toUserIdentity(src.getAuthorIdent()));
     info.setCommitter(toUserIdentity(src.getCommitterIdent()));
     info.setRevId(src.getName());
-
     return info;
   }
 
-  public PatchSetInfo get(PatchSet.Id patchSetId)
-      throws PatchSetInfoNotAvailableException {
-    ReviewDb db = null;
+  public PatchSetInfo get(ReviewDb db, PatchSet.Id patchSetId)
+    throws PatchSetInfoNotAvailableException {
     Repository repo = null;
     try {
-      db = schemaFactory.open();
       final PatchSet patchSet = db.patchSets().get(patchSetId);
       final Change change = db.changes().get(patchSet.getId().getParentKey());
       final Project.NameKey projectKey = change.getProject();
@@ -97,9 +90,6 @@
     } catch (IOException e) {
       throw new PatchSetInfoNotAvailableException(e);
     } finally {
-      if (db != null) {
-        db.close();
-      }
       if (repo != null) {
         repo.close();
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PublishComments.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PublishComments.java
index 1d43453..039e0f6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PublishComments.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PublishComments.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.patch;
 
-import com.google.gerrit.common.ChangeHookRunner;
+import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.common.data.ApprovalType;
 import com.google.gerrit.common.data.ApprovalTypes;
 import com.google.gerrit.reviewdb.ApprovalCategory;
@@ -57,7 +57,7 @@
 
   public interface Factory {
     PublishComments create(PatchSet.Id patchSetId, String messageText,
-        Set<ApprovalCategoryValue.Id> approvals);
+        Set<ApprovalCategoryValue.Id> approvals, boolean forceMessage);
   }
 
   private final ReviewDb db;
@@ -67,11 +67,12 @@
   private final PatchSetInfoFactory patchSetInfoFactory;
   private final ChangeControl.Factory changeControlFactory;
   private final FunctionState.Factory functionStateFactory;
-  private final ChangeHookRunner hooks;
+  private final ChangeHooks hooks;
 
   private final PatchSet.Id patchSetId;
   private final String messageText;
   private final Set<ApprovalCategoryValue.Id> approvals;
+  private final boolean forceMessage;
 
   private Change change;
   private PatchSet patchSet;
@@ -85,11 +86,12 @@
       final PatchSetInfoFactory patchSetInfoFactory,
       final ChangeControl.Factory changeControlFactory,
       final FunctionState.Factory functionStateFactory,
-      final ChangeHookRunner hooks,
+      final ChangeHooks hooks,
 
       @Assisted final PatchSet.Id patchSetId,
       @Assisted final String messageText,
-      @Assisted final Set<ApprovalCategoryValue.Id> approvals) {
+      @Assisted final Set<ApprovalCategoryValue.Id> approvals,
+      @Assisted final boolean forceMessage) {
     this.db = db;
     this.user = user;
     this.types = approvalTypes;
@@ -102,6 +104,7 @@
     this.patchSetId = patchSetId;
     this.messageText = messageText;
     this.approvals = approvals;
+    this.forceMessage = forceMessage;
   }
 
   @Override
@@ -116,18 +119,25 @@
     }
     drafts = drafts();
 
-    publishDrafts();
+    db.changes().beginTransaction(changeId);
+    try {
+      publishDrafts();
 
-    final boolean isCurrent = patchSetId.equals(change.currentPatchSetId());
-    if (isCurrent && change.getStatus().isOpen()) {
-      publishApprovals(ctl);
-    } else if (! approvals.isEmpty()) {
-      throw new InvalidChangeOperationException("Change is closed");
-    } else {
-      publishMessageOnly();
+      final boolean isCurrent = patchSetId.equals(change.currentPatchSetId());
+      if (isCurrent && change.getStatus().isOpen()) {
+        publishApprovals(ctl);
+      } else if (approvals.isEmpty() || forceMessage) {
+        publishMessageOnly();
+      } else {
+        throw new InvalidChangeOperationException("Change is closed");
+      }
+
+      touchChange();
+      db.commit();
+    } finally {
+      db.rollback();
     }
 
-    touchChange();
     email();
     fireHook();
     return VoidResult.INSTANCE;
@@ -255,7 +265,7 @@
     msgbuf.append(messageText != null ? messageText : "");
 
     message = new ChangeMessage(new ChangeMessage.Key(change.getId(),//
-        ChangeUtil.messageUUID(db)), user.getAccountId());
+        ChangeUtil.messageUUID(db)), user.getAccountId(), patchSetId);
     message.setMessage(msgbuf.toString());
     db.changeMessages().insert(Collections.singleton(message));
   }
@@ -280,7 +290,7 @@
   }
 
   private List<PatchLineComment> drafts() throws OrmException {
-    return db.patchComments().draft(patchSetId, user.getAccountId()).toList();
+    return db.patchComments().draftByPatchSetAuthor(patchSetId, user.getAccountId()).toList();
   }
 
   private void email() {
@@ -288,7 +298,7 @@
       if (message != null) {
         final CommentSender cm = commentSenderFactory.create(change);
         cm.setFrom(user.getAccountId());
-        cm.setPatchSet(patchSet, patchSetInfoFactory.get(patchSetId));
+        cm.setPatchSet(patchSet, patchSetInfoFactory.get(db, patchSetId));
         cm.setChangeMessage(message);
         cm.setPatchLineComments(drafts);
         cm.send();
@@ -300,14 +310,14 @@
     }
   }
 
-  private void fireHook() {
+  private void fireHook() throws OrmException {
     final Map<ApprovalCategory.Id, ApprovalCategoryValue.Id> changed =
         new HashMap<ApprovalCategory.Id, ApprovalCategoryValue.Id>();
     for (ApprovalCategoryValue.Id v : approvals) {
       changed.put(v.getParentKey(), v);
     }
 
-    hooks.doCommentAddedHook(change, user.getAccount(), patchSet, messageText, changed);
+    hooks.doCommentAddedHook(change, user.getAccount(), patchSet, messageText, changed, db);
   }
 
   private void summarizeInlineComments(StringBuilder in) {
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 6d44511..073372c 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
@@ -27,6 +27,7 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.client.ResultSet;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
@@ -36,7 +37,6 @@
 import com.googlecode.prolog_cafe.lang.Prolog;
 import com.googlecode.prolog_cafe.lang.PrologException;
 import com.googlecode.prolog_cafe.lang.StructureTerm;
-import com.googlecode.prolog_cafe.lang.SymbolTerm;
 import com.googlecode.prolog_cafe.lang.Term;
 import com.googlecode.prolog_cafe.lang.VariableTerm;
 
@@ -46,7 +46,6 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashSet;
-import java.util.LinkedList;
 import java.util.List;
 import java.util.Set;
 
@@ -110,18 +109,18 @@
     }
 
     public ChangeControl validateFor(final Change.Id id)
-        throws NoSuchChangeException {
-      return validate(controlFor(id));
+        throws NoSuchChangeException, OrmException {
+      return validate(controlFor(id), db.get());
     }
 
     public ChangeControl validateFor(final Change change)
-        throws NoSuchChangeException {
-      return validate(controlFor(change));
+        throws NoSuchChangeException, OrmException {
+      return validate(controlFor(change), db.get());
     }
 
-    private static ChangeControl validate(final ChangeControl c)
-        throws NoSuchChangeException {
-      if (!c.isVisible()) {
+    private static ChangeControl validate(final ChangeControl c, final ReviewDb db)
+        throws NoSuchChangeException, OrmException{
+      if (!c.isVisible(db)) {
         throw new NoSuchChangeException(c.getChange().getId());
       }
       return c;
@@ -161,10 +160,26 @@
   }
 
   /** Can this user see this change? */
-  public boolean isVisible() {
+  public boolean isVisible(ReviewDb db) throws OrmException {
+    if (change.getStatus() == Change.Status.DRAFT && !isDraftVisible(db)) {
+      return false;
+    }
+    return isRefVisible();
+  }
+
+  /** Can the user see this change? Does not account for draft status */
+  public boolean isRefVisible() {
     return getRefControl().isVisible();
   }
 
+  /** Can this user see the given patchset? */
+  public boolean isPatchVisible(PatchSet ps, ReviewDb db) throws OrmException {
+    if (ps.isDraft() && !isDraftVisible(db)) {
+      return false;
+    }
+    return isVisible(db);
+  }
+
   /** Can this user abandon this change? */
   public boolean canAbandon() {
     return isOwner() // owner (aka creator) of the change can abandon
@@ -174,6 +189,16 @@
     ;
   }
 
+  /** Can this user publish this draft change or any draft patch set of this change? */
+  public boolean canPublish(final ReviewDb db) throws OrmException {
+    return isOwner() && isVisible(db);
+  }
+
+  /** Can this user delete this draft change or any draft patch set of this change? */
+  public boolean canDeleteDraft(final ReviewDb db) throws OrmException {
+    return isOwner() && isVisible(db);
+  }
+
   /** Can this user restore this change? */
   public boolean canRestore() {
     return canAbandon(); // Anyone who can abandon the change can restore it back
@@ -203,6 +228,21 @@
     return false;
   }
 
+  /** Is this user a reviewer for the change? */
+  public boolean isReviewer(ReviewDb db) throws OrmException {
+    if (getCurrentUser() instanceof IdentifiedUser) {
+      final IdentifiedUser user = (IdentifiedUser) getCurrentUser();
+      ResultSet<PatchSetApproval> results =
+        db.patchSetApprovals().byChange(change.getId());
+      for (PatchSetApproval approval : results) {
+        if (user.getAccountId().equals(approval.getAccountId())) {
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+
   /** @return true if the user is allowed to remove this reviewer. */
   public boolean canRemoveReviewer(PatchSetApproval approval) {
     if (getChange().getStatus().isOpen()) {
@@ -244,6 +284,25 @@
       return ruleError("Patch set " + patchSetId + " is not current");
     }
 
+    try {
+      if (change.getStatus() == Change.Status.DRAFT){
+        if (!isVisible(db)) {
+          return ruleError("Patch set " + patchSetId + " not found");
+        } else {
+          return ruleError("Cannot submit draft changes");
+        }
+      }
+      if (isDraftPatchSet(patchSetId, db)) {
+        if (!isVisible(db)) {
+          return ruleError("Patch set " + patchSetId + " not found");
+        } else {
+          return ruleError("Cannot submit draft patch sets");
+        }
+      }
+    } catch (OrmException err) {
+      return logRuleError("Cannot read patch set " + patchSetId, err);
+    }
+
     List<Term> results = new ArrayList<Term>();
     Term submitRule;
     ProjectState projectState = getProjectControl().getProjectState();
@@ -315,7 +374,9 @@
                 filterRule,
                 resultsTerm,
                 new VariableTerm());
-            results.addAll(((ListTerm) template[2]).toJava());
+            @SuppressWarnings("unchecked")
+            final List<? extends Term> termList = ((ListTerm) template[2]).toJava();
+            results.addAll(termList);
           } catch (PrologException err) {
             return logRuleError("Exception calling " + filterRule + " on change "
                 + change.getId() + " of " + parentState.getProject().getName(), err);
@@ -449,6 +510,18 @@
     }
   }
 
+  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 static boolean isUser(Term who) {
     return who.isStructure()
         && who.arity() == 1
@@ -463,4 +536,4 @@
     }
     return list;
   }
-}
\ No newline at end of file
+}
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
new file mode 100644
index 0000000..474c5c4
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
@@ -0,0 +1,266 @@
+// 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.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.ProjectCreationFailedException;
+import com.google.gerrit.reviewdb.AccountGroup;
+import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.reviewdb.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.config.ProjectOwnerGroups;
+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.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.CommitBuilder;
+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.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.RefUpdate.Result;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Set;
+
+import com.google.gerrit.server.git.RepositoryCaseMismatchException;
+import com.google.gerrit.server.project.ProjectCache;
+
+
+/** Common class that holds the code to create projects */
+public class CreateProject {
+  private static final Logger log = LoggerFactory
+      .getLogger(CreateProject.class);
+
+  public interface Factory {
+    CreateProject create(CreateProjectArgs createProjectArgs);
+  }
+
+  private final Set<AccountGroup.UUID> projectOwnerGroups;
+  private final IdentifiedUser currentUser;
+  private final GitRepositoryManager repoManager;
+  private final ReplicationQueue replication;
+  private final PersonIdent serverIdent;
+  private CreateProjectArgs createProjectArgs;
+  private ProjectCache projectCache;
+  private GroupCache groupCache;
+  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,
+      @Assisted CreateProjectArgs createPArgs, ProjectCache pCache) {
+    this.projectOwnerGroups = pOwnerGroups;
+    this.currentUser = identifiedUser;
+    this.repoManager = gitRepoManager;
+    this.replication = replicateq;
+    this.serverIdent = personIdent;
+    this.createProjectArgs = createPArgs;
+    this.projectCache = pCache;
+    this.groupCache = groupCache;
+    this.metaDataUpdateFactory = metaDataUpdateFactory;
+  }
+
+  public void createProject() throws ProjectCreationFailedException {
+    validateParameters();
+    try {
+      final Project.NameKey nameKey = createProjectArgs.getProject();
+      final String head =
+          createProjectArgs.permissionsOnly ? GitRepositoryManager.REF_CONFIG
+              : createProjectArgs.branch;
+      final Repository repo = repoManager.createRepository(nameKey);
+      try {
+        replication.replicateNewProject(nameKey, head);
+
+        final RefUpdate u = repo.updateRef(Constants.HEAD);
+        u.disableRefLog();
+        u.link(head);
+
+        createProjectConfig();
+
+        if (!createProjectArgs.permissionsOnly
+            && createProjectArgs.createEmptyCommit) {
+          createEmptyCommit(repo, nameKey, createProjectArgs.branch);
+        }
+      } finally {
+        repo.close();
+      }
+    } catch (IllegalStateException e) {
+      handleRepositoryExistsException(createProjectArgs.getProject(), e);
+    } catch (RepositoryCaseMismatchException ee) {
+      handleRepositoryExistsException(ee.getNameOfExistingProject(), ee);
+    } catch (Exception e) {
+      final String msg = "Cannot create " + createProjectArgs.getProject();
+      log.error(msg, e);
+      throw new ProjectCreationFailedException(msg, e);
+    }
+  }
+
+  private void createProjectConfig() throws IOException, ConfigInvalidException {
+    final MetaDataUpdate md =
+        metaDataUpdateFactory.create(createProjectArgs.getProject());
+    try {
+      final ProjectConfig config = ProjectConfig.read(md);
+      config.load(md);
+
+      Project newProject = config.getProject();
+      newProject.setDescription(createProjectArgs.projectDescription);
+      newProject.setSubmitType(createProjectArgs.submitType);
+      newProject
+          .setUseContributorAgreements(createProjectArgs.contributorAgreements);
+      newProject.setUseSignedOffBy(createProjectArgs.signedOffBy);
+      newProject.setUseContentMerge(createProjectArgs.contentMerge);
+      newProject.setRequireChangeID(createProjectArgs.changeIdRequired);
+      if (createProjectArgs.newParent != null) {
+        newProject.setParentName(createProjectArgs.newParent.getProject()
+            .getNameKey());
+      }
+
+      if (!createProjectArgs.ownerIds.isEmpty()) {
+        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);
+            all.getPermission(Permission.OWNER, true).add(
+                new PermissionRule(group));
+          }
+        }
+      }
+
+      md.setMessage("Created project\n");
+      if (!config.commit(md)) {
+        throw new IOException("Cannot create "
+            + createProjectArgs.getProjectName());
+      }
+    } finally {
+      md.close();
+    }
+    projectCache.onCreateProject(createProjectArgs.getProject());
+    repoManager.setProjectDescription(createProjectArgs.getProject(),
+        createProjectArgs.projectDescription);
+    replication.scheduleUpdate(createProjectArgs.getProject(),
+        GitRepositoryManager.REF_CONFIG);
+  }
+
+  private void validateParameters() throws ProjectCreationFailedException {
+    if (createProjectArgs.getProjectName() == null
+        || createProjectArgs.getProjectName().isEmpty()) {
+      throw new ProjectCreationFailedException("Project name is required");
+    }
+
+    if (createProjectArgs.getProjectName().endsWith(Constants.DOT_GIT_EXT)) {
+      createProjectArgs.setProjectName(createProjectArgs.getProjectName()
+          .substring(
+              0,
+              createProjectArgs.getProjectName().length()
+                  - Constants.DOT_GIT_EXT.length()));
+    }
+
+    if (!currentUser.getCapabilities().canCreateProject()) {
+      throw new ProjectCreationFailedException(String.format(
+          "%s does not have \"Create Project\" capability.",
+          currentUser.getUserName()));
+    }
+
+    if (createProjectArgs.ownerIds == null
+        || createProjectArgs.ownerIds.isEmpty()) {
+      createProjectArgs.ownerIds =
+          new ArrayList<AccountGroup.UUID>(projectOwnerGroups);
+    }
+
+    while (createProjectArgs.branch.startsWith("/")) {
+      createProjectArgs.branch = createProjectArgs.branch.substring(1);
+    }
+    if (!createProjectArgs.branch.startsWith(Constants.R_HEADS)) {
+      createProjectArgs.branch = Constants.R_HEADS + createProjectArgs.branch;
+    }
+    if (!Repository.isValidRefName(createProjectArgs.branch)) {
+      throw new ProjectCreationFailedException(String.format(
+          "Branch \"%s\" is not a valid name.", createProjectArgs.branch));
+    }
+  }
+
+  private void createEmptyCommit(final Repository repo,
+      final Project.NameKey project, final String ref) throws IOException {
+    ObjectInserter oi = repo.newObjectInserter();
+    try {
+      CommitBuilder cb = new CommitBuilder();
+      cb.setTreeId(oi.insert(Constants.OBJ_TREE, new byte[] {}));
+      cb.setAuthor(metaDataUpdateFactory.getUserPersonIdent());
+      cb.setCommitter(serverIdent);
+      cb.setMessage("Initial empty repository\n");
+
+      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());
+        }
+      }
+    } catch (IOException e) {
+      log.error(
+          "Cannot create empty commit for "
+              + createProjectArgs.getProjectName(), e);
+      throw e;
+    } finally {
+      oi.release();
+    }
+  }
+
+  private void handleRepositoryExistsException(final Project.NameKey nameKey,
+      Exception e) throws ProjectCreationFailedException {
+    try {
+      Repository repo = repoManager.openRepository(nameKey);
+      try {
+        if (repo.getObjectDatabase().exists()) {
+          throw new ProjectCreationFailedException("Project \"" + nameKey
+              + "\" exists", e);
+        }
+      } finally {
+        repo.close();
+      }
+    } catch (RepositoryNotFoundException er) {
+      throw new ProjectCreationFailedException("Cannot create \"" + nameKey
+          + "\"", er);
+    }
+  }
+}
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
new file mode 100644
index 0000000..63e358f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProjectArgs.java
@@ -0,0 +1,56 @@
+// 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.project;
+
+import com.google.gerrit.reviewdb.AccountGroup;
+import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.reviewdb.Project.SubmitType;
+
+import java.util.List;
+
+public class CreateProjectArgs {
+
+  private Project.NameKey projectName;
+  public List<AccountGroup.UUID> ownerIds;
+  public ProjectControl newParent;
+  public String projectDescription;
+  public SubmitType submitType;
+  public boolean contributorAgreements;
+  public boolean signedOffBy;
+  public boolean permissionsOnly;
+  public String branch;
+  public boolean contentMerge;
+  public boolean changeIdRequired;
+  public boolean createEmptyCommit;
+
+  public CreateProjectArgs() {
+  }
+
+  public Project.NameKey getProject() {
+    return projectName;
+  }
+
+  public String getProjectName() {
+    return projectName != null ? projectName.get() : null;
+  }
+
+  public void setProjectName(String n) {
+    projectName = n != null ? new Project.NameKey(n) : null;
+  }
+
+  public void setProjectName(Project.NameKey n) {
+    projectName = n;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/PerRequestProjectControlCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/PerRequestProjectControlCache.java
index 500ac06..1a96381 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/PerRequestProjectControlCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/PerRequestProjectControlCache.java
@@ -49,4 +49,9 @@
     }
     return ctl;
   }
+
+  public void evict(Project project) {
+    projectCache.evict(project);
+    controls.remove(project.getNameKey());
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheClock.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheClock.java
new file mode 100644
index 0000000..f86562c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheClock.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.project;
+
+import com.google.gerrit.server.config.ConfigUtil;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.lib.Config;
+
+import java.util.concurrent.Executors;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.ThreadFactory;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+/** Ticks periodically to force refresh events for {@link ProjectCacheImpl}. */
+@Singleton
+public class ProjectCacheClock {
+  private volatile long generation;
+
+  @Inject
+  public ProjectCacheClock(@GerritServerConfig Config serverConfig) {
+    this(TimeUnit.MILLISECONDS.convert(
+        ConfigUtil.getTimeUnit(serverConfig,
+            "cache", "projects", "checkFrequency",
+            5, TimeUnit.MINUTES), TimeUnit.MINUTES));
+  }
+
+  public ProjectCacheClock(long checkFrequencyMillis) {
+    if (10 < checkFrequencyMillis) {
+      // Start with generation 1 (to avoid magic 0 below).
+      generation = 1;
+      ThreadFactory factory = new ThreadFactory() {
+        private final AtomicInteger id = new AtomicInteger();
+
+        @Override
+        public Thread newThread(Runnable runnable) {
+          Thread thread = Executors.defaultThreadFactory().newThread(runnable);
+          thread.setName(String.format("ProjectCacheClock-%d", id.incrementAndGet()));
+          thread.setDaemon(true);
+          thread.setPriority(Thread.MIN_PRIORITY);
+          return thread;
+        }
+      };
+      ScheduledExecutorService executor = Executors.newScheduledThreadPool(1, factory);
+      executor.scheduleAtFixedRate(new Runnable() {
+        @Override
+        public void run() {
+          // This is not exactly thread-safe, but is OK for our use.
+          // The only thread that writes the volatile is this task.
+          generation = generation + 1;
+        }
+      }, checkFrequencyMillis, checkFrequencyMillis, TimeUnit.MILLISECONDS);
+    } else {
+      // Magic generation 0 triggers ProjectState to always
+      // check on each needsRefresh() request we make to it.
+      generation = 0;
+    }
+  }
+
+  long read() {
+    return generation;
+  }
+}
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 ff68720..7a7f56e 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
@@ -19,8 +19,6 @@
 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.config.ConfigUtil;
-import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.inject.Inject;
@@ -30,7 +28,6 @@
 import com.google.inject.name.Named;
 
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.Repository;
 
 import java.util.Collections;
@@ -38,8 +35,6 @@
 import java.util.NoSuchElementException;
 import java.util.SortedSet;
 import java.util.TreeSet;
-import java.util.concurrent.Executors;
-import java.util.concurrent.TimeUnit;
 import java.util.concurrent.locks.Lock;
 import java.util.concurrent.locks.ReentrantLock;
 
@@ -71,39 +66,19 @@
   private final Cache<Project.NameKey, ProjectState> byName;
   private final Cache<ListKey,SortedSet<Project.NameKey>> list;
   private final Lock listLock;
-  private volatile long generation;
+  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,
-      @GerritServerConfig final Config serverConfig) {
+      ProjectCacheClock clock) {
     this.allProjectsName = allProjectsName;
     this.byName = byName;
     this.list = list;
     this.listLock = new ReentrantLock(true /* fair */);
-
-    long checkFrequencyMillis = TimeUnit.MILLISECONDS.convert(
-        ConfigUtil.getTimeUnit(serverConfig,
-            "cache", "projects", "checkFrequency",
-            5, TimeUnit.MINUTES), TimeUnit.MINUTES);
-    if (10 < checkFrequencyMillis) {
-      // Start with generation 1 (to avoid magic 0 below).
-      generation = 1;
-      Executors.newScheduledThreadPool(1).scheduleAtFixedRate(new Runnable() {
-        @Override
-        public void run() {
-          // This is not exactly thread-safe, but is OK for our use.
-          // The only thread that writes the volatile is this task.
-          generation = generation + 1;
-        }
-      }, checkFrequencyMillis, checkFrequencyMillis, TimeUnit.MILLISECONDS);
-    } else {
-      // Magic generation 0 triggers ProjectState to always
-      // check on each needsRefresh() request we make to it.
-      generation = 0;
-    }
+    this.clock = clock;
   }
 
   @Override
@@ -125,7 +100,7 @@
    */
   public ProjectState get(final Project.NameKey projectName) {
     ProjectState state = byName.get(projectName);
-    if (state != null && state.needsRefresh(generation)) {
+    if (state != null && state.needsRefresh(clock.read())) {
       byName.remove(projectName);
       state = byName.get(projectName);
     }
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 ee42833..d08f8e3 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
@@ -17,9 +17,9 @@
 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.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.reviewdb.AbstractAgreement;
 import com.google.gerrit.reviewdb.AccountAgreement;
 import com.google.gerrit.reviewdb.AccountGroup;
@@ -192,10 +192,14 @@
     return state.getProject();
   }
 
+  private boolean isHidden() {
+    return getProject().getState().equals(Project.State.HIDDEN);
+  }
+
   /** Can this user see this project exists? */
   public boolean isVisible() {
-    return visibleForReplication()
-        || canPerformOnAnyRef(Permission.READ);
+    return (visibleForReplication()
+        || canPerformOnAnyRef(Permission.READ)) && !isHidden();
   }
 
   public boolean canAddRefs() {
@@ -253,6 +257,19 @@
     return Capable.OK;
   }
 
+  public Set<GroupReference> getAllGroups() {
+    final Set<GroupReference> all = new HashSet<GroupReference>();
+    for (final SectionMatcher matcher : access()) {
+      final AccessSection section = matcher.section;
+      for (final Permission permission : section.getPermissions()) {
+        for (final PermissionRule rule : permission.getRules()) {
+          all.add(rule.getGroup());
+        }
+      }
+    }
+    return all;
+  }
+
   private Capable verifyActiveContributorAgreement() throws OrmException {
     if (! (user instanceof IdentifiedUser)) {
       return new Capable("Must be logged in to verify Contributor Agreement");
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 9c60133..e1341da 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
@@ -18,6 +18,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.Project;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.git.GitRepositoryManager;
@@ -97,8 +98,8 @@
 
   /** Can this user see this reference exists? */
   public boolean isVisible() {
-    return projectControl.visibleForReplication()
-        || canPerform(Permission.READ);
+    return (projectControl.visibleForReplication() || canPerform(Permission.READ))
+        && canRead();
   }
 
   /**
@@ -109,16 +110,16 @@
    *         ref
    */
   public boolean canUpload() {
-    return projectControl
-      .controlForRef("refs/for/" + getRefName())
-      .canPerform(Permission.PUSH);
+    return projectControl.controlForRef("refs/for/" + getRefName())
+        .canPerform(Permission.PUSH)
+        && canWrite();
   }
 
   /** @return true if this user can submit merge patch sets to this ref */
   public boolean canUploadMerges() {
-    return projectControl
-      .controlForRef("refs/for/" + getRefName())
-      .canPerform(Permission.PUSH_MERGE);
+    return projectControl.controlForRef("refs/for/" + getRefName())
+        .canPerform(Permission.PUSH_MERGE)
+        && canWrite();
   }
 
   /** @return true if this user can submit patch sets to this ref */
@@ -131,7 +132,8 @@
       // granting of powers beyond submitting to the configuration.
       return projectControl.isOwner();
     }
-    return canPerform(Permission.SUBMIT);
+    return canPerform(Permission.SUBMIT)
+        && canWrite();
   }
 
   /** @return true if the user can update the reference as a fast-forward. */
@@ -145,17 +147,28 @@
       // granting of powers beyond pushing to the configuration.
       return false;
     }
-    return canPerform(Permission.PUSH);
+    return canPerform(Permission.PUSH)
+        && canWrite();
   }
 
   /** @return true if the user can rewind (force push) the reference. */
   public boolean canForceUpdate() {
-    return canPushWithForce() || canDelete();
+    return (canPushWithForce() || canDelete()) && canWrite();
+  }
+
+  public boolean canWrite() {
+    return getProjectControl().getProject().getState().equals(
+        Project.State.ACTIVE);
+  }
+
+  public boolean canRead() {
+    return getProjectControl().getProject().getState().equals(
+        Project.State.READ_ONLY) || canWrite();
   }
 
   private boolean canPushWithForce() {
-    if (GitRepositoryManager.REF_CONFIG.equals(refName)
-        && !projectControl.isOwner()) {
+    if (!canWrite() || (GitRepositoryManager.REF_CONFIG.equals(refName)
+        && !projectControl.isOwner())) {
       // Pushing requires being at least project owner, in addition to push.
       // Pushing configuration changes modifies the access control
       // rules. Allowing this to be done by a non-project-owner opens
@@ -183,6 +196,9 @@
    * @return {@code true} if the user specified can create a new Git ref
    */
   public boolean canCreate(RevWalk rw, RevObject object) {
+    if (!canWrite()) {
+      return false;
+    }
     boolean owner;
     switch (getCurrentUser().getAccessPath()) {
       case WEB_UI:
@@ -242,7 +258,7 @@
    * @return {@code true} if the user specified can delete a Git ref.
    */
   public boolean canDelete() {
-    if (GitRepositoryManager.REF_CONFIG.equals(refName)) {
+    if (!canWrite() || (GitRepositoryManager.REF_CONFIG.equals(refName))) {
       // Never allow removal of the refs/meta/config branch.
       // Deleting the branch would destroy all Gerrit specific
       // metadata about the project, including its access rules.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SuggestParentCandidates.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SuggestParentCandidates.java
new file mode 100644
index 0000000..9585e1e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SuggestParentCandidates.java
@@ -0,0 +1,81 @@
+// 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.project;
+
+import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gwtorm.client.OrmException;
+import com.google.inject.Inject;
+
+import java.util.ArrayList;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Set;
+import java.util.TreeSet;
+
+public class SuggestParentCandidates {
+  public interface Factory {
+    SuggestParentCandidates create();
+  }
+
+  private final ProjectControl.Factory projectControlFactory;
+  private final ProjectCache projectCache;
+  private final AllProjectsName allProject;
+
+  @Inject
+  SuggestParentCandidates(final ProjectControl.Factory projectControlFactory,
+      final ProjectCache projectCache, final AllProjectsName allProject) {
+    this.projectControlFactory = projectControlFactory;
+    this.projectCache = projectCache;
+    this.allProject = allProject;
+  }
+
+  public List<Project.NameKey> getNameKeys() throws OrmException,
+      NoSuchProjectException {
+    List<Project> pList = getProjects();
+    final List<Project.NameKey> nameKeys =
+        new ArrayList<Project.NameKey>(pList.size());
+    for (Project p : pList) {
+      nameKeys.add(p.getNameKey());
+    }
+    return nameKeys;
+  }
+
+  public List<Project> getProjects() throws OrmException,
+      NoSuchProjectException {
+    Set<Project> projects = new TreeSet<Project>(new Comparator<Project>() {
+      @Override
+      public int compare(Project o1, Project o2) {
+        return o1.getName().compareTo(o2.getName());
+      }
+    });
+    for (Project.NameKey p : projectCache.all()) {
+      try {
+        final ProjectControl control = projectControlFactory.controlFor(p);
+        final Project.NameKey parentK = control.getProject().getParent();
+        if (parentK != null) {
+          ProjectControl pControl = projectControlFactory.controlFor(parentK);
+          if (pControl.isVisible() || pControl.isOwner()) {
+            projects.add(pControl.getProject());
+          }
+        }
+      } catch (NoSuchProjectException e) {
+        continue;
+      }
+    }
+    projects.add(projectControlFactory.controlFor(allProject).getProject());
+    return new ArrayList<Project>(projects);
+  }
+}
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 16b43ad..2f648a9 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
@@ -20,15 +20,23 @@
 import com.google.gerrit.reviewdb.PatchLineComment;
 import com.google.gerrit.reviewdb.PatchSet;
 import com.google.gerrit.reviewdb.PatchSetApproval;
+import com.google.gerrit.reviewdb.Project;
 import com.google.gerrit.reviewdb.ReviewDb;
 import com.google.gerrit.reviewdb.TrackingId;
 import com.google.gerrit.server.CurrentUser;
+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.PatchListEntry;
 import com.google.gwtorm.client.OrmException;
 import com.google.inject.Provider;
 
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
@@ -40,6 +48,7 @@
 public class ChangeData {
   private final Change.Id legacyId;
   private Change change;
+  private String commitMessage;
   private Collection<PatchSet> patches;
   private Collection<PatchSetApproval> approvals;
   private Map<PatchSet.Id,Collection<PatchSetApproval>> approvalsMap;
@@ -164,6 +173,28 @@
     return r;
   }
 
+  public String commitMessage(GitRepositoryManager repoManager,
+      Provider<ReviewDb> db) throws IOException, OrmException {
+    if (commitMessage == null) {
+      PatchSet.Id psId = change(db).currentPatchSetId();
+      String sha1 = db.get().patchSets().get(psId).getRevision().get();
+      Project.NameKey name = change.getProject();
+      Repository repo = repoManager.openRepository(name);
+      try {
+        RevWalk walk = new RevWalk(repo);
+        try {
+          RevCommit c = walk.parseCommit(ObjectId.fromString(sha1));
+          commitMessage = c.getFullMessage();
+        } finally {
+          walk.release();
+        }
+      } finally {
+        repo.close();
+      }
+    }
+    return commitMessage;
+  }
+
   public Collection<PatchSet> patches(Provider<ReviewDb> db)
       throws OrmException {
     if (patches == null) {
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 1e9d405..739d9bc 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,7 +55,7 @@
     }
     try {
       Change c = cd.change(db);
-      if (c != null && changeControl.controlFor(c, user).isVisible()) {
+      if (c != null && changeControl.controlFor(c, user).isVisible(db.get())) {
         cd.cacheVisibleTo(user);
         return true;
       } else {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LabelPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LabelPredicate.java
index ece61ef..7342c4f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LabelPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LabelPredicate.java
@@ -154,7 +154,7 @@
           try {
             ChangeControl cc = ccFactory.controlFor(object.change(dbProvider), //
                 userFactory.create(dbProvider, p.getAccountId()));
-            if (!cc.isVisible()) {
+            if (!cc.isVisible(dbProvider.get())) {
               // The user can't see the change anymore.
               //
               continue;
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 2abec9a..030e83f 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
@@ -23,6 +23,7 @@
 import com.google.gerrit.server.events.EventFactory;
 import com.google.gerrit.server.events.PatchSetAttribute;
 import com.google.gerrit.server.events.QueryStats;
+import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.query.Predicate;
 import com.google.gerrit.server.query.QueryParseException;
 import com.google.gson.Gson;
@@ -66,6 +67,7 @@
   private final ChangeQueryBuilder queryBuilder;
   private final ChangeQueryRewriter queryRewriter;
   private final Provider<ReviewDb> db;
+  private final GitRepositoryManager repoManager;
   private final int maxLimit;
 
   private OutputFormat outputFormat = OutputFormat.TEXT;
@@ -73,6 +75,8 @@
   private boolean includeCurrentPatchSet;
   private boolean includeApprovals;
   private boolean includeComments;
+  private boolean includeFiles;
+  private boolean includeCommitMessage;
 
   private OutputStream outputStream = DisabledOutputStream.INSTANCE;
   private PrintWriter out;
@@ -80,11 +84,13 @@
   @Inject
   QueryProcessor(EventFactory eventFactory,
       ChangeQueryBuilder.Factory queryBuilder, CurrentUser currentUser,
-      ChangeQueryRewriter queryRewriter, Provider<ReviewDb> db) {
+      ChangeQueryRewriter queryRewriter, Provider<ReviewDb> db,
+      GitRepositoryManager repoManager) {
     this.eventFactory = eventFactory;
     this.queryBuilder = queryBuilder.create(currentUser);
     this.queryRewriter = queryRewriter;
     this.db = db;
+    this.repoManager = repoManager;
     this.maxLimit = currentUser.getCapabilities()
       .getRange(GlobalCapability.QUERY_LIMIT)
       .getMax();
@@ -94,10 +100,18 @@
     includePatchSets = on;
   }
 
+  public boolean getIncludePatchSets() {
+    return includePatchSets;
+  }
+
   public void setIncludeCurrentPatchSet(boolean on) {
     includeCurrentPatchSet = on;
   }
 
+  public boolean getIncludeCurrentPatchSet() {
+    return includeCurrentPatchSet;
+  }
+
   public void setIncludeApprovals(boolean on) {
     includeApprovals = on;
   }
@@ -106,6 +120,18 @@
     includeComments = on;
   }
 
+  public void setIncludeFiles(boolean on) {
+    includeFiles = on;
+  }
+
+  public boolean getIncludeFiles() {
+    return includeFiles;
+  }
+
+  public void setIncludeCommitMessage(boolean on) {
+    includeCommitMessage = on;
+  }
+
   public void setOutput(OutputStream out, OutputFormat fmt) {
     this.outputStream = out;
     this.outputFormat = fmt;
@@ -172,9 +198,19 @@
           eventFactory.extend(c, d.getChange());
           eventFactory.addTrackingIds(c, d.trackingIds(db));
 
+          if (includeCommitMessage) {
+            eventFactory.addCommitMessage(c, d.commitMessage(repoManager, db));
+          }
+
           if (includePatchSets) {
-            eventFactory.addPatchSets(c, d.patches(db),
-              includeApprovals ? d.approvalsMap(db) : null);
+            if (includeFiles) {
+              eventFactory.addPatchSets(c, d.patches(db),
+                includeApprovals ? d.approvalsMap(db) : null,
+                includeFiles, d.change(db));
+            } else {
+              eventFactory.addPatchSets(c, d.patches(db),
+                  includeApprovals ? d.approvalsMap(db) : null);
+            }
           }
 
           if (includeCurrentPatchSet) {
@@ -183,6 +219,11 @@
               c.currentPatchSet = eventFactory.asPatchSetAttribute(current);
               eventFactory.addApprovals(c.currentPatchSet, //
                   d.approvalsFor(db, current.getId()));
+
+              if (includeFiles) {
+                eventFactory.addPatchSetFileNames(c.currentPatchSet,
+                    d.change(db), d.currentPatchSet(db));
+              }
             }
           }
 
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 701e97a..65f5820 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
@@ -87,11 +87,7 @@
 
     if (url == null || url.isEmpty()) {
       if (type == null) {
-        if (url != null && !url.isEmpty()) {
-          type = Type.JDBC;
-        } else {
-          type = Type.H2;
-        }
+        type = Type.H2;
       }
 
       switch (type) {
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 08e9b55..b8164b4 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
@@ -47,7 +47,9 @@
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
 
 import java.io.File;
@@ -209,6 +211,8 @@
       // inheritable permissions. For example 'All-Projects'.
       try {
         git = mgr.createRepository(allProjectsName);
+        final RefUpdate u = git.updateRef(Constants.HEAD);
+        u.link(GitRepositoryManager.REF_CONFIG);
       } catch (RepositoryNotFoundException err) {
         final String name = allProjectsName.get();
         throw new IOException("Cannot create repository " + name, err);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaModule.java
index ea277d7..5ba7d4c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaModule.java
@@ -16,14 +16,13 @@
 
 import static com.google.inject.Scopes.SINGLETON;
 
-import com.google.gerrit.lifecycle.LifecycleModule;
 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.AllProjectsNameProvider;
+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.git.GitRepositoryManager;
-import com.google.gerrit.server.git.LocalDiskRepositoryManager;
 
 import org.eclipse.jgit.lib.PersonIdent;
 
@@ -40,12 +39,7 @@
       .toProvider(AllProjectsNameProvider.class)
       .in(SINGLETON);
 
-    bind(GitRepositoryManager.class).to(LocalDiskRepositoryManager.class);
-    install(new LifecycleModule() {
-      @Override
-      protected void configure() {
-        listener().to(LocalDiskRepositoryManager.Lifecycle.class);
-      }
-    });
+    bind(String.class).annotatedWith(AnonymousCowardName.class).toProvider(
+        AnonymousCowardNameProvider.class);
   }
 }
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 66b5d8c..abdc0ee 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. */
-  private static final Class<? extends SchemaVersion> C = Schema_59.class;
+  public static final Class<Schema_61> C = Schema_61.class;
 
   public static class Module extends AbstractModule {
     @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_53.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_53.java
index 152f25a..89ee5cf 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
@@ -30,7 +30,6 @@
 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.reviewdb.AccountGroup;
 import com.google.gerrit.reviewdb.ApprovalCategory;
 import com.google.gerrit.reviewdb.Project;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_60.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_60.java
new file mode 100644
index 0000000..eec7256
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_60.java
@@ -0,0 +1,71 @@
+// 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.schema;
+
+import com.google.gerrit.reviewdb.Change;
+
+import com.google.gerrit.reviewdb.ChangeMessage;
+import com.google.gerrit.reviewdb.PatchSet;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.client.ResultSet;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+
+public class Schema_60 extends SchemaVersion {
+  @Inject
+  Schema_60(Provider<Schema_59> prior) {
+    super(prior);
+  }
+
+  @Override
+  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException {
+    Pattern patternA = Pattern.compile("Patch Set ([0-9]+):.*", Pattern.DOTALL);
+    Pattern patternB = Pattern.compile("Uploaded patch set ([0-9]+).");
+    ResultSet<ChangeMessage> results = db.changeMessages().all();
+    List<ChangeMessage> updates = new LinkedList<ChangeMessage>();
+    for (ChangeMessage cm : results) {
+      Change.Id id = cm.getKey().getParentKey();
+      String msg = cm.getMessage();
+      Matcher matcherA = patternA.matcher(msg);
+      Matcher matcherB = patternB.matcher(msg);
+      PatchSet.Id newId = null;
+      if (matcherA.matches()) {
+        int patchSetNum = Integer.parseInt(matcherA.group(1));
+        newId = new PatchSet.Id(id, patchSetNum);
+      } else if (matcherB.matches()) {
+        int patchSetNum = Integer.parseInt(matcherB.group(1));
+        newId = new PatchSet.Id(id, patchSetNum);
+      }
+      if (newId != null) {
+        cm.setPatchSetId(newId);
+        updates.add(cm);
+      }
+      if (updates.size() >= 100) {
+        db.changeMessages().update(updates);
+        updates.clear();
+      }
+    }
+    if (!updates.isEmpty()) {
+      db.changeMessages().update(updates);
+    }
+  }
+}
\ No newline at end of file
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_61.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_61.java
new file mode 100644
index 0000000..890c824
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_61.java
@@ -0,0 +1,25 @@
+// 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.schema;
+
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class Schema_61 extends SchemaVersion {
+  @Inject
+  Schema_61(Provider<Schema_60> prior) {
+    super(prior);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ssh/NoSshInfo.java b/gerrit-server/src/main/java/com/google/gerrit/server/ssh/NoSshInfo.java
new file mode 100644
index 0000000..882181d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ssh/NoSshInfo.java
@@ -0,0 +1,29 @@
+// 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.ssh;
+
+import com.google.gerrit.server.ssh.SshInfo;
+
+import com.jcraft.jsch.HostKey;
+
+import java.util.Collections;
+import java.util.List;
+
+class NoSshInfo implements SshInfo {
+  @Override
+  public List<HostKey> getHostKeys() {
+    return Collections.emptyList();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ssh/NoSshKeyCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/ssh/NoSshKeyCache.java
new file mode 100644
index 0000000..ad18b3f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ssh/NoSshKeyCache.java
@@ -0,0 +1,30 @@
+// 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.ssh;
+
+import com.google.gerrit.common.errors.InvalidSshKeyException;
+import com.google.gerrit.reviewdb.AccountSshKey;
+
+class NoSshKeyCache implements SshKeyCache {
+  @Override
+  public void evict(String username) {
+  }
+
+  @Override
+  public AccountSshKey create(AccountSshKey.Id id, String encoded)
+      throws InvalidSshKeyException {
+    throw new InvalidSshKeyException();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ssh/NoSshModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/ssh/NoSshModule.java
new file mode 100644
index 0000000..21b1a54
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ssh/NoSshModule.java
@@ -0,0 +1,28 @@
+// 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.ssh;
+
+import com.google.inject.AbstractModule;
+
+/**
+ * Disables the SSH support by stubbing out relevant objects.
+ */
+public class NoSshModule extends AbstractModule {
+  @Override
+  protected void configure() {
+    bind(SshInfo.class).to(NoSshInfo.class);
+    bind(SshKeyCache.class).to(NoSshKeyCache.class);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/MagicBranch.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/MagicBranch.java
new file mode 100644
index 0000000..ef08d78
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/MagicBranch.java
@@ -0,0 +1,111 @@
+// 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.util;
+
+import com.google.gerrit.common.data.Capable;
+import com.google.gerrit.reviewdb.Project;
+
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.Map;
+
+public final class MagicBranch {
+  private static final Logger log =
+    LoggerFactory.getLogger(MagicBranch.class);
+
+  public static final String NEW_CHANGE = "refs/for/";
+  public static final String NEW_DRAFT_CHANGE = "refs/drafts/";
+  public static final String NEW_PUBLISH_CHANGE = "refs/publish/";
+
+  /** Extracts the destination from a ref name */
+  public static String getDestBranchName(String refName) {
+    String magicBranch = NEW_CHANGE;
+    if (refName.startsWith(NEW_DRAFT_CHANGE)) {
+      magicBranch = NEW_DRAFT_CHANGE;
+    } else if (refName.startsWith(NEW_PUBLISH_CHANGE)) {
+      magicBranch = NEW_PUBLISH_CHANGE;
+    }
+    return refName.substring(magicBranch.length());
+  }
+
+  /** Checks if the supplied ref name is a magic branch */
+  public static boolean isMagicBranch(String refName) {
+    if (refName.startsWith(NEW_DRAFT_CHANGE) ||
+        refName.startsWith(NEW_PUBLISH_CHANGE) ||
+        refName.startsWith(NEW_CHANGE)) {
+      return true;
+    }
+    return false;
+  }
+
+  /**
+   * Checks if a (magic branch)/branch_name reference exists in the
+   * destination repository and only returns Capable.OK if it does not match any.
+   *
+   * These block the client from being able to even send us a pack file, as it
+   * is very unlikely the user passed the --force flag and the new commit is
+   * probably not going to fast-forward the branch.
+   */
+  public static Capable checkMagicBranchRefs(Repository repo,
+      Project project) {
+    Capable result = checkMagicBranchRef(NEW_CHANGE, repo, project);
+    if (result != Capable.OK) {
+      return result;
+    }
+    result = checkMagicBranchRef(NEW_DRAFT_CHANGE, repo, project);
+    if (result != Capable.OK) {
+      return result;
+    }
+    result = checkMagicBranchRef(NEW_PUBLISH_CHANGE, repo, project);
+    if (result != Capable.OK) {
+      return result;
+    }
+
+    return Capable.OK;
+  }
+
+  /** Checks if ref name matches the draft magic branch */
+  public static boolean isDraft(String refName) {
+    return refName.startsWith(MagicBranch.NEW_DRAFT_CHANGE);
+  }
+
+  private static Capable checkMagicBranchRef(String branchName, Repository repo,
+      Project project) {
+    Map<String, Ref> blockingFors;
+    try {
+      blockingFors = repo.getRefDatabase().getRefs(branchName);
+    } catch (IOException err) {
+      String projName = project.getName();
+      log.warn("Cannot scan refs in '" + projName + "'", err);
+      return new Capable("Server process cannot read '" + projName + "'");
+    }
+    if (!blockingFors.isEmpty()) {
+      String projName = project.getName();
+      log.error("Repository '" + projName
+          + "' needs the following refs removed to receive changes: "
+          + blockingFors.keySet());
+      return new Capable("One or more " + branchName + " names blocks change upload");
+    }
+
+    return Capable.OK;
+  }
+
+  private MagicBranch() {
+  }
+}
\ No newline at end of file
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/SubmoduleSectionParser.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/SubmoduleSectionParser.java
new file mode 100644
index 0000000..3148aaa
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/SubmoduleSectionParser.java
@@ -0,0 +1,125 @@
+// 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.util;
+
+import com.google.gerrit.reviewdb.Branch;
+import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.reviewdb.SubmoduleSubscription;
+import com.google.gerrit.server.git.GitRepositoryManager;
+
+import org.eclipse.jgit.lib.BlobBasedConfig;
+import org.eclipse.jgit.lib.Constants;
+
+import java.net.URI;
+import java.net.URISyntaxException;
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * It parses from a configuration file submodule sections.
+ * <p>
+ * Example of submodule sections:
+ *
+ * <pre>
+ * [submodule "project-a"]
+ *     url = http://localhost/a
+ *     path = a
+ *     branch = .
+ *
+ * [submodule "project-b"]
+ *     url = http://localhost/b
+ *     path = b
+ *     branch = refs/heads/test
+ * </pre>
+ */
+public class SubmoduleSectionParser {
+  private final BlobBasedConfig bbc;
+  private final String thisServer;
+  private final Branch.NameKey superProjectBranch;
+  private final GitRepositoryManager repoManager;
+
+  public SubmoduleSectionParser(final BlobBasedConfig bbc,
+      final String thisServer, final Branch.NameKey superProjectBranch,
+      final GitRepositoryManager repoManager) {
+    this.bbc = bbc;
+    this.thisServer = thisServer;
+    this.superProjectBranch = superProjectBranch;
+    this.repoManager = repoManager;
+  }
+
+  public List<SubmoduleSubscription> parseAllSections() {
+    List<SubmoduleSubscription> parsedSubscriptions =
+        new ArrayList<SubmoduleSubscription>();
+    for (final String id : bbc.getSubsections("submodule")) {
+      final SubmoduleSubscription subscription = parse(id);
+      if (subscription != null) {
+        parsedSubscriptions.add(subscription);
+      }
+    }
+    return parsedSubscriptions;
+  }
+
+  private SubmoduleSubscription parse(final String id) {
+    final String url = bbc.getString("submodule", id, "url");
+    final String path = bbc.getString("submodule", id, "path");
+    String branch = bbc.getString("submodule", id, "branch");
+
+    try {
+      if (url != null && url.length() > 0 && path != null && path.length() > 0
+          && branch != null && branch.length() > 0) {
+        // All required fields filled.
+
+        boolean urlIsRelative = url.startsWith("/");
+        String server = null;
+        if (!urlIsRelative) {
+          // It is actually an URI. It could be ssh://localhost/project-a.
+          server = new URI(url).getHost();
+        }
+        if ((urlIsRelative)
+            || (server != null && server.equalsIgnoreCase(thisServer))) {
+          // Subscription really related to this running server.
+          if (branch.equals(".")) {
+            branch = superProjectBranch.get();
+          } else if (!branch.startsWith(Constants.R_REFS)) {
+            branch = Constants.R_HEADS + branch;
+          }
+
+          final String urlExtractedPath = new URI(url).getPath();
+          String projectName = urlExtractedPath;
+          int fromIndex = urlExtractedPath.length() - 1;
+          while (fromIndex > 0) {
+            fromIndex = urlExtractedPath.lastIndexOf('/', fromIndex - 1);
+            projectName = urlExtractedPath.substring(fromIndex + 1);
+
+            if (projectName.endsWith(".git")) {
+              projectName = projectName.substring(0, projectName.length() - 4);
+            }
+
+            if (repoManager.list().contains(new Project.NameKey(projectName))) {
+              return new SubmoduleSubscription(
+                  superProjectBranch,
+                  new Branch.NameKey(new Project.NameKey(projectName), branch),
+                  path);
+            }
+          }
+        }
+      }
+    } catch (URISyntaxException e) {
+      // Error in url syntax (in fact it is uri syntax)
+    }
+
+    return null;
+  }
+}
diff --git a/gerrit-server/src/main/java/gerrit/AbstractCommitUserIdentityPredicate.java b/gerrit-server/src/main/java/gerrit/AbstractCommitUserIdentityPredicate.java
index 94d5b07..27bc286 100644
--- a/gerrit-server/src/main/java/gerrit/AbstractCommitUserIdentityPredicate.java
+++ b/gerrit-server/src/main/java/gerrit/AbstractCommitUserIdentityPredicate.java
@@ -15,13 +15,7 @@
 package gerrit;
 
 import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.PatchSet;
-import com.google.gerrit.reviewdb.PatchSetInfo;
 import com.google.gerrit.reviewdb.UserIdentity;
-import com.google.gerrit.rules.PrologEnvironment;
-import com.google.gerrit.rules.StoredValues;
-import com.google.gerrit.server.patch.PatchSetInfoFactory;
-import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
 
 import com.googlecode.prolog_cafe.lang.IntegerTerm;
 import com.googlecode.prolog_cafe.lang.Operation;
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
similarity index 96%
rename from gerrit-server/src/main/java/gerrit/PRED_$load_commit_labels_1.java
rename to gerrit-server/src/main/java/gerrit/PRED__load_commit_labels_1.java
index eff444e..8b35e12 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
@@ -23,14 +23,14 @@
 import com.googlecode.prolog_cafe.lang.Term;
 
 /** Exports list of {@code commit_label( label('Code-Review', 2), user(12345789) )}. */
-class PRED_$load_commit_labels_1 extends Predicate.P1 {
+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);
 
-  PRED_$load_commit_labels_1(Term a1, Operation n) {
+  PRED__load_commit_labels_1(Term a1, Operation n) {
     arg1 = a1;
     cont = n;
   }
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
similarity index 95%
rename from gerrit-server/src/main/java/gerrit/PRED_$user_label_range_4.java
rename to gerrit-server/src/main/java/gerrit/PRED__user_label_range_4.java
index 2fdf5ab..68f9bf6 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
@@ -37,10 +37,10 @@
  *   '$user_label_range'(+Label, +CurrentUser, -Min, -Max)
  * </pre>
  */
-class PRED_$user_label_range_4 extends Predicate.P4 {
+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) {
+  PRED__user_label_range_4(Term a1, Term a2, Term a3, Term a4, Operation n) {
     arg1 = a1;
     arg2 = a2;
     arg3 = a3;
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 4a9c71b..77bd81e 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
@@ -15,7 +15,6 @@
 package gerrit;
 
 import com.google.gerrit.reviewdb.Patch;
-import com.google.gerrit.reviewdb.PatchSetInfo;
 import com.google.gerrit.rules.StoredValues;
 import com.google.gerrit.server.patch.PatchList;
 import com.google.gerrit.server.patch.PatchListEntry;
@@ -80,7 +79,6 @@
     engine.areg3 = arg3;
     engine.areg4 = arg4;
 
-    PatchSetInfo psInfo = StoredValues.PATCH_SET_INFO.get(engine);
     PatchList pl = StoredValues.PATCH_LIST.get(engine);
     Iterator<PatchListEntry> iter = pl.getPatches().iterator();
 
@@ -99,6 +97,7 @@
       Term a5 = engine.areg5;
 
       Pattern regex = (Pattern)((JavaObjectTerm)a1).object();
+      @SuppressWarnings("unchecked")
       Iterator<PatchListEntry> iter =
         (Iterator<PatchListEntry>)((JavaObjectTerm)a5).object();
       while (iter.hasNext()) {
@@ -148,6 +147,7 @@
     public Operation exec(Prolog engine) {
       Term a5 = engine.areg5;
 
+      @SuppressWarnings("unchecked")
       Iterator<PatchListEntry> iter =
         (Iterator<PatchListEntry>)((JavaObjectTerm)a5).object();
       if (!iter.hasNext()) {
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 f9e3036..f0accf0 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
@@ -14,8 +14,6 @@
 
 package gerrit;
 
-import com.google.gerrit.reviewdb.PatchSetInfo;
-import com.google.gerrit.rules.PrologEnvironment;
 import com.google.gerrit.rules.StoredValues;
 import com.google.gerrit.server.patch.PatchList;
 import com.google.gerrit.server.patch.PatchListEntry;
@@ -28,7 +26,6 @@
 import com.googlecode.prolog_cafe.lang.Predicate;
 import com.googlecode.prolog_cafe.lang.Prolog;
 import com.googlecode.prolog_cafe.lang.PrologException;
-import com.googlecode.prolog_cafe.lang.SystemException;
 import com.googlecode.prolog_cafe.lang.Term;
 
 import org.eclipse.jgit.diff.Edit;
@@ -75,8 +72,6 @@
     Pattern fileRegex = getRegexParameter(a1);
     Pattern editRegex = getRegexParameter(a2);
 
-    PrologEnvironment env = (PrologEnvironment) engine.control;
-    PatchSetInfo psInfo = StoredValues.PATCH_SET_INFO.get(engine);
     PatchList pl = StoredValues.PATCH_LIST.get(engine);
     Repository repo = StoredValues.REPOSITORY.get(engine);
 
diff --git a/gerrit-server/src/main/prolog/gerrit_common.pl b/gerrit-server/src/main/prolog/gerrit_common.pl
index 42ad519..3313162 100644
--- a/gerrit-server/src/main/prolog/gerrit_common.pl
+++ b/gerrit-server/src/main/prolog/gerrit_common.pl
@@ -70,7 +70,7 @@
   hash_get(commit_labels, '$all', Rs)
   .
 get_commit_labels(Rs) :-
-  '$load_commit_labels'(Rs),
+  '_load_commit_labels'(Rs),
   set_commit_labels(Rs).
 
 set_commit_labels(Rs) :-
@@ -101,7 +101,7 @@
   Who = user(_), !,
   atom(Label),
   current_user(Who, User),
-  '$user_label_range'(Label, User, Min, Max).
+  '_user_label_range'(Label, User, Min, Max).
 user_label_range(Label, test_user(Name), Min, Max) :-
   clause(user:test_grant(Label, test_user(Name), range(Min, Max)), _)
   .
@@ -198,8 +198,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(_), true.
-legacy_submit_rule('NoOp', Label, Id, Min, Max, T) :- T = ok(_), true.
+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(Fun, Label, Id, Min, Max, T) :- T = impossible(unsupported(Fun)).
 
 
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Abandoned.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Abandoned.vm
index 20529b2..57f4f63 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Abandoned.vm
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Abandoned.vm
@@ -23,7 +23,7 @@
 ## Gerrit will use templates ending in ".vm" but will ignore templates ending
 ## in ".vm.example".  If a .vm template does not exist, the default internal
 ## gerrit template which is the same as the .vm.example will be used.  If you
-## want to override the default template, copy the .vm.exmaple file to a .vm
+## want to override the default template, copy the .vm.example file to a .vm
 ## file and edit it appropriately.
 ##
 ## This Template:
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 5b74453..f2f0fc76 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
@@ -23,7 +23,7 @@
 ## Gerrit will use templates ending in ".vm" but will ignore templates ending
 ## in ".vm.example".  If a .vm template does not exist, the default internal
 ## gerrit template which is the same as the .vm.example will be used.  If you
-## want to override the default template, copy the .vm.exmaple file to a .vm
+## want to override the default template, copy the .vm.example file to a .vm
 ## file and edit it appropriately.
 ##
 ## This Template:
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeSubject.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeSubject.vm
index 24cc23c..a67c38c 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeSubject.vm
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ChangeSubject.vm
@@ -23,7 +23,7 @@
 ## Gerrit will use templates ending in ".vm" but will ignore templates ending
 ## in ".vm.example".  If a .vm template does not exist, the default internal
 ## gerrit template which is the same as the .vm.example will be used.  If you
-## want to override the default template, copy the .vm.exmaple file to a .vm
+## want to override the default template, copy the .vm.example file to a .vm
 ## file and edit it appropriately.
 ##
 ## This Template:
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 4ad355b8..547c1b4 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
@@ -23,7 +23,7 @@
 ## Gerrit will use templates ending in ".vm" but will ignore templates ending
 ## in ".vm.example".  If a .vm template does not exist, the default internal
 ## gerrit template which is the same as the .vm.example will be used.  If you
-## want to override the default template, copy the .vm.exmaple file to a .vm
+## want to override the default template, copy the .vm.example file to a .vm
 ## file and edit it appropriately.
 ##
 ## This Template:
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/MergeFail.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/MergeFail.vm
index dfe3d92f..6ded252 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/MergeFail.vm
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/MergeFail.vm
@@ -23,7 +23,7 @@
 ## Gerrit will use templates ending in ".vm" but will ignore templates ending
 ## in ".vm.example".  If a .vm template does not exist, the default internal
 ## gerrit template which is the same as the .vm.example will be used.  If you
-## want to override the default template, copy the .vm.exmaple file to a .vm
+## want to override the default template, copy the .vm.example file to a .vm
 ## file and edit it appropriately.
 ##
 ## This Template:
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Merged.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Merged.vm
index 296a37a..bcbc7bd 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Merged.vm
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Merged.vm
@@ -23,7 +23,7 @@
 ## Gerrit will use templates ending in ".vm" but will ignore templates ending
 ## in ".vm.example".  If a .vm template does not exist, the default internal
 ## gerrit template which is the same as the .vm.example will be used.  If you
-## want to override the default template, copy the .vm.exmaple file to a .vm
+## want to override the default template, copy the .vm.example file to a .vm
 ## file and edit it appropriately.
 ##
 ## This Template:
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 ec7028b..8e08dc4 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
@@ -23,7 +23,7 @@
 ## Gerrit will use templates ending in ".vm" but will ignore templates ending
 ## in ".vm.example".  If a .vm template does not exist, the default internal
 ## gerrit template which is the same as the .vm.example will be used.  If you
-## want to override the default template, copy the .vm.exmaple file to a .vm
+## want to override the default template, copy the .vm.example file to a .vm
 ## file and edit it appropriately.
 ##
 ## This Template:
@@ -33,7 +33,7 @@
 ## ChangeSubject.vm and ChangeFooter.vm.
 ##
 #if($email.reviewerNames)
-Hello $StringUtils.join($email.reviewerNames, ', '),
+Hello $email.joinStrings($email.reviewerNames, ', '),
 
 I'd like you to do a code review.#if($email.changeUrl)  Please visit
 
@@ -49,4 +49,6 @@
 ......................................................................
 
 $email.changeDetail
+#if($email.sshHost)
   git pull ssh://$email.sshHost/$projectName $patchSet.refName
+#end
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/RegisterNewEmail.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/RegisterNewEmail.vm
index 34682f2..7e095fb 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/RegisterNewEmail.vm
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/RegisterNewEmail.vm
@@ -23,7 +23,7 @@
 ## Gerrit will use templates ending in ".vm" but will ignore templates ending
 ## in ".vm.example".  If a .vm template does not exist, the default internal
 ## gerrit template which is the same as the .vm.example will be used.  If you
-## want to override the default template, copy the .vm.exmaple file to a .vm
+## want to override the default template, copy the .vm.example file to a .vm
 ## file and edit it appropriately.
 ##
 ## This Template:
@@ -34,12 +34,12 @@
 Welcome to Gerrit Code Review at ${email.gerritHost}.
 
 To add a verified email address to your user account, please
-click on the following link:
+click on the following link#if($email.userNameEmail) while signed in as $email.userNameEmail#end:
 
 $email.gerritUrl#/VE/$email.emailRegistrationToken
 
 If you have received this mail in error, you do not need to take any
-action to cancel the account.  The account will not be activated, and
+action to cancel the account.  The address will not be activated, and
 you will not receive any further emails.
 
 If clicking the link above does not work, copy and paste the URL in a
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ReplacePatchSet.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ReplacePatchSet.vm
index 4575965..392df9d 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ReplacePatchSet.vm
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/ReplacePatchSet.vm
@@ -23,7 +23,7 @@
 ## Gerrit will use templates ending in ".vm" but will ignore templates ending
 ## in ".vm.example".  If a .vm template does not exist, the default internal
 ## gerrit template which is the same as the .vm.example will be used.  If you
-## want to override the default template, copy the .vm.exmaple file to a .vm
+## want to override the default template, copy the .vm.example file to a .vm
 ## file and edit it appropriately.
 ##
 ## This Template:
@@ -33,7 +33,7 @@
 ## ChangeEmail: see ChangeSubject.vm and ChangeFooter.vm.
 ##
 #if($email.reviewerNames)
-Hello $StringUtils.join($email.reviewerNames, ', '),
+Hello $email.joinStrings($email.reviewerNames, ', '),
 
 I'd like you to reexamine a change.#if($email.changeUrl)  Please visit
 
@@ -49,4 +49,6 @@
 ......................................................................
 
 $email.changeDetail
+#if($email.sshHost)
   git pull ssh://$email.sshHost/$projectName $patchSet.refName
+#end
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Restored.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Restored.vm
index c0e6b91..afcbcc5 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Restored.vm
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Restored.vm
@@ -23,7 +23,7 @@
 ## Gerrit will use templates ending in ".vm" but will ignore templates ending
 ## in ".vm.example".  If a .vm template does not exist, the default internal
 ## gerrit template which is the same as the .vm.example will be used.  If you
-## want to override the default template, copy the .vm.exmaple file to a .vm
+## want to override the default template, copy the .vm.example file to a .vm
 ## file and edit it appropriately.
 ##
 ## This Template:
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Reverted.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Reverted.vm
index a2350d8..a0aedd6 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Reverted.vm
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Reverted.vm
@@ -23,7 +23,7 @@
 ## Gerrit will use templates ending in ".vm" but will ignore templates ending
 ## in ".vm.example".  If a .vm template does not exist, the default internal
 ## gerrit template which is the same as the .vm.example will be used.  If you
-## want to override the default template, copy the .vm.exmaple file to a .vm
+## want to override the default template, copy the .vm.example file to a .vm
 ## file and edit it appropriately.
 ##
 ## This Template:
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/config/SitePathsTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/config/SitePathsTest.java
index f829cb5..9a8fc00 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/config/SitePathsTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/config/SitePathsTest.java
@@ -76,7 +76,7 @@
     }
   }
 
-  public void testResolve() throws FileNotFoundException {
+  public void testResolve() throws IOException {
     final File root = random();
     final SitePaths site = new SitePaths(root);
 
@@ -84,11 +84,11 @@
     assertNull(site.resolve(""));
 
     assertNotNull(site.resolve("a"));
-    assertEquals(new File(root, "a"), site.resolve("a"));
+    assertEquals(new File(root, "a").getCanonicalFile(), site.resolve("a"));
 
     final String pfx = HostPlatform.isWin32() ? "C:/" : "/";
     assertNotNull(site.resolve(pfx + "a"));
-    assertEquals(new File(pfx + "a"), site.resolve(pfx + "a"));
+    assertEquals(new File(pfx + "a").getCanonicalFile(), site.resolve(pfx + "a"));
   }
 
   private File random() {
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 e8a235f..c5b5164 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
@@ -113,7 +113,9 @@
         + "  submit = group Developers\n" //
         + "\tsubmit = group Staff\n" //
         + "  upload = group Developers\n" //
-        + "  read = group Developers\n", text(rev, "project.config"));
+        + "  read = group Developers\n"//
+        + "[project]\n"//
+        + "\tstate = active\n", text(rev, "project.config"));
   }
 
   @Test
@@ -140,7 +142,9 @@
         + "  submit = group People Who Can Submit\n" //
         + "\tsubmit = group Staff\n" //
         + "  upload = group Developers\n" //
-        + "  read = group Developers\n", text(rev, "project.config"));
+        + "  read = group Developers\n"//
+        + "[project]\n"//
+        + "\tstate = active\n", text(rev, "project.config"));
   }
 
   private ProjectConfig read(RevCommit rev) throws IOException,
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
new file mode 100644
index 0000000..e086070
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/git/SubmoduleOpTest.java
@@ -0,0 +1,998 @@
+// 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 org.easymock.EasyMock.createStrictMock;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.verify;
+
+import com.google.gerrit.reviewdb.Account;
+import com.google.gerrit.reviewdb.Branch;
+import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.SubmoduleSubscription;
+import com.google.gerrit.reviewdb.SubmoduleSubscriptionAccess;
+import com.google.gwtorm.client.KeyUtil;
+import com.google.gwtorm.client.ResultSet;
+import com.google.gwtorm.client.SchemaFactory;
+import com.google.gwtorm.client.impl.ListResultSet;
+import com.google.gwtorm.server.StandardKeyEncoder;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.dircache.DirCacheBuilder;
+import org.eclipse.jgit.dircache.DirCacheEntry;
+import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase;
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.FileMode;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.junit.Before;
+import org.junit.Test;
+
+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;
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+public class SubmoduleOpTest extends LocalDiskRepositoryTestCase {
+  static {
+    KeyUtil.setEncoderImpl(new StandardKeyEncoder());
+  }
+
+  private static final String newLine = System.getProperty("line.separator");
+
+  private SchemaFactory<ReviewDb> schemaFactory;
+  private SubmoduleSubscriptionAccess subscriptions;
+  private ReviewDb schema;
+  private Provider<String> urlProvider;
+  private GitRepositoryManager repoManager;
+  private ReplicationQueue replication;
+
+  @Override
+  @Before
+  public void setUp() throws Exception {
+    super.setUp();
+
+    schemaFactory = createStrictMock(SchemaFactory.class);
+    schema = createStrictMock(ReviewDb.class);
+    subscriptions = createStrictMock(SubmoduleSubscriptionAccess.class);
+    urlProvider = createStrictMock(Provider.class);
+    repoManager = createStrictMock(GitRepositoryManager.class);
+    replication = createStrictMock(ReplicationQueue.class);
+  }
+
+  private void doReplay() {
+    replay(schemaFactory, schema, subscriptions, urlProvider, repoManager,
+        replication);
+  }
+
+  private void doVerify() {
+    verify(schemaFactory, schema, subscriptions, urlProvider, repoManager,
+        replication);
+  }
+
+  /**
+   * It tests Submodule.update in the scenario a merged commit is an empty one
+   * (it does not have a .gitmodule file) and the project the commit was merged
+   * is not a submodule of other project.
+   *
+   * @throws Exception If an exception occurs.
+   */
+  @Test
+  public void testEmptyCommit() throws Exception {
+    expect(schemaFactory.open()).andReturn(schema);
+
+    final Repository realDb = createWorkRepository();
+    final Git git = new Git(realDb);
+
+    final RevCommit mergeTip = git.commit().setMessage("test").call();
+
+    final Branch.NameKey branchNameKey =
+        new Branch.NameKey(new Project.NameKey("test-project"), "test-branch");
+
+    expect(urlProvider.get()).andReturn("http://localhost:8080");
+
+    expect(schema.submoduleSubscriptions()).andReturn(subscriptions);
+    final ResultSet<SubmoduleSubscription> emptySubscriptions =
+        new ListResultSet<SubmoduleSubscription>(new ArrayList<SubmoduleSubscription>());
+    expect(subscriptions.bySubmodule(branchNameKey)).andReturn(
+        emptySubscriptions);
+
+    schema.close();
+
+    doReplay();
+
+    final SubmoduleOp submoduleOp =
+        new SubmoduleOp(branchNameKey, mergeTip, new RevWalk(realDb), urlProvider,
+            schemaFactory, realDb, null, new ArrayList<Change>(), null, null,
+            null, null);
+
+    submoduleOp.update();
+
+    doVerify();
+  }
+
+  /**
+   * It tests SubmoduleOp.update in a scenario considering:
+   * <ul>
+   * <li>no subscriptions existing to destination project</li>
+   * <li>a commit is merged to "dest-project"</li>
+   * <li>commit contains .gitmodules file with content</li>
+   * </ul>
+   *
+   * <pre>
+   *     [submodule "source"]
+   *       path = source
+   *       url = http://localhost:8080/source
+   *       branch = .
+   * </pre>
+   * <p>
+   * It expects to insert a new row in subscriptions table. The row inserted
+   * specifies:
+   * <ul>
+   * <li>target "dest-project" on branch "refs/heads/master"</li>
+   * <li>source "a" on branch "refs/heads/master"</li>
+   * <li>path "a"</li>
+   * </ul>
+   * </p>
+   *
+   * @throws Exception If an exception occurs.
+   */
+  @Test
+  public void testNewSubscriptionToDotBranchValue() throws Exception {
+    doOneSubscriptionInsert(buildSubmoduleSection("source", "source",
+        "http://localhost:8080/source", ".").toString(), "refs/heads/master");
+
+    doVerify();
+  }
+
+  /**
+   * It tests SubmoduleOp.update in a scenario considering:
+   * <ul>
+   * <li>no subscriptions existing to destination project</li>
+   * <li>a commit is merged to "dest-project"</li>
+   * <li>commit contains .gitmodules file with content</li>
+   * </ul>
+   *
+   * <pre>
+   *     [submodule "source"]
+   *       path = source
+   *       url = http://localhost:8080/source
+   *       branch = refs/heads/master
+   * </pre>
+   *
+   * <p>
+   * It expects to insert a new row in subscriptions table. The row inserted
+   * specifies:
+   * <ul>
+   * <li>target "dest-project" on branch "refs/heads/master"</li>
+   * <li>source "source" on branch "refs/heads/master"</li>
+   * <li>path "source"</li>
+   * </ul>
+   * </p>
+   *
+   * @throws Exception If an exception occurs.
+   */
+  @Test
+  public void testNewSubscriptionToSameBranch() throws Exception {
+    doOneSubscriptionInsert(buildSubmoduleSection("source", "source",
+        "http://localhost:8080/source", "refs/heads/master").toString(),
+        "refs/heads/master");
+
+    doVerify();
+  }
+
+  /**
+   * It tests SubmoduleOp.update in a scenario considering:
+   * <ul>
+   * <li>no subscriptions existing to destination project</li>
+   * <li>a commit is merged to "dest-project"</li>
+   * <li>commit contains .gitmodules file with content</li>
+   * </ul>
+   *
+   * <pre>
+   *     [submodule "source"]
+   *       path = source
+   *       url = http://localhost:8080/source
+   *       branch = refs/heads/test
+   * </pre>
+   * <p>
+   * It expects to insert a new row in subscriptions table. The row inserted
+   * specifies:
+   * <ul>
+   * <li>target "dest-project" on branch "refs/heads/master"</li>
+   * <li>source "source" on branch "refs/heads/test"</li>
+   * <li>path "source"</li>
+   * </ul>
+   * </p>
+   *
+   * @throws Exception If an exception occurs.
+   */
+  @Test
+  public void testNewSubscriptionToDifferentBranch() throws Exception {
+    doOneSubscriptionInsert(buildSubmoduleSection("source", "source",
+        "http://localhost:8080/source", "refs/heads/test").toString(),
+        "refs/heads/test");
+
+    doVerify();
+  }
+
+  /**
+   * It tests SubmoduleOp.update in a scenario considering:
+   * <ul>
+   * <li>no subscriptions existing to destination project</li>
+   * <li>a commit is merged to "dest-project" in "refs/heads/master" branch</li>
+   * <li>commit contains .gitmodules file with content</li>
+   * </ul>
+   *
+   * <pre>
+   *     [submodule "source-a"]
+   *       path = source-a
+   *       url = http://localhost:8080/source-a
+   *       branch = .
+   *
+   *     [submodule "source-b"]
+   *       path = source-b
+   *       url = http://localhost:8080/source-b
+   *       branch = .
+   * </pre>
+   * <p>
+   * It expects to insert new rows in subscriptions table. The rows inserted
+   * specifies:
+   * <ul>
+   * <li>target "dest-project" on branch "refs/heads/master"</li>
+   * <li>source "source-a" on branch "refs/heads/master" with "source-a" path</li>
+   * <li>source "source-b" on branch "refs/heads/master" with "source-b" path</li>
+   * </ul>
+   * </p>
+   *
+   * @throws Exception If an exception occurs.
+   */
+  @Test
+  public void testNewSubscriptionsWithDotBranchValue() throws Exception {
+    final StringBuilder sb =
+        buildSubmoduleSection("source-a", "source-a",
+            "http://localhost:8080/source-a", ".");
+    sb.append(buildSubmoduleSection("source-b", "source-b",
+        "http://localhost:8080/source-b", "."));
+
+    final Branch.NameKey mergedBranch =
+        new Branch.NameKey(new Project.NameKey("dest-project"),
+            "refs/heads/master");
+
+    final List<SubmoduleSubscription> subscriptionsToInsert =
+        new ArrayList<SubmoduleSubscription>();
+    subscriptionsToInsert
+        .add(new SubmoduleSubscription(mergedBranch, new Branch.NameKey(
+            new Project.NameKey("source-a"), "refs/heads/master"), "source-a"));
+    subscriptionsToInsert
+        .add(new SubmoduleSubscription(mergedBranch, new Branch.NameKey(
+            new Project.NameKey("source-b"), "refs/heads/master"), "source-b"));
+
+    doOnlySubscriptionInserts(sb.toString(), mergedBranch,
+        subscriptionsToInsert);
+
+    doVerify();
+  }
+
+  /**
+   * It tests SubmoduleOp.update in a scenario considering:
+   * <ul>
+   * <li>no subscriptions existing to destination project</li>
+   * <li>a commit is merged to "dest-project" in "refs/heads/master" branch</li>
+   * <li>commit contains .gitmodules file with content</li>
+   * </ul>
+   *
+   * <pre>
+   *     [submodule "source-a"]
+   *       path = source-a
+   *       url = http://localhost:8080/source-a
+   *       branch = .
+   *
+   *     [submodule "source-b"]
+   *       path = source-b
+   *       url = http://localhost:8080/source-b
+   *       branch = refs/heads/master
+   * </pre>
+   * <p>
+   * It expects to insert new rows in subscriptions table. The rows inserted
+   * specifies:
+   * <ul>
+   * <li>target "dest-project" on branch "refs/heads/master"</li>
+   * <li>source "source-a" on branch "refs/heads/master" with "source-a" path</li>
+   * <li>source "source-b" on branch "refs/heads/master" with "source-b" path</li>
+   * </ul>
+   * </p>
+   *
+   * @throws Exception If an exception occurs.
+   */
+  @Test
+  public void testNewSubscriptionsDotAndSameBranchValues() throws Exception {
+    final StringBuilder sb =
+        buildSubmoduleSection("source-a", "source-a",
+            "http://localhost:8080/source-a", ".");
+    sb.append(buildSubmoduleSection("source-b", "source-b",
+        "http://localhost:8080/source-b", "refs/heads/master"));
+
+    final Branch.NameKey mergedBranch =
+        new Branch.NameKey(new Project.NameKey("dest-project"),
+            "refs/heads/master");
+
+    final List<SubmoduleSubscription> subscriptionsToInsert =
+        new ArrayList<SubmoduleSubscription>();
+    subscriptionsToInsert
+        .add(new SubmoduleSubscription(mergedBranch, new Branch.NameKey(
+            new Project.NameKey("source-a"), "refs/heads/master"), "source-a"));
+    subscriptionsToInsert
+        .add(new SubmoduleSubscription(mergedBranch, new Branch.NameKey(
+            new Project.NameKey("source-b"), "refs/heads/master"), "source-b"));
+
+    doOnlySubscriptionInserts(sb.toString(), mergedBranch,
+        subscriptionsToInsert);
+
+    doVerify();
+  }
+
+  /**
+   * It tests SubmoduleOp.update in a scenario considering:
+   * <ul>
+   * <li>no subscriptions existing to destination project</li>
+   * <li>a commit is merged to "dest-project" in "refs/heads/master" branch</li>
+   * <li>commit contains .gitmodules file with content</li>
+   *
+   * <pre>
+   *     [submodule "source-a"]
+   *       path = source-a
+   *       url = http://localhost:8080/source-a
+   *       branch = refs/heads/test-a
+   *
+   *     [submodule "source-b"]
+   *       path = source-b
+   *       url = http://localhost:8080/source-b
+   *       branch = refs/heads/test-b
+   * </pre>
+   *
+   * <p>
+   * It expects to insert new rows in subscriptions table. The rows inserted
+   * specifies:
+   * <ul>
+   * <li>target "dest-project" on branch "refs/heads/master"</li>
+   * <li>source "source-a" on branch "refs/heads/test-a" with "source-a" path</li>
+   * <li>source "source-b" on branch "refs/heads/test-b" with "source-b" path</li>
+   * </ul>
+   * </p>
+   *
+   * @throws Exception If an exception occurs.
+   */
+  @Test
+  public void testNewSubscriptionsSpecificBranchValues() throws Exception {
+    final StringBuilder sb =
+        buildSubmoduleSection("source-a", "source-a",
+            "http://localhost:8080/source-a", "refs/heads/test-a");
+    sb.append(buildSubmoduleSection("source-b", "source-b",
+        "http://localhost:8080/source-b", "refs/heads/test-b"));
+
+    final Branch.NameKey mergedBranch =
+        new Branch.NameKey(new Project.NameKey("dest-project"),
+            "refs/heads/master");
+
+    final List<SubmoduleSubscription> subscriptionsToInsert =
+        new ArrayList<SubmoduleSubscription>();
+    subscriptionsToInsert
+        .add(new SubmoduleSubscription(mergedBranch, new Branch.NameKey(
+            new Project.NameKey("source-a"), "refs/heads/test-a"), "source-a"));
+    subscriptionsToInsert
+        .add(new SubmoduleSubscription(mergedBranch, new Branch.NameKey(
+            new Project.NameKey("source-b"), "refs/heads/test-b"), "source-b"));
+
+    doOnlySubscriptionInserts(sb.toString(), mergedBranch,
+        subscriptionsToInsert);
+
+    doVerify();
+  }
+
+  /**
+   * It tests SubmoduleOp.update in a scenario considering:
+   * <ul>
+   * <li>one subscription existing to destination project/branch</li>
+   * <li>a commit is merged to "dest-project" in "refs/heads/master" branch</li>
+   * <li>commit contains .gitmodules file with content</li>
+   * </ul>
+   *
+   * <pre>
+   *     [submodule "source"]
+   *       path = source
+   *       url = http://localhost:8080/source
+   *       branch = refs/heads/master
+   * </pre>
+   * <p>
+   * It expects to insert a new row in subscriptions table. The rows inserted
+   * specifies:
+   * <ul>
+   * <li>target "dest-project" on branch "refs/heads/master"</li>
+   * <li>source "source" on branch "refs/heads/master" with "source" path</li>
+   * </ul>
+   * </p>
+   * <p>
+   * It also expects to remove the row in subscriptions table specifying another
+   * project/branch subscribed to merged branch. This one to be removed is:
+   * <ul>
+   * <li>target "dest-project" on branch "refs/heads/master"</li>
+   * <li>source "old-source" on branch "refs/heads/master" with "old-source"
+   * path</li>
+   * </ul>
+   * </p>
+   *
+   * @throws Exception If an exception occurs.
+   */
+  @Test
+  public void testSubscriptionsInsertOneRemoveOne() throws Exception {
+    final Branch.NameKey mergedBranch =
+        new Branch.NameKey(new Project.NameKey("dest-project"),
+            "refs/heads/master");
+
+    final List<SubmoduleSubscription> subscriptionsToInsert =
+        new ArrayList<SubmoduleSubscription>();
+    subscriptionsToInsert.add(new SubmoduleSubscription(mergedBranch,
+        new Branch.NameKey(new Project.NameKey("source"), "refs/heads/master"),
+        "source"));
+
+    final List<SubmoduleSubscription> oldOnesToMergedBranch =
+        new ArrayList<SubmoduleSubscription>();
+    oldOnesToMergedBranch.add(new SubmoduleSubscription(mergedBranch,
+        new Branch.NameKey(new Project.NameKey("old-source"),
+            "refs/heads/master"), "old-source"));
+
+    doOnlySubscriptionTableOperations(buildSubmoduleSection("source", "source",
+        "http://localhost:8080/source", "refs/heads/master").toString(),
+        mergedBranch, subscriptionsToInsert, oldOnesToMergedBranch);
+
+    doVerify();
+  }
+
+  /**
+   * It tests SubmoduleOp.update in a scenario considering:
+   * <ul>
+   * <li>one subscription existing to destination project/branch with a source
+   * called old on refs/heads/master branch</li>
+   * <li>a commit is merged to "dest-project" in "refs/heads/master" branch</li>
+   * <li>
+   * commit contains .gitmodules file with content</li>
+   * </ul>
+   *
+   * <pre>
+   *     [submodule "new"]
+   *       path = new
+   *       url = http://localhost:8080/new
+   *       branch = refs/heads/master
+   *
+   *     [submodule "old"]
+   *       path = old
+   *       url = http://localhost:8080/old
+   *       branch = refs/heads/master
+   * </pre>
+   * <p>
+   * It expects to insert a new row in subscriptions table. It should not remove
+   * any row. The rows inserted specifies:
+   * <ul>
+   * <li>target "dest-project" on branch "refs/heads/master"</li>
+   * <li>source "new" on branch "refs/heads/master" with "new" path</li>
+   * </ul>
+   * </p>
+   *
+   * @throws Exception If an exception occurs.
+   */
+  @Test
+  public void testSubscriptionAddedAndMantainPreviousOne() throws Exception {
+    final StringBuilder sb =
+        buildSubmoduleSection("new", "new", "http://localhost:8080/new",
+            "refs/heads/master");
+    sb.append(buildSubmoduleSection("old", "old", "http://localhost:8080/old",
+        "refs/heads/master"));
+
+    final Branch.NameKey mergedBranch =
+        new Branch.NameKey(new Project.NameKey("dest-project"),
+            "refs/heads/master");
+
+    final SubmoduleSubscription old =
+        new SubmoduleSubscription(mergedBranch, new Branch.NameKey(new Project.NameKey(
+            "old"), "refs/heads/master"), "old");
+
+    final List<SubmoduleSubscription> extractedsubscriptions =
+        new ArrayList<SubmoduleSubscription>();
+    extractedsubscriptions.add(new SubmoduleSubscription(mergedBranch,
+        new Branch.NameKey(new Project.NameKey("new"), "refs/heads/master"),
+        "new"));
+    extractedsubscriptions.add(old);
+
+    final List<SubmoduleSubscription> oldOnesToMergedBranch =
+        new ArrayList<SubmoduleSubscription>();
+    oldOnesToMergedBranch.add(old);
+
+    doOnlySubscriptionTableOperations(sb.toString(), mergedBranch,
+        extractedsubscriptions, oldOnesToMergedBranch);
+
+    doVerify();
+  }
+
+  /**
+   * It tests SubmoduleOp.update in a scenario considering an empty .gitmodules
+   * file is part of a commit to a destination project/branch having two sources
+   * subscribed.
+   * <p>
+   * It expects to remove the subscriptions to destination project/branch.
+   * </p>
+   *
+   * @throws Exception If an exception occurs.
+   */
+  @Test
+  public void testRemoveSubscriptions() throws Exception {
+    final Branch.NameKey mergedBranch =
+        new Branch.NameKey(new Project.NameKey("dest-project"),
+            "refs/heads/master");
+
+    final List<SubmoduleSubscription> extractedsubscriptions =
+        new ArrayList<SubmoduleSubscription>();
+
+    final List<SubmoduleSubscription> oldOnesToMergedBranch =
+        new ArrayList<SubmoduleSubscription>();
+    oldOnesToMergedBranch
+        .add(new SubmoduleSubscription(mergedBranch, new Branch.NameKey(
+            new Project.NameKey("source-a"), "refs/heads/master"), "source-a"));
+    oldOnesToMergedBranch
+        .add(new SubmoduleSubscription(mergedBranch, new Branch.NameKey(
+            new Project.NameKey("source-b"), "refs/heads/master"), "source-b"));
+
+    doOnlySubscriptionTableOperations("", mergedBranch, extractedsubscriptions,
+        oldOnesToMergedBranch);
+  }
+
+  /**
+   * It tests SubmoduleOp.update in a scenario considering no .gitmodules file
+   * in a merged commit to a destination project/branch that is a source one to
+   * one called "target-project".
+   * <p>
+   * It expects to update the git link called "source-project" to be in target
+   * repository.
+   * </p>
+   *
+   * @throws Exception If an exception occurs.
+   */
+  @Test
+  public void testOneSubscriberToUpdate() throws Exception {
+    expect(schemaFactory.open()).andReturn(schema);
+
+    final Repository sourceRepository = createWorkRepository();
+    final Git sourceGit = new Git(sourceRepository);
+
+    addRegularFileToIndex("file.txt", "test content", sourceRepository);
+
+    final RevCommit sourceMergeTip =
+        sourceGit.commit().setMessage("test").call();
+
+    final Branch.NameKey sourceBranchNameKey =
+        new Branch.NameKey(new Project.NameKey("source-project"),
+            "refs/heads/master");
+
+    final CodeReviewCommit codeReviewCommit =
+        new CodeReviewCommit(sourceMergeTip.toObjectId());
+    final Change submitedChange =
+        new Change(new Change.Key(sourceMergeTip.toObjectId().getName()),
+            new Change.Id(1), new Account.Id(1), sourceBranchNameKey);
+    codeReviewCommit.change = submitedChange;
+
+    final Map<Change.Id, CodeReviewCommit> mergedCommits =
+        new HashMap<Change.Id, CodeReviewCommit>();
+    mergedCommits.put(codeReviewCommit.change.getId(), codeReviewCommit);
+
+    final List<Change> submited = new ArrayList<Change>();
+    submited.add(submitedChange);
+
+    final Repository targetRepository = createWorkRepository();
+    final Git targetGit = new Git(targetRepository);
+
+    addGitLinkToIndex("a", sourceMergeTip.copy(), targetRepository);
+
+    targetGit.commit().setMessage("test").call();
+
+    final Branch.NameKey targetBranchNameKey =
+        new Branch.NameKey(new Project.NameKey("target-project"),
+            sourceBranchNameKey.get());
+
+    expect(urlProvider.get()).andReturn("http://localhost:8080");
+
+    expect(schema.submoduleSubscriptions()).andReturn(subscriptions);
+    final ResultSet<SubmoduleSubscription> subscribers =
+        new ListResultSet<SubmoduleSubscription>(Collections
+            .singletonList(new SubmoduleSubscription(targetBranchNameKey,
+                sourceBranchNameKey, "source-project")));
+    expect(subscriptions.bySubmodule(sourceBranchNameKey)).andReturn(
+        subscribers);
+
+    expect(repoManager.openRepository(targetBranchNameKey.getParentKey()))
+        .andReturn(targetRepository);
+
+    replication.scheduleUpdate(targetBranchNameKey.getParentKey(),
+        targetBranchNameKey.get());
+
+    expect(schema.submoduleSubscriptions()).andReturn(subscriptions);
+    final ResultSet<SubmoduleSubscription> emptySubscriptions =
+        new ListResultSet<SubmoduleSubscription>(new ArrayList<SubmoduleSubscription>());
+    expect(subscriptions.bySubmodule(targetBranchNameKey)).andReturn(
+        emptySubscriptions);
+
+    schema.close();
+
+    final PersonIdent myIdent =
+        new PersonIdent("test-user", "test-user@email.com");
+
+    doReplay();
+
+    final SubmoduleOp submoduleOp =
+        new SubmoduleOp(sourceBranchNameKey, sourceMergeTip, new RevWalk(
+            sourceRepository), urlProvider, schemaFactory, sourceRepository,
+            new Project(sourceBranchNameKey.getParentKey()), submited,
+            mergedCommits, myIdent, repoManager, replication);
+
+    submoduleOp.update();
+
+    doVerify();
+  }
+
+  /**
+   * It tests SubmoduleOp.update in a scenario considering established circular
+   * reference in submodule_subscriptions table.
+   * <p>
+   * In the tested scenario there is no .gitmodules file in a merged commit to a
+   * destination project/branch that is a source one to one called
+   * "target-project".
+   * <p>
+   * submodule_subscriptions table will be incorrect due source appearing as a
+   * subscriber or target-project: according to database target-project has as
+   * source the source-project, and source-project has as source the
+   * target-project.
+   * <p>
+   * It expects to update the git link called "source-project" to be in target
+   * repository and ignoring the incorrect row in database establishing the
+   * circular reference.
+   * </p>
+   *
+   * @throws Exception If an exception occurs.
+   */
+  @Test
+  public void testAvoidingCircularReference() throws Exception {
+    expect(schemaFactory.open()).andReturn(schema);
+
+    final Repository sourceRepository = createWorkRepository();
+    final Git sourceGit = new Git(sourceRepository);
+
+    addRegularFileToIndex("file.txt", "test content", sourceRepository);
+
+    final RevCommit sourceMergeTip =
+        sourceGit.commit().setMessage("test").call();
+
+    final Branch.NameKey sourceBranchNameKey =
+        new Branch.NameKey(new Project.NameKey("source-project"),
+            "refs/heads/master");
+
+    final CodeReviewCommit codeReviewCommit =
+        new CodeReviewCommit(sourceMergeTip.toObjectId());
+    final Change submitedChange =
+        new Change(new Change.Key(sourceMergeTip.toObjectId().getName()),
+            new Change.Id(1), new Account.Id(1), sourceBranchNameKey);
+    codeReviewCommit.change = submitedChange;
+
+    final Map<Change.Id, CodeReviewCommit> mergedCommits =
+        new HashMap<Change.Id, CodeReviewCommit>();
+    mergedCommits.put(codeReviewCommit.change.getId(), codeReviewCommit);
+
+    final List<Change> submited = new ArrayList<Change>();
+    submited.add(submitedChange);
+
+    final Repository targetRepository = createWorkRepository();
+    final Git targetGit = new Git(targetRepository);
+
+    addGitLinkToIndex("a", sourceMergeTip.copy(), targetRepository);
+
+    targetGit.commit().setMessage("test").call();
+
+    final Branch.NameKey targetBranchNameKey =
+        new Branch.NameKey(new Project.NameKey("target-project"),
+            sourceBranchNameKey.get());
+
+    expect(urlProvider.get()).andReturn("http://localhost:8080");
+
+    expect(schema.submoduleSubscriptions()).andReturn(subscriptions);
+    final ResultSet<SubmoduleSubscription> subscribers =
+        new ListResultSet<SubmoduleSubscription>(Collections
+            .singletonList(new SubmoduleSubscription(targetBranchNameKey,
+                sourceBranchNameKey, "source-project")));
+    expect(subscriptions.bySubmodule(sourceBranchNameKey)).andReturn(
+        subscribers);
+
+    expect(repoManager.openRepository(targetBranchNameKey.getParentKey()))
+        .andReturn(targetRepository);
+
+    replication.scheduleUpdate(targetBranchNameKey.getParentKey(),
+        targetBranchNameKey.get());
+
+    expect(schema.submoduleSubscriptions()).andReturn(subscriptions);
+    final ResultSet<SubmoduleSubscription> incorrectSubscriptions =
+        new ListResultSet<SubmoduleSubscription>(Collections
+            .singletonList(new SubmoduleSubscription(sourceBranchNameKey,
+                targetBranchNameKey, "target-project")));
+    expect(subscriptions.bySubmodule(targetBranchNameKey)).andReturn(
+        incorrectSubscriptions);
+
+    schema.close();
+
+    final PersonIdent myIdent =
+        new PersonIdent("test-user", "test-user@email.com");
+
+    doReplay();
+
+    final SubmoduleOp submoduleOp =
+        new SubmoduleOp(sourceBranchNameKey, sourceMergeTip, new RevWalk(
+            sourceRepository), urlProvider, schemaFactory, sourceRepository,
+            new Project(sourceBranchNameKey.getParentKey()), submited,
+            mergedCommits, myIdent, repoManager, replication);
+
+    submoduleOp.update();
+
+    doVerify();
+  }
+
+  /**
+   * It calls SubmoduleOp.update considering only one insert on Subscriptions
+   * table.
+   * <p>
+   * It considers a commit containing a .gitmodules file was merged in
+   * refs/heads/master of a dest-project.
+   * </p>
+   * <p>
+   * The .gitmodules file content should indicate a source project called
+   * "source".
+   * </p>
+   *
+   * @param gitModulesFileContent The .gitmodules file content. During the test
+   *        this file is created, so the commit containing it.
+   * @param sourceBranchName The branch name of source project "pointed by"
+   *        .gitmodule file.
+   * @throws Exception If an exception occurs.
+   */
+  private void doOneSubscriptionInsert(final String gitModulesFileContent,
+      final String sourceBranchName) throws Exception {
+    final Branch.NameKey mergedBranch =
+        new Branch.NameKey(new Project.NameKey("dest-project"),
+            "refs/heads/master");
+
+    final List<SubmoduleSubscription> subscriptionsToInsert =
+        new ArrayList<SubmoduleSubscription>();
+    subscriptionsToInsert.add(new SubmoduleSubscription(mergedBranch,
+        new Branch.NameKey(new Project.NameKey("source"), sourceBranchName),
+        "source"));
+
+    doOnlySubscriptionInserts(gitModulesFileContent, mergedBranch,
+        subscriptionsToInsert);
+  }
+
+  /**
+   * It calls SubmoduleOp.update method considering scenario only inserting new
+   * subscriptions.
+   * <p>
+   * In this test a commit is created and considered merged to
+   * <code>mergedBranch</code> branch.
+   * </p>
+   * <p>
+   * The destination project the commit was merged is not considered to be a
+   * source of another project (no subscribers found to this project).
+   * </p>
+   *
+   * @param gitModulesFileContent The .gitmodule file content.
+   * @param mergedBranch The {@link Branch.NameKey} instance representing the
+   *        project/branch the commit was merged.
+   * @param extractedSubscriptions The subscription rows extracted from
+   *        gitmodules file.
+   * @throws Exception If an exception occurs.
+   */
+  private void doOnlySubscriptionInserts(final String gitModulesFileContent,
+      final Branch.NameKey mergedBranch,
+      final List<SubmoduleSubscription> extractedSubscriptions) throws Exception {
+    doOnlySubscriptionTableOperations(gitModulesFileContent, mergedBranch,
+        extractedSubscriptions, new ArrayList<SubmoduleSubscription>());
+  }
+
+  /**
+   * It calls SubmoduleOp.update method considering scenario only updating
+   * Subscriptions table.
+   * <p>
+   * In this test a commit is created and considered merged to
+   * <code>mergedBranch</code> branch.
+   * </p>
+   * <p>
+   * The destination project the commit was merged is not considered to be a
+   * source of another project (no subscribers found to this project).
+   * </p>
+   *
+   * @param gitModulesFileContent The .gitmodules file content.
+   * @param mergedBranch The {@link Branch.NameKey} instance representing the
+   *        project/branch the commit was merged.
+   * @param extractedSubscriptions The subscription rows extracted from
+   *        gitmodules file.
+   * @param previousSubscriptions The subscription rows to be considering as
+   *        existing and pointing as target to the <code>mergedBranch</code>
+   *        before updating the table.
+   * @throws Exception If an exception occurs.
+   */
+  private void doOnlySubscriptionTableOperations(
+      final String gitModulesFileContent, final Branch.NameKey mergedBranch,
+      final List<SubmoduleSubscription> extractedSubscriptions,
+      final List<SubmoduleSubscription> previousSubscriptions) throws Exception {
+    expect(schemaFactory.open()).andReturn(schema);
+
+    final Repository realDb = createWorkRepository();
+    final Git git = new Git(realDb);
+
+    addRegularFileToIndex(".gitmodules", gitModulesFileContent, realDb);
+
+    final RevCommit mergeTip = git.commit().setMessage("test").call();
+
+    expect(urlProvider.get()).andReturn("http://localhost:8080").times(2);
+
+    expect(schema.submoduleSubscriptions()).andReturn(subscriptions);
+    expect(subscriptions.bySuperProject(mergedBranch)).andReturn(
+        new ListResultSet<SubmoduleSubscription>(previousSubscriptions));
+
+    SortedSet<Project.NameKey> existingProjects =
+        new TreeSet<Project.NameKey>();
+
+    for (SubmoduleSubscription extracted : extractedSubscriptions) {
+      existingProjects.add(extracted.getSubmodule().getParentKey());
+    }
+
+    for (int index = 0; index < extractedSubscriptions.size(); index++) {
+      expect(repoManager.list()).andReturn(existingProjects);
+    }
+
+    final Set<SubmoduleSubscription> alreadySubscribeds =
+        new HashSet<SubmoduleSubscription>();
+    for (SubmoduleSubscription s : extractedSubscriptions) {
+      if (previousSubscriptions.contains(s)) {
+        alreadySubscribeds.add(s);
+      }
+    }
+
+    final Set<SubmoduleSubscription> subscriptionsToRemove =
+        new HashSet<SubmoduleSubscription>(previousSubscriptions);
+    final List<SubmoduleSubscription> subscriptionsToInsert =
+        new ArrayList<SubmoduleSubscription>(extractedSubscriptions);
+
+    subscriptionsToRemove.removeAll(subscriptionsToInsert);
+    subscriptionsToInsert.removeAll(alreadySubscribeds);
+
+    if (!subscriptionsToRemove.isEmpty()) {
+      expect(schema.submoduleSubscriptions()).andReturn(subscriptions);
+      subscriptions.delete(subscriptionsToRemove);
+    }
+
+    expect(schema.submoduleSubscriptions()).andReturn(subscriptions);
+    subscriptions.insert(subscriptionsToInsert);
+
+    expect(schema.submoduleSubscriptions()).andReturn(subscriptions);
+    expect(subscriptions.bySubmodule(mergedBranch)).andReturn(
+        new ListResultSet<SubmoduleSubscription>(new ArrayList<SubmoduleSubscription>()));
+
+    schema.close();
+
+    doReplay();
+
+    final SubmoduleOp submoduleOp =
+        new SubmoduleOp(mergedBranch, mergeTip, new RevWalk(realDb),
+            urlProvider, schemaFactory, realDb, new Project(mergedBranch
+                .getParentKey()), new ArrayList<Change>(), null, null,
+            repoManager, null);
+
+    submoduleOp.update();
+  }
+
+  /**
+   * It creates and adds a regular file to git index of a repository.
+   *
+   * @param fileName The file name.
+   * @param content File content.
+   * @param repository The Repository instance.
+   * @throws IOException If an I/O exception occurs.
+   */
+  private void addRegularFileToIndex(final String fileName,
+      final String content, final Repository repository) throws IOException {
+    final ObjectInserter oi = repository.newObjectInserter();
+    AnyObjectId objectId =
+        oi.insert(Constants.OBJ_BLOB, Constants.encode(content));
+    oi.flush();
+    addEntryToIndex(fileName, FileMode.REGULAR_FILE, objectId, repository);
+  }
+
+  /**
+   * It creates and adds a git link to git index of a repository.
+   *
+   * @param fileName The file name.
+   * @param objectId The sha-1 value of git link.
+   * @param repository The Repository instance.
+   * @throws IOException If an I/O exception occurs.
+   */
+  private void addGitLinkToIndex(final String fileName,
+      final AnyObjectId objectId, final Repository repository)
+      throws IOException {
+    addEntryToIndex(fileName, FileMode.GITLINK, objectId, repository);
+  }
+
+  /**
+   * It adds an entry to index.
+   *
+   * @param path The entry path.
+   * @param fileMode The entry file mode.
+   * @param objectId The ObjectId value of the entry.
+   * @param repository The repository instance.
+   * @throws IOException If an I/O exception occurs.
+   */
+  private void addEntryToIndex(final String path, final FileMode fileMode,
+      final AnyObjectId objectId, final Repository repository)
+      throws IOException {
+    final DirCacheEntry e = new DirCacheEntry(path);
+    e.setFileMode(fileMode);
+    e.setObjectId(objectId);
+
+    final DirCacheBuilder dirCacheBuilder = repository.lockDirCache().builder();
+    dirCacheBuilder.add(e);
+    dirCacheBuilder.commit();
+  }
+
+  private static StringBuilder buildSubmoduleSection(final String name,
+      final String path, final String url, final String branch) {
+    final StringBuilder sb = new StringBuilder();
+
+    sb.append("[submodule \"");
+    sb.append(name);
+    sb.append("\"]");
+    sb.append(newLine);
+
+    sb.append("\tpath = ");
+    sb.append(path);
+    sb.append(newLine);
+
+    sb.append("\turl = ");
+    sb.append(url);
+    sb.append(newLine);
+
+    sb.append("\tbranch = ");
+    sb.append(branch);
+    sb.append(newLine);
+
+    return sb;
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/mail/FromAddressGeneratorProviderTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/mail/FromAddressGeneratorProviderTest.java
index 6d56b4a..2332eb6 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/mail/FromAddressGeneratorProviderTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/mail/FromAddressGeneratorProviderTest.java
@@ -47,7 +47,8 @@
   }
 
   private FromAddressGenerator create() {
-    return new FromAddressGeneratorProvider(config, ident, accountCache).get();
+    return new FromAddressGeneratorProvider(config, "Anonymous Coward", ident,
+        accountCache).get();
   }
 
   private void setFrom(final String newFrom) {
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
new file mode 100644
index 0000000..b2c2fc9
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/util/SubmoduleSectionParserTest.java
@@ -0,0 +1,270 @@
+// 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.util;
+
+import static org.easymock.EasyMock.createStrictMock;
+import static org.easymock.EasyMock.expect;
+import static org.easymock.EasyMock.replay;
+import static org.easymock.EasyMock.verify;
+import static org.junit.Assert.assertEquals;
+
+import com.google.gerrit.reviewdb.Branch;
+import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.reviewdb.SubmoduleSubscription;
+import com.google.gerrit.server.git.GitRepositoryManager;
+
+import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase;
+import org.eclipse.jgit.lib.BlobBasedConfig;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.net.URI;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+import java.util.TreeSet;
+
+public class SubmoduleSectionParserTest extends LocalDiskRepositoryTestCase {
+  private final static String THIS_SERVER = "localhost";
+  private GitRepositoryManager repoManager;
+  private BlobBasedConfig bbc;
+
+  @Override
+  @Before
+  public void setUp() throws Exception {
+    super.setUp();
+
+    repoManager = createStrictMock(GitRepositoryManager.class);
+    bbc = createStrictMock(BlobBasedConfig.class);
+  }
+
+  private void doReplay() {
+    replay(repoManager, bbc);
+  }
+
+  private void doVerify() {
+    verify(repoManager, bbc);
+  }
+
+  @Test
+  public void testSubmodulesParseWithCorrectSections() throws Exception {
+    final Map<String, SubmoduleSection> sectionsToReturn =
+        new TreeMap<String, SubmoduleSection>();
+    sectionsToReturn.put("a", new SubmoduleSection("ssh://localhost/a", "a",
+        "."));
+    sectionsToReturn.put("b", new SubmoduleSection("ssh://localhost/b", "b",
+        "."));
+    sectionsToReturn.put("c", new SubmoduleSection("ssh://localhost/test/c",
+        "c-path", "refs/heads/master"));
+    sectionsToReturn.put("d", new SubmoduleSection("ssh://localhost/d",
+        "d-parent/the-d-folder", "refs/heads/test"));
+    sectionsToReturn.put("e", new SubmoduleSection("ssh://localhost/e.git", "e",
+        "."));
+
+    final Map<String, String> reposToBeFound = new HashMap<String, String>();
+    reposToBeFound.put("a", "a");
+    reposToBeFound.put("b", "b");
+    reposToBeFound.put("c", "test/c");
+    reposToBeFound.put("d", "d");
+    reposToBeFound.put("e", "e");
+
+    final Branch.NameKey superBranchNameKey =
+        new Branch.NameKey(new Project.NameKey("super-project"),
+            "refs/heads/master");
+
+    final List<SubmoduleSubscription> expectedSubscriptions =
+        new ArrayList<SubmoduleSubscription>();
+    expectedSubscriptions
+        .add(new SubmoduleSubscription(superBranchNameKey, new Branch.NameKey(
+            new Project.NameKey("a"), "refs/heads/master"), "a"));
+    expectedSubscriptions
+        .add(new SubmoduleSubscription(superBranchNameKey, new Branch.NameKey(
+            new Project.NameKey("b"), "refs/heads/master"), "b"));
+    expectedSubscriptions.add(new SubmoduleSubscription(superBranchNameKey,
+        new Branch.NameKey(new Project.NameKey("test/c"), "refs/heads/master"),
+        "c-path"));
+    expectedSubscriptions.add(new SubmoduleSubscription(superBranchNameKey,
+        new Branch.NameKey(new Project.NameKey("d"), "refs/heads/test"),
+        "d-parent/the-d-folder"));
+    expectedSubscriptions
+        .add(new SubmoduleSubscription(superBranchNameKey, new Branch.NameKey(
+            new Project.NameKey("e"), "refs/heads/master"), "e"));
+
+    execute(superBranchNameKey, sectionsToReturn, reposToBeFound,
+        expectedSubscriptions);
+  }
+
+  @Test
+  public void testSubmodulesParseWithAnInvalidSection() throws Exception {
+    final Map<String, SubmoduleSection> sectionsToReturn =
+        new TreeMap<String, SubmoduleSection>();
+    sectionsToReturn.put("a", new SubmoduleSection("ssh://localhost/a", "a",
+        "."));
+    // This one is invalid since "b" is not a recognized project
+    sectionsToReturn.put("b", new SubmoduleSection("ssh://localhost/b", "b",
+        "."));
+    sectionsToReturn.put("c", new SubmoduleSection("ssh://localhost/test/c",
+        "c-path", "refs/heads/master"));
+    sectionsToReturn.put("d", new SubmoduleSection("ssh://localhost/d",
+        "d-parent/the-d-folder", "refs/heads/test"));
+    sectionsToReturn.put("e", new SubmoduleSection("ssh://localhost/e.git", "e",
+        "."));
+
+    // "b" will not be in this list
+    final Map<String, String> reposToBeFound = new HashMap<String, String>();
+    reposToBeFound.put("a", "a");
+    reposToBeFound.put("c", "test/c");
+    reposToBeFound.put("d", "d");
+    reposToBeFound.put("e", "e");
+
+    final Branch.NameKey superBranchNameKey =
+        new Branch.NameKey(new Project.NameKey("super-project"),
+            "refs/heads/master");
+
+    final List<SubmoduleSubscription> expectedSubscriptions =
+        new ArrayList<SubmoduleSubscription>();
+    expectedSubscriptions
+        .add(new SubmoduleSubscription(superBranchNameKey, new Branch.NameKey(
+            new Project.NameKey("a"), "refs/heads/master"), "a"));
+    expectedSubscriptions.add(new SubmoduleSubscription(superBranchNameKey,
+        new Branch.NameKey(new Project.NameKey("test/c"), "refs/heads/master"),
+        "c-path"));
+    expectedSubscriptions.add(new SubmoduleSubscription(superBranchNameKey,
+        new Branch.NameKey(new Project.NameKey("d"), "refs/heads/test"),
+        "d-parent/the-d-folder"));
+    expectedSubscriptions
+        .add(new SubmoduleSubscription(superBranchNameKey, new Branch.NameKey(
+            new Project.NameKey("e"), "refs/heads/master"), "e"));
+
+    execute(superBranchNameKey, sectionsToReturn, reposToBeFound,
+        expectedSubscriptions);
+  }
+
+  @Test
+  public void testSubmoduleSectionToOtherServer() throws Exception {
+    Map<String, SubmoduleSection> sectionsToReturn =
+        new HashMap<String, SubmoduleSection>();
+    // The url is not to this server.
+    sectionsToReturn.put("a", new SubmoduleSection("ssh://review.source.com/a",
+        "a", "."));
+
+    execute(new Branch.NameKey(new Project.NameKey("super-project"),
+        "refs/heads/master"), sectionsToReturn, new HashMap<String, String>(),
+        new ArrayList<SubmoduleSubscription>());
+  }
+
+  @Test
+  public void testProjectNotFound() throws Exception {
+    Map<String, SubmoduleSection> sectionsToReturn =
+        new HashMap<String, SubmoduleSection>();
+    sectionsToReturn.put("a", new SubmoduleSection("ssh://localhost/a", "a",
+        "."));
+
+    execute(new Branch.NameKey(new Project.NameKey("super-project"),
+        "refs/heads/master"), sectionsToReturn, new HashMap<String, String>(),
+        new ArrayList<SubmoduleSubscription>());
+  }
+
+  @Test
+  public void testProjectWithSlashesNotFound() throws Exception {
+    Map<String, SubmoduleSection> sectionsToReturn =
+        new HashMap<String, SubmoduleSection>();
+    sectionsToReturn.put("project", new SubmoduleSection(
+        "ssh://localhost/company/tools/project", "project", "."));
+
+    execute(new Branch.NameKey(new Project.NameKey("super-project"),
+        "refs/heads/master"), sectionsToReturn, new HashMap<String, String>(),
+        new ArrayList<SubmoduleSubscription>());
+  }
+
+  private void execute(final Branch.NameKey superProjectBranch,
+      final Map<String, SubmoduleSection> sectionsToReturn,
+      final Map<String, String> reposToBeFound,
+      final List<SubmoduleSubscription> expectedSubscriptions) throws Exception {
+    expect(bbc.getSubsections("submodule"))
+        .andReturn(sectionsToReturn.keySet());
+
+    for (final String id : sectionsToReturn.keySet()) {
+      final SubmoduleSection section = sectionsToReturn.get(id);
+      expect(bbc.getString("submodule", id, "url")).andReturn(section.getUrl());
+      expect(bbc.getString("submodule", id, "path")).andReturn(
+          section.getPath());
+      expect(bbc.getString("submodule", id, "branch")).andReturn(
+          section.getBranch());
+
+      if (THIS_SERVER.equals(new URI(section.getUrl()).getHost())) {
+        String projectNameCandidate = null;
+        final String urlExtractedPath = new URI(section.getUrl()).getPath();
+        int fromIndex = urlExtractedPath.length() - 1;
+        while (fromIndex > 0) {
+          fromIndex = urlExtractedPath.lastIndexOf('/', fromIndex - 1);
+          projectNameCandidate = urlExtractedPath.substring(fromIndex + 1);
+          if (projectNameCandidate.endsWith(".git")) {
+            projectNameCandidate = projectNameCandidate.substring(0, projectNameCandidate.length() - 4);
+          }
+          if (projectNameCandidate.equals(reposToBeFound.get(id))) {
+            expect(repoManager.list()).andReturn(
+                new TreeSet<Project.NameKey>(Collections
+                    .singletonList(new Project.NameKey(projectNameCandidate))));
+            break;
+          } else {
+            expect(repoManager.list()).andReturn(
+                new TreeSet<Project.NameKey>(Collections.EMPTY_LIST));
+          }
+        }
+      }
+    }
+
+    doReplay();
+
+    final SubmoduleSectionParser ssp =
+        new SubmoduleSectionParser(bbc, THIS_SERVER, superProjectBranch,
+            repoManager);
+
+    List<SubmoduleSubscription> returnedSubscriptions = ssp.parseAllSections();
+
+    doVerify();
+
+    assertEquals(expectedSubscriptions, returnedSubscriptions);
+  }
+
+  private final static class SubmoduleSection {
+    private final String url;
+    private final String path;
+    private final String branch;
+
+    public SubmoduleSection(final String url, final String path,
+        final String branch) {
+      this.url = url;
+      this.path = path;
+      this.branch = branch;
+    }
+
+    public String getUrl() {
+      return url;
+    }
+
+    public String getPath() {
+      return path;
+    }
+
+    public String getBranch() {
+      return branch;
+    }
+  }
+}
diff --git a/gerrit-sshd/.gitignore b/gerrit-sshd/.gitignore
index 903c6c8..194bedc 100644
--- a/gerrit-sshd/.gitignore
+++ b/gerrit-sshd/.gitignore
@@ -2,3 +2,4 @@
 /.classpath
 /.project
 /.settings/org.maven.ide.eclipse.prefs
+/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
diff --git a/gerrit-sshd/pom.xml b/gerrit-sshd/pom.xml
index 0ccf0ec..c02e108 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.2-SNAPSHOT</version>
+    <version>2.3-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-sshd</artifactId>
@@ -64,5 +64,11 @@
       <artifactId>gerrit-server</artifactId>
       <version>${project.version}</version>
     </dependency>
+
+    <dependency>
+      <groupId>com.google.gerrit</groupId>
+      <artifactId>gerrit-ehcache</artifactId>
+      <version>${project.version}</version>
+    </dependency>
   </dependencies>
 </project>
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 89338c4..fb17169 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
@@ -109,6 +109,7 @@
 
     public void start(final Environment env) throws IOException {
       this.env = env;
+      final Context ctx = this.ctx;
       startExecutor.execute(new Runnable() {
         public void run() {
           try {
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 977d209..805549e 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
@@ -19,6 +19,7 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PeerDaemonUser;
+import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.sshd.SshScope.Context;
 import com.google.inject.Inject;
@@ -33,6 +34,7 @@
 import org.apache.sshd.common.util.Buffer;
 import org.apache.sshd.server.PublickeyAuthenticator;
 import org.apache.sshd.server.session.ServerSession;
+import org.eclipse.jgit.lib.Config;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -47,6 +49,7 @@
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
+import java.util.Locale;
 import java.util.Set;
 
 /**
@@ -61,17 +64,20 @@
   private final SshLog sshLog;
   private final IdentifiedUser.GenericFactory userFactory;
   private final PeerDaemonUser.Factory peerFactory;
+  private final Config config;
   private final Set<PublicKey> myHostKeys;
   private volatile PeerKeyCache peerKeyCache;
 
   @Inject
   DatabasePubKeyAuth(final SshKeyCacheImpl skc, final SshLog l,
       final IdentifiedUser.GenericFactory uf, final PeerDaemonUser.Factory pf,
-      final SitePaths site, final KeyPairProvider hostKeyProvider) {
+      final SitePaths site, final KeyPairProvider hostKeyProvider,
+      final @GerritServerConfig Config cfg) {
     sshKeyCache = skc;
     sshLog = l;
     userFactory = uf;
     peerFactory = pf;
+    config = cfg;
     myHostKeys = myHostKeys(hostKeyProvider);
     peerKeyCache = new PeerKeyCache(site.peer_keys);
   }
@@ -91,7 +97,7 @@
     }
   }
 
-  public boolean authenticate(final String username,
+  public boolean authenticate(String username,
       final PublicKey suppliedKey, final ServerSession session) {
     final SshSession sd = session.getAttribute(SshSession.KEY);
 
@@ -107,6 +113,10 @@
       }
     }
 
+    if (config.getBoolean("auth", "userNameToLowerCase", false)) {
+      username = username.toLowerCase(Locale.US);
+    }
+
     final Iterable<SshKeyCacheEntry> keyList = sshKeyCache.get(username);
     final SshKeyCacheEntry key = find(keyList, suppliedKey);
     if (key == null) {
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 fe9d9eb..d382a57 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
@@ -216,7 +216,7 @@
 
   @Override
   public synchronized void start() {
-    if (acceptor == null) {
+    if (acceptor == null && !listen.isEmpty()) {
       checkConfig();
 
       acceptor = createAcceptor();
@@ -257,6 +257,10 @@
   }
 
   private List<HostKey> computeHostKeys() {
+    if (listen.isEmpty()) {
+      return Collections.emptyList();
+    }
+
     final List<PublicKey> keys = myHostKeys();
     final ArrayList<HostKey> r = new ArrayList<HostKey>();
     for (final PublicKey pub : keys) {
@@ -348,6 +352,10 @@
       return bind;
     }
 
+    if (want.length == 1 && isOff(want[0])) {
+      return bind;
+    }
+
     for (final String desc : want) {
       try {
         bind.add(SocketUtil.resolve(desc, DEFAULT_PORT));
@@ -358,6 +366,12 @@
     return bind;
   }
 
+  private static boolean isOff(String listenHostname) {
+    return "off".equalsIgnoreCase(listenHostname)
+        || "none".equalsIgnoreCase(listenHostname)
+        || "no".equalsIgnoreCase(listenHostname);
+  }
+
   @SuppressWarnings("unchecked")
   private void initProviderBouncyCastle() {
     setKeyExchangeFactories(Arrays.<NamedFactory<KeyExchange>> asList(
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 e6cb6be..816955d 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
@@ -39,6 +39,7 @@
 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.ProjectNode;
 import com.google.gerrit.sshd.commands.QueryShell;
 import com.google.gerrit.util.cli.CmdLineParser;
 import com.google.gerrit.util.cli.OptionHandlerFactory;
@@ -80,6 +81,7 @@
     bind(QueueProvider.class).to(CommandExecutorQueueProvider.class).in(SINGLETON);
     bind(AccountManager.class);
     factory(ChangeUserName.Factory.class);
+    factory(ProjectNode.Factory.class);
 
     bind(PublickeyAuthenticator.class).to(DatabasePubKeyAuth.class);
     bind(KeyPairProvider.class).toProvider(HostKeyProvider.class).in(SINGLETON);
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 95bc563..f3bd051 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
@@ -30,8 +30,11 @@
 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;
+import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
@@ -39,10 +42,21 @@
 
 @AdminCommand
 final class AdminSetParent extends BaseCommand {
+  private static final Logger log = LoggerFactory.getLogger(AdminSetParent.class);
+
   @Option(name = "--parent", aliases = {"-p"}, metaVar = "NAME", usage = "new parent project")
   private ProjectControl newParent;
 
-  @Argument(index = 0, required = true, multiValued = true, metaVar = "NAME", usage = "projects to modify")
+  @Option(name = "--children-of", metaVar = "NAME",
+      usage = "parent project for which the child projects should be reparented")
+  private ProjectControl oldParent;
+
+  @Option(name = "--exclude", metaVar = "NAME",
+      usage = "child project of old parent project which should not be reparented")
+  private List<ProjectControl> excludedChildren = new ArrayList<ProjectControl>();
+
+  @Argument(index = 0, required = false, multiValued = true, metaVar = "NAME",
+      usage = "projects to modify")
   private List<ProjectControl> children = new ArrayList<ProjectControl>();
 
   @Inject
@@ -54,21 +68,37 @@
   @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 {
-        parseCommandLine();
-        updateParents();
+        stdout = toPrintWriter(out);
+        try {
+          parseCommandLine();
+          updateParents();
+        } finally {
+          stdout.flush();
+        }
       }
     });
   }
 
   private void updateParents() 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");
+    }
+    if (oldParent == null && !excludedChildren.isEmpty()) {
+      throw new UnloggedFailure(1, "fatal: --exclude can only be used together " +
+                                   "with --children-of");
+    }
+
     final StringBuilder err = new StringBuilder();
     final Set<Project.NameKey> grandParents = new HashSet<Project.NameKey>();
-    Project.NameKey newParentKey;
 
     grandParents.add(allProjectsName);
 
@@ -87,24 +117,28 @@
           break;
         }
       }
-    } else {
-      // If no parent was selected, set to NULL to use the default.
-      //
-      newParentKey = null;
     }
 
+    final List<Project> childProjects = new ArrayList<Project>();
     for (final ProjectControl pc : children) {
-      final Project.NameKey key = pc.getProject().getNameKey();
-      final String name = pc.getProject().getName();
+      childProjects.add(pc.getProject());
+    }
+    if (oldParent != null) {
+      childProjects.addAll(getChildrenForReparenting(oldParent));
+    }
 
-      if (allProjectsName.equals(key)) {
+    for (final Project project : childProjects) {
+      final String name = project.getName();
+      final Project.NameKey nameKey = project.getNameKey();
+
+      if (allProjectsName.equals(nameKey)) {
         // Don't allow the wild card project to have a parent.
         //
         err.append("error: Cannot set parent of '" + name + "'\n");
         continue;
       }
 
-      if (grandParents.contains(key) || key.equals(newParentKey)) {
+      if (grandParents.contains(nameKey) || nameKey.equals(newParentKey)) {
         // Try to avoid creating a cycle in the parent pointers.
         //
         err.append("error: Cycle exists between '" + name + "' and '"
@@ -114,11 +148,12 @@
       }
 
       try {
-        MetaDataUpdate md = metaDataUpdateFactory.create(key);
+        MetaDataUpdate md = metaDataUpdateFactory.create(nameKey);
         try {
           ProjectConfig config = ProjectConfig.read(md);
-          config.getProject().setParentName(newParentKey.get());
-          md.setMessage("Inherit access from " + newParentKey.get() + "\n");
+          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");
           }
@@ -128,10 +163,16 @@
       } catch (RepositoryNotFoundException notFound) {
         err.append("error: Project " + name + " not found\n");
       } catch (IOException e) {
-        throw new Failure(1, "Cannot update project " + name, e);
+        final String msg = "Cannot update project " + name;
+        log.error(msg, e);
+        err.append("error: " + msg + "\n");
       } catch (ConfigInvalidException e) {
-        throw new Failure(1, "Cannot update project " + name, e);
+        final String msg = "Cannot update project " + name;
+        log.error(msg, e);
+        err.append("error: " + msg + "\n");
       }
+
+      projectCache.evict(project);
     }
 
     if (err.length() > 0) {
@@ -141,4 +182,84 @@
       throw new UnloggedFailure(1, err.toString());
     }
   }
+
+  /**
+   * Returns the children of the specified parent project that should be
+   * reparented. The returned list of child projects does not contain projects
+   * that were specified to be excluded from reparenting.
+   */
+  private List<Project> getChildrenForReparenting(final ProjectControl parent) {
+    final List<Project> childProjects = new ArrayList<Project>();
+    final List<Project.NameKey> excluded =
+      new ArrayList<Project.NameKey>(excludedChildren.size());
+    for (final ProjectControl excludedChild : excludedChildren) {
+      excluded.add(excludedChild.getProject().getNameKey());
+    }
+    final List<Project.NameKey> automaticallyExcluded =
+      new ArrayList<Project.NameKey>(excludedChildren.size());
+    if (newParentKey != null) {
+      automaticallyExcluded.addAll(getAllParents(newParentKey));
+    }
+    for (final Project child : getChildren(parent.getProject().getNameKey())) {
+      final Project.NameKey childName = child.getNameKey();
+      if (!excluded.contains(childName)) {
+        if (!automaticallyExcluded.contains(childName)) {
+          childProjects.add(child);
+        } else {
+          stdout.println("Automatically excluded '" + childName + "' " +
+                         "from reparenting because it is in the parent " +
+                         "line of the new parent '" + newParentKey + "'.");
+        }
+      }
+    }
+    return childProjects;
+  }
+
+  private Set<Project.NameKey> getAllParents(final Project.NameKey projectName) {
+    final Set<Project.NameKey> parents = new HashSet<Project.NameKey>();
+    Project.NameKey p = projectName;
+    while (p != null && parents.add(p)) {
+      final ProjectState e = projectCache.get(p);
+      if (e == null) {
+        // If we can't get it from the cache, pretend it's not present.
+        break;
+      }
+      p = getParentName(e.getProject());
+    }
+    return parents;
+  }
+
+  private List<Project> getChildren(final Project.NameKey parentName) {
+    final List<Project> childProjects = new ArrayList<Project>();
+    for (final Project.NameKey projectName : projectCache.all()) {
+      final ProjectState e = projectCache.get(projectName);
+      if (e == null) {
+        // If we can't get it from the cache, pretend it's not present.
+        continue;
+      }
+
+      if (parentName.equals(getParentName(e.getProject()))) {
+        childProjects.add(e.getProject());
+      }
+    }
+    return childProjects;
+  }
+
+  /**
+   * Returns the project parent name.
+   *
+   * @return Project parent name, <code>null</code> for the 'All-Projects' root
+   *         project
+   */
+  private Project.NameKey getParentName(final Project project) {
+    if (project.getParent() != null) {
+      return project.getParent();
+    }
+
+    if (project.getNameKey().equals(allProjectsName)) {
+      return null;
+    }
+
+    return allProjectsName;
+  }
 }
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 313b9dc..083759c 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,7 +14,7 @@
 
 package com.google.gerrit.sshd.commands;
 
-import com.google.gerrit.server.cache.CachePool;
+import com.google.gerrit.ehcache.EhcachePoolImpl;
 import com.google.gerrit.sshd.BaseCommand;
 import com.google.inject.Inject;
 
@@ -27,7 +27,7 @@
 
 abstract class CacheCommand extends BaseCommand {
   @Inject
-  protected CachePool cachePool;
+  protected EhcachePoolImpl cachePool;
 
   protected SortedSet<String> cacheNames() {
     final SortedSet<String> names = new TreeSet<String>();
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProject.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProject.java
deleted file mode 100644
index 7a0c1a9..0000000
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProject.java
+++ /dev/null
@@ -1,300 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.sshd.commands;
-
-import com.google.gerrit.common.data.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.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.Project.SubmitType;
-import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.GroupCache;
-import com.google.gerrit.server.config.ProjectOwnerGroups;
-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.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectControl;
-import com.google.gerrit.sshd.BaseCommand;
-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.eclipse.jgit.lib.CommitBuilder;
-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.RefUpdate;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.lib.RefUpdate.Result;
-import org.kohsuke.args4j.Argument;
-import org.kohsuke.args4j.Option;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.IOException;
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
-/** Create a new project. **/
-final class CreateProject extends BaseCommand {
-  private static final Logger log = LoggerFactory.getLogger(CreateProject.class);
-
-  @Option(name = "--name", aliases = {"-n"}, metaVar = "NAME", usage = "name of project to be created (deprecated option)")
-  void setProjectNameFromOption(String name) {
-    if (projectName != null) {
-      throw new IllegalArgumentException("NAME already supplied");
-    } else {
-      projectName = name;
-    }
-  }
-
-  @Option(name = "--owner", aliases = {"-o"}, usage = "owner(s) of project")
-  private List<AccountGroup.UUID> ownerIds;
-
-  @Option(name = "--parent", aliases = {"-p"}, metaVar = "NAME", usage = "parent project")
-  private ProjectControl newParent;
-
-  @Option(name = "--permissions-only", usage = "create project for use only as parent")
-  private boolean permissionsOnly;
-
-  @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 = SubmitType.MERGE_IF_NECESSARY;
-
-  @Option(name = "--use-contributor-agreements", aliases = {"--ca"}, usage = "if contributor agreement is required")
-  private boolean contributorAgreements;
-
-  @Option(name = "--use-signed-off-by", aliases = {"--so"}, usage = "if signed-off-by is required")
-  private boolean signedOffBy;
-
-  @Option(name = "--use-content-merge", usage = "allow automatic conflict resolving within files")
-  private boolean contentMerge;
-
-  @Option(name = "--require-change-id", aliases = {"--id"}, usage = "if change-id is required")
-  private boolean requireChangeID;
-
-  @Option(name = "--branch", aliases = {"-b"}, metaVar = "BRANCH", usage = "initial branch name\n"
-      + "(default: master)")
-  private String branch = Constants.MASTER;
-
-  @Option(name = "--empty-commit", usage = "to create initial empty commit")
-  private boolean createEmptyCommit;
-
-  private String projectName;
-  @Argument(index = 0, metaVar="NAME", usage="name of project to be created")
-  void setProjectNameFromArgument(String name) {
-    if (projectName != null) {
-      throw new IllegalArgumentException("--name already supplied");
-    } else {
-      projectName = name;
-    }
-  }
-
-  @Inject
-  private GitRepositoryManager repoManager;
-
-  @Inject
-  private ProjectCache projectCache;
-
-  @Inject
-  private GroupCache groupCache;
-
-  @Inject
-  @ProjectOwnerGroups
-  private Set<AccountGroup.UUID> projectOwnerGroups;
-
-  @Inject
-  private IdentifiedUser currentUser;
-
-  @Inject
-  private ReplicationQueue rq;
-
-  @Inject
-  @GerritPersonIdent
-  private PersonIdent serverIdent;
-
-  @Inject
-  MetaDataUpdate.User metaDataUpdateFactory;
-
-  private Project.NameKey nameKey;
-
-  @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);
-        }
-
-        parseCommandLine();
-        validateParameters();
-
-        try {
-          nameKey = new Project.NameKey(projectName);
-
-          String head = permissionsOnly ? GitRepositoryManager.REF_CONFIG : branch;
-          final Repository repo = repoManager.createRepository(nameKey);
-          try {
-            rq.replicateNewProject(nameKey, head);
-
-            RefUpdate u = repo.updateRef(Constants.HEAD);
-            u.disableRefLog();
-            u.link(head);
-
-            createProjectConfig();
-
-            if (!permissionsOnly && createEmptyCommit) {
-              createEmptyCommit(repo, nameKey, branch);
-            }
-          } finally {
-            repo.close();
-          }
-        } catch (IllegalStateException err) {
-          try {
-            Repository repo = repoManager.openRepository(nameKey);
-            try {
-              if (repo.getObjectDatabase().exists()) {
-                throw new UnloggedFailure(1, "fatal: project \"" + projectName + "\" exists");
-              }
-            } finally {
-              repo.close();
-            }
-          } catch (RepositoryNotFoundException doesNotExist) {
-            throw new Failure(1, "fatal: Cannot create " + projectName, err);
-          }
-        } catch (RepositoryNotFoundException badName) {
-          throw new UnloggedFailure(1, "fatal: " + badName.getMessage());
-        } catch (Exception err) {
-          throw new Failure(1, "fatal: Cannot create " + projectName, err);
-        }
-      }
-    });
-  }
-
-  private void createEmptyCommit(final Repository repo,
-      final Project.NameKey project, final String ref) throws IOException {
-    ObjectInserter oi = repo.newObjectInserter();
-    try {
-      CommitBuilder cb = new CommitBuilder();
-      cb.setTreeId(oi.insert(Constants.OBJ_TREE, new byte[] {}));
-      cb.setAuthor(metaDataUpdateFactory.getUserPersonIdent());
-      cb.setCommitter(serverIdent);
-      cb.setMessage("Initial empty repository\n");
-
-      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:
-          rq.scheduleUpdate(project, ref);
-          break;
-        default: {
-          throw new IOException(result.name());
-        }
-      }
-    } catch (IOException e) {
-      log.error("Cannot create empty commit for " + projectName, e);
-      throw e;
-    } finally {
-      oi.release();
-    }
-  }
-
-  private void createProjectConfig() throws IOException, ConfigInvalidException {
-    MetaDataUpdate md = metaDataUpdateFactory.create(nameKey);
-    try {
-      ProjectConfig config = ProjectConfig.read(md);
-      config.load(md);
-
-      Project newProject = config.getProject();
-      newProject.setDescription(projectDescription);
-      newProject.setSubmitType(submitType);
-      newProject.setUseContributorAgreements(contributorAgreements);
-      newProject.setUseSignedOffBy(signedOffBy);
-      newProject.setUseContentMerge(contentMerge);
-      newProject.setRequireChangeID(requireChangeID);
-      if (newParent != null) {
-        newProject.setParentName(newParent.getProject().getName());
-      }
-
-      if (!ownerIds.isEmpty()) {
-        AccessSection all = config.getAccessSection(AccessSection.ALL, true);
-        for (AccountGroup.UUID ownerId : ownerIds) {
-          AccountGroup accountGroup = groupCache.get(ownerId);
-          GroupReference group = config.resolve(accountGroup);
-          all.getPermission(Permission.OWNER, true).add(
-              new PermissionRule(group));
-        }
-      }
-
-      md.setMessage("Created project\n");
-      if (!config.commit(md)) {
-        throw new IOException("Cannot create " + projectName);
-      }
-    } finally {
-      md.close();
-    }
-    projectCache.onCreateProject(nameKey);
-    repoManager.setProjectDescription(nameKey, projectDescription);
-    rq.scheduleUpdate(nameKey, GitRepositoryManager.REF_CONFIG);
-  }
-
-  private void validateParameters() throws Failure {
-    if (projectName == null || projectName.isEmpty()) {
-      throw new Failure(1, "fatal: Argument NAME is required");
-    }
-
-    if (projectName.endsWith(Constants.DOT_GIT_EXT)) {
-      projectName = projectName.substring(0, //
-          projectName.length() - Constants.DOT_GIT_EXT.length());
-    }
-
-    if (ownerIds != null && !ownerIds.isEmpty()) {
-      ownerIds =
-          new ArrayList<AccountGroup.UUID>(new HashSet<AccountGroup.UUID>(ownerIds));
-    } else {
-      ownerIds = new ArrayList<AccountGroup.UUID>(projectOwnerGroups);
-    }
-
-    while (branch.startsWith("/")) {
-      branch = branch.substring(1);
-    }
-    if (!branch.startsWith(Constants.R_HEADS)) {
-      branch = Constants.R_HEADS + branch;
-    }
-    if (!Repository.isValidRefName(branch)) {
-      throw new Failure(1, "--branch \"" + branch + "\" is not a valid name");
-    }
-  }
-}
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
new file mode 100644
index 0000000..f0327c5
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
@@ -0,0 +1,160 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.sshd.commands;
+
+import com.google.gerrit.common.errors.ProjectCreationFailedException;
+import com.google.gerrit.reviewdb.AccountGroup;
+import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.reviewdb.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.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 {
+  @Option(name = "--name", aliases = {"-n"}, metaVar = "NAME", usage = "name of project to be created (deprecated option)")
+  void setProjectNameFromOption(String name) {
+    if (projectName != null) {
+      throw new IllegalArgumentException("NAME already supplied");
+    } else {
+      projectName = name;
+    }
+  }
+
+  @Option(name = "--suggest-parents", aliases = {"-S"}, usage = "suggest parent candidates, "
+      + "if this option is used all other options and arguments are ignored")
+  private boolean suggestParent;
+
+  @Option(name = "--owner", aliases = {"-o"}, usage = "owner(s) of project")
+  private List<AccountGroup.UUID> ownerIds;
+
+  @Option(name = "--parent", aliases = {"-p"}, metaVar = "NAME", usage = "parent project")
+  private ProjectControl newParent;
+
+  @Option(name = "--permissions-only", usage = "create project for use only as parent")
+  private boolean permissionsOnly;
+
+  @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 = SubmitType.MERGE_IF_NECESSARY;
+
+  @Option(name = "--use-contributor-agreements", aliases = {"--ca"}, usage = "if contributor agreement is required")
+  private boolean contributorAgreements;
+
+  @Option(name = "--use-signed-off-by", aliases = {"--so"}, usage = "if signed-off-by is required")
+  private boolean signedOffBy;
+
+  @Option(name = "--use-content-merge", usage = "allow automatic conflict resolving within files")
+  private boolean contentMerge;
+
+  @Option(name = "--require-change-id", aliases = {"--id"}, usage = "if change-id is required")
+  private boolean requireChangeID;
+
+  @Option(name = "--branch", aliases = {"-b"}, metaVar = "BRANCH", usage = "initial branch name\n"
+      + "(default: master)")
+  private String branch = Constants.MASTER;
+
+  @Option(name = "--empty-commit", usage = "to create initial empty commit")
+  private boolean createEmptyCommit;
+
+  private String projectName;
+
+  @Argument(index = 0, metaVar = "NAME", usage = "name of project to be created")
+  void setProjectNameFromArgument(String name) {
+    if (projectName != null) {
+      throw new IllegalArgumentException("--name already supplied");
+    } else {
+      projectName = name;
+    }
+  }
+
+  @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);
+        }
+        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 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();
+        }
+      }
+    });
+  }
+}
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 a369b86..5cee06e 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
@@ -37,6 +37,7 @@
     command(gerrit).toProvider(new DispatchCommandProvider(gerrit));
     command(gerrit, "flush-caches").to(FlushCaches.class);
     command(gerrit, "ls-projects").to(ListProjects.class);
+    command(gerrit, "ls-groups").to(ListGroupsCommand.class);
     command(gerrit, "query").to(Query.class);
     command(gerrit, "show-caches").to(ShowCaches.class);
     command(gerrit, "show-connections").to(ShowConnections.class);
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
new file mode 100644
index 0000000..fd34ab5
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java
@@ -0,0 +1,99 @@
+// 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.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.server.account.VisibleGroups;
+import com.google.gerrit.reviewdb.Account;
+import com.google.gerrit.reviewdb.AccountGroup;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.sshd.BaseCommand;
+import com.google.gwtorm.client.OrmException;
+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 {
+
+  @Inject
+  private VisibleGroups.Factory visibleGroupsFactory;
+
+  @Inject
+  private IdentifiedUser.GenericFactory userFactory;
+
+  @Option(name = "--project", aliases = {"-p"},
+      usage = "projects for which the groups should be listed")
+  private final List<ProjectControl> projects = new ArrayList<ProjectControl>();
+
+  @Option(name = "--visible-to-all", usage = "to list only groups that are visible to all registered users")
+  private boolean visibleToAll;
+
+  @Option(name = "--type", usage = "type of group")
+  private AccountGroup.Type groupType;
+
+  @Option(name = "--user", aliases = {"-u"},
+      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();
+      }
+    });
+  }
+
+  private void display() throws Failure {
+    final PrintWriter stdout = toPrintWriter(out);
+    try {
+      if (user != null && !projects.isEmpty()) {
+        throw new UnloggedFailure(1, "fatal: --user and --project options are not compatible.");
+      }
+
+      final VisibleGroups visibleGroups = visibleGroupsFactory.create();
+      visibleGroups.setOnlyVisibleToAll(visibleToAll);
+      visibleGroups.setGroupType(groupType);
+      final GroupList groupList;
+      if (!projects.isEmpty()) {
+        groupList = visibleGroups.get(projects);
+      } else if (user != null) {
+        groupList = visibleGroups.get(userFactory.create(user));
+      } else {
+        groupList = visibleGroups.get();
+      }
+      for (final GroupDetail groupDetail : groupList.getGroups()) {
+        stdout.print(groupDetail.group.getName() + "\n");
+      }
+    } catch (OrmException e) {
+      throw die(e);
+    } catch (NoSuchGroupException e) {
+      throw die(e);
+    } finally {
+      stdout.flush();
+    }
+  }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListProjects.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListProjects.java
index 1ee3cfb..9aafc0e 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListProjects.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListProjects.java
@@ -16,7 +16,6 @@
 
 import com.google.gerrit.reviewdb.Project;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectControl;
@@ -35,19 +34,15 @@
 
 import java.io.IOException;
 import java.io.PrintWriter;
-import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.List;
+import java.util.SortedSet;
 import java.util.TreeMap;
+import java.util.TreeSet;
 
 final class ListProjects extends BaseCommand {
   private static final Logger log = LoggerFactory.getLogger(ListProjects.class);
 
-  private static final String NODE_PREFIX = "|-- ";
-  private static final String LAST_NODE_PREFIX = "`-- ";
-  private static final String DEFAULT_TAB_SEPARATOR = "|";
-  private static final String NOT_VISIBLE_PROJECT = "(x)";
-
   static enum FilterType {
     CODE {
       @Override
@@ -84,7 +79,7 @@
   private GitRepositoryManager repoManager;
 
   @Inject
-  private AllProjectsName allProjectsName;
+  private ProjectNode.Factory projectNodeFactory;
 
   @Option(name = "--show-branch", aliases = {"-b"}, multiValued = true,
       usage = "displays the sha of each project in the specified branch")
@@ -97,7 +92,11 @@
   @Option(name = "--type", usage = "type of project")
   private FilterType type = FilterType.CODE;
 
-  private String currentTabSeparator = DEFAULT_TAB_SEPARATOR;
+  @Option(name = "--description", aliases = {"-d"}, usage = "include description of project in list")
+  private boolean showDescription;
+
+  @Option(name = "--all", usage = "display all projects that are accessible by the calling user")
+  private boolean all;
 
   @Override
   public void start(final Environment env) {
@@ -115,8 +114,13 @@
       throw new UnloggedFailure(1, "fatal: --tree and --show-branch options are not compatible.");
     }
 
+    if (showTree && showDescription) {
+      throw new UnloggedFailure(1, "fatal: --tree and --description options are not compatible.");
+    }
+
     final PrintWriter stdout = toPrintWriter(out);
-    final TreeMap<String, TreeNode> treeMap = new TreeMap<String, TreeNode>();
+    final TreeMap<Project.NameKey, ProjectNode> treeMap =
+        new TreeMap<Project.NameKey, ProjectNode>();
     try {
       for (final Project.NameKey projectName : projectCache.all()) {
         final ProjectState e = projectCache.get(projectName);
@@ -127,9 +131,10 @@
         }
 
         final ProjectControl pctl = e.controlFor(currentUser);
-        final boolean isVisible = pctl.isVisible();
+        final boolean isVisible = pctl.isVisible() || (all && pctl.isOwner());
         if (showTree) {
-          treeMap.put(projectName.get(), new TreeNode(pctl.getProject(), isVisible));
+          treeMap.put(projectName,
+              projectNodeFactory.create(pctl.getProject(), isVisible));
           continue;
         }
 
@@ -184,7 +189,15 @@
           continue;
         }
 
-        stdout.print(projectName.get() + "\n");
+        stdout.print(projectName.get());
+
+        String desc;
+        if (showDescription && !(desc = e.getProject().getDescription()).isEmpty()) {
+          // We still want to list every project as one-liners, hence escaping \n.
+          stdout.print(" - " + desc.replace("\n", "\\n"));
+        }
+
+        stdout.print("\n");
       }
 
       if (showTree && treeMap.size() > 0) {
@@ -196,23 +209,18 @@
   }
 
   private void printProjectTree(final PrintWriter stdout,
-      final TreeMap<String, TreeNode> treeMap) {
-    final List<TreeNode> sortedNodes = new ArrayList<TreeNode>();
+      final TreeMap<Project.NameKey, ProjectNode> treeMap) {
+    final SortedSet<ProjectNode> sortedNodes = new TreeSet<ProjectNode>();
 
     // Builds the inheritance tree using a list.
     //
-    for (TreeNode key : treeMap.values()) {
-      if (allProjectsName.equals(key.getProject().getNameKey())) {
+    for (final ProjectNode key : treeMap.values()) {
+      if (key.isAllProjects()) {
         sortedNodes.add(key);
         continue;
       }
 
-      String parentName = key.getParentName();
-      if (parentName == null) {
-        parentName = allProjectsName.get();
-      }
-
-      TreeNode node = treeMap.get(parentName);
+      ProjectNode node = treeMap.get(key.getParentName());
       if (node != null) {
         node.addChild(key);
       } else {
@@ -220,24 +228,23 @@
       }
     }
 
-    // Builds a fake root node, which contains the sorted projects.
-    //
-    final TreeNode fakeRoot = new TreeNode(null, sortedNodes, false);
-    printElement(stdout, fakeRoot, -1, false, sortedNodes.get(sortedNodes.size() - 1));
+    final TreeFormatter treeFormatter = new TreeFormatter(stdout);
+    treeFormatter.printTree(sortedNodes);
     stdout.flush();
   }
 
   private List<Ref> getBranchRefs(Project.NameKey projectName,
       ProjectControl projectControl) {
-  Ref[] result = new Ref[showBranch.size()];
-  try {
+    Ref[] result = new Ref[showBranch.size()];
+    try {
       Repository git = repoManager.openRepository(projectName);
       try {
         for (int i = 0; i < showBranch.size(); i++) {
           Ref ref = git.getRef(showBranch.get(i));
           if (ref != null
             && ref.getObjectId() != null
-            && projectControl.controlForRef(ref.getLeaf().getName()).isVisible()) {
+            && (projectControl.controlForRef(ref.getLeaf().getName()).isVisible())
+                || (all && projectControl.isOwner())) {
             result[i] = ref;
           }
         }
@@ -258,139 +265,4 @@
     }
     return false;
   }
-
-  /** Class created to manipulate the nodes of the project inheritance tree **/
-  private static class TreeNode {
-    private final List<TreeNode> children;
-    private final Project project;
-    private final boolean isVisible;
-
-    /**
-     * Constructor
-     * @param p Project
-     */
-    public TreeNode(Project p, boolean visible) {
-      this.children = new ArrayList<TreeNode>();
-      this.project = p;
-      this.isVisible = visible;
-    }
-
-    /**
-     * Constructor used for creating the fake node
-     * @param p Project
-     * @param c List of nodes
-     */
-    public TreeNode(Project p, List<TreeNode> c, boolean visible) {
-      this.children = c;
-      this.project = p;
-      this.isVisible = visible;
-    }
-
-    /**
-     * Returns if the the node is leaf
-     * @return True if is lead, false, otherwise
-     */
-    public boolean isLeaf() {
-      return children.size() == 0;
-    }
-
-    /**
-     * Returns the project parent name
-     * @return Project parent name
-     */
-    public String getParentName() {
-      if (project.getParent() != null) {
-        return project.getParent().get();
-      }
-
-      return null;
-    }
-
-    /**
-     * Adds a child to the list
-     * @param node TreeNode child
-     */
-    public void addChild(TreeNode node) {
-      children.add(node);
-    }
-
-    /**
-     * Returns the project instance
-     * @return Project instance
-     */
-    public Project getProject() {
-      return project;
-    }
-
-    /**
-     * Returns the list of children nodes
-     * @return List of children nodes
-     */
-    public List<TreeNode> getChildren() {
-      return children;
-    }
-
-    /**
-     * Returns if the project is visible to the user
-     * @return True if is visible, false, otherwise
-     */
-    public boolean isVisible() {
-      return isVisible;
-    }
-  }
-
-  /**
-   * Used to display the project inheritance tree recursively
-   * @param stdout PrintWriter used do print
-   * @param node Tree node
-   * @param level Current level of the tree
-   * @param isLast True, if is the last node of a level, false, otherwise
-   * @param lastParentNode Last "root" parent node
-   */
-  private void printElement(final PrintWriter stdout, TreeNode node, int level, boolean isLast,
-      final TreeNode lastParentNode) {
-    // Checks if is not the "fake" root project.
-    //
-    if (node.getProject() != null) {
-
-      // Check if is not the last "root" parent node,
-      // so the "|" separator will not longer be needed.
-      //
-      if (!currentTabSeparator.equals(" ")) {
-        final String nodeProject = node.getProject().getName();
-        final String lastParentProject = lastParentNode.getProject().getName();
-
-        if (nodeProject.equals(lastParentProject)) {
-          currentTabSeparator = " ";
-        }
-      }
-
-      if (level > 0) {
-        stdout.print(String.format("%-" + 4 * level + "s", currentTabSeparator));
-      }
-
-      final String prefix = isLast ? LAST_NODE_PREFIX : NODE_PREFIX ;
-
-      String printout;
-
-      if (node.isVisible()) {
-        printout = prefix + node.getProject().getName();
-      } else {
-        printout = prefix + NOT_VISIBLE_PROJECT;
-      }
-
-      stdout.print(printout + "\n");
-    }
-
-    if (node.isLeaf()) {
-      return;
-    } else {
-      final List<TreeNode> children = node.getChildren();
-      ++level;
-      for(TreeNode treeNode : children) {
-        final boolean isLastIndex = children.indexOf(treeNode) == children.size() - 1;
-        printElement(stdout, treeNode, level, isLastIndex, lastParentNode);
-      }
-    }
-  }
 }
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 6a69791..34f64da 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
@@ -28,7 +28,8 @@
     command(gerrit, "approve").to(ReviewCommand.class);
     command(gerrit, "create-account").to(CreateAccountCommand.class);
     command(gerrit, "create-group").to(CreateGroupCommand.class);
-    command(gerrit, "create-project").to(CreateProject.class);
+    command(gerrit, "rename-group").to(RenameGroupCommand.class);
+    command(gerrit, "create-project").to(CreateProjectCommand.class);
     command(gerrit, "gsql").to(AdminQueryShell.class);
     command(gerrit, "set-reviewers").to(SetReviewersCommand.class);
     command(gerrit, "receive-pack").to(Receive.class);
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ProjectNode.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ProjectNode.java
new file mode 100644
index 0000000..8e12571
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ProjectNode.java
@@ -0,0 +1,92 @@
+// 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.sshd.commands;
+
+import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.sshd.commands.TreeFormatter.TreeNode;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+public class ProjectNode implements TreeNode, Comparable<ProjectNode> {
+
+  public interface Factory {
+    ProjectNode create(final Project project, final boolean isVisible);
+  }
+
+  private final AllProjectsName allProjectsName;
+
+  private final Project project;
+  private final boolean isVisible;
+
+  private final SortedSet<ProjectNode> children = new TreeSet<ProjectNode>();
+
+  @Inject
+  public ProjectNode(final AllProjectsName allProjectsName,
+      @Assisted final Project project, @Assisted final boolean isVisible) {
+    this.allProjectsName = allProjectsName;
+    this.project = project;
+    this.isVisible = isVisible;
+  }
+
+  /**
+   * Returns the project parent name.
+   *
+   * @return Project parent name, <code>null</code> for the 'All-Projects' root
+   *         project
+   */
+  public Project.NameKey getParentName() {
+    if (project.getParent() != null) {
+      return project.getParent();
+    }
+
+    if (project.getNameKey().equals(allProjectsName)) {
+      return null;
+    }
+
+    return allProjectsName;
+  }
+
+  public boolean isAllProjects() {
+    return allProjectsName.equals(project.getNameKey());
+  }
+
+  @Override
+  public String getDisplayName() {
+    return project.getName();
+  }
+
+  @Override
+  public boolean isVisible() {
+    return isVisible;
+  }
+
+  @Override
+  public SortedSet<? extends TreeNode> getChildren() {
+    return children;
+  }
+
+  public void addChild(final ProjectNode child) {
+    children.add(child);
+  }
+
+  @Override
+  public int compareTo(final ProjectNode o) {
+    return project.getNameKey().compareTo(o.project.getNameKey());
+  }
+}
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 b6f5d26..9bca0e5 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
@@ -56,6 +56,16 @@
     processor.setIncludeComments(on);
   }
 
+  @Option(name = "--files", usage = "Include file list on patch sets")
+  void setFiles(boolean on) {
+    processor.setIncludeFiles(on);
+  }
+
+  @Option(name = "--commit-message", usage = "Include the full commit message for a change")
+  void setCommitMessage(boolean on) {
+    processor.setIncludeCommitMessage(on);
+  }
+
   @Argument(index = 0, required = true, multiValued = true, metaVar = "QUERY", usage = "Query to execute")
   private List<String> query;
 
@@ -66,11 +76,19 @@
       public void run() throws Exception {
         processor.setOutput(out, QueryProcessor.OutputFormat.TEXT);
         parseCommandLine();
+        verifyCommandLine();
         processor.query(join(query, " "));
       }
     });
   }
 
+  private void verifyCommandLine() throws UnloggedFailure {
+    if (processor.getIncludeFiles() &&
+        !(processor.getIncludePatchSets() || processor.getIncludeCurrentPatchSet())) {
+      throw new UnloggedFailure(1, "--files option needs --patch-sets or --current-patch-set");
+    }
+  }
+
   private static String join(List<String> list, String sep) {
     StringBuilder r = new StringBuilder();
     for (int i = 0; i < list.size(); i++) {
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 425d4f8..c2b5e9c 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
@@ -85,6 +85,7 @@
     final ReceivePack rp = receive.getReceivePack();
     rp.setRefLogIdent(currentUser.newRefLogIdent());
     rp.setTimeout(config.getTimeout());
+    rp.setMaxObjectSizeLimit(config.getMaxObjectSizeLimit());
     try {
       receive.advertiseHistory();
       rp.receive(in, out, err);
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
new file mode 100644
index 0000000..b6d5bf5
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/RenameGroupCommand.java
@@ -0,0 +1,58 @@
+// 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.sshd.commands;
+
+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.gwtorm.client.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 {
+
+  @Argument(index = 0, required = true, metaVar = "GROUP", usage = "name of the group to be renamed")
+  private String groupName;
+
+  @Argument(index = 1, required = true, metaVar = "NEWNAME", usage = "new name of the group")
+  private String newGroupName;
+
+  @Inject
+  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);
+        }
+      }
+    });
+  }
+}
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 f6ba0a2..8eb6058 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
@@ -14,25 +14,23 @@
 
 package com.google.gerrit.sshd.commands;
 
-import com.google.gerrit.common.ChangeHookRunner;
 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.common.data.ReviewResult;
 import com.google.gerrit.reviewdb.ApprovalCategory;
 import com.google.gerrit.reviewdb.ApprovalCategoryValue;
-import com.google.gerrit.reviewdb.Branch;
 import com.google.gerrit.reviewdb.Change;
 import com.google.gerrit.reviewdb.PatchSet;
 import com.google.gerrit.reviewdb.PatchSetApproval;
 import com.google.gerrit.reviewdb.RevId;
 import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.git.MergeOp;
-import com.google.gerrit.server.git.MergeQueue;
-import com.google.gerrit.server.mail.AbandonedSender;
+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.mail.EmailException;
-import com.google.gerrit.server.mail.RestoredSender;
 import com.google.gerrit.server.patch.PublishComments;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
@@ -57,7 +55,6 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
-import java.util.concurrent.TimeUnit;
 
 public class ReviewCommand extends BaseCommand {
   private static final Logger log =
@@ -100,6 +97,16 @@
   @Option(name = "--submit", aliases = "-s", usage = "submit the patch set")
   private boolean submitChange;
 
+  @Option(name = "--force-message", usage = "publish the message, "
+      + "even if the label score cannot be applied due to change being closed")
+  private boolean forceMessage = false;
+
+  @Option(name = "--publish", usage = "publish a draft patch set")
+  private boolean publishPatchSet;
+
+  @Option(name = "--delete", usage = "delete a draft patch set")
+  private boolean deleteDraftPatchSet;
+
   @Inject
   private ReviewDb db;
 
@@ -107,19 +114,16 @@
   private IdentifiedUser currentUser;
 
   @Inject
-  private MergeQueue merger;
-
-  @Inject
-  private MergeOp.Factory opFactory;
-
-  @Inject
   private ApprovalTypes approvalTypes;
 
   @Inject
   private ChangeControl.Factory changeControlFactory;
 
   @Inject
-  private AbandonedSender.Factory abandonedSenderFactory;
+  private DeleteDraftPatchSet.Factory deleteDraftPatchSetFactory;
+
+  @Inject
+  private AbandonChange.Factory abandonChangeFactory;
 
   @Inject
   private FunctionState.Factory functionStateFactory;
@@ -128,15 +132,16 @@
   private PublishComments.Factory publishCommentsFactory;
 
   @Inject
-  private RestoredSender.Factory restoredSenderFactory;
+  private PublishDraft.Factory publishDraftFactory;
 
   @Inject
-  private ChangeHookRunner hooks;
+  private RestoreChange.Factory restoreChangeFactory;
+
+  @Inject
+  private Submit.Factory submitFactory;
 
   private List<ApproveOption> optionList;
 
-  private Set<PatchSet.Id> toSubmit = new HashSet<PatchSet.Id>();
-
   @Override
   public final void start(final Environment env) {
     startThread(new CommandRunnable() {
@@ -151,6 +156,23 @@
           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;
@@ -160,6 +182,9 @@
           } 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 "
@@ -173,36 +198,6 @@
               + " review output above");
         }
 
-        if (!toSubmit.isEmpty()) {
-          final Set<Branch.NameKey> toMerge = new HashSet<Branch.NameKey>();
-          try {
-            for (PatchSet.Id patchSetId : toSubmit) {
-              ChangeUtil.submit(patchSetId, currentUser, db, opFactory,
-                  new MergeQueue() {
-                    @Override
-                    public void merge(MergeOp.Factory mof, Branch.NameKey branch) {
-                      toMerge.add(branch);
-                    }
-
-                    @Override
-                    public void schedule(Branch.NameKey branch) {
-                      toMerge.add(branch);
-                    }
-
-                    @Override
-                    public void recheckAfter(Branch.NameKey branch, long delay,
-                        TimeUnit delayUnit) {
-                      toMerge.add(branch);
-                    }
-                  });
-            }
-            for (Branch.NameKey branch : toMerge) {
-              merger.merge(opFactory, branch);
-            }
-          } catch (OrmException updateError) {
-            throw new Failure(1, "one or more submits failed", updateError);
-          }
-        }
       }
     });
   }
@@ -211,6 +206,7 @@
       NoSuchChangeException, OrmException, EmailException, Failure {
 
     final Change.Id changeId = patchSetId.getParentKey();
+
     ChangeControl changeControl = changeControlFactory.validateFor(changeId);
 
     if (changeComment == null) {
@@ -227,89 +223,78 @@
     }
 
     try {
-      publishCommentsFactory.create(patchSetId, changeComment, aps).call();
+      publishCommentsFactory.create(patchSetId, changeComment, aps, forceMessage).call();
 
       if (abandonChange) {
-        if (changeControl.canAbandon()) {
-          ChangeUtil.abandon(patchSetId, currentUser, changeComment, db,
-              abandonedSenderFactory, hooks);
-        } else {
-          throw error("Not permitted to abandon change");
-        }
+        final ReviewResult result = abandonChangeFactory.create(
+            patchSetId, changeComment).call();
+        handleReviewResultErrors(result);
+      } else if (restoreChange) {
+        final ReviewResult result = restoreChangeFactory.create(
+            patchSetId, changeComment).call();
+        handleReviewResultErrors(result);
       }
-
-      if (restoreChange) {
-        if (changeControl.canRestore()) {
-          ChangeUtil.restore(patchSetId, currentUser, changeComment, db,
-              restoredSenderFactory, hooks);
-        } else {
-          throw error("Not permitted to restore change");
-        }
-        if (submitChange) {
-          changeControl = changeControlFactory.validateFor(changeId);
-        }
+      if (submitChange) {
+        final ReviewResult result = submitFactory.create(patchSetId).call();
+        handleReviewResultErrors(result);
       }
     } catch (InvalidChangeOperationException e) {
       throw error(e.getMessage());
+    } catch (IllegalStateException e) {
+      throw error(e.getMessage());
     }
 
-    if (submitChange) {
-      List<SubmitRecord> result = changeControl.canSubmit(db, patchSetId);
-      if (result.isEmpty()) {
-        throw new Failure(1, "ChangeControl.canSubmit returned empty list");
-      }
-      switch (result.get(0).status) {
-        case OK:
-          if (changeControl.getRefControl().canSubmit()) {
-            toSubmit.add(patchSetId);
-          } else {
-            throw error("change " + changeId + ": you do not have submit permission");
-          }
+    if (publishPatchSet) {
+      final ReviewResult result = publishDraftFactory.create(patchSetId).call();
+      handleReviewResultErrors(result);
+    } else if (deleteDraftPatchSet) {
+      final ReviewResult result =
+          deleteDraftPatchSetFactory.create(patchSetId).call();
+      handleReviewResultErrors(result);
+    }
+  }
+
+  private void handleReviewResultErrors(final ReviewResult result) {
+    for (ReviewResult.Error resultError : result.getErrors()) {
+      String errMsg = "error: (change " + result.getChangeId() + ") ";
+      switch (resultError.getType()) {
+        case ABANDON_NOT_PERMITTED:
+          errMsg += "not permitted to abandon change";
           break;
-
-        case NOT_READY: {
-          StringBuilder msg = new StringBuilder();
-          for (SubmitRecord.Label lbl : result.get(0).labels) {
-            switch (lbl.status) {
-              case OK:
-                break;
-
-              case REJECT:
-                if (msg.length() > 0) msg.append("\n");
-                msg.append("change " + changeId + ": blocked by " + lbl.label);
-                break;
-
-              case NEED:
-                if (msg.length() > 0) msg.append("\n");
-                msg.append("change " + changeId + ": needs " + lbl.label);
-                break;
-
-              case IMPOSSIBLE:
-                if (msg.length() > 0) msg.append("\n");
-                msg.append("change " + changeId + ": needs " + lbl.label
-                    + " (check project access)");
-                break;
-
-              default:
-                throw new Failure(1, "Unsupported label status " + lbl.status);
-            }
-          }
-          throw error(msg.toString());
-        }
-
-        case CLOSED:
-          throw error("change " + changeId + " is closed");
-
+        case RESTORE_NOT_PERMITTED:
+          errMsg += "not permitted to restore change";
+          break;
+        case SUBMIT_NOT_PERMITTED:
+          errMsg += "not permitted to submit change";
+          break;
+        case SUBMIT_NOT_READY:
+          errMsg += "approvals or dependencies lacking";
+          break;
+        case CHANGE_IS_CLOSED:
+          errMsg += "change is closed";
+          break;
+        case PUBLISH_NOT_PERMITTED:
+          errMsg += "not permitted to publish change";
+          break;
+        case DELETE_NOT_PERMITTED:
+          errMsg += "not permitted to delete change/patch set";
+          break;
         case RULE_ERROR:
-          if (result.get(0).errorMessage != null) {
-            throw error("change " + changeId + ": " + result.get(0).errorMessage);
-          } else {
-            throw error("change " + changeId + ": internal rule error");
-          }
-
+          errMsg += "rule error";
+          break;
+        case NOT_A_DRAFT:
+          errMsg += "change is not a draft";
+          break;
+        case GIT_ERROR:
+          errMsg += "error writing change to git repository";
+          break;
         default:
-          throw new Failure(1, "Unsupported status " + result.get(0).status);
+          errMsg += "failure in review";
       }
+      if (resultError.getMessage() != null) {
+        errMsg += ": " + resultError.getMessage();
+      }
+      writeError(errMsg);
     }
   }
 
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 845ae95..985c2d5 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
@@ -260,11 +260,13 @@
     try {
       if (change != null
           && inProject(change)
-          && changeControlFactory.controlFor(change).isVisible()) {
+          && changeControlFactory.controlFor(change).isVisible(db)) {
         matched.add(change.getId());
       }
     } catch (NoSuchChangeException e) {
       // Ignore this change.
+    } catch (OrmException e) {
+      log.warn("Error reading change " + change.getId(), e);
     }
   }
 
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
index e9e6015..ff6dea9 100755
--- 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
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.sshd.commands;
 
-import com.google.gerrit.common.ChangeHookRunner;
+import com.google.gerrit.common.ChangeHooks;
 import com.google.gerrit.common.ChangeListener;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.events.ChangeEvent;
@@ -43,7 +43,7 @@
   private IdentifiedUser currentUser;
 
   @Inject
-  private ChangeHookRunner hooks;
+  private ChangeHooks hooks;
 
   @Inject
   @StreamCommandExecutor
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/TreeFormatter.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/TreeFormatter.java
new file mode 100644
index 0000000..3aa83c3
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/TreeFormatter.java
@@ -0,0 +1,94 @@
+// 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.sshd.commands;
+
+import java.io.PrintWriter;
+import java.util.SortedSet;
+
+public class TreeFormatter {
+
+  public static interface TreeNode {
+    public String getDisplayName();
+    public boolean isVisible();
+    public SortedSet<? extends TreeNode> getChildren();
+  }
+
+  public static final String NOT_VISIBLE_NODE = "(x)";
+
+  private static final String NODE_PREFIX = "|-- ";
+  private static final String LAST_NODE_PREFIX = "`-- ";
+  private static final String DEFAULT_TAB_SEPARATOR = "|";
+
+  private final PrintWriter stdout;
+  private String currentTabSeparator = " ";
+
+  public TreeFormatter(final PrintWriter stdout) {
+    this.stdout = stdout;
+  }
+
+  public void printTree(final SortedSet<? extends TreeNode> rootNodes) {
+    if (rootNodes.isEmpty()) {
+      return;
+    }
+    if (rootNodes.size() == 1) {
+      printTree(rootNodes.first());
+    } else {
+      currentTabSeparator = DEFAULT_TAB_SEPARATOR;
+      int i = 0;
+      final int size = rootNodes.size();
+      for (final TreeNode rootNode : rootNodes) {
+        final boolean isLastRoot = ++i == size;
+        if (isLastRoot) {
+          currentTabSeparator = " ";
+        }
+        printTree(rootNode);
+      }
+    }
+  }
+
+  public void printTree(final TreeNode rootNode) {
+    printTree(rootNode, 0, true);
+  }
+
+  private void printTree(final TreeNode node, final int level,
+      final boolean isLast) {
+    printNode(node, level, isLast);
+    final SortedSet<? extends TreeNode> childNodes = node.getChildren();
+    int i = 0;
+    final int size = childNodes.size();
+    for (final TreeNode childNode : childNodes) {
+      final boolean isLastChild = ++i == size;
+      printTree(childNode, level + 1, isLastChild);
+    }
+  }
+
+  private void printIndention(final int level) {
+    if (level > 0) {
+      stdout.print(String.format("%-" + 4 * level + "s", currentTabSeparator));
+    }
+  }
+
+  private void printNode(final TreeNode node, final int level,
+      final boolean isLast) {
+    printIndention(level);
+    stdout.print(isLast ? LAST_NODE_PREFIX : NODE_PREFIX);
+    if (node.isVisible()) {
+      stdout.print(node.getDisplayName());
+    } else {
+      stdout.print(NOT_VISIBLE_NODE);
+    }
+    stdout.print("\n");
+  }
+}
diff --git a/gerrit-util-cli/.gitignore b/gerrit-util-cli/.gitignore
index 903c6c8..194bedc 100644
--- a/gerrit-util-cli/.gitignore
+++ b/gerrit-util-cli/.gitignore
@@ -2,3 +2,4 @@
 /.classpath
 /.project
 /.settings/org.maven.ide.eclipse.prefs
+/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
diff --git a/gerrit-util-cli/pom.xml b/gerrit-util-cli/pom.xml
index cfa302e..401a2ba 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.2-SNAPSHOT</version>
+    <version>2.3-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-util-cli</artifactId>
diff --git a/gerrit-util-ssl/.gitignore b/gerrit-util-ssl/.gitignore
index 903c6c8..194bedc 100644
--- a/gerrit-util-ssl/.gitignore
+++ b/gerrit-util-ssl/.gitignore
@@ -2,3 +2,4 @@
 /.classpath
 /.project
 /.settings/org.maven.ide.eclipse.prefs
+/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
diff --git a/gerrit-util-ssl/pom.xml b/gerrit-util-ssl/pom.xml
index 312a215..eefca62 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.2-SNAPSHOT</version>
+    <version>2.3-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-util-ssl</artifactId>
diff --git a/gerrit-war/.gitignore b/gerrit-war/.gitignore
index 903c6c8..194bedc 100644
--- a/gerrit-war/.gitignore
+++ b/gerrit-war/.gitignore
@@ -2,3 +2,4 @@
 /.classpath
 /.project
 /.settings/org.maven.ide.eclipse.prefs
+/.settings/org.eclipse.m2e.core.prefs
\ No newline at end of file
diff --git a/gerrit-war/pom.xml b/gerrit-war/pom.xml
index 110f966..ad58440 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.2-SNAPSHOT</version>
+    <version>2.3-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-war</artifactId>
@@ -53,7 +53,7 @@
       <groupId>com.google.gerrit</groupId>
       <artifactId>gerrit-main</artifactId>
       <version>${project.version}</version>
-      <scope>provided</scope>
+      <scope>runtime</scope>
     </dependency>
 
     <dependency>
@@ -82,6 +82,12 @@
 
     <dependency>
       <groupId>com.google.gerrit</groupId>
+      <artifactId>gerrit-openid</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>com.google.gerrit</groupId>
       <artifactId>gerrit-sshd</artifactId>
       <version>${project.version}</version>
     </dependency>
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 ac05f0f..ecb0977 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
@@ -17,14 +17,25 @@
 import static com.google.inject.Scopes.SINGLETON;
 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.lifecycle.LifecycleManager;
 import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.reviewdb.AuthType;
+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.GerritGlobalModule;
 import com.google.gerrit.server.config.GerritServerConfigModule;
 import com.google.gerrit.server.config.MasterNodeStartup;
 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.WorkQueue;
+import com.google.gerrit.server.mail.SignedTokenEmailTokenVerifier;
+import com.google.gerrit.server.mail.SmtpEmailSender;
 import com.google.gerrit.server.schema.DataSourceProvider;
 import com.google.gerrit.server.schema.DatabaseModule;
 import com.google.gerrit.server.schema.SchemaModule;
@@ -169,6 +180,7 @@
       modules.add(new GerritServerConfigModule());
     }
     modules.add(new SchemaModule());
+    modules.add(new LocalDiskRepositoryManager.Module());
     modules.add(SchemaVersionCheck.module());
     modules.add(new AuthConfigModule());
     return dbInjector.createChildInjector(modules);
@@ -176,7 +188,13 @@
 
   private Injector createSysInjector() {
     final List<Module> modules = new ArrayList<Module>();
+    modules.add(new WorkQueue.Module());
+    modules.add(new ChangeHookRunner.Module());
     modules.add(cfgInjector.getInstance(GerritGlobalModule.class));
+    modules.add(new EhcachePoolImpl.Module());
+    modules.add(new SmtpEmailSender.Module());
+    modules.add(new SignedTokenEmailTokenVerifier.Module());
+    modules.add(new PushReplication.Module());
     modules.add(new CanonicalWebUrlModule() {
       @Override
       protected Class<? extends Provider<String>> provider() {
@@ -196,7 +214,17 @@
 
   private Injector createWebInjector() {
     final List<Module> modules = new ArrayList<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());
+
+    AuthConfig authConfig = cfgInjector.getInstance(AuthConfig.class);
+    if (authConfig.getAuthType() == AuthType.OPENID) {
+      modules.add(new OpenIdModule());
+    }
+
     return sysInjector.createChildInjector(modules);
   }
 
diff --git a/pom.xml b/pom.xml
index e7add1a..f3f7c64 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.2-SNAPSHOT</version>
+  <version>2.3-SNAPSHOT</version>
 
   <name>Gerrit Code Review - Parent</name>
   <url>http://code.google.com/p/gerrit/</url>
@@ -46,8 +46,8 @@
   </issueManagement>
 
   <properties>
-    <jgitVersion>1.0.0.201106090707-r.19-g8d88a84</jgitVersion>
-    <gwtormVersion>1.1.5</gwtormVersion>
+    <jgitVersion>1.1.0.201109151100-r.141-gcd958ba</jgitVersion>
+    <gwtormVersion>1.2</gwtormVersion>
     <gwtjsonrpcVersion>1.2.5</gwtjsonrpcVersion>
     <gwtexpuiVersion>1.2.5</gwtexpuiVersion>
     <gwtVersion>2.3.0</gwtVersion>
@@ -74,9 +74,11 @@
 
     <module>gerrit-antlr</module>
     <module>gerrit-common</module>
+    <module>gerrit-ehcache</module>
     <module>gerrit-httpd</module>
     <module>gerrit-launcher</module>
     <module>gerrit-main</module>
+    <module>gerrit-openid</module>
     <module>gerrit-pgm</module>
     <module>gerrit-prettify</module>
     <module>gerrit-reviewdb</module>
diff --git a/tools/gwtui_dbg.launch b/tools/gwtui_dbg.launch
index 8a01fca..ea76ee1 100644
--- a/tools/gwtui_dbg.launch
+++ b/tools/gwtui_dbg.launch
@@ -20,7 +20,7 @@
 <listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;runtimeClasspathEntry internalArchive=&quot;/gerrit-reviewdb/src/main/java&quot; path=&quot;3&quot; type=&quot;2&quot;/&gt;&#10;"/>
 <listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;runtimeClasspathEntry internalArchive=&quot;/gerrit-common/src/main/java&quot; path=&quot;3&quot; type=&quot;2&quot;/&gt;&#10;"/>
 <listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;runtimeClasspathEntry internalArchive=&quot;/gerrit-gwtui/src/main/java&quot; path=&quot;3&quot; type=&quot;2&quot;/&gt;&#10;"/>
-<listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;runtimeClasspathEntry containerPath=&quot;org.maven.ide.eclipse.MAVEN2_CLASSPATH_CONTAINER&quot; path=&quot;3&quot; type=&quot;4&quot;/&gt;&#10;"/>
+<listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;runtimeClasspathEntry containerPath=&quot;org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER&quot; path=&quot;3&quot; type=&quot;4&quot;/&gt;&#10;"/>
 <listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;runtimeClasspathEntry internalArchive=&quot;/gwtexpui/src/main/java&quot; path=&quot;3&quot; type=&quot;2&quot;/&gt;&#10;"/>
 <listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;runtimeClasspathEntry internalArchive=&quot;/gwtjsonrpc/src/main/java&quot; path=&quot;3&quot; type=&quot;2&quot;/&gt;&#10;"/>
 <listEntry value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;runtimeClasspathEntry internalArchive=&quot;/gwtorm/src/main/java&quot; path=&quot;3&quot; type=&quot;2&quot;/&gt;&#10;"/>