Merge 'v2.1.9^0'

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

Change-Id: I7ee366b2f7f930da5f63dd5f173e47a33f280436
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/Makefile b/Documentation/Makefile
index f167cc5..5522239 100644
--- a/Documentation/Makefile
+++ b/Documentation/Makefile
@@ -15,7 +15,7 @@
 ASCIIDOC       ?= asciidoc
 ASCIIDOC_EXTRA ?=
 SVN            ?= svn
-PUB_ROOT       ?= https://gerrit.googlecode.com/svn/documentation
+PUB_ROOT       ?= https://gerrit-documentation.googlecode.com/svn/Documentation
 
 all: html
 
@@ -74,6 +74,7 @@
 	@echo "FORMAT $@"
 	@rm -f $@+ $@
 	@$(ASCIIDOC) -a toc \
+		-a data-uri \
 		-a 'revision=$(REVISION)' \
 		-b xhtml11 \
 		-f asciidoc.conf \
diff --git a/Documentation/access-control.txt b/Documentation/access-control.txt
index 0984935..8d89fa2 100644
--- a/Documentation/access-control.txt
+++ b/Documentation/access-control.txt
@@ -33,6 +33,7 @@
 to permit administrative users to otherwise access Gerrit as any
 other normal user would, without needing two different accounts.
 
+[[anonymous_users]]
 Anonymous Users
 ~~~~~~~~~~~~~~~
 
@@ -44,14 +45,47 @@
 Administrators and project owners can grant access rights to this
 group in order to permit anonymous users to view project changes,
 without requiring sign in first.  Currently it is only worthwhile
-to grant `Read Access` to this group as Gerrit requires an account
+to grant `Read` access to this group as Gerrit requires an account
 identity for all other operations.
 
+[[non-interactive_users]]
+Non-Interactive Users
+~~~~~~~~~~~~~~~~~~~~~
+
+This is an internal user group, members of this group are not expected
+to perform interactive operations on the Gerrit web frontend.
+
+However, sometimes such a user may need a separate thread pool in
+order to prevent it from grabbing threads from the interactive users.
+
+These users live in a second thread pool, which separates operations
+made by the non-interactive users from the ones made by the interactive
+users. This ensures that the interactive users can keep working when
+resources are tight.
+
+[[project_owners]]
+Project Owners
+~~~~~~~~~~~~~~
+
+Access rights assigned to this group are always evaluated within the
+context of a project to which the access rights apply. These rights
+therefore apply to all the users who are owners of this project.
+
+By assigning access rights to this group on a parent project Gerrit
+administrators can define a set of default access rights for
+<<category_owner,project owners>>. Child projects inherit these
+access rights where they are resolved to the users that own the child
+project.  Having default access rights for
+<<category_owner,project owners>> assigned on a parent project may
+avoid the need to initially configure access rights for
+newly created child projects.
+
+[[registered_users]]
 Registered Users
 ~~~~~~~~~~~~~~~~
 
 All signed-in users are automatically a member of this group (and
-also 'Anonymous Users', see above).
+also <<anonymous_users,'Anonymous Users'>>, see above).
 
 Any access rights assigned to this group are inherited by all
 users as soon as they sign-in to Gerrit.  If OpenID authentication
@@ -64,22 +98,7 @@
 cause it to become approved or rejected.
 
 Registered users are always permitted to make and publish comments
-on any change in any project they have `Read Access` to.
-
-Project Owners
-~~~~~~~~~~~~~~
-
-Access rights assigned to this group are always evaluated within the
-context of a project and are resolved to access rights for all users
-which own the project.
-
-By assigning access rights to this group on a parent project Gerrit
-administrators can define a set of default access rights for project
-owners. Child projects inherit these access rights where they are
-resolved to the users that own the child project.
-Having default access rights for projects owners assigned on a parent
-project may avoid the need to initially configure access rights for
-newly created child projects.
+on any change in any project they have `Read` access to.
 
 
 Account Groups
@@ -92,8 +111,8 @@
 Every group has one other group designated as its owner.  Users who
 are members of the owner group can:
 
-* Add users to this group
-* Remove users from this group
+* Add users and other groups to this group
+* Remove users and other groups from this group
 * Change the name of this group
 * Change the description of this group
 * Change the owner of this group, to another group
@@ -120,7 +139,7 @@
 ----------------------------
 
 A system wide access control list affecting all projects is stored in
-project "`\-- All Projects \--`".  This inheritance can be configured
+project "`All-Projects`".  This inheritance can be configured
 through link:cmd-set-project-parent.html[gerrit set-project-parent].
 
 Per-project access control lists are also supported.
@@ -147,13 +166,13 @@
 
 Permissions can be set on a single reference name to match one
 branch (e.g. `refs/heads/master`), or on a reference namespace
-(e.g. `refs/heads/\*`) to match any branch starting with that
-prefix. So a permission with `refs/heads/\*` will match
+(e.g. `refs/heads/*`) to match any branch starting with that
+prefix. So a permission with `refs/heads/*` will match
 `refs/heads/master` and `refs/heads/experimental`, etc.
 
 Reference names can also be described with a regular expression
-by prefixing the reference name with `\^`.  For example
-`\^refs/heads/[a-z]\{1,8\}` matches all lower case branch names
+by prefixing the reference name with `^`.  For example
+`^refs/heads/[a-z]{1,8}` matches all lower case branch names
 between 1 and 8 characters long.  Within a regular expression `.`
 is a wildcard matching any character, but may be escaped as `\.`.
 The link:http://www.brics.dk/automaton/[dk.brics.automaton library]
@@ -164,7 +183,7 @@
 References can have the current user name automatically included,
 creating dynamic access controls that change to match the currently
 logged in user.  For example to provide a personal sandbox space
-to all developers, `refs/heads/sandbox/$\{username\}/*` allowing
+to all developers, `refs/heads/sandbox/${username}/*` allowing
 the user 'joe' to use 'refs/heads/sandbox/joe/foo'.
 
 When evaluating a reference-level access right, Gerrit will use
@@ -175,12 +194,12 @@
 on the project:
 
 [options="header"]
-|=====================================================
-|Group            |Reference Name|Category   |Range
-|Registered Users |refs/heads/*  |Code Review| -1..+1
-|Foo Leads        |refs/heads/*  |Code Review| -2..+2
-|QA Leads         |refs/heads/qa |Code Review| -2..+2
-|=====================================================
+|===============================================================
+|Group            |Reference Name|Category   |Range   |Exclusive
+|Registered Users |refs/heads/*  |Code Review| -1..+1 |
+|Foo Leads        |refs/heads/*  |Code Review| -2..+2 |
+|QA Leads         |refs/heads/qa |Code Review| -2..+2 |
+|===============================================================
 
 Then the effective range permitted to be used by the user is
 `-2..+2`, as the user's membership of `Foo Leads` effectively grant
@@ -190,20 +209,21 @@
 
 It is possible to configure Gerrit to grant an exclusive ref level
 access control so that only users of a specific group can perform
-an operation on a project/reference pair. This is done by prefixing
-the reference specified with a `'-'`.
+an operation on a project/reference pair. This is done by ticking
+the exclusive flag when setting the permission for the
+`refs/heads/qa` branch.
 
 For example, if a user who is a member of `Foo Leads` tries to
 review a change destined for branch `refs/heads/qa` in a project,
 and the following ACLs are granted:
 
 [options="header"]
-|=====================================================
-|Group           |Reference Name|Category   |Range
-|Registered Users|refs/heads/*  |Code Review| -1..+1
-|Foo Leads       |refs/heads/*  |Code Review| -2..+2
-|QA Leads        |-refs/heads/qa|Code Review| -2..+2
-|=====================================================
+|==============================================================
+|Group           |Reference Name|Category   |Range   |Exclusive
+|Registered Users|refs/heads/*  |Code Review| -1..+1 |
+|Foo Leads       |refs/heads/*  |Code Review| -2..+2 |
+|QA Leads        |refs/heads/qa |Code Review| -2..+2 |X
+|==============================================================
 
 Then this user will not have `Code Review` rights on that change,
 since there is an exclusive access right in place for the
@@ -216,14 +236,13 @@
 would be needed:
 
 [options="header"]
-|=====================================================
-|Group           |Reference Name|Category   |Range
-|Registered Users|refs/heads/*  |Code Review| -1..+1
-|Foo Leads       |refs/heads/*  |Code Review| -2..+2
-|QA Leads        |-refs/heads/qa|Code Review| -2..+2
-|Foo Leads       |refs/heads/qa |Code Review| -2..+2
-|=====================================================
-
+|==============================================================
+|Group           |Reference Name|Category   |Range   |Exclusive
+|Registered Users|refs/heads/*  |Code Review| -1..+1 |
+|Foo Leads       |refs/heads/*  |Code Review| -2..+2 |
+|QA Leads        |refs/heads/qa |Code Review| -2..+2 |X
+|Foo Leads       |refs/heads/qa |Code Review| -2..+2 |
+|==============================================================
 
 OpenID Authentication
 ~~~~~~~~~~~~~~~~~~~~~
@@ -237,249 +256,64 @@
 All Projects
 ~~~~~~~~~~~~
 
-Any access right granted to a group within `\-- All Projects \--`
+Any access right granted to a group within `All-Projects`
 is automatically inherited by every other project in the same
 Gerrit instance.  These rights can be seen, but not modified,
 in any other project's `Access` administration tab.
 
-Only members of the group `Administrators` may edit the access
-control list for `\-- All Projects \--`.
+Only members of the groups with the `Administrate Server` capability
+may edit the access control list for `All-Projects`. By default this
+capability is given to the group `Administrators`, but can be given
+to more groups.
 
 Ownership of this project cannot be delegated to another group.
 This restriction is by design.  Granting ownership to another
 group gives nearly the same level of access as membership in
 `Administrators` does, as group members would be able to alter
-permissions for every managed project.
+permissions for every managed project including global capabilities.
 
 Per-Project
 ~~~~~~~~~~~
 
-The per-project ACL is evaluated before the global
-`\-- All Projects \--` ACL, permitting some limited override
-capability to project owners.  This behavior is generally only
-useful on the `Read Access` category when granting `-1 No Access`
-within a specific project to deny access to a group.
+The per-project ACL is evaluated before the global `All-Projects` ACL,
+permitting some limited override capability to project owners. This
+behavior is generally only useful on the `Read` category when
+granting 'DENY' within a specific project to deny a group access.
 
 
-Categories
-----------
+[[access_category]]
+Access Categories
+-----------------
 
 Gerrit comes pre-configured with several default categories that
 can be granted to groups within projects, enabling functionality
 for that group's members.
 
-[[category_OWN]]
-Owner
-~~~~~
+With the release of the Gerrit 2.2.x series, the web GUI for ACL
+configuration was rewritten from scratch.  Use this
+<<conversion_table,table>> to better understand the access rights
+conversions from the Gerrit 2.1.x to the Gerrit 2.2.x series.
 
-The `Owner` category controls which groups can modify the project's
-configuration.  Users who are members of an owner group can:
 
-* Change the project description
-* Create/delete a branch through the web UI (not SSH)
-* Grant/revoke any access rights, including `Owner`
+[[category_label-Verified]]
+Label: Verified
+~~~~~~~~~~~~~~~
 
-Note that project owners implicitly have branch creation or deletion
-through the web UI, but not through SSH.  To get SSH branch access
-project owners must grant an access right to a group they are a
-member of, just like for any other user.
-
-Ownership over a particular branch subspace may be delegated by
-entering a branch pattern.  To delegate control over all branches
-that begin with `qa/` to the QA group, add `Owner` category
-for reference `refs/heads/qa/\*`.  Members of the QA group can
-further refine access, but only for references that begin with
-`refs/heads/qa/`.
-
-[[category_READ]]
-Read Access
-~~~~~~~~~~~
-
-The `Read Access` category controls visibility to the project's
-changes, comments, code diffs, and Git access over SSH or HTTP.
-A user must have `Read Access +1` in order to see a project, its
-changes, or any of its data.
-
-This category has a special behavior, where the per-project ACL is
-evaluated before the global all projects ACL.  If the per-project
-ACL has granted `Read Access -1`, and does not otherwise grant
-`Read Access \+1`, then a `Read Access +1` in the all projects ACL
-is ignored.  This behavior is useful to hide a handful of projects
-on an otherwise public server.
-
-For an open source, public Gerrit installation it is common to grant
-`Read Access +1` to `Anonymous Users` in the `\-- All Projects
-\--` ACL, enabling casual browsing of any project's changes,
-as well as fetching any project's repository over SSH or HTTP.
-New projects can be temporarily hidden from public view by granting
-`Read Access -1` to `Anonymous Users` and granting `Read Access +1`
-to the project owner's group within the per-project ACL.
-
-For a private Gerrit installation using a trusted HTTP authentication
-source, granting `Read Access +1` to `Registered Users` may be more
-typical, enabling read access only to those users who have been
-able to authenticate through the HTTP access controls.  This may
-be suitable in a corporate deployment if the HTTP access control
-is already restricted to the correct set of users.
-
-[[category_READ_2]]
-Upload Access
-~~~~~~~~~~~~~
-
-The `Read Access +2` permits the user to upload a non-merge commit
-to the project's `refs/for/BRANCH` namespace, creating a new change
-for code review.
-
-Rather than place this permission in its own category, its chained
-into the Read Access category as a higher level of access.  A user
-must be able to clone or fetch the project in order to create a new
-commit on their local system, so in practice they must also have
-Read Access +1 to even develop a change.  Therefore upload access
-implies read access by simply being a higher level of it.
-
-For an open source, public Gerrit installation, it is common to
-grant `Read Access +1..+2` to `Registered Users` in the `\-- All
-Projects \--` ACL.  For more private installations, its common to
-simply grant `Read Access +1..+2` to all users of a project.
-
-[[category_READ_3]]
-Upload Merge Access
-~~~~~~~~~~~~~~~~~~~
-The `Read Access +3` permits the user to upload merge commits, but is
-otherwise identical to `Read Access +2`. Some projects wish to
-restrict merges to being created by Gerrit. By granting,
-`Read Access +1..+2`, the only merges that enter the system will be
-those created by Gerrit, or those pushed directly.
-
-[[category_pTAG]]
-Push Tag
-~~~~~~~~
-
-This category permits users to push an annotated tag object over
-SSH into the project's repository.  Typically this would be done
-with a command line such as:
-
-====
-  git push ssh://USER@HOST:PORT/PROJECT tag v1.0
-====
-
-Tags must be annotated (created with `git tag -a` or `git tag -s`),
-should exist in the `refs/tags/` namespace, and should be new.
-
-This category is intended to be used to publish tags when a project
-reaches a stable release point worth remembering in history.
-
-The range of values is:
-
-* +1 Create Signed Tag
-+
-A new signed tag may be created.  The tagger email address must be
-verified for the current user.
-
-* +2 Create Annotated Tag
-+
-A new annotated (unsigned) tag may be created.  The tagger email
-address must be verified for the current user.
-
-To push tags created by users other than the current user (such
-as tags mirrored from an upstream project), `Forge Identity +2`
-must be also granted in addition to `Push Tag >= +1`.
-
-To push lightweight (non annotated) tags, grant `Push Branch +2
-Create Branch` for reference name `refs/tags/*`, as lightweight
-tags are implemented just like branches in Git.
-
-To delete or overwrite an existing tag, grant `Push Branch +3
-Force Push Branch; Delete Branch` for reference name `refs/tags/*`,
-as deleting a tag requires the same permission as deleting a branch.
-
-[[category_pHD]]
-Push Branch
-~~~~~~~~~~~
-
-This category permits users to push directly into a branch over SSH,
-bypassing any code review process that would otherwise be used.
-
-This category has several possible values:
-
-* +1 Update Branch
-+
-Any existing branch can be fast-forwarded to a new commit.
-Creation of new branches is rejected.  Deletion of existing branches
-is rejected.  This is the safest mode as commits cannot be discarded.
-
-* +2 Create Branch
-+
-Implies 'Update Branch', but also allows the creation of a new branch
-if the name does not not already designate an existing branch name.
-Like update branch, existing commits cannot be discarded.
-
-* +3 Force Push Branch; Delete Branch
-+
-Implies both 'Update Branch' and 'Create Branch', but also allows an
-existing branch to be deleted. Since a force push is effectively a
-delete immediately followed by a create, but performed atomically on
-the server and logged, this level also permits forced push updates
-to branches.  This level may allow existing commits to be discarded
-from a project history.
-
-This category is primarily useful for projects that only want to
-take advantage of Gerrit's access control features and do not need
-its code review functionality.  Projects that need to require code
-reviews should not grant this category.
-
-[[category_FORG]]
-Forge Identity
-~~~~~~~~~~~~~~
-
-Normally Gerrit requires the author and the committer identity
-lines in a Git commit object (or tagger line in an annotated tag) to
-match one of the registered email addresses of the uploading user.
-This permission allows users to bypass that validation, which may
-be necessary when mirroring changes from an upstream project.
-
-* +1 Forge Author Identity
-+
-Permits the use of an unverified author line in commit objects.
-This can be useful when applying patches received by email from
-3rd parties, when cherry-picking changes written by others across
-branches, or when amending someone else's commit to fix up a minor
-problem before submitting.
-+
-By default this is granted to `Registered Users` in all projects,
-but a site administrator may disable it if verified authorship
-is required.
-
-* +2 Forge Committer or Tagger Identity
-+
-Implies 'Forge Author Identity', but also allows the use of an
-unverified committer line in commit objects, or an unverified tagger
-line in annotated tag objects.  Typically this is only required
-when mirroring commits from an upstream project repository.
-
-* +3 Forge Gerrit Code Review Server Identity
-+
-Implies 'Forge Committer or Tagger Identity' as well as 'Forge
-Author Identity', but additionally allows the use of the server's
-own name and email on the committer line of a new commit object.
-This should only be necessary when force pushing a commit history
-which has been rewritten by 'git filter-branch' and that contains
-merge commits previously created by this Gerrit Code Review server.
-
-[[category_VRIF]]
-Verified
-~~~~~~~~
-
-The verified category can have any meaning the project desires.
-It was originally invented by the Android Open Source Project to
-mean 'compiles, passes basic unit tests'.
+The verified category is one of two default categories that is
+configured upon the creation of a Gerrit instance. It may have
+any meaning the project desires.  It was originally invented by
+the Android Open Source Project to mean
+'compiles, passes basic unit tests'.
 
 The range of values is:
 
 * -1 Fails
 +
 Tried to compile, but got a compile error, or tried to run tests,
-but one or more tests did not pass.
+but one or more tests did not pass.  This value is valid
+across all patch sets in the same change, i.e. the reviewer must
+actively change his/her review to something else before the change
+is submittable.
 +
 *Any -1 blocks submit.*
 
@@ -493,8 +327,8 @@
 +
 *Any +1 enables submit.*
 
-In order to submit a change, the change must have a `+1 Verified` in
-this category from at least one authorized user, and no `-1 Fails`
+For a change to be submittable, the change must have a `+1 Verified`
+in this category from at least one authorized user, and no `-1 Fails`
 from an authorized user.  Thus, `-1 Fails` can block a submit,
 while `+1 Verified` enables a submit.
 
@@ -508,7 +342,7 @@
 
 If a Gerrit installation wants to modify the description text
 associated with these category values, the text can be updated
-in the `name` column of the `category_id = \'VRIF'` rows in the
+in the `name` column of the `category_id = 'VRIF'` rows in the
 `approval_category_values` table.
 
 Additional values could also be added to this category, to allow it
@@ -520,20 +354,25 @@
 A restart is required after making database changes.
 See <<restart_changes,below>>.
 
-[[category_CVRW]]
-Code Review
-~~~~~~~~~~~
+[[category_label-Code-Review]]
+Label: Code Review
+~~~~~~~~~~~~~~~~~~
 
-The code review category can have any meaning the project desires.
-It was originally invented by the Android Open Source Project to
-mean 'I read the code and it seems reasonably correct'.
+The code review category is the second of two default categories that
+is configured upon the creation of a Gerrit instance. It may have
+any meaning the project desires.  It was originally invented by the
+Android Open Source Project to mean 'I read the code and it seems
+reasonably correct'.
 
 The range of values is:
 
 * -2 Do not submit
 +
 The code is so horribly incorrect/buggy/broken that it must not be
-submitted to this project, or to this branch.
+submitted to this project, or to this branch.  This value is valid
+across all patch sets in the same change, i.e. the reviewer must
+actively change his/her review to something else before the change
+is submittable.
 +
 *Any -2 blocks submit.*
 
@@ -568,10 +407,11 @@
 +
 *Any +2 enables submit.*
 
-In order to submit a change, the change must have a `+2 Looks good to
-me, approved` in this category from at least one authorized user,
-and no `-2 Do not submit` from an authorized user.  Thus `-2`
-can block a submit, while `+2` can enable it.
+For a change to be submittable, the latest patch set must have a
+`+2 Looks good to me, approved` in this category from at least one
+authorized user, and no `-2 Do not submit` from an authorized user.
+Thus `-2` on any patch set can block a submit, while `+2` on the
+latest patch set can enable it.
 
 If a Gerrit installation does not wish to use this category in any
 project, it can be deleted from the database:
@@ -583,7 +423,7 @@
 
 If a Gerrit installation wants to modify the description text
 associated with these category values, the text can be updated
-in the `name` column of the `category_id = \'CRVW'` rows in the
+in the `name` column of the `category_id = 'CRVW'` rows in the
 `approval_category_values` table.
 
 Additional values could be inserted into `approval_category_values`
@@ -604,7 +444,275 @@
 A restart is required after making database changes.
 See <<restart_changes,below>>.
 
-[[category_SUBM]]
+[[category_create]]
+Create reference
+~~~~~~~~~~~~~~~~
+
+The create reference category controls whether it is possible to
+create new references, branches or tags.  This implies that the
+reference must not already exist, it's not a destructive permission
+in that you can't overwrite or remove any previosuly existing
+references (and also discard any commits in the process).
+
+It's probably most common to either permit the creation of a single
+branch in many gits (by granting permission on a parent project), or
+to grant this permission to a name pattern of branches.
+
+This permission is often given in conjunction with regular push
+branch permissions, allowing the holder of both to create new branches
+as well as bypass review for new commits on that branch.
+
+To push lightweight (non annotated) tags, grant
+`Create Reference` for reference name `refs/tags/*`, as lightweight
+tags are implemented just like branches in Git.
+
+For example, to grant the possibility to create new branches under the
+namespace `foo`, you have to grant this permission on
+`refs/heads/foo/*` for the group that should have it.
+Finally, if you plan to grant each user a personal namespace in
+where they are free to create as many branches as they wish, you
+should grant the create reference permission so it's possible
+to create new branches. This is done by using the special
+`${username}` keyword in the reference pattern, e.g.
+`refs/heads/sandbox/${username}/*`. If you do, it's also recommended
+you grant the users the push force permission to be able to clean up
+stale branches.
+
+
+[[category_forge_author]]
+Forge Author
+~~~~~~~~~~~~
+
+Normally Gerrit requires the author and the committer identity
+lines in a Git commit object (or tagger line in an annotated tag) to
+match one of the registered email addresses of the uploading user.
+This permission allows users to bypass parts of that validation, which
+may be necessary when mirroring changes from an upstream project.
+
+Permits the use of an unverified author line in commit objects.
+This can be useful when applying patches received by email from
+3rd parties, when cherry-picking changes written by others across
+branches, or when amending someone else's commit to fix up a minor
+problem before submitting.
+
+By default this is granted to `Registered Users` in all projects,
+but a site administrator may disable it if verified authorship
+is required.
+
+
+[[category_forge_committer]]
+Forge Committer
+~~~~~~~~~~~~~~~
+
+Normally Gerrit requires the author and the committer identity
+lines in a Git commit object (or tagger line in an annotated tag) to
+match one of the registered email addresses of the uploading user.
+This permission allows users to bypass parts of that validation, which
+may be necessary when mirroring changes from an upstream project.
+
+Allows the use of an unverified committer line in commit objects, or an
+unverified tagger line in annotated tag objects.  Typically this is only
+required when mirroring commits from an upstream project repository.
+
+
+[[category_forge_server]]
+Forge Server
+~~~~~~~~~~~~
+
+Normally Gerrit requires the author and the committer identity
+lines in a Git commit object (or tagger line in an annotated tag) to
+match one of the registered email addresses of the uploading user.
+This permission allows users to bypass parts of that validation, which
+may be necessary when mirroring changes from an upstream project.
+
+Allows the use of the server's own name and email on the committer
+line of a new commit object.  This should only be necessary when force
+pushing a commit history which has been rewritten by 'git filter-branch'
+and that contains merge commits previously created by this Gerrit Code
+Review server.
+
+
+[[category_owner]]
+Owner
+~~~~~
+
+The `Owner` category controls which groups can modify the project's
+configuration.  Users who are members of an owner group can:
+
+* Change the project description
+* Create/delete a branch through the web UI (not SSH)
+* Grant/revoke any access rights, including `Owner`
+
+Note that project owners implicitly have branch creation or deletion
+through the web UI, but not through SSH.  To get SSH branch access
+project owners must grant an access right to a group they are a
+member of, just like for any other user.
+
+Ownership over a particular branch subspace may be delegated by
+entering a branch pattern.  To delegate control over all branches
+that begin with `qa/` to the QA group, add `Owner` category
+for reference `refs/heads/qa/\*`.  Members of the QA group can
+further refine access, but only for references that begin with
+`refs/heads/qa/`. See <<project_owners,project owners>> to find
+out more about this role.
+
+
+[[category_push]]
+Push
+~~~~
+
+This category controls how users are allowed to upload new commits
+to projects in Gerrit. It can either give permission to push
+directly into a branch, bypassing any code review process
+that would otherwise be used. Or it may give permission to upload
+new changes for code review, this depends on which namespace the
+permission is granted to.
+
+
+[[category_push_direct]]
+Direct Push
+^^^^^^^^^^^
+
+Any existing branch can be fast-forwarded to a new commit.
+Creation of new branches is controlled by the 
+link:access-control.html#category_create['Create Reference']
+category.  Deletion of existing branches is rejected.  This is the
+safest mode as commits cannot be discarded.
+
+* Force option
++
+Allows an existing branch to be deleted. Since a force push is
+effectively a delete immediately followed by a create, but performed
+atomically on the server and logged, this option also permits forced
+push updates to branches.  Enabling this option allows existing commits
+to be discarded from a project history.
+
+The push category is primarily useful for projects that only want to
+take advantage of Gerrit's access control features and do not need
+its code review functionality.  Projects that need to require code
+reviews should not grant this category.
+
+
+[[category_push_review]]
+Upload To Code Review
+^^^^^^^^^^^^^^^^^^^^^
+
+The `Push` access right granted on the namespace
+`refs/for/refs/heads/BRANCH` permits the user to upload a non-merge
+commit to the project's `refs/for/BRANCH` namespace, creating a new
+change for code review.
+
+A user must be able to clone or fetch the project in order to create
+a new commit on their local system, so in practice they must also
+have the `Read` access granted to upload a change.
+
+For an open source, public Gerrit installation, it is common to
+grant `Read` and `Push` for `refs/for/refs/heads/*`
+to `Registered Users` in the `All-Projects` ACL.  For more
+private installations, its common to simply grant `Read` and
+`Push` for `refs/for/refs/heads/*` to all users of a project.
+
+* Force option
++
+The force option has no function when granted to a branch in the
+`refs/for/refs/heads/*` namespace.
+
+
+[[category_push_merge]]
+Push Merge Commits
+~~~~~~~~~~~~~~~~~~~~
+
+The `Push Merge Commit` permits the user to upload merge commits.
+It's an addon to the <<category_push,Push>> access right, and so it
+won't be sufficient with only `Push Merge Commit` granted for a push
+to happen.  Some projects wish to restrict merges to being created by
+Gerrit. By granting `Push` without `Push Merge Commit`, the only
+merges that enter the system will be those created by Gerrit.
+
+
+[[category_push_annotated]]
+Push Annotated Tag
+~~~~~~~~~~~~~~~~~~
+
+This category permits users to push an annotated tag object over
+SSH into the project's repository.  Typically this would be done
+with a command line such as:
+
+====
+  git push ssh://USER@HOST:PORT/PROJECT tag v1.0
+====
+
+Tags must be annotated (created with `git tag -a` or `git tag -s`),
+should exist in the `refs/tags/` namespace, and should be new.
+
+This category is intended to be used to publish tags when a project
+reaches a stable release point worth remembering in history.
+
+It allows for a new annotated (unsigned) tag to be created.  The
+tagger email address must be verified for the current user.
+
+To push tags created by users other than the current user (such
+as tags mirrored from an upstream project), `Forge Committer Identity`
+must be also granted in addition to `Push Annotated Tag`.
+
+To push lightweight (non annotated) tags, grant
+<<category_create,`Create Reference`>> for reference name
+`refs/tags/*`, as lightweight tags are implemented just like
+branches in Git.
+
+To delete or overwrite an existing tag, grant `Push` with the force
+option enabled for reference name `refs/tags/*`, as deleting a tag
+requires the same permission as deleting a branch.
+
+
+[[category_read]]
+Read
+~~~~
+
+The `Read` category controls visibility to the project's
+changes, comments, code diffs, and Git access over SSH or HTTP.
+A user must have this access granted in order to see a project, its
+changes, or any of its data.
+
+This category has a special behavior, where the per-project ACL is
+evaluated before the global all projects ACL.  If the per-project
+ACL has granted `Read` with 'DENY', and does not otherwise grant
+`Read` with 'ALLOW', then a `Read` in the all projects ACL
+is ignored.  This behavior is useful to hide a handful of projects
+on an otherwise public server.
+
+For an open source, public Gerrit installation it is common to grant
+`Read` to `Anonymous Users` in the `All-Projects` ACL, enabling
+casual browsing of any project's changes, as well as fetching any
+project's repository over SSH or HTTP.  New projects can be
+temporarily hidden from public view by granting `Read` with 'DENY'
+to `Anonymous Users` and granting `Read` to the project owner's
+group within the per-project ACL.
+
+For a private Gerrit installation using a trusted HTTP authentication
+source, granting `Read` to `Registered Users` may be more
+typical, enabling read access only to those users who have been
+able to authenticate through the HTTP access controls.  This may
+be suitable in a corporate deployment if the HTTP access control
+is already restricted to the correct set of users.
+
+
+[[category_rebase]]
+Rebase
+~~~~~~
+
+This category permits users to rebase changes via the web UI by pushing
+the `Rebase Change` button.
+
+The change owner and submitters can always rebase changes in the web UI
+(even without having the `Rebase` access right assigned).
+
+Users without this access right who are able to upload new patch sets
+can still do the rebase locally and upload the rebased commit as a new
+patch set.
+
+
+[[category_submit]]
 Submit
 ~~~~~~
 
@@ -619,23 +727,22 @@
 `Code Review`, above) must enable submit, and also must not block it.
 See above for details on each category.
 
+
 [[category_makeoneup]]
 Your Category Here
 ~~~~~~~~~~~~~~~~~~
 
 Gerrit administrators can also make up their own categories.
 
-See above for descriptions of how `Verified` and `Code Review` work,
-and insert your own category with `function_name = \'MaxWithBlock'`
-to get the same behavior over your own range of values, in any
-category you desire.
+See above for descriptions of how <<category_verified,`Verified`>>
+and <<category_review,`Code Review`>> work, and insert your own
+category with `function_name = 'MaxWithBlock'` to get the same
+behavior over your own range of values, in any category you desire.
 
 Ensure `category_id` is unique within your `approval_categories`
 table.  The default values `VRIF` and `CVRF` used for the categories
 described above are simply that, defaults, and have no special
-meaning to Gerrit.  The other standard category_id values like
-`OWN`, `READ`, `SUBM`, `pTAG` and `pHD` have special meaning and
-should not be modified or reused.
+meaning to Gerrit.
 
 The `position` column of `approval_categories` controls which column
 of the 'Approvals' table the category appears in, providing some
@@ -665,7 +772,7 @@
   VALUES
     ('Copyright Check'
     ,3
-    'MaxWithBlock'
+    ,'MaxWithBlock'
     ,'copy');
 
   INSERT INTO approval_category_values
@@ -696,6 +803,369 @@
 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
+--------------------------------------------------
+
+[options="header"]
+|=================================================================================
+|Gerrit 2.1.x                 |Gerrit 2.2.x
+|Code review                  |<<category_label-Code-Review,Label: Code review>>
+|Verify                       |<<category_label-Verified,Label: Verify>>
+|Forge Identity +1            |Forge <<category_forge_author,author>> identity
+|Forge Identity +2            |Forge <<category_forge_committer,committer>> & <<category_forge_author,author>> identity
+|Forge Identity +3            |Forge <<category_forge_server,server>> & <<category_forge_committer,committer>> & <<category_forge_author,author>> identity
+|Owner                        |<<category_owner,Owner>>
+|Push branch +1               |<<category_push_direct,Push>>
+|Push branch +2               |<<category_create,Create reference>> & <<category_push_direct,Push>>
+|Push branch +3               |<<category_push_direct,Push>> (with force) & <<category_create,Create reference>>
+|Push tag +1 & Push Branch +2 |No support to limit to push signed tag
+|Push tag +2 & Push Branch +2 |<<category_push_annotated,Push annotated tag>>
+|Push Branch +2 (refs/tags/*) |<<category_create,Create reference>> (refs/tags/...)
+|Push Branch +3 (refs/tags/*) |<<category_push_direct,Push>> (with force on refs/tags/...)
+|Read +1                      |<<category_read,Read>>
+|Read +2                      |<<category_read,Read>> & <<category_push_review,Push>> (refs/for/refs/...)
+|Read +3                      |<<category_read,Read>> & <<category_push_review,Push>> (refs/for/refs/...) & <<category_push_merge,Push Merge Commit>>
+|Submit                       |<<category_submit,Submit>>
+|=================================================================================
+
+
+[NOTE]
+In Gerrit 2.2.x, the way to set permissions for upload has changed entirely.
+To upload a change for review is no longer a separate permission type,
+instead you grant ordinary push permissions to the actual
+recieving reference. In practice this means that you set push permissions
+on `refs/for/refs/heads/<branch>` rather than permissions to upload changes
+on `refs/heads/<branch>`.
+
+
+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]
+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
 ------
 Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/cmd-cherry-pick.txt b/Documentation/cmd-cherry-pick.txt
index 0831d9d..568c872 100644
--- a/Documentation/cmd-cherry-pick.txt
+++ b/Documentation/cmd-cherry-pick.txt
@@ -9,10 +9,8 @@
 --------
 [verse]
 'gerrit-cherry-pick' <remote> <changeid>...
-
-'gerrit-cherry-pick' \--continue | \--skip | \--abort
-
-'gerrit-cherry-pick' \--close <remote>
+'gerrit-cherry-pick' --continue | --skip | --abort
+'gerrit-cherry-pick' --close <remote>
 
 DESCRIPTION
 -----------
@@ -22,13 +20,13 @@
 
 If a merge failure prevents this from being completely automatic,
 you will be asked to resolve the conflict and restart the command
-with the `\--continue` option.
+with the `--continue` option.
 
 Change ids may be specified as either the change id (e.g. 1234)
 or as change id slash patch set number (e.g. 1234/8).  If the patch
 set number is not supplied, `/1` is assumed.
 
-The `\--close` command line option is now deprecated, as closing
+The `--close` command line option is now deprecated, as closing
 existing changes post cherry-pick is better handled simply by
 ensuring link:user-changeid.html[Change-Id lines] are present in
 each commit message.
@@ -38,9 +36,11 @@
 To obtain the 'gerrit-cherry-pick' script use scp, curl or wget to
 copy it to your local system:
 
+====
   $ scp -p -P 29418 john.doe@review.example.com:bin/gerrit-cherry-pick ~/bin/
 
   $ curl http://review.example.com/tools/bin/gerrit-cherry-pick
+====
 
 GERRIT
 ------
diff --git a/Documentation/cmd-create-account.txt b/Documentation/cmd-create-account.txt
index ab5663c..98f950f 100644
--- a/Documentation/cmd-create-account.txt
+++ b/Documentation/cmd-create-account.txt
@@ -8,12 +8,12 @@
 SYNOPSIS
 --------
 [verse]
-'ssh' -p <port> <host> 'gerrit create-account' \
-[\--group <GROUP>] \
-[\--full-name <FULLNAME>] \
-[\--email <EMAIL>] \
-[\--ssh-key -|<KEY>] \
-<USERNAME>
+'ssh' -p <port> <host> 'gerrit create-account'
+  [--group <GROUP>]
+  [--full-name <FULLNAME>]
+  [--email <EMAIL>]
+  [--ssh-key - | <KEY>]
+  <USERNAME>
 
 DESCRIPTION
 -----------
@@ -27,7 +27,9 @@
 
 ACCESS
 ------
-Caller must be a member of the privileged 'Administrators' group.
+Caller must be a member of the privileged 'Administrators' group,
+or have been granted
+link:access-control.html#capability_createAccount[the 'Create Account' global capability].
 
 SCRIPTING
 ---------
@@ -38,23 +40,23 @@
 <USERNAME>::
 	Required; SSH username of the user account.
 
-\--ssh-key::
+--ssh-key::
 	Content of the public SSH key to load into the account's
 	keyring.  If `-` the key is read from stdin, rather than
 	from the command line.
 
-\--group::
-	Name of the group to put the user into.  Multiple \--group
+--group::
+	Name of the group to put the user into.  Multiple --group
 	options may be specified to add the user to multiple groups.
 
-\--full-name::
+--full-name::
 	Display name of the user account.
 +
-Names containing spaces should be quoted in single quotes (\').
+Names containing spaces should be quoted in single quotes (').
 This most likely requires double quoting the value, for example
-`\--full-name "\'A description string\'"`.
+`--full-name "'A description string'"`.
 
-\--email::
+--email::
 	Preferred email address for the user account.
 
 EXAMPLES
diff --git a/Documentation/cmd-create-group.txt b/Documentation/cmd-create-group.txt
index 1e46e14..475d2c5 100644
--- a/Documentation/cmd-create-group.txt
+++ b/Documentation/cmd-create-group.txt
@@ -8,12 +8,13 @@
 SYNOPSIS
 --------
 [verse]
-'ssh' -p <port> <host> 'gerrit create-group' \
-[\--owner <GROUP>] \
-[\--description <DESC>] \
-[\--member <USERNAME>] \
-[\--group <GROUP>] \
-<GROUP>
+'ssh' -p <port> <host> 'gerrit create-group'
+  [--owner <GROUP>]
+  [--description <DESC>]
+  [--member <USERNAME>]
+  [--group <GROUP>]
+  [--visible-to-all]
+  <GROUP>
 
 DESCRIPTION
 -----------
@@ -26,7 +27,9 @@
 
 ACCESS
 ------
-Caller must be a member of the privileged 'Administrators' group.
+Caller must be a member of the privileged 'Administrators' group,
+or have been granted
+link:access-control.html#capability_createGroup[the 'Create Group' global capability].
 
 SCRIPTING
 ---------
@@ -37,24 +40,27 @@
 <GROUP>::
 	Required; name of the new group.
 
-\--owner, -o::
+--owner, -o::
 	Name of the owning group. If not specified the group will be self-owning.
 
-\--description, -d::
+--description, -d::
 	Description of group.
 +
 Description values containing spaces should be quoted in single quotes
-(\').  This most likely requires double quoting the value, for example
-`\--description "\'A description string\'"`.
+(').  This most likely requires double quoting the value, for example
+`--description "'A description string'"`.
 
-\--member::
-	User name to become initial member of the group.  Multiple \--member
+--member::
+	User name to become initial member of the group.  Multiple --member
 	options may be specified to add more initial members.
 
-\--group::
-	Group name to include in the group.  Multiple \--group options may
+--group::
+	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 71c0d1f..f22141c 100644
--- a/Documentation/cmd-create-project.txt
+++ b/Documentation/cmd-create-project.txt
@@ -8,18 +8,20 @@
 SYNOPSIS
 --------
 [verse]
- 'ssh' -p <port> <host> 'gerrit create-project' \
- --name <NAME> \
- [--branch <REF>] \
- [\--owner <GROUP> ...] \
- [\--parent <NAME>] \
- [\--permissions-only] \
- [\--description <DESC>] \
- [\--submit-type <TYPE>] \
- [\--use-content-merge] \
- [\--use-contributor-agreements] \
- [\--use-signed-off-by] \
- [\--empty-commit]
+'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>]
+  [--use-contributor-agreements | --ca]
+  [--use-signed-off-by | --so]
+  [--use-content-merge]
+  [--require-change-id | --id]
+  [--branch <REF> | -b <REF>]
+  [--empty-commit]
+  { <NAME> | --name <NAME> }
 
 DESCRIPTION
 -----------
@@ -36,11 +38,9 @@
 
 ACCESS
 ------
-Caller must be a member of any of the groups defined by
-repository.*.createGroup in gerrit.config.
-
-If there is no such declaration, caller is required to be a member
-of the privileged 'Administrators' group.
+Caller must be a member of the privileged 'Administrators' group,
+or have been granted
+link:access-control.html#capability_createProject[the 'Create Project' global capability].
 
 SCRIPTING
 ---------
@@ -48,43 +48,58 @@
 
 OPTIONS
 -------
-\--name::
-	Required; name of the project to create.  If name ends with
-	`.git` the suffix will be automatically removed.
+<NAME>::
+	Required; name of the new project to create.  If name ends
+	with `.git` the suffix will be automatically removed.
 
-\--branch::
+--name::
+-n::
+	Deprecated alias for the <NAME> argument. This option may
+	be removed in a future release.
+
+--branch::
+-b::
 	Name of the initial branch in the newly created project.
 	Defaults to 'master'.
 
-\--owner::
+--owner::
+-o::
 	Name of the group(s) which will initially own this repository.
 	The specified group(s) must already be defined within Gerrit.
 	Several groups can be specified on the command line.
 +
-Defaults to what is specified by repository.*.ownerGroup
-in gerrit.config. If no such declaration(s) exist,
-repository.*.createGroup will be used. If they don't exist,
-`Administrators` will be used.
+Defaults to what is specified by `repository.*.ownerGroup`
+in gerrit.config.
 
-\--parent::
+--parent::
+-p::
 	Name of the parent project to inherit access rights
 	through. If not specified, the parent is set to the default
-	project `\-- All Projects \--`.
+	project `All-Projects`.
 
-\--permissions-only::
+--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 not be
-	initialized, and cannot be cloned.
+	projects.  The new project's Git repository will be
+	initialized to have 'HEAD' point to 'refs/meta/config'.
 
-\--description::
+--description::
+-d::
 	Initial description of the project.  If not specified,
 	no description is stored.
 +
 Description values containing spaces should be quoted in single quotes
-(\').  This most likely requires double quoting the value, for example
-`\--description "\'A description string\'"`.
+(').  This most likely requires double quoting the value, for example
+`--description "'A description string'"`.
 
-\--submit-type::
+--submit-type::
+-t::
 	Action used by Gerrit to submit an approved change to its
 	destination branch.  Supported options are:
 +
@@ -97,24 +112,32 @@
 Defaults to MERGE_IF_NECESSARY.  For more details see
 link:project-setup.html#submit_type[Change Submit Actions].
 
-\--use-content-merge::
+--use-content-merge::
 	If enabled, Gerrit will try to perform a 3-way merge of text
 	file content when a file has been modified by both the
 	destination branch and the change being submitted.  This
 	option only takes effect if submit type is not
 	FAST_FORWARD_ONLY.  Disabled by default.
 
-\--use-contributor-agreements::
+--use-contributor-agreements::
+--ca::
 	If enabled, authors must complete a contributor agreement
 	on the site before pushing any commits or changes to this
 	project.  Disabled by default.
 
-\--use-signed-off-by::
+--use-signed-off-by::
+--so:
 	If enabled, each change must contain a Signed-off-by line
 	from either the author or the uploader in the commit message.
 	Disabled by default.
 
-\--empty-commit:
+--require-change-id::
+--id::
+	Require a valid link:user-changeid.html[Change-Id] footer
+	in any commit uploaded for review. This does not apply to
+	commits pushed directly to a branch or tag.
+
+--empty-commit::
 	Creates an initial empty commit for the Git repository of the
 	project that is newly created.
 
@@ -124,13 +147,13 @@
 Create a new project called `tools/gerrit`:
 
 ====
-	$ ssh -p 29418 review.example.com gerrit create-project --name tools/gerrit.git
+	$ ssh -p 29418 review.example.com gerrit create-project tools/gerrit.git
 ====
 
 Create a new project with a description:
 
 ====
-	$ ssh -p 29418 review.example.com gerrit create-project --name tool.git --description "'Tools used by build system'"
+	$ ssh -p 29418 review.example.com gerrit create-project tool.git --description "'Tools used by build system'"
 ====
 
 Note that it is necessary to quote the description twice.  The local
diff --git a/Documentation/cmd-flush-caches.txt b/Documentation/cmd-flush-caches.txt
index 573cd78..bc6fac5 100644
--- a/Documentation/cmd-flush-caches.txt
+++ b/Documentation/cmd-flush-caches.txt
@@ -8,8 +8,9 @@
 SYNOPSIS
 --------
 [verse]
- 'ssh' -p <port> <host> 'gerrit flush-caches' \
- [\--all | \--list | \--cache <NAME> ...]
+'ssh' -p <port> <host> 'gerrit flush-caches' --all
+'ssh' -p <port> <host> 'gerrit flush-caches' --list
+'ssh' -p <port> <host> 'gerrit flush-caches' --cache <NAME> ...
 
 DESCRIPTION
 -----------
@@ -24,7 +25,9 @@
 
 ACCESS
 ------
-Caller must be a member of the privileged 'Administrators' group.
+Caller must be a member of the privileged 'Administrators' group,
+or in a group that have been granted
+link:access-control.html#capability_flushCaches[the 'Flush Caches' global capability].
 
 SCRIPTING
 ---------
@@ -32,19 +35,19 @@
 
 OPTIONS
 -------
-\--all::
+--all::
 	Flush all known caches.  This is like applying a big hammer,
 	it will force everything out, potentially more than was
-	necessary for the change made.	This option automatically
+	necessary for the change made. This option automatically
 	skips flushing potentially dangerous caches such as
 	"web_sessions".  To flush one of these caches, the caller
 	must specifically name them on the command line, e.g. pass
-	`\--cache=web_sessions`.
+	`--cache web_sessions`.
 
-\--list::
+--list::
 	Show a list of the caches.
 
-\--cache=<NAME>::
+--cache <NAME>::
 	Flush only the cache called <NAME>.  May be supplied more
 	than once to flush multiple caches in a single command
 	execution.
diff --git a/Documentation/cmd-gsql.txt b/Documentation/cmd-gsql.txt
index 7fc8a9b..3c4f1b5 100644
--- a/Documentation/cmd-gsql.txt
+++ b/Documentation/cmd-gsql.txt
@@ -8,9 +8,9 @@
 SYNOPSIS
 --------
 [verse]
- 'ssh' -p <port> <host> 'gerrit gsql' \
- [\--format \{PRETTY | JSON\}] \
- [\-c QUERY]
+'ssh' -p <port> <host> 'gerrit gsql'
+  [--format {PRETTY | JSON}]
+  [-c QUERY]
 
 DESCRIPTION
 -----------
@@ -20,7 +20,7 @@
 
 OPTIONS
 -------
-\--format::
+--format::
 	Set the format records are output in.  In PRETTY (the
 	default) records are displayed in a tabular output suitable
 	for reading by a human on a sufficiently wide terminal.
diff --git a/Documentation/cmd-hook-commit-msg.txt b/Documentation/cmd-hook-commit-msg.txt
index b21f5c0..6318bba 100644
--- a/Documentation/cmd-hook-commit-msg.txt
+++ b/Documentation/cmd-hook-commit-msg.txt
@@ -53,12 +53,25 @@
 
 OBTAINING
 ---------
-To obtain the 'commit-msg' script use scp, wget or curl to copy it
-to your local system:
+To obtain the 'commit-msg' script use scp, wget or curl to download it
+to your local system from your gerrit server.
 
-  $ scp -p -P 29418 john.doe@review.example.com:hooks/commit-msg .git/hooks/
+You can use either of the below commands:
 
-  $ curl http://review.example.com/tools/hooks/commit-msg
+====
+  $ scp -p -P 29418 <your username>@<your Gerrit review server>:hooks/commit-msg <local path to your git>/.git/hooks/
+
+  $ curl -o <local path to your git>/.git/hooks/commit-msg <your Gerrit http URL>/tools/hooks/commit-msg
+====
+
+A specific example of this might look something like this:
+
+.Example
+====
+  $ scp -p -P 29418 john.doe@review.example.com:hooks/commit-msg ~/duhproject/.git/hooks/
+
+  $ curl -o ~/duhproject/.git/hooks/commit-msg http://review.example.com/tools/hooks/commit-msg
+====
 
 SEE ALSO
 --------
diff --git a/Documentation/cmd-index.txt b/Documentation/cmd-index.txt
index 8a6cb6d3..b09c3b3 100644
--- a/Documentation/cmd-index.txt
+++ b/Documentation/cmd-index.txt
@@ -51,6 +51,33 @@
 [[user_commands]]User Commands
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
+'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.
+
+link:cmd-query.html[gerrit query]::
+	Query the change database.
+
+'gerrit 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 occurring in real time.
+
 git upload-pack::
 	Standard Git server side command for client side `git fetch`.
 
@@ -60,25 +87,7 @@
 Also implements the magic associated with uploading commits for
 review.  See link:user-upload.html#push_create[Creating Changes].
 
-link:cmd-review.html[gerrit approve]::
-	Alias for 'gerrit review'.
-
-link:cmd-ls-projects.html[gerrit ls-projects]::
-	List projects visible to the caller.
-
-link:cmd-query.html[gerrit query]::
-	Query the change database.
-
-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.
-
-gerrit receive-pack::
-	Legacy alias for `git receive-pack`.
-
-[[admin_commands]]Adminstrator Commands
+[[admin_commands]]Administrator Commands
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
 link:cmd-create-account.html[gerrit create-account]::
@@ -96,6 +105,9 @@
 link:cmd-gsql.html[gerrit gsql]::
 	Administrative interface to active database.
 
+link:cmd-replicate.html[gerrit replicate]::
+	Manually trigger replication, to recover a node.
+
 link:cmd-set-project-parent.html[gerrit set-project-parent]::
 	Change the project permissions are inherited from.
 
@@ -108,9 +120,6 @@
 link:cmd-show-queue.html[gerrit show-queue]::
 	Display the background work queues, including replication.
 
-link:cmd-replicate.html[gerrit replicate]::
-	Manually trigger replication, to recover a node.
-
 link:cmd-kill.html[kill]::
 	Kills a scheduled or running task.
 
diff --git a/Documentation/cmd-kill.txt b/Documentation/cmd-kill.txt
index a76be84..f09053e 100644
--- a/Documentation/cmd-kill.txt
+++ b/Documentation/cmd-kill.txt
@@ -18,7 +18,8 @@
 
 ACCESS
 ------
-Caller must be a member of the privileged 'Administrators' group.
+Caller must be a member of the privileged 'Administrators' group,
+or have been granted link:access-control.html#capability_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 1e7b4cc..7782aa8 100644
--- a/Documentation/cmd-ls-projects.txt
+++ b/Documentation/cmd-ls-projects.txt
@@ -8,7 +8,10 @@
 SYNOPSIS
 --------
 [verse]
-'ssh' -p <port> <host> 'gerrit ls-projects' [\--show-branch <BRANCH>]
+'ssh' -p <port> <host> 'gerrit ls-projects'
+  [--show-branch <BRANCH> ...]
+  [--tree]
+  [--type {code | permissions | all}]
 
 DESCRIPTION
 -----------
@@ -28,15 +31,47 @@
 
 OPTIONS
 -------
-\--show-branch::
-\-b::
-	Name of the branch for which the command will display the sha of each project.
+--show-branch::
+-b::
+	Branch for which the command will display the sha of each project.
+	The command may have multiple --show-branch parameters, in this case
+	sha will be shown for each of the branches.
+	If the user does not have READ access to some branch or the branch does not
+	exist then stub (40 `-` symbols) is shown.
+	If the user does not have access to any branch in the project then the
+	whole project is not shown.
 
-\--tree::
-\-t::
+--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.
 	This option does not work together with the show-branch option.
 
+--type::
+	Display only projects of the specified type.  If not
+	specified, defaults to `code`. Supported types:
++
+--
+`code`:: Any project likely to contain user files.
+`permissions`:: Projects created with the `--permissions-only` flag.
+`all`:: Any type of project.
+--
+
+--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).
+
 EXAMPLES
 --------
 
diff --git a/Documentation/cmd-query.txt b/Documentation/cmd-query.txt
index c8996eb..253bed1 100644
--- a/Documentation/cmd-query.txt
+++ b/Documentation/cmd-query.txt
@@ -8,14 +8,18 @@
 SYNOPSIS
 --------
 [verse]
- 'ssh' -p <port> <host> 'gerrit query' \
- [\--format {TEXT | JSON}] \
- [\--current-patch-set] \
- [\--patch-sets|--all-approvals] \
- [\--] \
- <query> \
- [limit:<n>] \
- [resume\_sortkey:<sortKey>]
+'ssh' -p <port> <host> 'gerrit query'
+  [--format {TEXT | JSON}]
+  [--current-patch-set]
+  [--patch-sets | --all-approvals]
+  [--files]
+  [--comments]
+  [--commit-message]
+  [--dependencies]
+  [--]
+  <query>
+  [limit:<n>]
+  [resume_sortkey:<sortKey>]
 
 DESCRIPTION
 -----------
@@ -39,27 +43,50 @@
 
 OPTIONS
 -------
-\--current-patch-set::
+--format::
+	Formatting method for the results. TEXT is the default,
+	presenting a human readable display. JSON creates one line
+	per matching record, with embedded LFs escaped.
+
+--current-patch-set::
 	Include information about the current patch set in the results.
 
-\--patch-sets::
+--patch-sets::
 	Include information about all patch sets.  If combined with
-	the \--current-patch-set flag then the current patch set
+	the --current-patch-set flag then the current patch set
 	information will be output twice, once in each field.
 
-\--all-approvals::
+--all-approvals::
 	Include information about all patch sets along with the
 	approval information for each patch set.  If combined with
-	the \--current-patch-set flag then the current patch set
+	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.
+
+--dependencies::
+	Show information about patch sets which depend on, or are needed by,
+	each patch set.
+
 limit:<n>::
 	Maximum number of results to return.  This is actually a
 	query operator, and not a command line option.	If more
 	than one limit: operator is provided, the smallest limit
 	will be used to cut the result set.
 
-resume\_sortkey:<sortKey>::
+resume_sortkey:<sortKey>::
 	Resume results from this sort key.  Callers should pass
 	the sortKey of the last change of the prior result set to
 	resume a prior query.  This is actually a query operator,
@@ -77,20 +104,20 @@
 --------
 
 Find the 2 most recent open changes in the tools/gerrit project:
------
+====
   $ ssh -p 29418 review.example.com gerrit query --format=JSON status:open project:tools/gerrit limit:2
   {"project":"tools/gerrit", ...}
   {"project":"tools/gerrit", ..., sortKey:"000e6aee00003e26", ...}
   {"type":"stats","rowCount":2,"runningTimeMilliseconds:15}
------
+====
 
 Resume the same query and obtain the final results:
------
+====
   $ ssh -p 29418 review.example.com gerrit query --format=JSON status:open project:tools/gerrit limit:2 resume_sortkey:000e6aee00003e26
   {"project":"tools/gerrit", ...}
   {"project":"tools/gerrit", ...}
   {"type":"stats","rowCount":1,"runningTimeMilliseconds:15}
------
+====
 
 
 SCHEMA
diff --git a/Documentation/cmd-receive-pack.txt b/Documentation/cmd-receive-pack.txt
index ca96550..7e5ca09 100644
--- a/Documentation/cmd-receive-pack.txt
+++ b/Documentation/cmd-receive-pack.txt
@@ -8,7 +8,7 @@
 SYNOPSIS
 --------
 [verse]
-git receive-pack [\--reviewer <address>] [\--cc <address>] <project>
+'git receive-pack' [--reviewer <address>] [--cc <address>] <project>
 
 DESCRIPTION
 -----------
@@ -24,11 +24,11 @@
 <project>::
 	The remote repository that will receive the pushed objects,
 	and create (or update) changes.  Within Gerrit Code Review
-	this is the name of a project.	The optional leading `/`
+	this is the name of a project.  The optional leading `/`
 	and or trailing `.git` suffix will be removed, if supplied.
 
-\--re <address>::
-\--reviewer <address>::
+--reviewer <address>::
+--re <address>::
 	Automatically add <address> as a reviewer to any change
 	created or updated by the pushed commit objects.  These
 	changes will appear in the reviewer's dashboard, and will
@@ -38,10 +38,10 @@
 +
 This is a Gerrit Code Review specific extension.
 
-\--cc <address>::
+--cc <address>::
 	Carbon-copy <address> on the created or updated changes,
 	but don't request them to perform a review.  Like with
-	\--reviewer the changes will appear in the CC'd user's
+	--reviewer the changes will appear in the CC'd user's
 	dashboard, and will be emailed to them.
 +
 May be specified more than once to specify multiple CCs.
@@ -82,12 +82,12 @@
 ====
 
 afterwards `.git/config` contains the following:
-====
-	[remote "charlie"]
-		url = ssh://review.example.com:29418/project
-		push = HEAD:refs/for/master
-		receivepack = git receive-pack --reviewer charlie@example.com --cc alice@example.com --cc bob@example.com
-====
+----
+[remote "charlie"]
+  url = ssh://review.example.com:29418/project
+  push = HEAD:refs/for/master
+  receivepack = git receive-pack --reviewer charlie@example.com --cc alice@example.com --cc bob@example.com
+----
 
 and now sending a new change for review to charlie, CC'ing both
 alice and bob is much easier:
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 7f88b47..7722027 100644
--- a/Documentation/cmd-replicate.txt
+++ b/Documentation/cmd-replicate.txt
@@ -8,9 +8,9 @@
 SYNOPSIS
 --------
 [verse]
-'ssh' -p <port> <host> 'gerrit replicate' \
-[\--url <PATTERN>] \
-\{\--all | <PROJECT> ...}
+'ssh' -p <port> <host> 'gerrit replicate'
+  [--url <PATTERN>]
+  {--all | <PROJECT> ...}
 
 DESCRIPTION
 -----------
@@ -32,13 +32,13 @@
   pack files to the destinations.
 +
 If the local server is repacked, and then the resulting pack files
-are sent to remote peers using `rsync -a \--delete-after`, there
+are sent to remote peers using `rsync -a --delete-after`, there
 is a chance that the rsync missed a change that was added during
 the rsync data transfer, and the rsync will remove that changes's
 data from the remote, even though the automatic replication pushed
 it there in parallel to the rsync.
 +
-Its a good idea to run replicate with `\--all` to ensure all
+Its a good idea to run replicate with `--all` to ensure all
 projects are consistent after the rsync is complete.
 
 * After deleting a ref by hand.
@@ -52,7 +52,9 @@
 
 ACCESS
 ------
-Caller must be a member of the privileged 'Administrators' group.
+Caller must be a member of the privileged 'Administrators' group,
+or have been granted
+link:access-control.html#capability_startReplication[the 'Start Replication' global capability].
 
 SCRIPTING
 ---------
@@ -60,10 +62,10 @@
 
 OPTIONS
 -------
-\--all::
+--all::
 	Schedule replicating for all projects.
 
-\--url=<PATTERN>::
+--url <PATTERN>::
 	Replicate only to replication destinations whose URL
 	contains the substring <PATTERN>.  This can be useful to
 	replicate only to a previously down node, which has been
@@ -74,21 +76,21 @@
 Replicate every project, to every configured remote:
 
 ====
-	$ ssh -p 29418 review.example.com gerrit replicate --all
+  $ ssh -p 29418 review.example.com gerrit replicate --all
 ====
 
 Replicate only to `srv2` now that it is back online:
 
 ====
-	$ ssh -p 29418 review.example.com gerrit replicate --url=srv2 --all
+  $ ssh -p 29418 review.example.com gerrit replicate --url srv2 --all
 ====
 
 Replicate only the `tools/gerrit` project, after deleting a ref
 locally by hand:
 
 ====
-	$ git --git-dir=/home/git/tools/gerrit.git update-ref -d refs/changes/00/100/1
-	$ ssh -p 29418 review.example.com gerrit replicate tools/gerrit
+  $ git --git-dir=/home/git/tools/gerrit.git update-ref -d refs/changes/00/100/1
+  $ ssh -p 29418 review.example.com gerrit replicate tools/gerrit
 ====
 
 SEE ALSO
diff --git a/Documentation/cmd-review.txt b/Documentation/cmd-review.txt
index 5645f51..ac613e5 100644
--- a/Documentation/cmd-review.txt
+++ b/Documentation/cmd-review.txt
@@ -8,8 +8,16 @@
 SYNOPSIS
 --------
 [verse]
-'ssh' -p <port> <host> 'gerrit approve' [\--project <PROJECT>] [\--message <MESSAGE>] [\--verified <N>] [\--code-review <N>] [\--abandon] [\--restore] [\--submit]  {COMMIT | CHANGEID,PATCHSET}...
-'ssh' -p <port> <host> 'gerrit review' [\--project <PROJECT>] [\--message <MESSAGE>] [\--verified <N>] [\--code-review <N>] [\--abandon] [\--restore] [\--submit]  {COMMIT | CHANGEID,PATCHSET}...
+'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}...
 
 DESCRIPTION
 -----------
@@ -19,7 +27,7 @@
 
 Patch sets should be specified as complete or abbreviated commit
 SHA-1s.  If the same commit is available in multiple projects the
-\--project option may be used to limit where Gerrit searches for
+--project option may be used to limit where Gerrit searches for
 the change to only the contents of the specified project.
 
 For current backward compatibility with user tools patch sets may
@@ -31,42 +39,63 @@
 OPTIONS
 -------
 
-\--project::
+--project::
 -p::
 	Name of the project the intended changes are contained
 	within.  This option must be supplied before the commit
 	SHA-1 in order to take effect.
 
-\--message::
+--message::
 -m::
 	Optional cover letter to include as part of the message
 	sent to reviewers when the approval states are updated.
 
-\--help::
+--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
 	complete listing of supported approval categories and values.
 
-\--code-review::
-\--verified::
-	Set the approval category to the value 'N'.  The exact
-	option names supported and the range of values permitted
-	differs per site, check the output of \--help, or contact
-	your site administrator for further details.
-
-\--abandon::
+--abandon::
 	Abandon the specified patch set(s).
 	(option is mutually exclusive with --submit and --restore)
 
-\--restore::
-	Restore the specified abandonned patch set(s).
+--restore::
+	Restore the specified abandoned patch set(s).
 	(option is mutually exclusive with --abandon)
 
-\--submit::
+--submit::
 -s::
 	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
+	option names supported and the range of values permitted
+	differs per site, check the output of --help, or contact
+	your site administrator for further details.
+
 ACCESS
 ------
 Any user who has configured an SSH key.
@@ -80,25 +109,30 @@
 
 Approve the change with commit c0ff33 as "Verified +1"
 =====
-	$ ssh -p 29418 review.example.com gerrit review --verified=+1 c0ff33
+	$ ssh -p 29418 review.example.com gerrit review --verified +1 c0ff33
 =====
 
 Append the message "Build Successful". Notice two levels of quoting is
 required, one for the local shell, and another for the argument parser
 inside the Gerrit server:
 =====
-	$ ssh -p 29418 review.example.com gerrit review -m '"Build Successful"'
+	$ ssh -p 29418 review.example.com gerrit review -m '"Build Successful"' c0ff33
 =====
 
 Mark the unmerged commits both "Verified +1" and "Code Review +2" and
 submit them for merging:
 ====
-	$ ssh -p 29418 review.example.com gerrit review \
-	--verified=+1 \
-	--code-review=+2 \
-	--submit \
-	--project=this/project \
-	$(git rev-list origin/master..HEAD)
+  $ ssh -p 29418 review.example.com gerrit review \
+    --verified +1 \
+    --code-review +2 \
+    --submit \
+    --project this/project \
+    $(git rev-list origin/master..HEAD)
+====
+
+Abandon an active change:
+====
+  $ ssh -p 29418 review.example.com gerrit review --abandon c0ff33
 ====
 
 SEE ALSO
diff --git a/Documentation/cmd-set-project-parent.txt b/Documentation/cmd-set-project-parent.txt
index 699d76f..1e7e6c5 100644
--- a/Documentation/cmd-set-project-parent.txt
+++ b/Documentation/cmd-set-project-parent.txt
@@ -8,15 +8,17 @@
 SYNOPSIS
 --------
 [verse]
-'ssh' -p <port> <host> 'gerrit set-project-parent' \
-[\--parent <NAME>] \
-<NAME> ...
+'ssh' -p <port> <host> 'gerrit set-project-parent'
+  [--parent <NAME>]
+  [--children-of <NAME>]
+  [--exclude <NAME>]
+  <NAME> ...
 
 DESCRIPTION
 -----------
 Changes the project that permissions are inherited through.
 Every project inherits permissions from another project, by
-default this is `\-- All Projects \--`.  This command sets
+default this is `All-Projects`.  This command sets
 the project to inherit through another one.
 
 ACCESS
@@ -29,9 +31,22 @@
 
 OPTIONS
 -------
-\--parent::
-	Name of the parent to inherit through.	If not specified,
-	the parent is set back to the default `\-- All Projects \--`.
+--parent::
+	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
 --------
@@ -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-set-reviewers.txt b/Documentation/cmd-set-reviewers.txt
new file mode 100644
index 0000000..9e08e39
--- /dev/null
+++ b/Documentation/cmd-set-reviewers.txt
@@ -0,0 +1,89 @@
+gerrit set-reviewers
+====================
+
+NAME
+----
+gerrit set-reviewers - Add or remove reviewers to a change
+
+SYNOPSIS
+--------
+[verse]
+'ssh' -p <port> <host> 'gerrit set-reviewers'
+  [--project <PROJECT>]
+  [--add REVIEWER ...]
+  [--remove REVIEWER ...]
+  [--]
+  {COMMIT | CHANGE-ID}...
+
+DESCRIPTION
+-----------
+Adds or removes reviewers to the specified change, sending email
+notifications when changes are made.
+
+Changes should be specified as complete or abbreviated Change-Ids
+such as 'Iac6b2ac2'.  They may also be specified by numeric change
+identifiers, such as '8242' or by complete or abbreviated commit
+SHA-1s.
+
+OPTIONS
+-------
+
+--project::
+-p::
+	Name of the project the intended change is contained within.  This
+	option must be supplied before Change-ID in order to take effect.
+
+--add::
+-a::
+	A user that should be added as reviewer to the change or a group
+	for which all members should be added as reviewers to the change.
+	Multiple users and groups can be added at once as reviewers by
+	using this option multiple times.
+
+--remove::
+-r::
+	Remove this user from the reviewer list of the change. Multiple
+	users can be removed at once from the reviewer list by using this
+	option multiple times.
+
+--help::
+-h::
+	Display site-specific usage information
+
+ACCESS
+------
+Any user who has configured an SSH key.
+
+SCRIPTING
+---------
+This command is intended to be used in scripts.
+
+EXAMPLES
+--------
+
+Add reviewers alice and bob, but remove eve from change Iac6b2ac2.
+=====
+	$ ssh -p 29418 review.example.com gerrit set-reviewers \
+	  -a alice@example.com -a bob@example.com \
+	  -r eve@example.com \
+	  Iac6b2ac2
+=====
+
+Add reviewer elvis to old-style change id 1935 specifying that the change is in project "graceland"
+=====
+	$ ssh -p 29418 review.example.com gerrit set-reviewers \
+	  --project graceland \
+	  -a elvis@example.com \
+	  1935
+=====
+
+Add all project owners as reviewers to change Iac6b2ac2.
+=====
+	$ ssh -p 29418 review.example.com gerrit set-reviewers \
+	  -a "'Project Owners'" \
+	  Iac6b2ac2
+=====
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/cmd-show-caches.txt b/Documentation/cmd-show-caches.txt
index 5998859..126b2a0 100644
--- a/Documentation/cmd-show-caches.txt
+++ b/Documentation/cmd-show-caches.txt
@@ -8,15 +8,28 @@
 SYNOPSIS
 --------
 [verse]
-'ssh' -p <port> <host> 'gerrit show-caches'
+'ssh' -p <port> <host> 'gerrit show-caches' [--gc] [--show-jvm]
 
 DESCRIPTION
 -----------
 Display statistics about the size and hit ratio of in-memory caches.
 
+OPTIONS
+-------
+--gc::
+	Request Java garbage collection before displaying information
+	about the Java memory heap.
+
+--show-jvm::
+	List the name and version of the Java virtual machine, host
+	operating system, and other details about the environment
+	that Gerrit Code Review is running in.
+
 ACCESS
 ------
-Caller must be a member of the privileged 'Administrators' group.
+Caller must be a member of the privileged 'Administrators' group,
+or have been granted
+link:access-control.html#capability_viewCaches[the 'View Caches' global capability].
 
 SCRIPTING
 ---------
@@ -27,27 +40,33 @@
 
 ====
 	$ ssh -p 29418 review.example.com gerrit show-caches
+	Gerrit Code Review        2.2.2                     now    10:03:34   PDT
+	                                                 uptime     1 min 39 sec
+	
 	  Name               Max |Object Count        |  AvgGet  |Hit Ratio     |
 	                     Age |  Disk    Mem    Cnt|          |Disk Mem  Agg |
 	-------------------------+--------------------+----------+--------------+
-	  accounts           90d |                 295|          |           99%|
-	  accounts_byemail   90d |                 109|          |           97%|
-	D diff               90d |  2695    128   2707|   0.4ms  | 11%  86%  98%|
-	  groups             90d |                  94|          |           80%|
-	  openid             5m  |                  30|   0.4ms  |            9%|
-	  projects           90d |                 188|          |           99%|
-	  sshkeys            90d |                   9|          |           94%|
-	D web_sessions       12h |           30     30|          |  0%  99%  99%|
-
-	JGit Buffer Cache:
-	  open files  :              23
-	  loaded      :   6.82 mb
-	  mem%        :   2%
-
-	JVM Heap:
-	  max         : 880.00 mb
-	  inuse       : 136.57 mb
-	  mem%        :  44%
+	  accounts           90d |                   1|          |           95%|
+	  accounts_byemail   90d |                    |          |              |
+	  accounts_byname    90d |                   1|          |              |
+	  adv_bases          10m |                    |          |              |
+	D diff               90d |     8             8|          |              |
+	D diff_intraline     90d |     1             1|          |              |
+	  groups             90d |                  19|          |            0%|
+	  groups_byext       90d |                    |          |              |
+	  groups_byinclude   90d |                  21|          |           80%|
+	  groups_byname      90d |                    |          |              |
+	  groups_byuuid      90d |                    |          |              |
+	  project_list       90d |                    |          |              |
+	  projects           90d |                   1|          |           80%|
+	  sshkeys            90d |                   1|          |           90%|
+	D web_sessions       12h |                    |          |              |
+	
+	SSH:      1  users, oldest session started 782 ms ago
+	Tasks:    2  total =    1 running +      0 ready +    1 sleeping
+	Mem:  46.13m total =  16.17m used +  29.96m free +   0.00k buffers
+	     246.56m max
+	           0 open files,        6 cpus available,       23 threads
 ====
 
 SEE ALSO
diff --git a/Documentation/cmd-show-connections.txt b/Documentation/cmd-show-connections.txt
index 177a915..b5d41bd 100644
--- a/Documentation/cmd-show-connections.txt
+++ b/Documentation/cmd-show-connections.txt
@@ -18,7 +18,9 @@
 
 ACCESS
 ------
-Caller must be a member of the privileged 'Administrators' group.
+Caller must be a member of the privileged 'Administrators' group,
+or have been granted
+link:access-control.html#capability_viewConnections[the 'View Connections' global capability].
 
 SCRIPTING
 ---------
@@ -26,8 +28,8 @@
 
 OPTIONS
 -------
+--numeric::
 -n::
-\--numeric::
 	Show client hostnames as IP addresses instead of DNS hostname.
 
 DISPLAY
@@ -35,7 +37,7 @@
 
 Session::
 	Unique session identifier on this server.  Session
-	identifiers have a period of 2\^32-1 and start from a
+	identifiers have a period of 2^32-1 and start from a
 	random value.
 
 Start::
diff --git a/Documentation/cmd-show-queue.txt b/Documentation/cmd-show-queue.txt
index 3e3638e..f99e342 100644
--- a/Documentation/cmd-show-queue.txt
+++ b/Documentation/cmd-show-queue.txt
@@ -8,8 +8,8 @@
 SYNOPSIS
 --------
 [verse]
-'ssh' -p <port> <host> 'ps'
 'ssh' -p <port> <host> 'gerrit show-queue'
+'ssh' -p <port> <host> 'ps'
 
 DESCRIPTION
 -----------
@@ -24,7 +24,14 @@
 
 ACCESS
 ------
-Caller must be a member of the privileged 'Administrators' group.
+End-users may see a task in the queue only if they can also see
+the project the task is associated with. Tasks operating on other
+projects, or that do not have a specific project are hidden.
+
+Members of the group 'Administrators', or any group that has been
+granted
+link:access-control.html#capability_viewQueue[the 'View Queue' capability]
+can see all queue entries.
 
 SCRIPTING
 ---------
@@ -36,7 +43,7 @@
 Task::
 	Unique task identifier on this server.	May be passed into
 	link:cmd-kill.html[kill] to cancel or terminate the task.
-	Task identifiers have a period of 2\^32-1, and start from
+	Task identifiers have a period of 2^32-1, and start from
 	a random value.
 
 State::
diff --git a/Documentation/cmd-stream-events.txt b/Documentation/cmd-stream-events.txt
index fb54f67..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
@@ -32,11 +32,11 @@
 EXAMPLES
 --------
 
------
+====
   $ ssh -p 29418 review.example.com gerrit stream-events
   {"type":"comment-added",change:{"project":"tools/gerrit", ...}, ...}
   {"type":"comment-added",change:{"project":"tools/gerrit", ...}, ...}
------
+====
 
 SCHEMA
 ------
diff --git a/Documentation/cmd-suexec.txt b/Documentation/cmd-suexec.txt
index 98a66ad..baffd53 100644
--- a/Documentation/cmd-suexec.txt
+++ b/Documentation/cmd-suexec.txt
@@ -8,28 +8,38 @@
 SYNOPSIS
 --------
 [verse]
-'ssh' -p <port> 'Gerrit Code Review'@localhost -i <private host key> 'suexec' \--as <EMAIL> [\--from HOST:PORT] [\--] [COMMAND]
+'ssh' -p <port>
+  -i SITE_PATH/etc/ssh_host_rsa_key
+  '"Gerrit Code Review@localhost"'
+  'suexec'
+  --as <EMAIL>
+  [--from HOST:PORT]
+  [--]
+  [COMMAND]
 
 DESCRIPTION
 -----------
-The suexec command can only be invoked by the magic user Gerrit Code Review
-and permits executing any other command as any other registered user account.
+The suexec command can only be invoked by the magic user `Gerrit
+Code Review` and permits executing any other command as any other
+registered user account.
 
 OPTIONS
 -------
 
-\--as::
+--as::
 	Email address of the user you want to impersonate.
-\--from::
-	Hostname and port of the machine you want to impersonate the command
-	coming from.
+
+--from::
+	Hostname and port of the machine you want to impersonate
+	the command coming from.
+
 COMMAND::
 	Gerrit command you want to run.
 
 ACCESS
 ------
-Caller must be the magic user Gerrit Code Review using the SSH daemon's host key
-or a key on this daemon's peer host key ring.
+Caller must be the magic user Gerrit Code Review using the SSH
+daemon's host key or a key on this daemon's peer host key ring.
 
 SCRIPTING
 ---------
@@ -40,9 +50,13 @@
 
 Approve the change with commit c0ff33 as "Verified +1" as user bob@example.com
 =====
-	$ sudo -u gerrit ssh -p 29418 -i site_path/etc/ssh_host_rsa_key \
-	'Gerrit Code Review'@localhost suexec --as bob@example.com -- \
-	gerrit approve --verified=+1 c0ff33
+  $ sudo -u gerrit ssh -p 29418 \
+    -i site_path/etc/ssh_host_rsa_key \
+    "Gerrit Code Review@localhost" \
+    suexec \
+    --as bob@example.com \
+    -- \
+    gerrit approve --verified +1 c0ff33
 =====
 
 GERRIT
diff --git a/Documentation/config-contact.txt b/Documentation/config-contact.txt
index 845a1ba..5c633cf 100644
--- a/Documentation/config-contact.txt
+++ b/Documentation/config-contact.txt
@@ -69,11 +69,11 @@
 
 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.
 
-* link:http://android.git.kernel.org/?p=tools/gerrit-contactstore.git[gerrit-contactstore]
+* link:https://code.google.com/p/gerrit/source/checkout?repo=contactstore[gerrit-contactstore]
 
 Configure `'$site_path'/etc/gerrit.config` with the contact store's
 URL (in `contactstore.url`), and if needed, APPSEC value (in
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index 8f751b4..535aaa8 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -9,7 +9,7 @@
 
 [NOTE]
 The contents of the `etc/gerrit.config` file are cached at startup
-by Gerrit.  If you modify any propeties in this file, Gerrit needs
+by Gerrit.  If you modify any properties in this file, Gerrit needs
 to be restarted before it will use the new values.
 
 Sample `etc/gerrit.config`:
@@ -24,6 +24,54 @@
   diskbuffer = 10 m
 ----
 
+[[accounts]]Section accounts
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+[[accounts.visibility]]accounts.visibility::
++
+Controls visibility of other users' dashboard pages and
+completion suggestions to web users.
++
+If `ALL`, all users are visible to all other users, even
+anonymous users.
++
+If `SAME_GROUP`, only users who are also members of a group the
+current user is a member of are visible.
++
+If `VISIBLE_GROUP`, only users who are members of at least one group
+that is visible to the current user are visible.
++
+If `NONE`, no users other than the current user are visible.
++
+Default is `ALL`.
+
+[[addreviewer]]Section addreviewer
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+[[addreviewer.maxWithoutConfirmation]]addreviewer.maxWithoutConfirmation::
++
+The maximum number of reviewers a user can add at once by adding a
+group as reviewer without being asked to confirm the operation.
++
+If set to 0, the user will never be asked to confirm adding a group
+as reviewer.
++
+Default is 10.
++
+This setting only applies for adding reviewers in the Gerrit WebUI,
+but is ignored when adding reviewers with the
+link:cmd-set-reviewers.html[set-reviewers] command.
+
+[[addreviewer.maxAllowed]]addreviewer.maxAllowed::
++
+The maximum number of reviewers a user can add at once by adding a
+group as reviewer.
++
+If set to 0, there is no limit for the number of reviewers that can
+be added at once by adding a group as reviewer.
++
+Default is 20.
+
 [[auth]]Section auth
 ~~~~~~~~~~~~~~~~~~~~
 
@@ -43,7 +91,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.
@@ -165,6 +213,22 @@
 +
 Default is -1, permitting infinite time between authentications.
 
+[[auth.maxRegisterEmailTokenAge]]auth.maxRegisterEmailTokenAge::
++
+Time in seconds before an email verification token sent to a user in
+order to validate their email address expires.
++
+* s, sec, second, seconds
+* m, min, minute, minutes
+* h, hr, hour, hours
+* d, day, days
+* w, week, weeks (`1 week` is treated as `7 days`)
+* mon, month, months (`1 month` is treated as `30 days`)
+* y, year, years (`1 year` is treated as `365 days`)
+
++
+Default is 12 hours.
+
 [[auth.httpHeader]]auth.httpHeader::
 +
 HTTP header to trust the username from, or unset to select HTTP basic
@@ -205,7 +269,7 @@
 user login names.  Only used if auth.type is `HTTP`, `HTTP_LDAP`
 or `LDAP`.
 +
-This value can be set to a format string, where `\{0\}` is replaced
+This value can be set to a format string, where `{0}` is replaced
 with the login name.  E.g. "\{0\}+gerrit@example.com" with a user
 login name of "foo" will produce "foo+gerrit@example.com" during
 the first time user "foo" registers.
@@ -244,6 +308,36 @@
 +
 By default, unset/false.
 
+[[auth.trustContainerAuth]]auth.trustContainerAuth::
++
+If true then it is the responsibility of the container hosting
+Gerrit to authenticate users. In this case Gerrit will blindly trust
+the container.
++
+This parameter only affects git over http traffic. If set to false
+then Gerrit will do the authentication (using DIGEST authentication).
++
+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
 ~~~~~~~~~~~~~~~~~~~~~~
 
@@ -416,6 +510,13 @@
 cache automatically updates when a user first creates their account
 within Gerrit, so the cache expire time is largely irrelevant.
 
+cache `"permission_sort"`::
++
+Caches the order access control sections must be applied to a
+reference.  Sorting the sections can be expensive when regular
+expressions are used, so this cache remembers the ordering for
+each branch.
+
 cache `"projects"`::
 +
 Caches the project description records, from the `projects` table
@@ -497,9 +598,38 @@
 +
 Default is true, enabled.
 
+cache.projects.checkFrequency::
++
+How often project configuration should be checked for update from Git.
+Gerrit Code Review caches project access rules and configuration in
+memory, checking the refs/meta/config branch every checkFrequency
+minutes to see if a new revision should be loaded and used for future
+access. Values can be specified using standard time unit abbreviations
+('ms', 'sec', 'min', etc.).
++
+If set to 0, checks occur every time, which may slow down operations.
+Administrators may force the cache to flush with
+link:cmd-flush-caches.html[gerrit flush-caches].
++
+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
+triggered. The submit button will be enabled or disabled according to
+the result.
+
+----
+[changeMerge]
+  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.
@@ -717,6 +847,15 @@
 Default on JGit is false. Although potentially slower, it yields
 much more predictable behavior.
 
+[[core.asyncLoggingBufferSize]]core.asyncLoggingBufferSize::
++
+Size of the buffer to store logging events for asynchronous logging.
+Putting a larger value can protect threads from stalling when the
+AsyncAppender threads are not fast enough to consume the logging events
+from the buffer. It also protects from loosing log entries in this case.
++
+Default is 64 entries.
+
 [[database]]Section database
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -893,10 +1032,19 @@
 Local filesystem directory holding all Git repositories that
 Gerrit knows about and can process changes for.  A project
 entity in Gerrit maps to a local Git repository by creating
-the path string `"$\{basePath}/$\{project_name}.git"`.
+the path string `"${basePath}/${project_name}.git"`.
 +
 If relative, the path is resolved relative to `'$site_path'`.
 
+[[gerrit.allProjects]]gerrit.allProjects::
++
+Name of the permissions-only project defining global server
+access controls and settings. These are inherited into every
+other project managed by the running server. The name is
+relative to `gerrit.basePath`.
++
+Defaults to `All-Projects` if not set.
+
 [[gerrit.canonicalWebUrl]]gerrit.canonicalWebUrl::
 +
 The default URL for Gerrit to be accessed through.
@@ -920,6 +1068,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
@@ -953,36 +1111,70 @@
 [[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::
+[[gitweb.revision]]gitweb.revision::
 +
 Optional pattern to use for constructing the gitweb URL when pointing
 at a specific commit when `custom` is used above.
 +
-Valid replacements are `$\{project\}` for the project name in Gerrit
-and `$\{commit\}` for the SHA1 hash for the commit.
+Valid replacements are `${project}` for the project name in Gerrit
+and `${commit}` for the SHA1 hash for the commit.
 
-[[gitweb.type]]gitweb.project::
+[[gitweb.project]]gitweb.project::
 +
 Optional pattern to use for constructing the gitweb URL when pointing
 at a specific project when `custom` is used above.
 +
-Valid replacements are `$\{project\}` for the project name in Gerrit.
+Valid replacements are `${project}` for the project name in Gerrit.
 
-[[gitweb.type]]gitweb.branch::
+[[gitweb.branch]]gitweb.branch::
 +
 Optional pattern to use for constructing the gitweb URL when pointing
 at a specific branch when `custom` is used above.
 +
-Valid replacements are `$\{project\}` for the project name in Gerrit
-and `$\{branch\}` for the name of the branch.
+Valid replacements are `${project}` for the project name in Gerrit
+and `${branch}` for the name of the branch.
 
+[[gitweb.filehistory]]gitweb.filehistory::
++
+Optional pattern to use for constructing the gitweb URL when pointing
+at the history of a file in a specific branch when `custom` is used
+above.
++
+Valid replacements are `${project}` for the project name in Gerrit,
+`${file}` for the file name 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].
 
@@ -1040,7 +1232,7 @@
 [[httpd.listenUrl]]httpd.listenUrl::
 +
 Specifies the URLs the internal HTTP daemon should listen for
-connections on.  The special hostname '\*' may be used to listen
+connections on.  The special hostname '*' may be used to listen
 on all local addresses.  A context path may optionally be included,
 placing Gerrit Code Review's web address within a subdirectory of
 the server.
@@ -1269,6 +1461,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
@@ -1290,17 +1492,17 @@
 Query pattern to use when searching for a user account.  This may be
 any valid LDAP query expression, including the standard `(&...)` and
 `(|...)` operators.  If auth.type is `HTTP_LDAP` then the variable
-`$\{username\}` is replaced with a parameter set to the username
+`${username}` is replaced with a parameter set to the username
 that was supplied by the HTTP server.  If auth.type is `LDAP` then
-the variable `$\{username\}` is replaced by the string entered by
+the variable `${username}` is replaced by the string entered by
 the end user.
 +
 This pattern is used to search the objects contained directly under
 the `ldap.accountBase` tree.  A typical setting for this parameter
-is `(uid=$\{username\})` or `(cn=$\{username\})`, but the proper
+is `(uid=${username})` or `(cn=${username})`, but the proper
 setting depends on the LDAP schema used by the directory server.
 +
-Default is `(uid=$\{username\})` for RFC 2307 servers,
+Default is `(uid=${username})` for RFC 2307 servers,
 and `(&(objectClass=user)(sAMAccountName=${username}))`
 for Active Directory.
 
@@ -1313,7 +1515,7 @@
 +
 Attribute values may be concatenated with literal strings, for
 example to join given name and surname together use the pattern
-`$\{givenName\} $\{SN\}`.
+`${givenName} ${SN}`.
 +
 If set, users will be unable to modify their full name field, as
 Gerrit will populate it only from the LDAP data.
@@ -1330,7 +1532,7 @@
 Attribute values may be concatenated with literal strings,
 for example to set the email address to the lowercase form
 of sAMAccountName followed by a constant domain name, use
-`$\{sAMAccountName.toLowerCase\}@example.com`.
+`${sAMAccountName.toLowerCase}@example.com`.
 +
 If set, the preferred email address will be prefilled from LDAP,
 but users may still be able to register additional email address,
@@ -1348,12 +1550,12 @@
 SSH clients will default to.
 +
 Attribute values may also be forced to lowercase, or to uppercase in
-an expression.  For example, `$\{sAMAccountName.toLowerCase\}` will
+an expression.  For example, `${sAMAccountName.toLowerCase}` will
 force the value of sAMAccountName, if defined, to be all lowercase.
 The suffix `.toUpperCase` can be used for the other direction.
 The suffix `.localPart` can be used to split attribute values of
 the form 'user@example.com' and return only the left hand side, for
-example `$\{userPrincipalName.localPart\}` would provide only 'user'.
+example `${userPrincipalName.localPart}` would provide only 'user'.
 +
 If set, users will be unable to modify their SSH username field, as
 Gerrit will populate it only from the LDAP data.
@@ -1391,11 +1593,11 @@
 Query pattern used when searching for an LDAP group to connect
 to a Gerrit group.  This may be any valid LDAP query expression,
 including the standard `(&...)` and `(|...)` operators.  The variable
-`$\{groupname\}` is replaced with the search term supplied by the
+`${groupname}` is replaced with the search term supplied by the
 group owner.
 +
-Default is `(cn=$\{groupname\})` for RFC 2307,
-and `(&(objectClass=group)(cn=$\{groupname\}))` for Active Directory.
+Default is `(cn=${groupname})` for RFC 2307,
+and `(&(objectClass=group)(cn=${groupname}))` for Active Directory.
 
 [[ldap.groupMemberPattern]]ldap.groupMemberPattern::
 +
@@ -1403,17 +1605,35 @@
 account is currently a member of.  This may be any valid LDAP query
 expression, including the standard `(&...)` and `(|...)` operators.
 +
-If auth.type is `HTTP_LDAP` then the variable `$\{username\}` is
+If auth.type is `HTTP_LDAP` then the variable `${username}` is
 replaced with a parameter set to the username that was supplied
 by the HTTP server.  Other variables appearing in the pattern,
-such as `$\{fooBarAttribute\}`, are replaced with the value of the
+such as `${fooBarAttribute}`, are replaced with the value of the
 corresponding attribute (in this case, `fooBarAttribute`) as read
 from the user's account object matched under `ldap.accountBase`.
-Attributes such as `$\{dn\}` or `$\{uidNumber\}` may be useful.
+Attributes such as `${dn}` or `${uidNumber}` may be useful.
 +
-Default is `(memberUid=$\{username\})` for RFC 2307,
+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
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -1423,8 +1643,8 @@
 If set to true, files with the MIME type `<name>` will be sent as
 direct downloads to the user's browser, rather than being wrapped up
 inside of zipped archives.  The type name may be a complete type
-name, e.g. `image/gif`, a generic media type, e.g. `image/\*`,
-or the wildcard `\*/*` to match all types.
+name, e.g. `image/gif`, a generic media type, e.g. `image/*`,
+or the wildcard `*/*` to match all types.
 +
 By default, false for all MIME types.
 
@@ -1471,14 +1691,17 @@
 
 [[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. It also contains some advanced options for tuning the
+behavior of Gerrit's 'receive-pack' mechanism.
 
 ----
 [receive]
   allowGroup = GROUP_ALLOWED_TO_EXECUTE
   allowGroup = YET_ANOTHER_GROUP_ALLOWED_TO_EXECUTE
+  maxObjectSizeLimit = 40 m
 ----
 
 [[receive.allowGroup]]receive.allowGroup::
@@ -1489,20 +1712,48 @@
 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.
+
+[[receive.threadPoolSize]]receive.threadPoolSize::
++
+Maximum size of the thread pool in which the change data in received packs is
+processed.
++
+Defaults to the number of available CPUs according to the Java runtime.
+
+[[receive.timeout]]receive.timeout::
++
+Overall timeout on the time taken to process the change data in
+received packs. Only includes the time processing Gerrit changes
+and updating references, not the time to index the pack. Values can
+be specified using standard time unit abbreviations ('ms', 'sec',
+'min', etc.).
++
+Default is 2 minutes. If no unit is specified, millisconds
+is assumed.
+
 
 [[repository]]Section repository
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 Repositories in this sense are the same as projects.
 
-In the following example configuration the `Administrators` and the
-`Registered Users` groups are set to be the ones to be allowed to
-create projects matching `*` (any project).  `Registered Users` is
-set to be the default owner of new projects.
+In the following example configuration `Registered Users` is set
+to be the default owner of new projects.
 
 ----
 [repository "*"]
-  createGroup = Administrators
-  createGroup = Registered Users
   ownerGroup = Registered Users
 ----
 
@@ -1510,24 +1761,22 @@
 Currently only the repository name `*` is supported.
 This is a wildcard designating all repositories.
 
-[[repository.name.createGroup]]repository.<name>.createGroup::
-+
-A name of a group which exists in the database. Zero, one or many
-groups are allowed.  Each on its own line.  Groups which don't exist
-in the database are ignored.
-+
-If no groups are declared (or only non-existing ones), the default
-value `Administrators` is used.
-
 [[repository.name.ownerGroup]]repository.<name>.ownerGroup::
 +
 A name of a group which exists in the database. Zero, one or many
 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 no groups are declared (or only non-existing ones), it defaults
-to whatever is declared by `repository.<name>.createGroup` (including
-any fallback to `Administrators`.)
+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
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -1554,7 +1803,7 @@
 +
 * `MIXED`
 +
-Shorthand for `$\{user\} (Code Review) <review@example.com>` where
+Shorthand for `${user} (Code Review) <review@example.com>` where
 `review@example.com` is the same as <<user.email,user.email>>.
 See below for a description of how the replacement is handled.
 +
@@ -1570,7 +1819,7 @@
 If set to a name and email address in brackets, Gerrit will use
 this name and email address for any messages, overriding the name
 that may have been selected for commits by user.name and user.email.
-Optionally, the name portion may contain the placeholder `$\{user\}`,
+Optionally, the name portion may contain the placeholder `${user}`,
 which is replaced by the Full Name of the current user.
 
 +
@@ -1640,6 +1889,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
 ~~~~~~~~~~~~~~~~~~~~~
 
@@ -1653,14 +1921,34 @@
 * 'hostname':'port' (for example `review.example.com:29418`)
 * 'IPv4':'port' (for example `10.0.0.1:29418`)
 * ['IPv6']:'port' (for example `[ff02::1]:29418`)
-* \*:'port' (for example `*:29418`)
+* *:'port' (for example `*:29418`)
 
 +
 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::
++
+Specifies the addresses clients should be told to connect to.
+This may differ from sshd.listenAddress if a firewall based port
+redirector is being used, making Gerrit appear to answer on port
+22. The following forms may be used to specify an address.  In any
+form, `:'port'` may be omitted to use the default SSH port of 22.
++
+* 'hostname':'port' (for example `review.example.com:22`)
+* 'IPv4':'port' (for example `10.0.0.1:29418`)
+* ['IPv6']:'port' (for example `[ff02::1]:29418`)
+
++
+If multiple values are supplied, the daemon will advertise all
+of them.
++
+By default, sshd.listenAddress.
+
 [[sshd.reuseAddress]]sshd.reuseAddress::
 +
 If true, permits the daemon to bind to the port even if the port
@@ -1688,8 +1976,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
@@ -1710,7 +1999,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
@@ -1781,21 +2070,22 @@
 [[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,
-or a user to a group.
+If `true`, visible user accounts (according to the value of
+`accounts.visibility`) will be offered as completion suggestions
+when adding a reviewer to a change, or a user to a group.
 +
-If `SAME_GROUP`, only users who are also members of a group the
-current user is a member of will be offered.
+If `false`, account suggestion is disabled.
 +
-If `VISIBLE_GROUP`, only users who are members of at least one group
-that is visible to the current user will be offered.
+Older configurations may also have one of the `accounts.visibility`
+values for this field, including `OFF` as a synonym for `NONE`. If
+`accounts.visibility` is also set, that value overrides this one;
+otherwise, this value applies to both `suggest.accounts` and
+`accounts.visibility`.
 +
-If `OFF`, no account suggestions are given.
-+
-Default is `ALL`.
+New configurations should prefer the boolean value for this field
+and an enum value for `accounts.visibility`.
 
 [[theme]] Section theme
 ~~~~~~~~~~~~~~~~~~~~~~~
@@ -1864,7 +2154,7 @@
 this section, existing changes must be reindexed with the
 link:pgm-ScanTrackingIds.html[ScanTrackingIds] program.
 
-The tracking ids are serachable using tr:<tracking id> or
+The tracking ids are searchable using tr:<tracking id> or
 bug:<tracking id>.
 
 ----
@@ -1882,7 +2172,9 @@
 [[trackingid.name.footer]]trackingid.<name>.footer::
 +
 A prefix tag that identify the footer line to parse for tracking ids.
-Several trakingid entries can have the same footer tag.
+Several trackingid entries can have the same footer tag. A single
+trackingid entry can have multiple footer tags. If multiple footer
+tags are specified, each tag will be parsed separately.
 (the trailing ":" is optional)
 
 [[trackingid.name.match]]trackingid.<name>.match::
@@ -1924,7 +2216,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.
@@ -1964,9 +2256,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
@@ -1975,6 +2274,9 @@
 
 Sample `etc/secure.config`:
 ----
+[auth]
+  registerEmailPrivateKey = 2zHNrXE2bsoylzUqDxZp0H1cqUmjgWb6
+
 [database]
   username = webuser
   password = s3kr3t
@@ -2001,6 +2303,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
 ----------------------
 
@@ -2028,64 +2340,6 @@
 * link:config-headerfooter.html[Site Header/Footer]
 * link:config-replication.html[Git Replication/Mirroring]
 
-Not User Serviceable
-~~~~~~~~~~~~~~~~~~~~
-
-These fields generally shouldn't be modified.
-
-register_email_private_key::
-+
-Private key used to sign the links emailed to users when they
-request to register a new email address on their user account.
-When the link is activated, the private key authenticates the link
-was created and sent by this Gerrit server, proving that the user
-can receive email at the address they are registering.
-+
-This column is automatically generated when the database is
-initialized.  Changing it to a new value would cause all current
-links to be invalidated.
-+
-Changing it is not recommended.
-
-admin_group_id::
-+
-Unique identity of the group with full privileges.  Any user who
-is a member of this group may manage any other group, any project,
-and other system settings over the web.
-+
-This is initialized by Gerrit to be the "Administrators" group.
-+
-Changing it is not recommended.
-
-anonymous_group_id::
-+
-Unique identity of the group for anonymous (not authenticated) users.
-+
-All users are a member of this group, whether or not they are
-actually signed in to Gerrit.  Any access rights assigned to
-this group are inherited by all users.
-+
-This is initialized by Gerrit to be the "Anonymous Users" group.
-+
-Changing it is not recommended.
-
-registered_group_id::
-+
-Unique identity of the group for all authenticated users.
-+
-All signed-in users are a member of this group.  Any access rights
-assigned to this group are inherited by all users once they have
-authenticated to Gerrit.
-+
-Since account registration is open and fairly easy to obtain,
-moving from the "Anonymous Users" group to this group is not
-very difficult.  Caution should be taken when assigning any
-permissions to this group.
-+
-This is initialized by Gerrit to be the "Registered Users" group.
-+
-Changing it is not recommended.
-
 GERRIT
 ------
 Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/config-hooks.txt b/Documentation/config-hooks.txt
index fd2ae82..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
@@ -75,6 +75,15 @@
   ref-updated --oldrev <old rev> --newrev <new rev> --refname <ref name> --project <project name> --submitter <submitter>
 ====
 
+cla-signed
+~~~~~~~~~~~
+
+Called whenever a user signs a contributor license agreement
+
+====
+  cla-signed --submitter <submitter> --user-id <user_id> --cla-id <cla_id>
+====
+
 
 Configuration Settings
 ----------------------
@@ -91,7 +100,7 @@
 -------------------
 
 If link:config-gerrit.html#gerrit.canonicalWebUrl[gerrit.canonicalWebUrl]
-is not set in `gerrit.config` the `\--change-url` flag may not be
+is not set in `gerrit.config` the `--change-url` flag may not be
 passed to all hooks.  Hooks started out of an SSH context (for example
 the patchset-created hook) don't know the server's web URL, unless
 this variable is configured.
diff --git a/Documentation/config-mail.txt b/Documentation/config-mail.txt
index 168bbfe..8aa7d08 100644
--- a/Documentation/config-mail.txt
+++ b/Documentation/config-mail.txt
@@ -83,6 +83,14 @@
 related to a user submitting a new patchset for a change.  It is a
 `ChangeEmail`: see `ChangeSubject.vm` and `ChangeFooter.vm`.
 
+Restored.vm
+~~~~~~~~~~~
+
+The `Restored.vm` template will determine the contents of the email related
+to a change being restored.  It is a `ChangeEmail`: see `ChangeSubject.vm` and
+`ChangeFooter.vm`.
+
+
 
 Mail Variables and Methods
 --------------------------
diff --git a/Documentation/config-replication.txt b/Documentation/config-replication.txt
index 53c69a5..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
@@ -84,7 +88,7 @@
 threads in the thread pool, Gerrit pushes to all URLs in parallel,
 using one thread per URL.
 +
-Within each URL value the magic placeholder `$\{name}` is replaced
+Within each URL value the magic placeholder `${name}` is replaced
 with the Gerrit project name.  This is a Gerrit specific extension
 to the otherwise standard Git URL syntax and it must be included
 in each URL so that Gerrit can figure out where each project needs
@@ -123,16 +127,16 @@
 [[remote.name.push]]remote.<name>.push::
 +
 Standard Git refspec denoting what should be replicated.  Setting this
-to `+refs/heads/\*:refs/heads/\*` would mirror only the active
+to `+refs/heads/*:refs/heads/*` would mirror only the active
 branches, but not the change refs under `refs/changes/`, or the tags
 under `refs/tags/`.
 +
 Multiple push keys can be supplied, to specify multiple patterns
 to match against.  In the example file above, remote "pubmirror"
-uses two push keys to match both `refs/heads/\*` and `refs/tags/*`,
+uses two push keys to match both `refs/heads/*` and `refs/tags/*`,
 but excludes all others, including `refs/changes/*`.
 +
-Defaults to `+refs/\*:refs/*` (all refs) if not specified.
+Defaults to `+refs/*:refs/*` (all refs) if not specified.
 
 [[remote.name.timeout]]remote.<name>.timeout::
 +
@@ -193,6 +197,22 @@
 By default, replicates without group control, i.e replicates
 everything to all remotes.
 
+[[remote.name.replicatePermissions]]remote.<name>.replicatePermissions::
++
+If true, permissions-only projects and the refs/meta/config branch
+will also be replicated to the remote site.  These projects and
+branches may be needed to keep a backup or slave server current.
++
+By default, true, replicating everything.
+
+[[remote.name.mirror]]remote.<name>.mirror::
++
+If true, replication will remove remote branches that absent locally
+or invisible to the replication (i.e. read access denied via 'authGroup'
+option).
++
+By default, false, do not remove remote branches.
+
 
 [[secure_config]]File `secure.config`
 -----------------------------------------------
diff --git a/Documentation/config-sso.txt b/Documentation/config-sso.txt
index 37f5b05..9aa06be 100644
--- a/Documentation/config-sso.txt
+++ b/Documentation/config-sso.txt
@@ -88,7 +88,7 @@
 standard `Authorization` HTTP header.
 
 The auth.emailFormat field ('optional') sets the preferred email
-address during first login.  Gerrit will replace `\{0\}` with the
+address during first login.  Gerrit will replace `{0}` with the
 username, as obtained from the Authorization header.  A format such
 as shown in the example would be typical, to add the domain name
 of the organization.
@@ -152,7 +152,7 @@
 single sign-on or security group to ensure the setting is correct.
 
 The auth.emailFormat field ('optional') sets the user's preferred
-email address when they first login.  Gerrit will replace `\{0\}`
+email address when they first login.  Gerrit will replace `{0}`
 with the username, as supplied by Siteminder.  A format such as
 shown in the example would be typical, to add the domain name of
 the organization.
diff --git a/Documentation/dev-contributing.txt b/Documentation/dev-contributing.txt
new file mode 100644
index 0000000..2609b05
--- /dev/null
+++ b/Documentation/dev-contributing.txt
@@ -0,0 +1,243 @@
+Gerrit Code Review - Contributing
+=================================
+
+Gerrit is developed as a self-hosting open source project and
+very much welcomes contributions from anyone with a contributor's
+agreement on file with the project.
+
+* https://gerrit-review.googlesource.com/
+
+The Contributor License Agreements:
+
+* https://gerrit-review.googlesource.com/static/cla_individual.html
+* https://gerrit-review.googlesource.com/static/cla_corporate.html
+
+As Gerrit is a code review tool, naturally contributions will
+be reviewed before they will get submitted to the code base.  To
+start your contribution, please make a git commit and upload it
+for review to the main Gerrit review server.  To help speed up the
+review of your change, review these guidelines before submitting
+your change.  You can view the pending Gerrit contributions and
+their statuses here:
+
+* https://gerrit-review.googlesource.com/#/q/status:open+project:gerrit,n,z
+
+Depending on the size of that list it might take a while for
+your change to get reviewed.  Naturally there are fewer
+approvers than contributors; so anything that you can do to
+ensure that your contribution will undergo fewer revisions
+will speed up the contribution process.  This includes helping
+out reviewing other people's changes to relieve the load from
+the approvers.  Even if you are not familiar with Gerrit's
+internals, it would be of great help if you can download, try
+out, and comment on new features.  If it works as advertised,
+say so, and if you have the priviliges to do so, go ahead
+and give it a +1 Verified.  If you would find the feature
+useful, say so and give it a +1 code review.
+
+And finally, the quicker you respond to the comments of your
+reviewers, the quicker your change might get merged!  Try to
+reply to every comment after submitting your new patch,
+particularly if you decided against making the suggested change.
+Reviewers don't want to seem like nags and pester you if you
+haven't replied or made a fix, so it helps them know if you
+missed it or decided against it.
+
+
+Review Criteria
+---------------
+
+Here are some hints as to what approvers may be looking for
+before approving or submitting changes to the Gerrit project.
+Let's start with the simple nit picky stuff.  You are likely
+excited that your code works; help us share your excitement
+by not distracting us with the simple stuff.  Thanks to Gerrit,
+problems are often highlighted and we find it hard to look
+beyond simple spacing issues.  Blame it on our short attention
+spans, we really do want your code.
+
+
+Commit Message
+--------------
+
+It is essential to have a good commit message if you want your
+change to be reviewed.
+
+  * Keep lines no longer than 72 chars
+  * Start with a short one line summary
+  * Followed by a blank line
+  * Followed by one or more explanatory paragraphs
+  * Use the present tense (fix instead of fixed)
+  * Include a Bug: Issue <#> line if fixing a Gerrit issue
+  * Include a Change-Id line
+
+
+A sample good Gerrit commit message:
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+====
+  Add sample commit message to guidelines doc
+
+  The original patch set for the contributing guidelines doc did not
+  include a sample commit message, this new patchset does.  Hopefully this
+  makes things a bit clearer since examples can sometimes help when
+  explanations don't.
+
+  Note that the body of this commit message can be several paragraphs, and
+  that I word wrap it at 72 characters.  Also note that I keep the summary
+  line under 50 characters since it is often truncated by tools which
+  display just the git summary.
+
+  Bug: Issue 98765605
+  Change-Id: Ic4a7c07eeb98cdeaf44e9d231a65a51f3fceae52
+====
+
+
+Style
+-----
+
+The basic coding style is covered by the tools/GoogleFormat.xml
+doc, see the link:dev-eclipse.html#Formatting[Eclipse Setup]
+for that.
+
+Highlighted/additional styling notes:
+
+  * It is generally more important to match the style of the nearby
+    code which you are modifying than it is to match the style
+    in the formatting guidelines.  This is especially true within the
+    same file.
+  * Review your change in Gerrit to see if it highlights
+    mistakingly deleted/added spaces on lines, trailing spaces.
+  * Line length should be 80 or less, unless the code reads
+    better with something slightly longer.  Shorter lines not only
+    help reviewers who may use a tablet to review the code, but future
+    contributors may also like to open several editors side by
+    side while editing new changes.
+  * Use 2 spaces for indent (no tabs)
+  * Use brackets in all ifs, spaces before/after if parens.
+  * Use /** */ style Javadocs for variables.
+
+Additionally, you will notice that most of the newline spacing
+is fairly consistent throughout the code in Gerrit, it helps to
+stick to the blank line conventions.  Here are some specific
+examples:
+
+  * Keep a blank line between all class and method declarations.
+  * Do not add blank lines at the beginning or end of class/methods.
+  * Put a blank line between external import sources, but not
+    between internal ones.
+
+
+Code Organization
+-----------------
+
+Do your best to organize classes and methods in a logical way.
+Here are some guidelines that Gerrit uses:
+
+  * Ensure a standard copyright header is included at the top
+    of any new files (copy it from another file, update the year).
+  * Always place loggers first in your class!
+  * Define any static interfaces next in your class.
+  * Define non static interfaces after static interfaces in your
+    class.
+  * Next you should define static types and members.
+  * Finally instance members, then constuctors, and then instance
+    methods.
+  * Some common exceptions are private helper static methods which
+    might appear near the instance methods which they help.
+  * Getters and setters for the same instance field should usually
+    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).
+
+Wow that's a lot!  But don't worry, you'll get the habit and most
+of the code is organized this way already; so if you pay attention
+to the class you are editing you will likely pick up on it.
+Naturally new classes are a little harder; you may want to come
+back and consult this section when creating them.
+
+
+Design
+------
+
+Here are some design level ojectives that you should keep in mind
+when coding:
+
+  * ORM entity objects should match exactly one row in the database.
+  * Most client pages should perform only one RPC to load so as to
+    keep latencies down.  Exceptions would apply to RPCs which need
+    to load large data sets if splitting them out will help the
+    page load faster.  Generally page loads are expected to complete
+    in under 100ms.  This will be the case for most operations,
+    unless the data being fetched is not using Gerrit's caching
+    infrastructure.  In these slower cases, it is worth considering
+    mitigating this longer load by using a second RPC to fill in
+    this data after the page is displayed (or alternatively it might
+    be worth proposing caching this data).
+  * @Inject should be used on constructors, not on fields.  The
+    current exceptions are the ssh commands, these were implemented
+    earlier in Gerrit's development.  To stay consistent, new ssh
+    commands should follow this older pattern; but eventually these
+    should get converted to eliminate this exception.
+  * Don't leave repository objects (git or schema) open.  A .close()
+    after every open should be placed in a finally{} block.
+  * Don't leave UI components, which can cause new actions to occur,
+    enabled during RPCs which update the DB.  This is to prevent
+    people from submitting actions more than once when operating
+    on slow links.  If the action buttons are disabled, they cannot
+    be resubmitted and the user can see that Gerrit is still busy.
+  * GWT EventBus is the new way forward.
+
+
+Tests
+-----
+
+  * Tests for new code will greatly help your change get approved.
+
+
+Change Size/Number of Files Touched
+-----------------------------------
+
+And finally, I probably cannot say enough about change sizes.
+Generally, smaller is better, hopefully within reason.  Do try to
+keep things which will be confusing on their own together,
+especially if changing one without the other will break something!
+
+  * If a new feature is implemented and it is a larger one, try to
+    identify if it can be split into smaller logical features; when
+    in doubt, err on the smaller side.
+  * Separate bug fixes from feature improvements.  The bug fix may
+    be an easy candidate for approval and should not need to wait
+    for new features to be approved.  Also, combining the two makes
+    reviewing harder since then there is no clear line between the
+    fix and the feature.
+  * Separate supporting refactoring from feature changes.  If your
+    new feature requires some refactoring, it helps to make the
+    refactoring a separate change which your feature change
+    depends on.  This way, reviewers can easily review the refactor
+    change as a something that should not alter the current
+    functionality, and feel more confident they can more easily
+    spot errors this way.  Of course, it also makes it easier to
+    test and locate later on if an unfortunate error does slip in.
+    Lastly, by not having to see refactoring changes at the same
+    time, it helps reviewers understand how your feature changes
+    the current functionality.
+  * Separate logical features into separate changes.  This
+    is often the hardest part.  Here is an example:  when adding a
+    new ability, make separate changes for the UI and the ssh
+    commands if possible.
+  * Do only what the commit message describes.  In other words, things which
+    are not strictly related to the commit message shouldn't be part of
+    a change, even trivial things like externalizing a string somewhere
+    or fixing a typo.  This help keep "git blame" more useful in the future
+    and it also makes "git revert" more useful.
+  * Use topic branches to link your separate changes together.
+
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/dev-design.txt b/Documentation/dev-design.txt
index 571ec6c..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
 -------------------
@@ -345,7 +353,7 @@
 to be used with the JSON-RPC interface.
 
 * link:http://json-rpc.org/wd/JSON-RPC-1-1-WD-20060807.html[JSON-RPC 1.1]
-* link:http://android.git.kernel.org/?p=tools/gwtjsonrpc.git;a=blob;f=README;hb=HEAD[XSRF JSON-RPC]
+* link:http://code.google.com/p/gerrit/source/browse/README?repo=gwtjsonrpc&name=master[XSRF JSON-RPC]
 
 
 Privacy Considerations
@@ -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/dev-eclipse.txt b/Documentation/dev-eclipse.txt
index 82d0a67..e239a63 100644
--- a/Documentation/dev-eclipse.txt
+++ b/Documentation/dev-eclipse.txt
@@ -13,9 +13,10 @@
 
 Install the Maven Integration plugins:
 
-http://m2eclipse.codehaus.org/[m2eclipse]
+http://www.eclipse.org/m2e/download/[m2eclipse]
 
 
+[[Formatting]]
 Code Formatter Settings
 -----------------------
 
diff --git a/Documentation/dev-readme.txt b/Documentation/dev-readme.txt
index 2ae9fac..552b5a8 100644
--- a/Documentation/dev-readme.txt
+++ b/Documentation/dev-readme.txt
@@ -11,7 +11,7 @@
 Create a new client workspace:
 
 ----
-  git clone git://android.git.kernel.org/tools/gerrit.git
+  git clone https://gerrit.googlesource.com/gerrit
   cd gerrit
 ----
 
diff --git a/Documentation/error-branch-not-found.txt b/Documentation/error-branch-not-found.txt
index 43d3546..e2dcff1 100644
--- a/Documentation/error-branch-not-found.txt
+++ b/Documentation/error-branch-not-found.txt
@@ -25,8 +25,8 @@
   'Admin' -> 'Projects' and browse your project, in the 'Branches'
   tab you can then create a new branch).
 
-Please note that you need the access right '+2 Create Branch' in the
-link:access-control.html#category_pHD['Push Branch'] category to create new branches.
+Please note that you need to be granted the
+link:access-control.html#category_create['Create reference'] access to create new branches.
 
 
 GERRIT
diff --git a/Documentation/error-not-a-gerrit-project.txt b/Documentation/error-not-a-gerrit-project.txt
index f6e90fa..368a102 100644
--- a/Documentation/error-not-a-gerrit-project.txt
+++ b/Documentation/error-not-a-gerrit-project.txt
@@ -16,10 +16,11 @@
 . Verify that you are pushing to the correct Gerrit server.
 . Go in the Gerrit WebUI to 'Admin' -> 'Projects' and check that the
   project is listed. If the project is not listed the project either
-  does not exist or you don't have read access ('+1 Read Access' in
-  the link:access-control.html#category_READ['Read Access'] category) for it. This means if you certain that
-  the project name is right you should contact the Gerrit
-  Administrator or project owner to request access to the project.
+  does not exist or you don't have
+  link:access-control.html#category_read['Read'] access for it. This
+  means if you certain that the project name is right you should
+  contact the Gerrit Administrator or project owner to request access
+  to the project.
 
 This error message might be misleading if the project actually exists
 but the push is failing because the pushing user has no read access
diff --git a/Documentation/error-not-allowed-to-upload-merges.txt b/Documentation/error-not-allowed-to-upload-merges.txt
index 3b52bc2..981ba91c 100644
--- a/Documentation/error-not-allowed-to-upload-merges.txt
+++ b/Documentation/error-not-allowed-to-upload-merges.txt
@@ -6,13 +6,14 @@
 project to which the push is done.
 
 If you need to upload merge commits, you can contact one of the
-project owners and request for this project permissions to upload
-merge commits (access right '+3 Upload merges permission' in the
-link:access-control.html#category_READ['Read Access'] category).
+project owners and request permissions to upload merge commits
+(access right link:access-control.html#category_push_merge['Push Merge Commit'])
+for this project.
 
 If one of your changes could not be merged in Gerrit due to conflicts
 and you created the merge commit to resolve the conflicts, you might
-want to revert the merge and instead of this do a link:http://www.kernel.org/pub/software/scm/git/docs/git-rebase.html[rebase].
+want to revert the merge and instead of this do a
+link:http://www.kernel.org/pub/software/scm/git/docs/git-rebase.html[rebase].
 
 
 GERRIT
diff --git a/Documentation/error-not-signed-off-by.txt b/Documentation/error-not-signed-off-by.txt
index 4f88774..00f179b 100644
--- a/Documentation/error-not-signed-off-by.txt
+++ b/Documentation/error-not-signed-off-by.txt
@@ -7,8 +7,8 @@
 required and the commit message does not contain it, Gerrit rejects
 to push the commit with this error message.
 
-This policy can be bypassed by having the access right '+2 Forge
-Committer or Tagger Identity' in the link:access-control.html#category_FORG['Forge Identity'] category.
+This policy can be bypassed by having the access right
+link:access-control.html#category_forge_committer['Forge Committer'].
 
 This error may happen for different reasons if you do not have the
 access right to forge the committer identity:
diff --git a/Documentation/error-prohibited-by-gerrit.txt b/Documentation/error-prohibited-by-gerrit.txt
index 90c937e..69f80c1 100644
--- a/Documentation/error-prohibited-by-gerrit.txt
+++ b/Documentation/error-prohibited-by-gerrit.txt
@@ -8,15 +8,17 @@
 In particular this error occurs:
 
 1. if you push a commit for code review to a branch for which you
-   don't have upload permissions (access right '+2 Upload permission'
-   in the link:access-control.html#category_READ['Read Access'] category)
-2. if you bypass code review without sufficient privileges in the
-   link:access-control.html#category_pHD['Push Branch'] category
-3. if you push a signed or annotated tag without sufficient
-   privileges in the link:access-control.html#category_pTAG['Push Tag'] category
-4. if you push a lightweight tag without the access right '+2 Create
-   Branch' for the reference name 'refs/tags/*' in the link:access-control.html#category_pHD['Push Branch']
-   category
+   don't have upload permissions (access right
+   link:access-control.html#category_push_review['Push'] on
+   `refs/for/refs/heads/*`)
+2. if you bypass code review without
+   link:access-control.html#category_push_direct['Push'] access right
+   on `refs/heads/*`
+3. if you push an annotated tag without
+   link:access-control.html#category_push_annotated['Push Annotated Tag']
+   access right on 'refs/tags/*'
+4. if you push a lightweight tag without the access right link:access-control.html#category_create['Create
+   Reference'] for the reference name 'refs/tags/*'
 
 For new users it happens often that they accidentally try to bypass
 code review. The push then fails with the error message 'prohibited
diff --git a/Documentation/error-upload-denied.txt b/Documentation/error-upload-denied.txt
index 947280f..5dec8ab 100644
--- a/Documentation/error-upload-denied.txt
+++ b/Documentation/error-upload-denied.txt
@@ -8,8 +8,8 @@
 There are two possibilities how to continue in this situation:
 
 . contact one of the project owners and request upload permissions
-  for the project (access right '+2 Upload permission' in the
-  link:access-control.html#category_READ['Read Access'] category)
+  for the project (access right
+  link:access-control.html#category_push['Push'])
 . export your commit as a patch using the link:http://www.kernel.org/pub/software/scm/git/docs/git-format-patch.html[git format-patch] command
   and provide the patch file to one of the project owners
 
diff --git a/Documentation/error-you-are-not-author.txt b/Documentation/error-you-are-not-author.txt
index 47a7652..a245252 100644
--- a/Documentation/error-you-are-not-author.txt
+++ b/Documentation/error-you-are-not-author.txt
@@ -5,8 +5,8 @@
 the author matches one of the registered e-mail addresses of the
 pushing user. If this is not the case pushing the commit fails with
 the error message "you are not author ...". This policy can be
-bypassed by having the access right '+1 Forge Author Identity' in the
-link:access-control.html#category_FORG['Forge Identity'] category.
+bypassed by having the access right
+link:access-control.html#category_forge_author['Forge Author'].
 
 This error may happen for two reasons:
 
@@ -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/error-you-are-not-committer.txt b/Documentation/error-you-are-not-committer.txt
index 26e0cae..b5b8c44 100644
--- a/Documentation/error-you-are-not-committer.txt
+++ b/Documentation/error-you-are-not-committer.txt
@@ -5,8 +5,8 @@
 the committer matches one of the registered e-mail addresses of the
 pushing user. If this is not the case pushing the commit fails with
 the error message "you are not committer ...". This policy can be
-bypassed by having the access right '+2 Forge Committer or Tagger
-Identity' in the link:access-control.html#category_FORG['Forge Identity'] category.
+bypassed by having the access right
+link:access-control.html#category_forge_committer['Forge Committer'].
 
 This error may happen for two reasons:
 
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/images/intro-quick-central-gerrit.dia b/Documentation/images/intro-quick-central-gerrit.dia
new file mode 100644
index 0000000..8192ba9
--- /dev/null
+++ b/Documentation/images/intro-quick-central-gerrit.dia
Binary files differ
diff --git a/Documentation/images/intro-quick-central-gerrit.png b/Documentation/images/intro-quick-central-gerrit.png
new file mode 100644
index 0000000..61b9638
--- /dev/null
+++ b/Documentation/images/intro-quick-central-gerrit.png
Binary files differ
diff --git a/Documentation/images/intro-quick-central-repo.dia b/Documentation/images/intro-quick-central-repo.dia
new file mode 100644
index 0000000..9916dbb
--- /dev/null
+++ b/Documentation/images/intro-quick-central-repo.dia
Binary files differ
diff --git a/Documentation/images/intro-quick-central-repo.png b/Documentation/images/intro-quick-central-repo.png
new file mode 100644
index 0000000..84ffeb0
--- /dev/null
+++ b/Documentation/images/intro-quick-central-repo.png
Binary files differ
diff --git a/Documentation/images/intro-quick-hot-key-help.jpg b/Documentation/images/intro-quick-hot-key-help.jpg
new file mode 100644
index 0000000..41bcbe4
--- /dev/null
+++ b/Documentation/images/intro-quick-hot-key-help.jpg
Binary files differ
diff --git a/Documentation/images/intro-quick-new-review.jpg b/Documentation/images/intro-quick-new-review.jpg
new file mode 100644
index 0000000..99e6c55
--- /dev/null
+++ b/Documentation/images/intro-quick-new-review.jpg
Binary files differ
diff --git a/Documentation/images/intro-quick-review-2-patches.jpg b/Documentation/images/intro-quick-review-2-patches.jpg
new file mode 100644
index 0000000..29c99cc
--- /dev/null
+++ b/Documentation/images/intro-quick-review-2-patches.jpg
Binary files differ
diff --git a/Documentation/images/intro-quick-review-line-comment.jpg b/Documentation/images/intro-quick-review-line-comment.jpg
new file mode 100644
index 0000000..eeb144a
--- /dev/null
+++ b/Documentation/images/intro-quick-review-line-comment.jpg
Binary files differ
diff --git a/Documentation/images/intro-quick-reviewing-the-change.jpg b/Documentation/images/intro-quick-reviewing-the-change.jpg
new file mode 100644
index 0000000..bfded9e
--- /dev/null
+++ b/Documentation/images/intro-quick-reviewing-the-change.jpg
Binary files differ
diff --git a/Documentation/images/intro-quick-verifying.jpg b/Documentation/images/intro-quick-verifying.jpg
new file mode 100644
index 0000000..7679c0a
--- /dev/null
+++ b/Documentation/images/intro-quick-verifying.jpg
Binary files differ
diff --git a/Documentation/index.txt b/Documentation/index.txt
index 1ad51a3..5143bf7 100644
--- a/Documentation/index.txt
+++ b/Documentation/index.txt
@@ -1,6 +1,11 @@
 Gerrit Code Review for Git
 ==========================
 
+Getting Started
+---------------
+
+* link:intro-quick.html[A Quick Introduction To Gerrit]
+
 User Guide
 ----------
 
@@ -13,12 +18,14 @@
 * 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
 ------------
 
 * link:licenses.html[Licenses and Notices]
 * link:install.html[Installation Guide]
+* link:install-quick.html[Quick Installation in 10 Minutes]
 * link:project-setup.html[Project Setup]
 
 Configuration
@@ -39,6 +46,7 @@
 
 * link:dev-readme.html[Developer Setup]
 * link:dev-eclipse.html[Eclipse Setup]
+* link:dev-contributing.html[Contributing to Gerrit]
 * link:dev-design.html[System Design]
 * link:i18n-readme.html[i18n Support]
 
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/install-quick.txt b/Documentation/install-quick.txt
new file mode 100644
index 0000000..6bea7f8
--- /dev/null
+++ b/Documentation/install-quick.txt
@@ -0,0 +1,333 @@
+Gerrit Code Review - Quick get started guide
+============================================
+
+****
+This guide was made with the impatient in mind, ready to try out Gerrit on their
+own server but not prepared to make the full installation procedure yet.
+
+Explanation is sparse and you should not use a server installed this way in a
+live setup, this is made with proof of concept activities in mind.
+
+It is presumed you install it on a Unix based server such as any of the Linux
+flavors or BSD.
+
+It's also presumed that you have access to an OpenID enabled email address.
+Examples of OpenID enable email providers are gmail, yahoo and hotmail.
+It's also possible to register a custom email address with OpenID, but that is
+outside the scope of this quick installation guide. For testing purposes one of
+the above providers should be fine. Please note that network access to the
+OpenID provider you choose is necessary for both you and your Gerrit instance.
+****
+
+
+[[requirements]]
+Requirements
+------------
+
+Most distributions come with Java today. Do you already have Java installed?
+
+----
+  $ java -version
+  java version "1.6.0_26"
+  Java(TM) SE Runtime Environment (build 1.6.0_26-b03-384-10M3425)
+  Java HotSpot(TM) 64-Bit Server VM (build 20.1-b02-384, mixed mode)
+----
+
+If Java isn't installed, get it:
+
+* JDK, minimum version 1.6 http://www.oracle.com/technetwork/java/javase/downloads/index.html[Download]
+
+
+[[user]]
+Create a user to host the Gerrit service
+----------------------------------------
+
+We will run the service as a non privileged user on your system.
+First create the user and then become the user:
+
+----
+  $ sudo adduser gerrit2
+  $ sudo su gerrit2
+----
+
+If you don't have root privileges you could skip this step and run gerrit
+as your own user as well.
+
+
+[[download]]
+Download Gerrit
+---------------
+
+It's time to download the archive that contains the gerrit web and ssh service.
+
+You can choose from different versions to download from here:
+
+* http://code.google.com/p/gerrit/downloads/list[A list of releases available]
+
+This tutorial is based on version 2.2.2, and you can download that from this link
+
+* http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.2.2.war[Link to the 2.2.2 war archive]
+
+
+[[initialization]]
+Initialize the Site
+-------------------
+
+It's time to run the initialization, and with the batch switch enabled, we don't have to answer any questions at all:
+
+----
+  gerrit2@host:~$ java -jar gerrit.war init --batch -d ~/gerrit_testsite
+  Generating SSH host key ... rsa(simple)... done
+  Initialized /home/gerrit2/gerrit_testsite
+  Executing /home/gerrit2/gerrit_testsite/bin/gerrit.sh start
+  Starting Gerrit Code Review: OK
+  gerrit2@host:~$
+----
+
+When the init is complete, you can review your settings in the
+file `'$site_path/etc/gerrit.config'`.
+
+An important setting will be the canonicalWebUrl which will
+be needed later to access gerrit's web interface.
+
+----
+  gerrit2@host:~$ cat ~/gerrit_testsite/etc/gerrit.config | grep canonical
+  canonicalWebUrl = http://localhost:8080/
+  gerrit2@host:~$
+----
+[[usersetup]]
+The first user
+--------------
+
+It's time to exit the gerrit2 account as you now have Gerrit running on your
+host and setup your first workspace.
+
+Start a shell with the credentials of the account you will perform
+development under.
+
+Check whether there are any ssh keys already. You're looking for two files,
+id_rsa and id_rsa.pub.
+
+----
+  user@host:~$ ls .ssh
+  authorized_keys  config  id_rsa  id_rsa.pub  known_hosts
+  user@host:~$
+----
+
+If you have the files, you may skip the key generating step.
+
+If you don't see the files in your listing, your will have to generate rsa
+keys for your ssh sessions:
+
+SSH key generation
+~~~~~~~~~~~~~~~~~~
+
+*Please don't generate new keys if you already have a valid keypair!*
+*They will be overwritten!*
+
+----
+  user@host:~$ ssh-keygen -t rsa
+  Generating public/private rsa key pair.
+  Enter file in which to save the key (/home/user/.ssh/id_rsa):
+  Created directory '/home/user/.ssh'.
+  Enter passphrase (empty for no passphrase):
+  Enter same passphrase again:
+  Your identification has been saved in /home/user/.ssh/id_rsa.
+  Your public key has been saved in /home/user/.ssh/id_rsa.pub.
+  The key fingerprint is:
+  00:11:22:00:11:22:00:11:44:00:11:22:00:11:22:99 user@host
+  The key's randomart image is:
+  +--[ RSA 2048]----+
+  |     ..+.*=+oo.*E|
+  |      u.OoB.. . +|
+  |       ..*.      |
+  |       o         |
+  |      . S ..     |
+  |                 |
+  |                 |
+  |          ..     |
+  |                 |
+  +-----------------+
+  user@host:~$
+----
+
+Registering your key in Gerrit
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Open a browser and enter the canonical url you got before when
+initializing Gerrit.
+
+----
+  Canonical URL                [http://localhost:8080/]:
+----
+
+Register a new account in Gerrit through the web interface with the
+email address of your choice.
+The first user to sign-in and register an account will be
+automatically placed into the fully privileged Administrators group,
+permitting server management over the web and over SSH.  Subsequent
+users will be automatically registered as unprivileged users.
+
+Once signed in as your user, you find a little wizard to get you started.
+The wizard helps you fill out:
+
+* Real name (visible name in Gerrit)
+* Register your email (it must be confirmed later)
+* Select a username with which to communicate with Gerrit over ssh+git
+
+* The server will ask you for an RSA public key.
+That's the key we generated above, and it's time to make sure that Gerrit knows
+about our new key and can identify us by it.
+
+----
+  user@host:~$ cat .ssh/id_rsa.pub
+  ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEA5E785mWtMckorP5v40PyFeui9T50dKpaGYw67Mlv2J3aGBG3tS0qBQxKEpiV0J4+W0RgQHbWfNqdUYen9bC5VVH/GatYWkpL9TjjUcHzF1rX3Eyv7PHuHLAyd/8Zdv6R3saF+hNpp1JW0BSa7HXzK7iNCVA3kBuBthxeGh3OoFbaXHn1zwwVQw8I5+Lp9OOIY7sJEsM/kW699XDV6z2zlkByNVEp45j+g26x5rCnGS8GJM7A0uHsaWJddO6TiyR6/2SOBF1VtKw49XLTQcmDInFAZzUsAZSDKlfYloPkpA6YdqeG0eJqau+jtzuigydoVj4j9xidcJ9HtxZcJNuraw== user@host
+  user@host:~$
+----
+
+Copy the string starting with ssh-rsa to your clipboard and then paste it
+into the box for RSA keys. Make *absolutely sure* no extra spaces or line feeds
+are entered in the middle of the RSA string.
+
+Verify that the ssh connection works for you.
+
+----
+  user@host:~$ ssh user@localhost -p 29418
+  The authenticity of host '[localhost]:29418 ([127.0.0.1]:29418)' can't be established.
+  RSA key fingerprint is db:07:3d:c2:94:25:b5:8d:ac:bc:b5:9e:2f:95:5f:4a.
+  Are you sure you want to continue connecting (yes/no)? yes
+  Warning: Permanently added '[localhost]:29418' (RSA) to the list of known hosts.
+
+  ****    Welcome to Gerrit Code Review    ****
+
+  Hi user, you have successfully connected over SSH.
+
+  Unfortunately, interactive shells are disabled.
+  To clone a hosted Git repository, use:
+
+  git clone ssh://user@localhost:29418/REPOSITORY_NAME.git
+
+  user@host:~$
+----
+
+Project creation
+----------------
+
+Your base Gerrit server is now running and you have a user that's ready
+to interact with it.  You now have two options, either you create a new
+test project to work with or you already have a git with history that
+you would like to import into gerrit and try out code review on.
+
+New project from scratch
+~~~~~~~~~~~~~~~~~~~~~~~~
+If you choose to create a new repository from scratch, it's easier for
+you to create a project with an initial commit in it. That way first
+time setup between client and server is easier.
+
+This is done via the SSH port:
+
+----
+  user@host:~$ ssh -p 29418 user@localhost gerrit create-project --empty-commit --name demo-project
+  user@host:~$
+----
+
+This will create a repository that you could clone to work with.
+
+Already existing project
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+The other alternative is if you already have a git project that you
+want to try out Gerrit on.
+First you have to create the project, this is done via the SSH port:
+
+----
+  user@host:~$ ssh -p 29418 user@localhost gerrit create-project --name demo-project
+  user@host:~$
+----
+
+You need to make sure that at least initially your account is granted
+"Create Reference" privileges for the refs/heads/* reference.
+This is done via the web interface in the Admin/Projects/Access page
+that correspond to your project.
+
+After that it's time to upload the previous history to the server:
+
+----
+  user@host:~/my-project$ git push ssh://user@localhost:29418/demo-project *:*
+  Counting objects: 2011, done.
+  Writing objects: 100% (2011/2011), 456293 bytes, done.
+  Total 2011 (delta 0), reused 0 (delta 0)
+  To ssh://user@localhost:29418/demo-project
+   * [new branch]      master -> master
+  user@host:~/my-project$
+----
+
+This will create a repository that you could clone to work with.
+
+
+My first change
+---------------
+
+Download a local clone of the repository and move into it
+
+----
+  user@host:~$ git clone ssh://user@host:29418/demo-project
+  Cloning into demo-project...
+  remote: Counting objects: 2, done
+  remote: Finding sources: 100% (2/2)
+  remote: Total 2 (delta 0), reused 0 (delta 0)
+  user@host:~$ cd demo-project
+  user@host:~/demo-project$
+----
+
+Then make a change to it and upload it as a reviewable change in Gerrit.
+
+----
+  user@host:~/demo-project$ date > testfile.txt
+  user@host:~/demo-project$ git add testfile.txt
+  user@host:~/demo-project$ git commit -m "My pretty test commit"
+  [master ff643a5] My pretty test commit
+   1 files changed, 1 insertions(+), 0 deletions(-)
+   create mode 100644 testfile.txt
+  user@host:~/demo-project$
+----
+
+Usually when you push to a remote git, you push to the reference
+`'/refs/heads/branch'`, but when working with Gerrit you have to push to a
+virtual branch representing "code review before submittal to branch".
+This virtual name space is known as /refs/for/<branch>
+
+----
+  user@host:~/demo-project$ git push origin HEAD:refs/for/master
+  Counting objects: 4, done.
+  Writing objects: 100% (3/3), 293 bytes, done.
+  Total 3 (delta 0), reused 0 (delta 0)
+  remote:
+  remote: New Changes:
+  remote:   http://localhost:8080/1
+  remote:
+  To ssh://user@localhost:29418/demo-project
+   * [new branch]      HEAD -> refs/for/master
+  user@host:~/demo-project$
+----
+
+You should now be able to access your change by browsing to the http URL
+suggested above, http://localhost:8080/1
+
+
+Quick Installation Complete
+---------------------------
+
+This covers the scope of getting Gerrit started and your first change uploaded.
+It doesn't give any clue as to how the review workflow works, please find
+link:http://source.android.com/submit-patches/workflow[Default Workflow] to
+learn more about the workflow of Gerrit.
+
+To read more on the installation of Gerrit please read link:install.html[the detailed
+installation page].
+
+
+GERRIT
+------
+
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/install.txt b/Documentation/install.txt
index 6d477bb..b90bbce 100644
--- a/Documentation/install.txt
+++ b/Documentation/install.txt
@@ -1,8 +1,17 @@
 Gerrit Code Review - Installation Guide
 =======================================
 
-You need a SQL database to house the review metadata.  Currently H2,
-MySQL and PostgreSQL are the only supported databases.
+[[requirements]]
+Requirements
+-----------
+To run the Gerrit service, the following requirements must be met on
+the host:
+
+* JDK, minimum version 1.6 http://www.oracle.com/technetwork/java/javase/downloads/index.html[Download]
+
+You'll also need a SQL database to house the review metadata. You have the
+choice of either using the embedded H2 or to host your own MySQL or PostgreSQL.
+
 
 [[download]]
 Download Gerrit
@@ -20,6 +29,7 @@
 If you would prefer to build Gerrit directly from source, review
 the notes under link:dev-readme.html[developer setup].
 
+
 [[createdb]]
 Database Setup
 --------------
@@ -28,15 +38,27 @@
 H2
 ~~
 
-During init Gerrit will automatically configure the embedded H2
-database.  No additional configuration is necessary.  Using the
-embedded H2 database is the easiest way to get a Gerrit site up
-and running.
+During the init phase of Gerrit you will need to specify which database to use.
+If you choose H2, Gerrit will automatically set up the embedded H2 database as
+backend so no set up in advance is necessary.  Also, no additional configuration is
+necessary.  Using the embedded H2 database is the easiest way to get a Gerrit
+site up and running, making it ideal for proof of concepts or small team
+servers.  On the flip side, H2 is not the recommended option for large
+corporate installations. This is because there is no easy way to interact
+with the database while Gerrit is offline, it's not easy to backup the data,
+and it's not possible to set up H2 in a load balanced/hotswap configuration.
+
+
+If this option interests you, you might want to consider link:install-quick.html[the quick guide].
 
 [[createdb_postgres]]
 PostgreSQL
 ~~~~~~~~~~
 
+This option is more complicated than the H2 option but is recommended
+for larger installations. It's the database backend with the largest userbase
+in the Gerrit community.
+
 Create a user for the web application within Postgres, assign it a
 password, create a database to store the metadata, and grant the user
 full rights on the newly created database:
@@ -46,10 +68,14 @@
   createdb -E UTF-8 -O gerrit2 reviewdb
 ----
 
+
 [[createdb_mysql]]
 MySQL
 ~~~~~
 
+This option is also more complicated than the H2 option. Just as with
+PostgreSQL it's also recommended for larger installations.
+
 Create a user for the web application within the database, assign it a
 password, create a database, and give the newly created user full
 rights on it:
@@ -74,6 +100,11 @@
 to as `'$site_path'`.  If the embedded H2 database is being used,
 its data files will also be stored under this directory.
 
+You also have to decide where to store your server side git repositories. This
+can either be a relative path under `'$site_path'` or an absolute path
+anywhere on your server system. You have to choose a place before commencing
+your init phase.
+
 Initialize a new site directory by running the init command, passing
 the path of the site directory to be created as an argument to the
 '-d' option.  Its recommended that Gerrit Code Review be given its
@@ -82,23 +113,27 @@
 ----
   sudo adduser gerrit2
   sudo su gerrit2
-  cd ~gerrit2
 
-  java -jar gerrit.war init -d review_site
+  java -jar gerrit.war init -d /path/to/your/gerrit_application_directory
 ----
 
-If run from an interactive terminal, 'init' will prompt through a
+'Please note:' If you choose a location where your new user doesn't
+have any privileges, you may have to manually create the directory first and
+then give ownership of that location to the `'gerrit2'` user.
+
+If run from an interactive terminal, the init command will prompt through a
 series of configuration questions, including gathering information
-about the database created above.  If the terminal is not interactive
-init will make some reasonable default selections, and will use the
-embedded H2 database.
+about the database created above.  If the terminal is not interactive,
+running the init command will choose some reasonable default selections,
+and will use the embedded H2 database. Once the init phase is complete,
+you can review your settings in the file `'$site_path/etc/gerrit.config'`.
 
-Init may need to download additional JARs to support optional selected
-functionality.  If a download fails a URL will be displayed and init
-will wait for the user to manually download the JAR and store it in
-the target location.
+When running the init command, additional JARs might be downloaded to
+support optional selected functionality.  If a download fails a URL will
+be displayed and init will wait for the user to manually download the JAR
+and store it in the target location.
 
-When 'init' is complete, the daemon will be automatically started
+When the init phase is complete, the daemon will be automatically started
 in the background and your web browser will open to the site:
 
 ----
@@ -116,6 +151,14 @@
 users will be automatically registered as unprivileged users.
 
 
+Installation Complete
+---------------------
+
+Your base Gerrit server is now installed and running.  You're now ready to
+either set up more projects or start working with the projects you've already
+imported.
+
+
 [[project_setup]]
 Project Setup
 -------------
diff --git a/Documentation/intro-quick.txt b/Documentation/intro-quick.txt
new file mode 100644
index 0000000..3d5cbcb
--- /dev/null
+++ b/Documentation/intro-quick.txt
@@ -0,0 +1,390 @@
+Gerrit Code Review - A Quick Introduction
+=========================================
+
+Gerrit is a web-based code review tool built on top of the git version
+control system, but if you've got as far as reading this guide then
+you probably already know that. The purpose of this introduction is to
+allow you to answer the question, is Gerrit the right tool for me?
+Will it fit in my work flow and in my organization?
+
+What is Gerrit?
+---------------
+
+I assume that if you're reading this then you're already convinced of
+the benefits of code review in general but want some technical support
+to make it easy. Code reviews mean different things to different people.
+To some it's a formal meeting with a projector and an entire team
+going through the code line by line. To others it's getting someone to
+glance over the code before it is committed.
+
+Gerrit is intended to provide a light weight framework for reviewing
+every commit before it is accepted into the code base. Changes are
+uploaded to Gerrit but don't actually become a part of the project
+until they've been reviewed and accepted. In many ways this is simply
+tooling to support the standard open source process of submitting
+patches which are then reviewed by the project members before being
+applied to the code base. However Gerrit goes a step further making it
+simple for all committers on a project to ensure that changes are
+checked over before they're actually applied. Because of this Gerrit
+is equally useful where all users are trusted committers such as may
+the case with closed-source commercial development. Either way it's
+still desirable to have code reviewed to improve the quality and
+maintainability of the code. After all, if only one person has seen
+the code it may be a little difficult to maintain when that person
+leaves.
+
+Gerrit is firstly a staging area where changes can be checked over
+before becoming a part of the code base. It is also an enabler for
+this review process, capturing notes and comments about the changes to
+enable discussion of the change. This is particularly useful with
+distributed teams where this conversation can't happen face to face.
+Even with a co-located team having a review tool as an option is
+beneficial because reviews can be done at a time that is convenient
+for the reviewer. This allows the developer to create the review and
+explain the change while it is fresh in their mind. Without such a
+tool they either need to interrupt someone to review the code or
+switch context to explain the change when they've already moved on to
+the next task.
+
+This also creates a lasting record of the conversation which can be
+useful for answering the inevitable "I know we changed this for a
+reason" questions.
+
+Where does Gerrit fit in?
+-------------------------
+
+Any team with more than one member has a central source repository of
+some kind (or they should). Git can theoretically work without such a
+central location but in practice there is usually a central
+repository. This serves as the authoritative copy of what is actually in
+the project. This is what everyone fetches from and pushes to and is
+generally where build servers and other such tools get the source
+from.
+
+.Central Source Repository
+image::images/intro-quick-central-repo.png[Authoritative Source Repository]
+
+Gerrit is deployed in place of this central repository and adds an
+additional concept, a store of pending changes. Everyone still fetches
+from the authoritative repository but instead of pushing back to it,
+they push to this pending changes location. A change can only be submitted
+into the authoritative repository and become an accepted part of the project
+once the change has been reviewed and approved.
+
+.Gerrit in place of Central Repository
+image::images/intro-quick-central-gerrit.png[Gerrit in place of Central Repository]
+
+Like any repository hosting solution, Gerrit has a powerful
+link:access-control.html[access control model.]
+Users can even be granted access to push directly into the central
+repository, bypassing code review entirely. Gerrit can even be used
+without code review, used simply to host the repositories and
+controlling access. But generally it's just simpler and safer to go
+through the review process even for users who are allowed to directly
+push.
+
+The Life and Times of a Change
+------------------------------
+
+The easiest way to get a feel for how Gerrit works is to follow a
+change through its entire life cycle. For the purpose of this example
+we'll assume that the Gerrit Server is running on a server called
++gerrithost+ with the HTTP interface on port +8080+ and the SSH
+interface on port +29418+. The project we'll be working on is called
++RecipeBook+ and we'll be developing a change for the +master+ branch.
+
+Cloning the Repository
+~~~~~~~~~~~~~~~~~~~~~~
+
+Obviously the first thing we need to do is get the source that we're
+going to be modifying. As with any git project you do this by cloning
+the central repository that Gerrit is hosting. e.g.
+
+----
+$ git clone ssh://gerrithost:29418/RecipeBook.git RecipeBook
+Cloning into RecipeBook...
+----
+
+Then we need to make our actual change and commit it locally. Gerrit
+doesn't really change anything here, this is just the standard editing
+and git. While not strictly required, it's best to include a Change-Id
+in your commit message so that Gerrit can link together different
+versions of the same change being reviewed. Gerrit contains a standard
+link:user-changeid.html[Change-Id commit-msg hook]
+that will generate a unique Change-Id when you commit. If you don't do
+this then Gerrit will generate a Change-Id when you push your change
+for review. But because you don't have the Change-Id in your commit
+message you'll need to manually copy it in if you need to upload
+another version of your change. Because of this it's best to just
+install the hook and forget about it.
+
+Creating the Review
+~~~~~~~~~~~~~~~~~~~
+
+Once you've made your change and committed it locally it's time to
+push it to Gerrit so that it can be reviewed. This is done with a git
+push to the Gerrit server. Since we cloned our local repository
+directly from Gerrit it is the origin so we don't have to redefine the
+remote.
+
+----
+$ <work>
+$ git commit
+[master 9651f22] Change to a proper, yeast based pizza dough.
+ 1 files changed, 3 insertions(+), 2 deletions(-)
+$ git push origin HEAD:refs/for/master
+Counting objects: 5, done.
+Delta compression using up to 8 threads.
+Compressing objects: 100% (2/2), done.
+Writing objects: 100% (3/3), 542 bytes, done.
+Total 3 (delta 0), reused 0 (delta 0)
+remote: 
+remote: New Changes:
+remote:   http://gerrithost:8080/68
+remote: 
+To ssh://gerrithost:29418/RecipeBook.git
+ * [new branch]      HEAD -> refs/for/master
+----
+
+The only different thing about this is the +refs/for/master+ branch.
+This is a magic branch that creates reviews that target the master
+branch. For every branch Gerrit tracks there is a magic
++refs/for/<branch_name>+ that you push to to create reviews.
+
+In the output of this command you'll notice that there is a link to
+the HTTP interface of the Gerrit server we just pushed to. This is the
+web interface where we will review this commit. Let's follow that link
+and see what we get.
+
+.Gerrit Code Review Screen
+image::images/intro-quick-new-review.jpg[Gerrit Review Screen]
+
+This is the Gerrit code review screen where someone will come to
+review the change. There isn't too much to see here yet, you can look
+at the diff of your change, add some comments explaining what you did
+and why, you may even add a list of people that should review the change.
+
+Reviewers can find changes that they want to review in any number of
+ways. Gerrit has a capable
+link:user-search.html[search]
+that allows project leaders (or anyone else) to find changes that need
+to be reviewed. Users can also setup watches on Gerrit projects with a
+search expression, this causes Gerrit to notify them of matching
+changes. So adding a reviewer when creating a review is just a
+recommendation.
+
+At this point the change is available for review and we need to switch
+roles to continue following the change. Now let's pretend we're the
+reviewer.
+
+Reviewing the Change
+~~~~~~~~~~~~~~~~~~~~
+
+The reviewer's life starts at the code review screen shown above. He
+can get here in a number of ways, but for some reason they've decided
+to review this change. Of particular note on this screen are the two
+"Need" lines:
+
+----
+* Need Verified
+* Need Code-Review
+----
+
+Gerrit's default work-flow requires two checks before a change is
+accepted. Code-Review is someone looking at the code, ensuring it
+meets the project guidelines, intent etc. Verifying is checking that
+the code actually compiles, unit tests pass etc. Verification is
+usually done by an automated build server rather than a person. There
+is even a
+link:https://wiki.jenkins-ci.org/display/JENKINS/Gerrit+Trigger[Gerrit Trigger Jenkins Plugin]
+that will automatically build each uploaded change and update the
+verified score accordingly.
+
+It is important to note that Code-Review and Verification are
+different permissions in Gerrit, allowing these tasks to be separated.
+For example, an automated process would have rights to verify but not
+to code-review.
+
+Since we are the code reviewer, we're going to review the code. To do
+this we can view it within the Gerrit web interface as either a
+unified or side-by-side diff by selecting the appropriate option. In
+the example below we've selected the side-by-side view. In either of
+these views you can add comments by double clicking on the line (or
+single click the line number) that you want to comment on. Once
+published these comments are viewable to all, allowing discussion
+of the change to take place.
+
+.Side By Side Patch View
+image::images/intro-quick-review-line-comment.jpg[Adding a Comment]
+
+Code reviewers end up spending a lot of time navigating these screens,
+looking at and commenting on these changes. To make this as efficient
+as possible Gerrit has keyboard shortcuts for most operations (and
+even some operations that are only accessible via the hot-keys). At
+any time you can hit the +?+ key to see the keyboard shortcuts.
+
+.Gerrit Hot Key Help
+image::images/intro-quick-hot-key-help.jpg[Hot Key Help]
+
+Once we've looked over the changes we need to complete reviewing the
+submission. To do this we click the _Review_ button on the change
+screen where we started. This allows us to enter a Code Review label
+and message.
+
+.Reviewing the Change
+image::images/intro-quick-reviewing-the-change.jpg[Reviewing the Change]
+
+The label that the reviewer selects determines what can happen next.
+The +1 and -1 level are just an opinion where as the +2 and -2 levels
+are allowing or blocking the change. In order for a change to be
+accepted it must have at least one +2 and no -2 votes.
+Although these are numeric values, they in no way accumulate;
+two +1s do not equate to a +2.
+
+Regardless of what label is selected, once the _Publish Comments_
+button has been clicked, the cover message and any comments on the
+files become visible to all users.
+
+In this case the change was not accepted so the creator needs to
+rework it. So let's switch roles back to the creator where we
+started.
+
+Reworking the Change
+~~~~~~~~~~~~~~~~~~~~
+
+As long as we set up the
+link:user-changeid.html[Change-Id commit-msg hook]
+before we uploaded the change, re-working it is easy. All we need
+to do to upload a re-worked change is to push another commit that has
+the same Change-Id in the message. Since the hook added a Change-ID in
+our initial commit we can simply checkout and then amend that commit.
+Then push it to Gerrit in the same way as we did to create the review. E.g.
+
+----
+$ <checkout first commit>
+$ <rework>
+$ git commit --amend
+$ git push origin HEAD:refs/for/master
+Counting objects: 5, done.
+Delta compression using up to 8 threads.
+Compressing objects: 100% (2/2), done.
+Writing objects: 100% (3/3), 546 bytes, done.
+Total 3 (delta 0), reused 0 (delta 0)
+To ssh://gerrithost:29418/RecipeBook.git
+ * [new branch]      HEAD -> refs/for/master
+----
+
+Note that the output is slightly different this time around. We don't
+get told about a new review because we're adding to an existing
+review. Having uploaded the reworked commit we can go back into the
+Gerrit web interface and look at our change.
+
+.Reviewing the Rework
+image::images/intro-quick-review-2-patches.jpg[Reviewing the Rework]
+
+If you look closely you'll notice that there are now two patch sets
+associated with this change, the initial submission and the rework.
+Rather than repeating ourselves lets assume that this time around the
+patch is given a +2 score by the code reviewer.
+
+Trying out the Change
+~~~~~~~~~~~~~~~~~~~~~
+
+With Gerrit's default work-flow there are two sign-offs, code review
+and verify. Verifying means checking that the change actually works.
+This would typically be checking that the code compiles, unit tests
+pass and similar checks. Really a project can decide how much or
+little they want to do here. It's also worth noting that this is only
+Gerrit's default work-flow, the verify check can actually be removed
+or others added.
+
+As mentioned in the code review section, verification is typically an
+automated process using the
+link:https://wiki.jenkins-ci.org/display/JENKINS/Gerrit+Trigger[Gerrit Trigger Jenkins Plugin]
+or similar. But there are times when the code needs to be manually
+verified, or the reviewer needs to check that something actually works
+or how it works. Sometimes it's just nice to work through the code in a
+development environment rather than the web interface. All of these
+involve someone needing to get the change into their development
+environment. Gerrit makes this process easy by exposing each change as
+a git branch. So all the reviewers need to do is fetch and checkout that
+branch from Gerrit and they will have the change.
+
+We don't even need to think about it that hard, if you look at the
+earlier screen shots of the Gerrit Code Review Screen you'll notice a
+_download_ command. All we need to do to get the change is copy
+paste this command and run it in our Gerrit checkout.
+
+----
+$ git fetch http://gerrithost:8080/p/RecipeBook refs/changes/68/68/2
+From http://gerrithost:8080/p/RecipeBook
+ * branch            refs/changes/68/68/2 -> FETCH_HEAD
+$ git checkout FETCH_HEAD
+Note: checking out 'FETCH_HEAD'.
+
+You are in 'detached HEAD' state. You can look around, make experimental
+changes and commit them, and you can discard any commits you make in this
+state without impacting any branches by performing another checkout.
+
+If you want to create a new branch to retain commits you create, you may
+do so (now or later) by using -b with the checkout command again. Example:
+
+  git checkout -b new_branch_name
+
+HEAD is now at d5dacdb... Change to a proper, yeast based pizza dough.
+----
+
+Easy as that, we now have the change in our working copy to play with.
+You might be interested in what the numbers of the refspec mean.
+
+* The first *68* is the id if the change +mod 100+.  The only reason
+for this initial number is to reduce the number of files in any given
+directory within the git repository.
+* The second *68* is the full id of the change. You'll notice this in
+the URL of the Gerrit review screen.
+* The *2* is the patch-set within the change. In this example we
+uploaded some fixes so we want the second patch set rather than the
+initial one which the reviewer rejected.
+
+Manually Verifying the Change
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+For simplicity we're just going to manually verify the change.
+The Verifier may be the same person as the code reviewer or a
+different person entirely. It really depends on the size of the
+project and what works. If you have Verify permission then when you
+click the _Review_ button in the Gerrit web interface you'll be
+presented with a verify score.
+
+.Verifying the Change
+image::images/intro-quick-verifying.jpg[Verifying the Change]
+
+Unlike the code review the verify check doesn't have a +2 or -2 level,
+it's either a pass or fail so all we need for the change to be
+submitted is a +1 score (and no -1's).
+
+Submitting the Change
+~~~~~~~~~~~~~~~~~~~~~
+
+You might have noticed that in the verify screen shot there are two
+buttons for submitting the score _Publish Comments_ and _Publish
+and Submit_. The publish and submit button is always visible, but will
+only work if the change meets the criteria for being submitted (I.e.
+has been both verified and code reviewed). So it's a convenience to be
+able to post review scores as well as submitting the change by clicking
+a single button. If you choose just to publish comments at this point then
+the score will be stored but the change won't yet be accepted into the code
+base. In this case there will be a _Submit Patch Set X_ button on the
+main screen. Just as Code Review and Verify are different operations
+that can be done by different users, Submission is a third operation
+that can be limited down to another group of users.
+
+Activating the _Publish and Submit_ or _Submit Patch Set X_ button
+will merge the change into the main part of the repository so that it
+becomes an accepted part of the project. After this anyone fetching
+the git repository will receive this change as a part of the master
+branch.
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
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/licenses.txt b/Documentation/licenses.txt
index 6442e9c..69018d8 100644
--- a/Documentation/licenses.txt
+++ b/Documentation/licenses.txt
@@ -36,6 +36,7 @@
 |Ehcache                    | <<apache2,Apache License 2.0>>
 |mime-util                  | <<apache2,Apache License 2.0>>
 |Jetty                      | <<apache2,Apache License 2.0>>, or link:http://www.eclipse.org/legal/epl-v10.html[EPL]
+|Prolog Cafe                | <<prolog_cafe,EPL or GPL>>
 |Google Code Prettify       | <<apache2,Apache License 2.0>>
 |JGit                       | <<jgit,New-Style BSD>>
 |JSch                       | <<sshd,New-Style BSD>>
@@ -399,6 +400,23 @@
 POSSIBILITY OF SUCH DAMAGE.
 ----
 
+[[prolog_cafe]]
+Prolog Cafe - EPL or GPL
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+Originally developed by Mutsunori BANBARA and Naoyuki TAMURA at the
+Kobe University, JAPAN. Gerrit Code Review uses a fork derived from
+the 1.2.5 release.
+
+Prolog Cafe is dual licensed and available under either the
+link:http://opensource.org/licenses/eclipse-1.0.php[Eclipse Public License],
+or the
+link:http://www.gnu.org/licenses/gpl-2.0.html[GPL version 2.0 (or later)].
+
+In the context of Gerrit Code Review, Prolog Cafe is consumed
+under either the EPL or GPL version 3.0 as GPL version 2.0 is
+not compatible with Apache License 2.0.
+
 [[h2]]
 H2 Database - EPL or modified MPL
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
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 ce9de71..5cbe6ba0 100644
--- a/Documentation/pgm-index.txt
+++ b/Documentation/pgm-index.txt
@@ -10,22 +10,34 @@
 --------------------
 
 link:pgm-init.html[init]::
-  Initialize a new Gerrit server installation
+	Initialize a new Gerrit server installation.
 
 link:pgm-daemon.html[daemon]::
-  Gerrit HTTP, SSH network server.
+	Gerrit HTTP, SSH network server.
 
 link:pgm-gsql.html[gsql]::
 	Administrative interface to idle database.
 
+link:pgm-prolog-shell.html[prolog-shell]::
+	Simple interactive Prolog interpreter.
+
+link:pgm-rulec.html[rulec]::
+	Compile project-specific Prolog rules to JARs.
+
+version::
+	Display the release version of Gerrit Code Review.
+
+Transition Utilities
+--------------------
+
 link:pgm-ExportReviewNotes.html[ExportReviewNotes]::
 	Export submitted review information to refs/notes/review.
 
 link:pgm-ScanTrackingIds.html[ScanTrackingIds]::
 	Rescan all changes after configuring trackingids.
 
-version::
-  Display the release version of Gerrit Code Review.
+link:pgm-LocalUsernamesToLowerCase.html[LocalUsernamesToLowerCase]::
+	Convert the local username of every account to lower case.
 
 GERRIT
 ------
diff --git a/Documentation/pgm-init.txt b/Documentation/pgm-init.txt
index 9644e92..c53c57d 100644
--- a/Documentation/pgm-init.txt
+++ b/Documentation/pgm-init.txt
@@ -11,7 +11,6 @@
 'java' -jar gerrit.war 'init'
 	-d <SITE_PATH>
 	[\--batch]
-	[\--import-projects]
 	[\--no-auto-start]
 
 DESCRIPTION
@@ -21,7 +20,7 @@
 into a newly created `$site_path`.
 
 If run an an existing `$site_path`, init will upgrade some resources
-as necessary.  This can be useful to import newly created projects.
+as necessary.
 
 OPTIONS
 -------
@@ -30,22 +29,16 @@
 	configuration defaults are chosen based on the whims of
 	the Gerrit developers.
 
-\--import-projects::
-	Recursively search
-	link:config-gerrit.html#gerrit.basePath[gerrit.basePath]
-	for any Git repositories not yet registered as a project,
-	and initializes a new project for them.
-
 \--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-prolog-shell.txt b/Documentation/pgm-prolog-shell.txt
new file mode 100644
index 0000000..f3fa2d8
--- /dev/null
+++ b/Documentation/pgm-prolog-shell.txt
@@ -0,0 +1,57 @@
+prolog-shell
+============
+
+NAME
+----
+prolog-shell - Simple interactive Prolog interpreter
+
+SYNOPSIS
+--------
+[verse]
+'java' -jar gerrit.war 'prolog-shell' [-s FILE.pl ...]
+
+DESCRIPTION
+-----------
+Provides a simple interactive Prolog interpreter for development
+and testing.
+
+OPTIONS
+-------
+-s::
+	Dynamically load the Prolog source code at startup,
+	as though the user had entered `['FILE.pl'].` into
+	the interepter once it was running. This option may
+	be supplied more than once to load multiple files.
+
+EXAMPLES
+--------
+Define a simple predicate and test it:
+
+====
+	$ cat >simple.pl
+	food(apple).
+	food(orange).
+	^D
+
+	$ java -jar gerrit.war prolog-shell -s simple.pl
+	Gerrit Code Review 2.2.1-84-ge9c3992 - Interactive Prolog Shell
+	based on Prolog Cafe 1.2.5 (mantis)
+	         Copyright(C) 1997-2009 M.Banbara and N.Tamura
+	(type Ctrl-D or "halt." to exit, "['path/to/file.pl']." to load a file)
+
+	{consulting /usr/local/google/users/sop/gerrit2/gerrit/simple.pl ...}
+	{/usr/local/google/users/sop/gerrit2/gerrit/simple.pl consulted 99 msec}
+
+	| ?- food(Type).
+
+	Type = apple ? ;
+
+	Type = orange ? ;
+
+	no
+	| ?-
+====
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/pgm-rulec.txt b/Documentation/pgm-rulec.txt
new file mode 100644
index 0000000..6d0a632
--- /dev/null
+++ b/Documentation/pgm-rulec.txt
@@ -0,0 +1,54 @@
+rulec
+=====
+
+NAME
+----
+rulec - Compile project-specific Prolog rules to JARs
+
+SYNOPSIS
+--------
+[verse]
+'java' -jar gerrit.war 'rulec' -d <SITE_PATH> [--all | <PROJECT>...]
+
+DESCRIPTION
+-----------
+Looks for a Prolog rule file named `rules.pl` on the repository's
+`refs/meta/config` branch. If rules.pl exists, creates a JAR file
+named `rules-'SHA1'.jar` in `'$site_path'/cache/rules`.
+
+OPTIONS
+-------
+-d::
+--site-path::
+	Location of the gerrit.config file, and all other per-site
+	configuration data, supporting libraries and log files.
+
+--all::
+	Compile rules for all projects.
+
+--quiet::
+	Suppress non-error output messages.
+
+<PROJECT>:
+	Compile rules for the specified project.
+
+CONTEXT
+-------
+This command can only be run on a server which has direct
+connectivity to the metadata database, and local access to the
+managed Git repositories.
+
+Caching needs to be enabled. See
+link:config-gerrit.html#cache.directory[cache.directory].
+
+EXAMPLES
+--------
+To compile a rule JAR file for test/project:
+
+====
+	$ java -jar gerrit.war rulec -d site_path test/project
+====
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/project-setup.txt b/Documentation/project-setup.txt
index e775e9a..3d979d3 100644
--- a/Documentation/project-setup.txt
+++ b/Documentation/project-setup.txt
@@ -1,10 +1,6 @@
 Gerrit Code Review - Project Configuration
 ==========================================
 
-All Git repositories under gerrit.basePath must be registered in
-the Gerrit database in order to be accessed through SSH, or through
-the web interface.
-
 Create Through SSH
 ------------------
 
@@ -22,7 +18,7 @@
 Manual Creation
 ---------------
 
-Projects may also be manually registered with the database.
+Projects may also be manually created.
 
 Create Git Repository
 ~~~~~~~~~~~~~~~~~~~~~
@@ -47,23 +43,10 @@
 Register Project
 ~~~~~~~~~~~~~~~~
 
-One insert is needed to register a project with Gerrit.
-
-[NOTE]
-Note that the `.git` suffix is not typically included in the
-project name, as it looks cleaner in the web when not shown.
-Gerrit automatically assumes that `project.git` is the Git repository
-for a project named `project`.
+Either restart the server, or flush the `project_list` cache:
 
 ====
-  INSERT INTO projects
-  (use_contributor_agreements
-   ,submit_type
-   ,name)
-  VALUES
-  ('N'
-  ,'M'
-  ,'new/project');
+  ssh -p 29418 localhost gerrit flush-caches --cache project_list
 ====
 
 [[submit_type]]
@@ -85,20 +68,19 @@
 
 * Merge If Necessary
 +
-This is the default for a new project (and why `\'M'` is suggested
-above in the insert statement).
+This is the default for a new project.
 +
 If the change being submitted is a strict superset of the destination
 branch, then the branch is fast-forwarded to the change.  If not,
 then a merge commit is automatically created.  This is identical
-to the classical `git merge` behavior, or `git merge \--ff`.
+to the classical `git merge` behavior, or `git merge --ff`.
 
 * Always Merge
 +
 Always produce a merge commit, even if the change is a strict
 superset of the destination branch.  This is identical to the
-behavior of `git merge \--no-ff`, and may be useful if the
-project needs to follow submits with `git log \--first-parent`.
+behavior of `git merge --no-ff`, and may be useful if the
+project needs to follow submits with `git log --first-parent`.
 
 * Cherry Pick
 +
@@ -117,13 +99,17 @@
 the right order since inter-change dependencies will not be
 enforced for them.
 
+When Gerrit tries to do a merge, by default the merge will only
+succeed if there is no path conflict. By selecting the checkbox
+`Automatically resolve conflicts` Gerrit will try do a content merge
+if a path conflict occurs.
+
 
 Registering Additional Branches
 -------------------------------
 
 Branches can be created over the SSH port by any `git push` client,
-if the user has been granted the `Push Branch` > `Create Branch`
-(or higher) access right.
+if the user has been granted the `Create Reference` access right.
 
 Additional branches can also be created through the web UI, assuming
 at least one commit already exists in the project repository.
diff --git a/Documentation/user-changeid.txt b/Documentation/user-changeid.txt
index 1fa627c..124ec31 100644
--- a/Documentation/user-changeid.txt
+++ b/Documentation/user-changeid.txt
@@ -57,7 +57,7 @@
 Change Upload
 --------------
 
-During upload by pushing to a `refs/for/\*` or `refs/heads/\*`
+During upload by pushing to a `refs/for/*` or `refs/heads/*`
 branch, Gerrit will use the Change-Id line to:
 
 * Create a new change
@@ -103,7 +103,7 @@
 Amending a commit
 ~~~~~~~~~~~~~~~~~
 
-When amending a commit with `git commit \--amend`, leave the
+When amending a commit with `git commit --amend`, leave the
 Change-Id line unmodified in the commit message.  This will allow
 Gerrit to automatically update the change with the amended commit.
 
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index ba031db..3aafe8c 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.
@@ -109,12 +109,9 @@
 [[branch]]
 branch:'BRANCH'::
 +
-Changes for 'BRANCH'.  The branch name is the short name shown
-in the web interface, without the traditional 'refs/heads/'
-prefix.  This operator is a shorthand for 'refs:'.  Searching for
-'branch:master' really means 'ref:refs/heads/master', and searching
-for 'branch:refs/heads/master' is the same as searching for
-'ref:refs/heads/refs/heads/master'.
+Changes for 'BRANCH'.  The branch name is either the short name shown
+in the web interface or the full name of the destination branch with
+the traditional 'refs/heads/' prefix.
 +
 If 'BRANCH' starts with `^` it matches branch names by regular
 expression patterns.  The
@@ -181,11 +178,11 @@
 anchoring the match to the start of the string.  To match all Java
 files, use `file:^.*\.java`.
 +
-The entire regular expression pattern, including the `\^` character,
+The entire regular expression pattern, including the `^` character,
 should be double quoted when using more complex construction (like
 ones using a bracket expression). For example, to match all XML
 files named like 'name1.xml', 'name2.xml', and 'name3.xml' use
-`\file:"\^name[1-3].xml"`.
+`file:"^name[1-3].xml"`.
 +
 Currently this operator is only available on a watched project
 and may not be used in the search bar.
@@ -314,8 +311,8 @@
 `label:CodeReview=+2`::
 `label:CodeReview+2`::
 +
-Matches changes where there is at least one \+2 score for Code Review.
-The \+ prefix is optional for positive score values.  If the + is used,
+Matches changes where there is at least one +2 score for Code Review.
+The + prefix is optional for positive score values.  If the + is used,
 the = operator is optional.
 
 `label:CodeReview=-2`::
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 8a08317..8e05e72 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
@@ -60,14 +61,25 @@
 port 29418, using the same hostname as the web server:
 
 ====
+..................................................................
   $ ssh -p 29418 sshusername@hostname
-  gerrit: no shell available
+
+    ****    Welcome to Gerrit Code Review    ****
+
+    Hi John Doe, you have successfully connected over SSH.
+
+    Unfortunately, interactive shells are disabled.
+    To clone a hosted Git repository, use:
+
+    git clone ssh://sshusername@hostname:29418/REPOSITORY_NAME.git
+
   Connection to hostname closed.
+..................................................................
 ====
 
-In the command above, `sshusername` was configured on the `SSH Keys`
-tab of the `Settings` screen.  If it is not set, propose a name
-and use `Change Username` to select the name.
+In the command above, `sshusername` was configured as `Username` on
+the `Profile` tab of the `Settings` screen.  If it is not set,
+propose a name and use `Select Username` to select the name.
 
 To determine the port number Gerrit is running on, visit the special
 information URL `http://'hostname'/ssh_info`, and copy the port
@@ -142,15 +154,15 @@
   $ 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`:
 
 ====
   git push --receive-pack='git receive-pack --reviewer=a@a.com --cc=b@o.com' tr:kernel/common HEAD:refs/for/experimental
 ====
 
-The `\--reviewer='email'` and `\--cc='email'` options may be
+The `--reviewer='email'` and `--cc='email'` options may be
 specified as many times as necessary to cover all interested
 parties.  Gerrit will automatically avoid sending duplicate email
 notifications, such as if one of the specified reviewers or CC
@@ -268,19 +280,26 @@
 * `refs/tags/*`: annotated tag objects pointing to any other type
 of Git object can be created.
 
-To push branches, the `Push Branch` project 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
@@ -323,17 +342,17 @@
 ~~~~~~~~~~~~~~~
 
 To replace changes, ensure Change-Id lines were created in the
-commit messages, and just use `repo upload` without the `\--replace`
-command line flag.  Gerrit Code Review will automatically match
-the commits back to their original changes by taking advantage of
-their Change-Id lines.
+commit messages, and just use `repo upload`.
+Gerrit Code Review will automatically match the commits back to
+their original changes by taking advantage of their Change-Id lines.
 
 If Change-Id lines are not present in the commit messages, consider
 amending the message and copying the line from the change's page
 on the web.
 
 If Change-Id lines are not available, then the user must use the much
-more manual mapping technique offered by `repo upload \--replace`.
+more <<manual_replacement_mapping,manual mapping technique>> offered
+by using `git push` to a specific `refs/changes/change#` reference.
 
 For more about Change-Ids, see link:user-changeid.html[Change-Id Lines].
 
@@ -345,8 +364,8 @@
 own process space, Gerrit maintains complete control over how the
 repository is updated, and what responses are sent to the `git push`
 client invoked by the end-user, or by `repo upload`.  This allows
-Gerrit to provide magical refs, such as `refs/for/\*` for new
-change submission and `refs/changes/\*` for change replacement.
+Gerrit to provide magical refs, such as `refs/for/*` for new
+change submission and `refs/changes/*` for change replacement.
 When a push request is received to create a ref in one of these
 namespaces Gerrit performs its own logic to update the database,
 and then lies to the client about the result of the operation.
diff --git a/ReleaseNotes/Makefile b/ReleaseNotes/Makefile
index 219498b..5137b59 100644
--- a/ReleaseNotes/Makefile
+++ b/ReleaseNotes/Makefile
@@ -15,7 +15,7 @@
 ASCIIDOC       ?= asciidoc
 ASCIIDOC_EXTRA ?=
 SVN            ?= svn
-PUB_ROOT       ?= https://gerrit.googlecode.com/svn/ReleaseNotes
+PUB_ROOT       ?= https://gerrit-documentation.googlecode.com/svn/ReleaseNotes
 
 DOC_HTML      := $(patsubst %.txt,%.html,$(wildcard ReleaseNotes*.txt))
 COMMIT        := $(shell git describe HEAD | sed s/^v//)
diff --git a/ReleaseNotes/ReleaseNotes-2.1.7.2.txt b/ReleaseNotes/ReleaseNotes-2.1.7.2.txt
index 2d7622e..9172b4a 100644
--- a/ReleaseNotes/ReleaseNotes-2.1.7.2.txt
+++ b/ReleaseNotes/ReleaseNotes-2.1.7.2.txt
@@ -1,9 +1,9 @@
-Release notes for Gerrit 2.1.7.1
+Release notes for Gerrit 2.1.7.2
 ================================
 
-Gerrit 2.1.7.1 is now available:
+Gerrit 2.1.7.2 is now available:
 
-link:http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.1.7.1.war[http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.1.7.1.war]
+link:http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.1.7.2.war[http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.1.7.2.war]
 
 Bug Fixes
 ---------
diff --git a/ReleaseNotes/ReleaseNotes-2.2.0.txt b/ReleaseNotes/ReleaseNotes-2.2.0.txt
new file mode 100644
index 0000000..58605fe
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.2.0.txt
@@ -0,0 +1,65 @@
+Release notes for Gerrit 2.2.0
+==============================
+
+Gerrit 2.2.0 is now available:
+
+link:http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.2.0.war[http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.2.0.war]
+
+Schema Change
+-------------
+*WARNING:* Upgrading to 2.2.0 requires the server be first upgraded
+to 2.1.7, and then to 2.2.0.
+
+*WARNING:* This release contains schema changes.  To upgrade:
+----
+  java -jar gerrit.war init -d site_path
+----
+
+*WARNING:* The "projects" and "ref_rights" tables are no longer
+stored in the SQL database. The tables have been moved to Git
+storage, inside of the `refs/meta/config` branch of each managed
+Git repository. The init based upgrade tool will automatically
+export the current table contents and create the Git data.
+
+New Features
+------------
+
+Project Administration
+~~~~~~~~~~~~~~~~~~~~~~
+* issue 436 List projects by scanning the managed Git directory
++
+Instead of generating the list of projects from SQL database, the
+project list is obtained by recursively scanning the Git directory.
+Adding new projects is now simply a matter of creating the Git
+repository under the directory and flushing the "projects" cache
+to force the server to rescan the directory. Administrators may
+also continue to use `gerrit create-project`.
+
+* Move "projects" table into Git
++
+The projects table columns are now stored in the `project.config`
+file of the `refs/meta/config` branch of each managed Git repository.
+
+* Move "ref_rights" table into Git
++
+The "ref_rights" table is now stored in the "access" sections of
+the `project.config` file on the `refs/meta/config` branch of each
+managed Git repository. This brings version control auditing to the
+access data of each project.
+
+* New project Access screen to edit access controls
++
+The Access panel of the project administration has been rewritten
+with a new UI that reflects the new Git based storage format.
+
+Bug Fixes
+---------
+
+Project Administration
+~~~~~~~~~~~~~~~~~~~~~~
+* Avoid unnecessary updates to $GIT_DIR/description
++
+Gerrit always tried to rewrite the gitweb "description" file when the
+project was modified. This lead to unnecessary changes in the local
+filesystem, leading to more data to rsync to backups than necessary.
+Fixed to only update the file if the content changes.
diff --git a/ReleaseNotes/ReleaseNotes-2.2.1.txt b/ReleaseNotes/ReleaseNotes-2.2.1.txt
new file mode 100644
index 0000000..19af06d
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.2.1.txt
@@ -0,0 +1,79 @@
+Release notes for Gerrit 2.2.1
+==============================
+
+Gerrit 2.2.1 is now available:
+
+link:http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.2.1.war[http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.2.1.war]
+
+Schema Change
+-------------
+*WARNING:* This release contains schema changes.  To upgrade:
+----
+  java -jar gerrit.war init -d site_path
+----
+
+*WARNING:* Upgrading to 2.2.x requires the server be first upgraded
+to 2.1.7, and then to 2.2.x.
+
+New Features
+------------
+* Add 'Expand All Comments' checkbox in PatchScreen
++
+Allows users to save a user preference that automatically expands
+any inline comment boxes when a page displays.
+
+* Multiple branches in ls-project
++
+The -b option may be supplied multiple times to ls-project, each
+usage adds a new column of output per project line listing the
+current value of that branch.
+
+Bug Fixes
+---------
+* issue 994 Rename "-- All Projects --" to "All-Projects"
++
+The name "-- All Projects --.git" is difficult to work with on
+the UNIX command line, due to tools assuming the name is actually
+part of a long option. The project has been renamed to remove these
+leading hypens, and remove spaces, making it more friendly to work
+with on the command line.
+
+* issue 997 Resolve Project Owners when checking access rights
++
+Members of the 'Project Owners' magical group did not always have
+their project owner privileges when Gerrit Code Review was looking
+for "access to any ref" at the project level. This was caused by
+not expanding the 'Project Owner's group to the actual ownership
+list. Fixed.
+
+* issue 999 Do not reset Patch History selection on navigation
++
+Navigating to the next/previous file lost the setting of the
+"Old Version" made under the "Patch History" expandable control
+on a specific file view. This was accidentally broken when the
+"Old Version History" control was added to the change page. Fixed.
+
+* issue 1001 Fix search by codereview status
++
+Searching for labels (or any approval scores) was broken due to an
+incorrect usage of the Java "equals()" method. Fixed.
+
+* issue 1000 Fix administration of projects with no access controls
++
+Projects that had no access sections could not have additional
+sections added to them, due to a bug in the web UI. Fixed.
+
+* Fix API breakage on ChangeDetailService
++
+Version 2.1.7 broke the Gerrit Code Review plugin for Mylyn Reviews
+due to an accidential signature change of one of the remote JSON
+APIs. The ChangeDetailService.patchSetDetail() method is back to the
+old signature and a new patchSetDetail2() method has been added to
+handle the newer calling convention used in some contexts of the
+web UI.
+
+* Add error messages for abandon and restore when in bad state
++
+Instead of crashing with internal NullPointerExceptions, report
+a better error message to clients when a change is being moved
+between states.
diff --git a/ReleaseNotes/ReleaseNotes-2.2.2.1.txt b/ReleaseNotes/ReleaseNotes-2.2.2.1.txt
new file mode 100644
index 0000000..4613787
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.2.2.1.txt
@@ -0,0 +1,53 @@
+Release notes for Gerrit 2.2.2.1
+================================
+
+Gerrit 2.2.2.1 is now available:
+
+link:http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.2.2.1.war[http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.2.2.1.war]
+
+
+There are no schema changes from 2.2.2.  However, if upgrading from
+anything but 2.2.2, follow the upgrade procedure in the 2.2.2
+link:ReleaseNotes-2.2.2.html[ReleaseNotes].
+
+
+Bug Fixes
+---------
+* issue 1139 Fix change state in patch set approval if reviewer is added to
+closed change
++
+For the dummy patch set approval that is created when a reviewer is
+added the cached change state is always open, which is incorrect if a
+reviewer is added to a closed change. As a result the closed change will
+appear in the reviewers dashboard in the 'Review Requests' section and will
+stay there forever.  Ensure the correct change state is cached in the dummy
+patch set approval when it is created.
+
+* issue 1171 Fix ownerin and reviewerin searches
++
+Update the ownerin and reviewerin searches to use AccountGroup.UUID as
+required by commit e662fb3d4d7d0ad05791b8d2143ac5ce58117335.
+
+* issue 871 Display hash of the cherry-pick merge in comment
++
+After merging a change via cherry-pick, we add the commit's
+hash to the comment. This was accidentally removed in
+commit 14246de3c0f81c06bba8d4530e6bf00e918c11b0
+
+
+Documentation
+-------------
+* Update top level SUBMITTING_PATCHES
++
+This document is out of date, the URLs are from last August.
+Direct readers to the new server.
+
+* Add contributing guideline document
+
+* Documentation: update version references for 2.2.2
++
+Correct wording and instructions to be sure they match what would
+be observed with the indicated version of gerrit.
+Expand instructions when needed to ensure all commands could be
+executed and were successful.
+Indent commands and output based on a run of the instructions
diff --git a/ReleaseNotes/ReleaseNotes-2.2.2.2.txt b/ReleaseNotes/ReleaseNotes-2.2.2.2.txt
new file mode 100644
index 0000000..db5d750
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.2.2.2.txt
@@ -0,0 +1,24 @@
+Release notes for Gerrit 2.2.2.2
+================================
+
+Gerrit 2.2.2.2 is now available:
+
+link:http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.2.2.2.war[http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.2.2.2.war]
+
+There are no schema changes from 2.2.2, or 2.2.2.1.
+
+However, if upgrading from anything earlier, follow the upgrade
+procedure in the 2.2.2 link:ReleaseNotes-2.2.2.html[ReleaseNotes].
+
+Security Fixes
+--------------
+* Some access control sections may be ignored
++
+Gerrit sometimes ignored an access control section in a project
+if the exact same section name appeared in All-Projects. The bug
+required an unrelated project to have access.inheritFrom set to
+All-Projects and be accessed before the project that has the same
+section name as All-Projects. This is an unlikely scenario for
+most servers, as Gerrit does not normally set inheritFrom equal to
+All-Projects. The usual behavior is to not supply this property in
+project.config, and permit the implicit inheritence to take place.
diff --git a/ReleaseNotes/ReleaseNotes-2.2.2.txt b/ReleaseNotes/ReleaseNotes-2.2.2.txt
new file mode 100644
index 0000000..3f1f76f
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.2.2.txt
@@ -0,0 +1,669 @@
+Release notes for Gerrit 2.2.2
+==============================
+
+Gerrit 2.2.2 is now available:
+
+link:http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.2.2.war[http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.2.2.war]
+
+Schema Change
+-------------
+*WARNING:* This release contains schema changes.  To upgrade:
+----
+  java -jar gerrit.war init -d site_path
+----
+
+*WARNING:* Upgrading to 2.2.x requires the server be first upgraded
+to 2.1.7 (or a later 2.1.x version), and then to 2.2.x.
+
+New Features
+------------
+
+Prolog
+~~~~~~
+* issue 971 Use Prolog Cafe for ChangeControl.canSubmit()
+
+*  Add per-project prolog submit rule files
++
+When loading the prolog environment, now checks refs/meta/config
+branch for a file called rules.pl. If it exists, consult the
+file. Expects a predicate called submit_rule. If no file is found,
+uses the default_submit predicate in common_rules.pl.
+
+*  Add inheritance of prolog rules
++
+Projects now inherit the prolog rules defined in their parent
+project. Submit results from the child project are filtered by the
+parent project using the filter predicate defined the parent's
+rules.pl. The results of the filtering are then passed up to the
+parent's parent and filtered, repeating this process up to the top
+level All-Projects.
+
+* Load precompiled prolog rules from jar file
++
+Looks in (site)/cache/rules for a jar file called:
+  rules-(sha1 of rules.pl).jar
+Loads the precompiled prolog rules and uses them instead of
+consulting rules.pl. If the jar does not exist, consults rules.pl.
+If rules.pl does not exist, uses the default submit rules.
+
+* Cmd line tool rulec to compile jar from prolog
++
+Rulec takes rules.pl from the refs/meta/config branch and creates a
+jar file named rules-(sha1 of rules.pl).jar in (sitepath)/cache/rules.
+Generates temporary prolog, java src, and class files which are
+deleted afterwards.
+
+* prolog-shell: Simple command line Prolog interpreter
++
+Define a small interactive interpreter that users or site
+administartors can play around with by downloading the Gerrit WAR
+file and executing: java -jar gerrit.war prolog-shell
+
+Prolog Predicates
+^^^^^^^^^^^^^^^^^
+*  Add Prolog Predicates to check commit messages and edits
++
+commit_message returns the commit message as a symbol.
++
+commit_message_matches takes in a regex pattern and checks it against
+the commit message.
++
+commit_edits takes in a regex pattern for filenames and a regex
+pattern for edits. For all files in a commit that match the filename
+regex.  Returns true if the edits in any of those files match the
+edit regex.
+
+* Add Prolog  Predicates to expose commit filelist
++
+commit_delta/1,3,4 each takes a regular expression and matches it to
+the path of all the files in the latest patchset of a commit.
+If applicable (changes where the file is renamed or copied), the
+regex is also checked against the old path.
++
+commit_delta/1 returns true if any files match the regex
++
+commit_delta/3 returns the changetype and path, if the changetype is
+renamed, it also returns the old path. If the changetype is rename,
+it returns a delete for oldpath and an add for newpath. If the
+changetype is copy, an add is returned along with newpath.
++
+commit_delta/4 returns the changetype, new path, and old path
+ (if applicable).
+
+* Add Prolog predicates that expose the branch, owner,
+project, and  topic of a change, the author and committer of the most
+recent patchset in the change, and who is the current user.
+
+* For user-related predicates, if the user is not a gerrit user, will
+return user(anonymous) or similar. Author and committer predicates
+for commits return user(id), name, and email.
+
+* Make max_with_block/4 public
++
+This is the current rule generally applied to a label function. Make
+it exportable for now until we can come back and clean up the legacy
+approval data code.
+
+Web
+~~~
+
+* Support in Firefox delete key in NpIntTextBox
++
+Pressing the delete key while being in a NpIntTextBox (e.g. in the
+text box for the Tab Width or Columns preference when comparing a
+file) now works in Firefox.
+
+* Make sure special keys work in text fields
++
+There is a bug in gwt 2.1.0 that prevents pressing special keys like
+Enter, Backspace etc. from being properly recognized and so they have no effect.
+
+ChangeScreen
+^^^^^^^^^^^^
+* issue 855 Indicate outdated dependencies on the ChangeScreen
++
+If a change dependency is no longer the latest patchSet for that
+change, mark it OUTDATED in the dependencies table and make
+its row red, and add a warning message to the dependencies
+header, also keep the dependencies disclosure panel open
+even when an outdated dependent change is merged.
+Additionally make the link for dependencies link to the
+exact patchSet of the dependent change.
+
+* issue 881 Allow adding groups as reviewer
++
+On the ChangeScreen it is now possible to add a group as reviewer for
+a change. When a group is added as reviewer the group is resolved and
+all its members are added as reviewers to the change.
+
+* Update approvals in web UI to adapt to rules.pl submit_rule
++
+The UI now shows whatever the results of the submit_rule are, which
+permits the submit_rule to make an ApprovalCategory optional, or to
+make a new label required.
+
+Diff Screen
+^^^^^^^^^^^
+* Add top level menus for a new PatchScreen header
++
+Modify the PatchScreen so that the header contents is selectable
+using top level menus. Allow the header to display the commit
+message, the preferences, the Patch Sets, or the File List.
+
+* Add SideBySide and Unified links to Differences top level menus
++
+These new menu entries allow a user to switch view types easily
+without returning to the ChangeScreen.  Also, they double as a
+way to hide the header on the PatchScreen (when clicking on the
+currently displayed type).
+
+* Add user pref to retain PatchScreen Header when changing files
+
+* Flip the orientation of PatchHistory Table
+
+* Remove the 'Change SHA1:' from the PatchScreen title
+
+* Remove scrollbar from Commit Message
+
+* Allow comment editing with single click on line numbers
++
+Make it easier to comment (and now possible on android devices which
+zoom on double click) on a patch by simply clicking on the line number.
+
+* Add a "Save" button to the PatchScriptSettingsPanel
++
+The "Update" button now only updates the display.  Addittionally,
+for logged in users, a "Save" button now behaves the way that
+"Update" used to behave for logged in users.
+
+* issue 665 Display merge changes as differences from automatic result
++
+Instead of displaying nothing for a two-parent merge commit, compute
+the automatic merge result and display the difference between the
+automatic result that Git would create, and the actual result that
+was uploaded by the author/committer of the merge.
+
+Groups
+^^^^^^
+* Add menu to AccountGroupScreen
++
+This change introduces a menu in the AccountGroupScreen and
+different screens for subsets of the functionality (similar as it's
+done for the ProjectScreen).  Links from other screens to the
+AccountGroupScreen are resolved depending on the group type.
+
+* Display groupUUID on AccountGroupInfoScreen
++
+To assign a privilege to a new group by editing the
+'project.config' file, the new group needs to be added to the
+'groups' file in the 'refs/meta/config' branch which requires
+the UUID of the group to be known.
+
+Project Access
+^^^^^^^^^^^^^^
+* Automatically add new rule when adding new permission
++
+If a new permission was added to a block, immediately create the new
+group entry box and focus it, so the user can assign the permission.
+
+* Only show Exclusive checkbox on reference sections
++
+In the access editor, hide the Exclusive checkbox on the
+Global Capabilities section since it has no inheritance and
+the exclusive bit isn't supported.
+
+* Disable editing after successful save of Access screen
++
+When the access has been successfully modified for a project,
+switch back to the "read-only" view where the widgets are all
+disabled and the Edit button is enabled.
+
+Project Branches
+^^^^^^^^^^^^^^^^
+* Display refs/meta/config branch on ProjectBranchesScreen
++
+The new refs/meta/config branch was not shown in the ProjectBranchesScreen.
+Since refs/meta/config is not just any branch, but has a special
+meaning to Gerrit it is now displayed at the top below HEAD.
+
+* Highlight HEAD and refs/meta/config
++
+Since HEAD and refs/meta/config do not represent ordinary branches,
+highlight their rows with a special style in the ProjectBranchesScreen.
+
+URLs
+^^^^
+* Modernize URLs to be shorter and consistent
++
+Instead of http://site/#change,1234 we now use a slightly more
+common looking   http://site/#/c/1234  URL to link to a change.
++
+Files within a patch set are now denoted below the change, as in
+http://site/#/c/1234/1/src/module/foo.c
++
+Also fix the dynamic redirects of http://site/1234
+and http://site/r/deadbeef to jump directly to the corresponding
+change if there is exactly one possible URL.
++
+Entities that have multiple views suffix the URL with ",view-name"
+to indicate which view the user wants to see.
+
+* issue 1018 Accept ~ in linkify() URLs
+
+SSH
+~~~
+* Added a set-reviewers ssh command
+
+* Support removing more than one reviewer at once
++
+This way we can batch delete reviewers from a change.
+
+* issue 881 Support adding groups as reviewer by SSH command
++
+With the set-reviewers SSH command it is now possible to also add
+groups as reviewer for a change.
+
+* Fail review command for changing labels when change is closed
++
+If a reviewer attempts to change a review label (approval) after a
+change is closed using the ssh review command, cause it to fail the
+command and output a message.
+
+* ls-projects: Fix display of All-Projects under --tree
++
+Everything should be nested below All-Projects, since that is actually
+the root level.
+
+* ls-projects: Add --type to filter by project type
++
+ls-projects now supports --type code|permissions|all.  The default is
+code which now skips permissions only projects, restoring the output
+to what appears from Gerrit 2.1.7 and earlier.
+
+* show-caches: Improve memory reporting
++
+Change the way memory is reported to show the actual values,
+and the equation that determines how these are put together
+to form the current usage.  Include some additional data including
+server version, current time, process uptime, active SSH
+connections, and tasks in the task queue. The --show-jvm option
+will report additional data about the JVM, and tell the caller
+where it is running.
+
+Queries
+^^^^^^^
+* Output patchset creation date for 'query' command.
+
+* issue 1053 Support comments option in query command
++
+Query SSH command will show all comments if option --comments is
+used. If --comments is used together with --patch-sets all inline
+comments are included in the output.
+
+Config
+~~~~~~
+* Move batch user priority to a capability
++
+Instead of using a magical group, use a special capability to
+denote users that should get the batch priority behavior.
+
+* issue 742 Make administrator, create-project a global capability
++
+This gets rid of the special entries in system_config and
+gerrit.config related to who the Administrators group is,
+or which groups are permitted to create new projects on
+this server.
+
+* issue 48 & 742  Add fine-grained capabilities for administrative actions
++
+The Global Capabilities section in All-Projects can now be used to
+grant subcommands that are available over SSH and were previously
+restricted to only Administrators.
+
+* Disallow project names ending in "/"
+
+* issue 934 query: Enable configurable result limit
++
+Allow site administrators to configure the query limit for user to be
+above the default hard-coded value of 500 by adding a global
+[capability] block to All-Projects project.config file with group(s)
+that should have different limits.
+
+* Introduced a new PermissionRule.Action: BLOCK.
++
+Besides already existing ALLOW and DENY actions this change
+introduces the BLOCK action in order to enable blocking some
+permission rules globally.
+
+* issue 813 Use remote.name.replicatePermissions to hide permissions
++
+Administrators can now disable replication of permissions-only
+projects and the per-project refs/meta/config in replication.config
+by setting the replicatePermissions field to false.
+
+* Add a Restored.vm template and use it.
++
+The restore action has been erroneously using the Abandoned.vm
+template.  Create a template and sender for the restorecommand.
+
+* sshd.advertisedAddress: specify the displayed SSH host/port
++
+This allows aliases which redirect to gerrit's ssh port (say
+from port 22) to be setup and advertised to users.
+
+Dev
+~~~
+* Updated eclipse settings for 3.7 and m2e 1.0
+
+* Fix build in m2eclipse 1.0
++
+Ignore the antrun and the build-helper-maven-plugin tasks in m2eclipse.
+
+* Make Gerrit with gwt 2.3.0 run in gwtdebug mode
+
+* Fix a number of build warnings that have crept in
+
+* Accept email address automatically
++
+Enable Gerrit to accept email address automatically in
+"DEVELOPMENT_BECOME_ANY_ACCOUNT" mode without a confirmation email.
+
+* Added clickable user names at the BecomeAnyAccountLoginServlet.
++
+The first 5 (by accountId) user names are displayed as clickable
+links. Clicking a user name logs in as this user, speeding up
+switching between different users when using the
+DEVELOPMENT_BECOME_ANY_ACCOUNT authentication type.
+
+Miscellaneous
+~~~~~~~~~~~~~
+* Permit adding reviewers to closed changes
++
+Permit adding a reviewer to closed changes to support post-submit
+discussion threads.
+
+* issue 805 Don't check for multiple change-ids when pushing directly
+to refs/heads.
+
+* Avoid costly findMergedInto during push to refs/for/*
++
+No longer close a change when a commit is pushed to res/for/* and the
+Change-Id in the commit message footer matches another commit on an
+existing branch or tag.
+
+* Allow serving static files in subdirectories
+
+* issue 1019 Normalize OpenID URLs with http:// prefix
++
+No longer violate OpenID 1.1 and 2.0, both of which require
+OpenIDs to be normalized (http:// added).
+
+* Allow container-based authentication for git over http
++
+Gerrit was insisting on DIGEST authentication when doing git over
+http. A new boolean configuration parameter auth.trustContainerAuth
+allows gerrit to be configured to trust the container to do the
+authentication.
+
+* issue 848 Add rpc method for GerritConfig
++
+Exposes what types of reviews are possible via json rpc, so that the
+Eclipse Reviews plugin currently can parse the javascript from a
+gerrit page load.
+
+
+Performance
+-----------
+* Bumped Brics version to 1.11.8
++
+This Brics version fixes a performance issue in some larger Gerrit systems.
+
+* Add permission_sort cache to remember sort orderings
++
+Cache the order AccessSections should be sorted in, making any future
+sorting for the same reference name and same set of section patterns
+cheaper.
+
+* Refactor how permissions are matched by ProjectControl, RefControl
++
+More aggressively cache many of the auth objects at a cost of memory,
+but this should be an improvement in response timse.
+
+* Substantialy speed up pushing changes for review
++
+Pushing a new change for review checks if the change is related to
+the branch it's destined for. It used to do this in a way that
+required a topo-sort of the rev history, and now uses JGit's
+merge-base functionality.
+
+* Add cache for tag advertisements
++
+To make the general case more efficient, introduce a cache called "git_tags".
++
+On a trivial usage of the Linux kernel repository, the average
+running time of the VisibleRefFilter when caches were hot was
+7195.68 ms.  With this commit, it is a mere 5.07 milliseconds
+on a hot cache.  A reduction of 99% of the running time.
+
+* Don't set lastCheckTime in ProjectState
++
+The lastCheckTime/generation fields are actually a counter that
+is incremented using a background thread. The values don't match
+the system clock, and thus reading System.currentTimeMillis()
+during the construction of ProjectState is a waste of resources.
+
+
+Upgrades
+--------
+* Upgrade to GWT 2.3.0
+* Upgrade to Gson to 1.7.1
+* Upgrade to gwtjsonrpc 1.2.4
+* Upgrade to gwtexpui 1.2.5
+* Upgrade to Jsch 0.1.44-1
+* Upgrade to Brics 1.11.8
+
+
+Bug Fixes
+---------
+* Fix: Issue where Gerrit could not linkify certain URLs
+
+* issue 1015 Fix handling of regex ref patterns in Access panel
++
+regex patterns such as "\^refs/heads/[A-Z]{2,}\-[0-9]\+.\*" were being
+prefixed with "refs/heads/", resulting in invalid reference patterns
+like "refs/heads/^refs/heads/[A-Z]{2,}-[0-9]+.*".
+
+* issue 1002 Check for and disallow pushing of invalid refs/meta/config
++
+If the project.config or groups files are somehow invalid on
+the refs/meta/config branch, or would be made invalid due to
+a bad code review being submitted to this branch, reject the
+user's attempt to push.
+
+* issue 1002 Fix NPE in PermissionRuleEditor when group lacks UUID
++
+If a group does not have an entry in the "groups" table within
+the refs/meta/config branch render the group name as a span,
+without the link instead of crashing the UI.
+
+* issue 972 Filter access section rules to only visible groups
++
+Users who are not the owner of an access section can now only
+see group names and rules for groups which they are a member of,
+are visible to all users, or that they own.
+
+* Correctly handle missing refs/meta/config branch
++
+If the refs/meta/config branch did not exist, getRevision() no longer
+throws an NPE when trying to access the ProjectDetail.
+
+* Allow loading Project Access when there is no refs/meta/config
++
+Enable loading the access screen with a null revision field,
+and on save of any edits require the branch to be new.
+
+* create-project: Fix creation vs. replication order
++
+Create the project on remote mirrors before creating either the
+refs/meta/config or the initial empty branch. This way those can be
+replicated to the remote mirrors once they have been created locally.
+
+* create-project: Bring back --permissions-only flag
++
+If a project is permissions only, assign HEAD to point to
+refs/meta/config. This way the gitweb view of the project
+shows the permissions history by default, and clients that
+clone the project are able to get a detached HEAD pointing
+to the current permission state, rather than an empty
+repository.
+
+* create-project: Fix error reporting when repository exists
++
+If a repository already exists, tell the user it already is
+available, without disclosing the server side path from gerrit.basePath.
+
+* Do not log timeout errors on upload and receive connections
+
+* Only automatically create accounts for LDAP systems
++
+If the account management is LDAP, try to automatically create
+accounts by looking up the data in LDAP. Otherwise fail and reject an
+invalid account reference that was supplied on the command line via
+SSH.
+
+* Add missing RevWalk.reset() after checking merge base
++
+This fixes an exception from RevWalk when trying to push a new
+commit for review.
+
+* issue 1069 Do not send an email on reviews when there is no message.
++
+No longer send an email when reviewing a change via ssh, and
+the change message is blank (when no change message is actually
+added to the review).
+
+* Ignore PartialResultException from LDAP.
++
+This exception occurs when the server isn't following referrals for
+you, and thus the result contains a referral. That happens when
+you're using Active Directory. You almost certainly don't really want
+to follow referrals in AD *anyways*, so just ignore these exceptions,
+so we can still use the actual data.
+
+* issue 518 Fix MySQL counter resets
++
+gwtorm 1.1.5 was patched to leave in the dummy row that incremented
+the counter, ensuring the server will use MAX() + 1 instead of 1 on
+the next increment after restart.
+
+* Don't delete account_id row on MySQL
++
+If the table is an InnoDB table deleting the row after allocation may
+cause the sequence to reset when the server restarts, giving out
+duplicate account_ids later.
+
+
+Documentation
+-------------
+
+New Documents
+~~~~~~~~~~~~~
+* First Cut of Gerrit Walkthrough Introduction documentation.
++
+Add a new document intended to be a complement for the existing
+reference documentation to allow potential users to easily get a
+feel for how Gerrit is used, where it fits and whether it will
+work for them.
+
+* Introducing a quick and dirty setup tutorial
++
+The new document covers quick installation, new project and first
+upload.  It contains lots of quoted output, with a demo style to it.
+
+Access Control
+~~~~~~~~~~~~~~
+* Code review
+
+* Conversion table between 2.1 and 2.2
++
+Add a table to ease conversion from 2.1.x. The table tries to address
+the old permissions one by one except for the push tag permission which
+in effect needed two permissions to work properly. This should
+be familiar to the administrator used to the 2.1.x permission model
+however.
+
+* Reformatted text
+
+* Verify
++
+Updated some text in the Per project-section and edited the verified
+section to reflect the current label.
+
+* Capabilities
++
+Adds general information about global capabilities, how the server
+ownership is administered.
+
+* Added non-interactive users
++
+This change adds the non-interactive user group.
+It also adds that groups can be members of other groups.
+The groups are now sorted in alphabetical order.
+
+* Reordering categories
++
+Access categories are now sorted to match drop down box in UI
+
+Other Documentation
+~~~~~~~~~~~~~~~~~~~
+* Added additional information on the install instructions.
++
+The installation instructions presumes much prior knowledge,
+make some of that knowledge less implicit.
+
+* Provides a template to the download example.
++
+Clarifies that the example host must be replaced with proper
+hostname.
+
+* Provided an example on how to abandon a change from
+the command line
+
+* update links from kernel.org to code.google.com
+
+
+* Rename '-- All Projects --' in documentation to 'All-Projects'
+
+* Explain 'Automatically resolve conflicts'
+
+* Update documentation for testing SSH connection
++
+The command output that is shown in the example and the description
+how to set the ssh username were outdated.
+
+* Remove unneeded escape characters from the documentation
++
+The old version of asciidoc required certain characters to be escaped
+with a backslash and when the upgrade to the new version was done all
+those backslashes that were used for escaping became visible.
+
+* Clean up pgm-index
++
+Break out the utilities into their own section, and correct
+some of the item descriptions.
+
+* Update manual project creation instructions
+
+* Update project configuration documentation
++
+Remove the textual reference to obsolete SQL insert statement to
+create new projects.
+
+* Clean up command line documentation, examples
++
+The formatting was pretty wrong after upgrading to a newer version
+of AsciiDoc, so fix up most of the formatting, correct some order
+of commands in the index, and make create-project conform to the
+same format used by create-account and create-group.
+
+* Correct syntax of SQL statement for inserting approval category
diff --git a/ReleaseNotes/ReleaseNotes-2.3.1.txt b/ReleaseNotes/ReleaseNotes-2.3.1.txt
new file mode 100644
index 0000000..324a3c1
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.3.1.txt
@@ -0,0 +1,24 @@
+Release notes for Gerrit 2.3.1
+==============================
+
+Gerrit 2.3.1 is now available:
+
+link:http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.3.1.war[http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.3.1.war]
+
+There are no schema changes from 2.3.
+
+However, if upgrading from anything earlier, follow the upgrade
+procedure in the 2.3 link:ReleaseNotes-2.3.html[ReleaseNotes].
+
+Security Fixes
+--------------
+* Some access control sections may be ignored
++
+Gerrit sometimes ignored an access control section in a project
+if the exact same section name appeared in All-Projects. The bug
+required an unrelated project to have access.inheritFrom set to
+All-Projects and be accessed before the project that has the same
+section name as All-Projects. This is an unlikely scenario for
+most servers, as Gerrit does not normally set inheritFrom equal to
+All-Projects. The usual behavior is to not supply this property in
+project.config, and permit the implicit inheritence to take place.
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/ReleaseNotes-2.4.1.txt b/ReleaseNotes/ReleaseNotes-2.4.1.txt
new file mode 100644
index 0000000..15dc1d3
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.4.1.txt
@@ -0,0 +1,55 @@
+Release notes for Gerrit 2.4.1
+==============================
+
+Gerrit 2.4.1 is now available:
+
+link:http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.4.1.war[http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.4.1.war]
+
+
+There are no schema changes from 2.4.  However, if upgrading from
+anything but 2.4, follow the upgrade procedure in the 2.4
+link:ReleaseNotes-2.4.html[ReleaseNotes].
+
+
+Bug Fixes
+---------
+* Catch all exceptions when async emailing
++
+This fixes email notification issues reported
+link:https://groups.google.com/group/repo-discuss/browse_thread/thread/dd157ebc55b962ef/652822d6fbe61e71[here].
+
+* Fixed cleanup of propagated SshScopes
++
+This improves error reporting in case of email notification errors.
+
+* issue 1394 Fix lookup of the 'Commit Message' file in patch set
++
+There is an assumption that the commit message is always first in the list of
+files of a patch set. However, there was another place in Gerrit code, which
+did binary search through the list of the files, without taking this assumption
+into account. In case when a patch set contained a file which lexicographically
+sorted before '/COMMIT_MSG' (like '.gitignore' for example) it could have
+happened that the commit message was not found and, as a side effect, it wasn't
+possible to review it.
+
+* issue 1162 Fix deadlock on destroy of CommandFactoryProvider
+
+* Honor the sendmail.smtpUser from gerrit.config on upgrade
++
+If sendmail.smtpUser was not present in the gerrit.config then don't set it in
+site upgrade.
+
+* issue 1420 Forge committer bypassed
++
+It was possible to forge committer even without having permission for that.
+This was a regression from 2.3.
+
+* Make sure the "Object too large..." error message is printed when an object
+larger than receive.maxObjectSizeLimit is rejected by Gerrit
+
+* Display proper error if file diff fails because content is too large
+
+* Get around a log4j bug that causes AsyncAppender-Dispatcher thread to die and
+block other threads
+** Make async logging buffer size configurable
+** Make logging events discardable, prevent NPE in AsyncAppender-Dispatcher thread
diff --git a/ReleaseNotes/ReleaseNotes-2.4.2.txt b/ReleaseNotes/ReleaseNotes-2.4.2.txt
new file mode 100644
index 0000000..afa1d96
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.4.2.txt
@@ -0,0 +1,24 @@
+Release notes for Gerrit 2.4.2
+==============================
+
+Gerrit 2.4.2 is now available:
+
+link:http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.4.2.war[http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.4.2.war]
+
+There are no schema changes from 2.4, or 2.4.1.
+
+However, if upgrading from anything earlier, follow the upgrade
+procedure in the 2.4 link:ReleaseNotes-2.4.html[ReleaseNotes].
+
+Security Fixes
+--------------
+* Some access control sections may be ignored
++
+Gerrit sometimes ignored an access control section in a project
+if the exact same section name appeared in All-Projects. The bug
+required an unrelated project to have access.inheritFrom set to
+All-Projects and be accessed before the project that has the same
+section name as All-Projects. This is an unlikely scenario for
+most servers, as Gerrit does not normally set inheritFrom equal to
+All-Projects. The usual behavior is to not supply this property in
+project.config, and permit the implicit inheritence to take place.
diff --git a/ReleaseNotes/ReleaseNotes-2.4.txt b/ReleaseNotes/ReleaseNotes-2.4.txt
new file mode 100644
index 0000000..82f3ed4
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.4.txt
@@ -0,0 +1,257 @@
+Release notes for Gerrit 2.4
+============================
+
+Gerrit 2.4 is now available:
+
+link:http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.4.war[http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.4.war]
+
+Schema Change
+-------------
+*WARNING:* This release contains schema changes.  To upgrade:
+----
+  java -jar gerrit.war init -d site_path
+----
+
+*WARNING:* Upgrading to 2.4.x requires the server be first upgraded to 2.1.7 (or
+a later 2.1.x version), and then to 2.4.x.  If you are upgrading from 2.2.x.x or
+newer, you may ignore this warning and upgrade directly to 2.4.x.
+
+New Features
+------------
+
+Security
+~~~~~~~~
+
+* Restrict visibility to arbitrary user dashboards
++
+Administrators have some expectation when using the 'suggest.accounts'
+visibility restriction feature that users cannot get the names or
+email addresses for arbitrary accounts. In fact, because account IDs
+are sequential, it would be easy for an adversary to get personal
+information of all users on the server by requesting every user's
+dashboard.
++
+This includes changing the meaning of the 'suggest.accounts' config
+option to be a boolean indicating whether account suggestion should
+happen at all, which is now orthogonal to the account visibility
+restriction policy. We still recognize the old values for
+'suggest.accounts', with the slight behavior change that
+'suggest.accounts=OFF' now means that users cannot access the dashboards
+of any other users. Administrators who do not want this behavior can
+update their configuration.
+
+* Indicate that 'not found' may actually be a permission issue
+
+Web
+~~~
+
+* Add user preference to mark files reviewed automatically or manually
++
+Add a checkbox to the preferences header on the diff
+screen which allows a user to specify whether they
+want manual-reviewing enabled or disabled.  Previously,
+every file was auto marked reviewed when a user first
+displayed it.  The new manual mode prevents this auto
+marking and only marks a file reviewed when the user
+explicitly clicks on the reviewed checkbox.
+
+* Use 'Auto Merge' for merge commit's base comparison
++
+When reviewing a merge commit, the old wording in the version history dropdown
+of 'Base' doesn't really match Gerrit's behavior.  Updating this to use
+'Auto Merge' as suggested by Shawn Pearce on IRC.
+
+* issue 1035 Add rebase button to the change screen
++
+This change adds a rebase button along with the rest of
+the action buttons in the change page. When pressing the
+button, the most recent patch set will be rebased onto
+the tip of the destination branch or the latest patchset
+of the change we depend upon. A new patch set containing
+the rebased commit will be produced and added to the
+change.
++
+Rebasing of a change in web UI is restricted to change owner, submitter or
+those with the (new) 'rebase' permission.
+
+* Add a new permission 'rebase' to permit rebasing changes in the web UI
+
+* Make a user's dashboard visible if any of the changes are visible to the
+current user.
+
+* Change 'Loading ...' to say 'Working ...' as, often, there is more going on
+than just loading a response.
+
+Performance
+~~~~~~~~~~~
+
+* Asynchronously send email so it does not block the UI
+* Optimize queries for open/merged changes by project + branch
+
+Git
+~~~
+
+* Implement a multi-sub-task progress monitor for ReceiveCommits
+
+* Close corresponding change when pushing to 'refs/heads/*'
++
+Gerrit would not close the open changes with matching change-ids,
+when the user pushes commits directly to 'refs/heads/*'.
++
+This issue could be triggered for two reasons:
+
+. It is triggered when Gerrit detects no changes between the
+pushed commits and the current patchset on the open changes. This
+patch make sure that the matching open change is always closed when
+pushing to 'refs/heads/*', even if no visible changes is detected.
+
+. The same commit exists on another branch than the destination
+branch. This could trick gerrit into just "re-closing" the wrong
+change.
+
+* Run ReceiveCommits in a shared thread pool
++
+Since the work to ReceiveCommits may take a long, potentially unbounded
+amount of time, we would like to have it run in the background so it
+can be monitored for timeouts and cancelled, and have stalls reported
+to the user from the main thread.
+
+Search
+~~~~~~
+
+* Add the '--dependencies' option to the 'query' command.
++
+This option includes information about patch sets which depend on, or are
+needed by, each patch set.
+
+* Branch Operator: Support full branch names
++
+The search operator for branches required the provided value to be the
+short branch name that is shown in the web interface (without the
+'refs/heads/' prefix). Change the branch operator so that it also
+supports full branch names as value.
++
+It is intuive that searching with 'branch:master' and searching with
+'branch:refs/for/master' deliver the same result. So far
+'branch:refs/for/master' was the same as searching with
+'refs:refs/heads/refs/heads/master' which is unexpected for most users.
+
+* Add comment inclusion via '&comments=true' over HTTP
++
+With this change, we can fetch the comments on a patchset by sending a
+request to 'https://site/query?comments=true'
+
+Access Rights
+~~~~~~~~~~~~~
+
+* Added the 'emailReviewers' as a global capability.
++
+This replaces the 'emailOnlyAuthors' flag of account groups.
+
+Dev
+~~~
+
+* issue 1272 Add scripts to create release notes from git log
++
+These script generates a list of commits from git log between two given commits
+and outputs the asciidoc format containting list of commits subject and body.
+
+* Update URL for m2eclipse
++
+The project is now under the Eclipse Foundation umbrella.
+
+* Add missing ignore for m2e prefs in gerrit-ehcache
+
+* Add '--issues' and '--issue_numbers' options to the 'gitlog2asciidoc.py'
+
+Miscellaneous
+~~~~~~~~~~~~~
+
+* Remove perl from 'commit-msg' hook
++
+Removing perl from the commit-msg hook reduces the dependencies
+gerrit imposes on its users.
+
+* updating contrib 'trivial_rebase.py' for 2.2.2.1
+
+Upgrades
+--------
+
+* Updated to Guice 3.0.
+* Updated to gwtorm 1.4.
+* Update JGit to 1.3.0.201202151440-r.75-gff13648
+* Update to gwtjsonrpc 1.3
++
+The change also shrinks the built WAR from 38M to 23M
+by excluding the now unnecessary GWT server code.
+
+Bug Fixes
+---------
+
+* issue 904 Users who starred a change should receive all the emails about a change.
+
+* Fix: 'Diff All Side-by-Side' and 'Diff All Unified' buttons
++
+When pressing the 'Diff All Side-by-Side' or
+'Diff All Unified' button on the change screen, the
+opened browser windows/tabs shows diffs using "Base"
+as old version and the latest one as active patch set,
+regardless what has been set using the
+"Old Version History:" drop down menu and what is
+currently active patch set.
++
+Gerrit doesn't remember the base patch set in the URL,
+making it impossible to copy-and-paste the URL to
+co-workers to show them the same diff a user is
+looking at.
++
+This change fixes this behavior to make sure that
+the opened new browser windows shows diffs using the
+correct old patch set and active patch set.
+
+* Fix NPEs looking up groups by UUID in GroupCache
+
+* Fix default 'receive.timeout'
++
+This should be in milliseconds, not seconds. Set the default to be
+2 minutes in milliseconds and update the documentation to reflect
+that milliseconds are the default unit of time used here.
+
+* Fix 'development_become_any_account' redirects
+* issue 1299 Allow configuration of optional pattern for gitweb file history link
+* Use servlet context path during logout
+
+* issue 1353 Fix case check for project name so that symlinks work again
+* Fix merging of access sections
+* Fix inconsistent behaviour when replicating refs/meta/config
+* Fix duplicated results on status:open project:P branch:B
+
+Documentation
+-------------
+
+Access Rights
+~~~~~~~~~~~~~
+* Capabilities introduced
+* Kill and priority capabilities
+* Administrate server capability
+* Create account capability
+* Create group and project capability
+* Flush caches capability
+* Capability replication and view caches
+* Capability view conn. & queue
+* Example roles introduced
+* Developer example role
+* CI system example role
+* Integrator example role
+* Project owner example role
+* Administrator example role
+
+Miscellaneous
+~~~~~~~~~~~~~
+* User upload documentation: Replace changes
+* Add visible-to-all flag in the documentation for cmd-create-group
+* Add a contributing guideline for annotations
+* Add missing header for suggest.accounts documentation
+* Fix anchors for description of gitweb config parameters
+* Add missing section name to config-gerrit documentation
+* Fix documentation of ls-projects
diff --git a/ReleaseNotes/index.txt b/ReleaseNotes/index.txt
index 48bf6ef..5f8de28 100644
--- a/ReleaseNotes/index.txt
+++ b/ReleaseNotes/index.txt
@@ -1,6 +1,28 @@
 Gerrit Code Review - Release Notes
 ==================================
 
+[[2_4]]
+Version 2.4.x
+-------------
+* link:ReleaseNotes-2.4.2.html[2.4.2]
+* link:ReleaseNotes-2.4.1.html[2.4.1]
+* link:ReleaseNotes-2.4.html[2.4]
+
+[[2_3]]
+Version 2.3.x
+-------------
+* link:ReleaseNotes-2.3.html[2.3]
+* link:ReleaseNotes-2.3.1.html[2.3.1]
+
+[[2_2]]
+Version 2.2.x
+-------------
+* link:ReleaseNotes-2.2.2.html[2.2.2],
+* link:ReleaseNotes-2.2.2.2.html[2.2.2.2],
+* link:ReleaseNotes-2.2.2.1.html[2.2.2.1],
+* link:ReleaseNotes-2.2.1.html[2.2.1],
+* link:ReleaseNotes-2.2.0.html[2.2.0]
+
 [[2_1]]
 Version 2.1.x
 -------------
diff --git a/SUBMITTING_PATCHES b/SUBMITTING_PATCHES
index b4448620..e766ef1 100644
--- a/SUBMITTING_PATCHES
+++ b/SUBMITTING_PATCHES
@@ -5,7 +5,7 @@
  - Make sure all code is under the Apache License, 2.0.
  - Publish your changes for review:
 
-   git push ssh://review.source.android.com:29418/tools/gerrit.git HEAD:refs/for/master
+   git push https://gerrit-review.googlesource.com/gerrit HEAD:refs/for/master
 
 
 Long Version:
@@ -55,23 +55,22 @@
 
 Instead, login to the Gerrit Code Review tool at:
 
-  https://review.source.android.com/
+  https://gerrit-review.googlesource.com/
 
 Ensure you have completed one of the necessary contributor
 agreements, providing documentation to the project maintainers that
 they have right to redistribute your work under the Apache License:
 
-  https://review.source.android.com/#settings,agreements
+  https://gerrit-review.googlesource.com/#/settings/agreements
 
-Ensure you have registered one or more SSH public keys, so you can
-push your commits directly over SSH:
+Ensure you have obtained a unique HTTP password to identify yourself:
 
-  https://review.source.android.com/#settings,ssh-keys
+  https://gerrit-review.googlesource.com/#/settings/http-password
 
-Push your patches over SSH to the review server, possibly through
+Push your patches over HTTPS to the review server, possibly through
 a remembered remote to make this easier in the future:
 
-   git config remote.review.url ssh://review.source.android.com:29418/tools/gerrit.git
+   git config remote.review.url https://google-review.googlesource.com/gerrit
    git config remote.review.push HEAD:refs/for/master
 
    git push review
diff --git a/contrib/trivial_rebase.py b/contrib/trivial_rebase.py
index a5123a3..a514b4c 100755
--- a/contrib/trivial_rebase.py
+++ b/contrib/trivial_rebase.py
@@ -93,8 +93,8 @@
 
   Returns a list of approval dicts"""
   sql_query = ("\"SELECT value,account_id,category_id FROM patch_set_approvals "
-               "WHERE change_id = (SELECT change_id FROM changes WHERE "
-               "patch_set_id = %s AND change_key = \'%s\') AND value <> 0\""
+               "WHERE patch_set_id = %s AND change_id = (SELECT change_id FROM "
+               "changes WHERE change_key = \'%s\') AND value <> 0\""
                % ((patchset - 1), changeId))
   gsql_out = GsqlQuery(sql_query, server, port)
   approvals = []
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/.settings/org.eclipse.core.resources.prefs b/gerrit-antlr/.settings/org.eclipse.core.resources.prefs
index 82eb859..589908f 100644
--- a/gerrit-antlr/.settings/org.eclipse.core.resources.prefs
+++ b/gerrit-antlr/.settings/org.eclipse.core.resources.prefs
@@ -1,3 +1,4 @@
-#Tue Sep 02 16:59:24 PDT 2008
+#Thu Jul 28 11:02:35 PDT 2011
 eclipse.preferences.version=1
+encoding//src/main/java=UTF-8
 encoding/<project>=UTF-8
diff --git a/gerrit-antlr/pom.xml b/gerrit-antlr/pom.xml
index 139cb30..aa0d7fd 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.1-SNAPSHOT</version>
+    <version>2.4-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/.settings/org.eclipse.core.resources.prefs b/gerrit-common/.settings/org.eclipse.core.resources.prefs
index 82eb859..fc11c3f 100644
--- a/gerrit-common/.settings/org.eclipse.core.resources.prefs
+++ b/gerrit-common/.settings/org.eclipse.core.resources.prefs
@@ -1,3 +1,5 @@
-#Tue Sep 02 16:59:24 PDT 2008
+#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-common/.settings/org.eclipse.jdt.core.prefs b/gerrit-common/.settings/org.eclipse.jdt.core.prefs
index 04afc7f..470942d 100644
--- a/gerrit-common/.settings/org.eclipse.jdt.core.prefs
+++ b/gerrit-common/.settings/org.eclipse.jdt.core.prefs
@@ -1,4 +1,4 @@
-#Tue May 12 17:44:13 PDT 2009
+#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
@@ -9,6 +9,7 @@
 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
diff --git a/gerrit-common/pom.xml b/gerrit-common/pom.xml
index e878b80..e7933ea 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.1-SNAPSHOT</version>
+    <version>2.4-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-common</artifactId>
@@ -90,6 +90,17 @@
           </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/src/main/java/com/google/gerrit/Common.gwt.xml b/gerrit-common/src/main/java/com/google/gerrit/Common.gwt.xml
index 171ae8a..468b477 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/Common.gwt.xml
+++ b/gerrit-common/src/main/java/com/google/gerrit/Common.gwt.xml
@@ -14,7 +14,7 @@
  limitations under the License.
 -->
 <module>
-  <inherits name='com.google.gerrit.ReviewDB' />
+  <inherits name='com.google.gerrit.reviewdb.ReviewDB' />
   <inherits name='com.google.gwtjsonrpc.GWTJSONRPC'/>
   <source path='common' />
 </module>
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 efe311f..71df400 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
@@ -16,42 +16,47 @@
 
 import com.google.gerrit.common.data.AccountInfo;
 import com.google.gerrit.common.data.ChangeInfo;
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.PatchSet;
-import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.Change.Status;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.Change.Status;
 import com.google.gwtorm.client.KeyUtil;
 
 public class PageLinks {
-  public static final String SETTINGS = "settings";
-  public static final String SETTINGS_PREFERENCES = "settings,preferences";
-  public static final String SETTINGS_SSHKEYS = "settings,ssh-keys";
-  public static final String SETTINGS_HTTP_PASSWORD = "settings,http-password";
-  public static final String SETTINGS_WEBIDENT = "settings,web-identities";
-  public static final String SETTINGS_MYGROUPS = "settings,group-memberships";
-  public static final String SETTINGS_AGREEMENTS = "settings,agreements";
-  public static final String SETTINGS_CONTACT = "settings,contact";
-  public static final String SETTINGS_PROJECTS = "settings,projects";
-  public static final String SETTINGS_NEW_AGREEMENT = "settings,new-agreement";
-  public static final String REGISTER = "register";
+  public static final String SETTINGS = "/settings/";
+  public static final String SETTINGS_PREFERENCES = "/settings/preferences";
+  public static final String SETTINGS_SSHKEYS = "/settings/ssh-keys";
+  public static final String SETTINGS_HTTP_PASSWORD = "/settings/http-password";
+  public static final String SETTINGS_WEBIDENT = "/settings/web-identities";
+  public static final String SETTINGS_MYGROUPS = "/settings/group-memberships";
+  public static final String SETTINGS_AGREEMENTS = "/settings/agreements";
+  public static final String SETTINGS_CONTACT = "/settings/contact";
+  public static final String SETTINGS_PROJECTS = "/settings/projects";
+  public static final String SETTINGS_NEW_AGREEMENT = "/settings/new-agreement";
+  public static final String REGISTER = "/register";
 
   public static final String TOP = "n,z";
 
-  public static final String MINE = "mine";
-  public static final String ADMIN_GROUPS = "admin,groups";
-  public static final String ADMIN_PROJECTS = "admin,projects";
+  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());
   }
 
   public static String toChange(final Change.Id c) {
-    return "change," + c.toString();
+    return "/c/" + c + "/";
   }
 
   public static String toChange(final PatchSet.Id ps) {
-    return "change," + ps.getParentKey().toString() + ",patchset=" + ps.get();
+    return "/c/" + ps.getParentKey() + "/" + ps.get();
+  }
+
+  public static String toProjectAcceess(final Project.NameKey p) {
+    return "/admin/projects/" + p.get() + ",access";
   }
 
   public static String toAccountDashboard(final AccountInfo acct) {
@@ -59,11 +64,16 @@
   }
 
   public static String toAccountDashboard(final Account.Id acct) {
-    return "dashboard," + acct.toString();
+    return "/dashboard/" + acct.toString();
   }
 
   public static String toChangeQuery(final String query) {
-    return "q," + KeyUtil.encode(query) + "," + TOP;
+    return toChangeQuery(query, TOP);
+  }
+
+  public static String toChangeQuery(String query, String page) {
+    query = KeyUtil.encode(query).replaceAll("%3[Aa]", ":");
+    return "/q/" + query + "," + page;
   }
 
   public static String projectQuery(Project.NameKey proj, Status status) {
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/auth/openid/OpenIdProviderPattern.java b/gerrit-common/src/main/java/com/google/gerrit/common/auth/openid/OpenIdProviderPattern.java
index 731d765..c80d3eb 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/auth/openid/OpenIdProviderPattern.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/auth/openid/OpenIdProviderPattern.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.common.auth.openid;
 
-import com.google.gerrit.reviewdb.AccountExternalId;
+import com.google.gerrit.reviewdb.client.AccountExternalId;
 
 public class OpenIdProviderPattern {
   public static OpenIdProviderPattern create(String pattern) {
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/auth/openid/OpenIdService.java b/gerrit-common/src/main/java/com/google/gerrit/common/auth/openid/OpenIdService.java
index 0a24cd5..0deba34 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/auth/openid/OpenIdService.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/auth/openid/OpenIdService.java
@@ -15,11 +15,11 @@
 package com.google.gerrit.common.auth.openid;
 
 import com.google.gerrit.common.auth.SignInMode;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import com.google.gwtjsonrpc.client.AllowCrossSiteRequest;
-import com.google.gwtjsonrpc.client.RemoteJsonService;
-import com.google.gwtjsonrpc.client.RpcImpl;
-import com.google.gwtjsonrpc.client.RpcImpl.Version;
+import com.google.gwtjsonrpc.common.AsyncCallback;
+import com.google.gwtjsonrpc.common.AllowCrossSiteRequest;
+import com.google.gwtjsonrpc.common.RemoteJsonService;
+import com.google.gwtjsonrpc.common.RpcImpl;
+import com.google.gwtjsonrpc.common.RpcImpl.Version;
 
 @RpcImpl(version = Version.V2_0)
 public interface OpenIdService extends RemoteJsonService {
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..e89cdd2 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.client.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/auth/userpass/UserPassAuthService.java b/gerrit-common/src/main/java/com/google/gerrit/common/auth/userpass/UserPassAuthService.java
index 5f39bc3..1d25a3d 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/auth/userpass/UserPassAuthService.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/auth/userpass/UserPassAuthService.java
@@ -14,11 +14,11 @@
 
 package com.google.gerrit.common.auth.userpass;
 
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import com.google.gwtjsonrpc.client.AllowCrossSiteRequest;
-import com.google.gwtjsonrpc.client.RemoteJsonService;
-import com.google.gwtjsonrpc.client.RpcImpl;
-import com.google.gwtjsonrpc.client.RpcImpl.Version;
+import com.google.gwtjsonrpc.common.AsyncCallback;
+import com.google.gwtjsonrpc.common.AllowCrossSiteRequest;
+import com.google.gwtjsonrpc.common.RemoteJsonService;
+import com.google.gwtjsonrpc.common.RpcImpl;
+import com.google.gwtjsonrpc.common.RpcImpl.Version;
 
 @RpcImpl(version = Version.V2_0)
 public interface UserPassAuthService extends RemoteJsonService {
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccessSection.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccessSection.java
new file mode 100644
index 0000000..cd64b0a
--- /dev/null
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccessSection.java
@@ -0,0 +1,121 @@
+// 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.common.data;
+
+import com.google.gerrit.reviewdb.client.Project;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+
+/** Portion of a {@link Project} describing access rules. */
+public class AccessSection extends RefConfigSection implements
+    Comparable<AccessSection> {
+  /** Special name given to the global capabilities; not a valid reference. */
+  public static final String GLOBAL_CAPABILITIES = "GLOBAL_CAPABILITIES";
+
+  protected List<Permission> permissions;
+
+  protected AccessSection() {
+  }
+
+  public AccessSection(String refPattern) {
+    super(refPattern);
+  }
+
+  public List<Permission> getPermissions() {
+    if (permissions == null) {
+      permissions = new ArrayList<Permission>();
+    }
+    return permissions;
+  }
+
+  public void setPermissions(List<Permission> list) {
+    Set<String> names = new HashSet<String>();
+    for (Permission p : list) {
+      if (!names.add(p.getName().toLowerCase())) {
+        throw new IllegalArgumentException();
+      }
+    }
+
+    permissions = list;
+  }
+
+  public Permission getPermission(String name) {
+    return getPermission(name, false);
+  }
+
+  public Permission getPermission(String name, boolean create) {
+    for (Permission p : getPermissions()) {
+      if (p.getName().equalsIgnoreCase(name)) {
+        return p;
+      }
+    }
+
+    if (create) {
+      Permission p = new Permission(name);
+      permissions.add(p);
+      return p;
+    } else {
+      return null;
+    }
+  }
+
+  public void remove(Permission permission) {
+    if (permission != null) {
+      removePermission(permission.getName());
+    }
+  }
+
+  public void removePermission(String name) {
+    if (permissions != null) {
+      for (Iterator<Permission> itr = permissions.iterator(); itr.hasNext();) {
+        if (name.equalsIgnoreCase(itr.next().getName())) {
+          itr.remove();
+        }
+      }
+    }
+  }
+
+  public void mergeFrom(AccessSection section) {
+    for (Permission src : section.getPermissions()) {
+      Permission dst = getPermission(src.getName());
+      if (dst != null) {
+        dst.mergeFrom(src);
+      } else {
+        permissions.add(src);
+      }
+    }
+  }
+
+  @Override
+  public int compareTo(AccessSection o) {
+    return comparePattern().compareTo(o.comparePattern());
+  }
+
+  private String comparePattern() {
+    if (getName().startsWith(REGEX_PREFIX)) {
+      return getName().substring(REGEX_PREFIX.length());
+    }
+    return getName();
+  }
+
+  @Override
+  public String toString() {
+    return "AccessSection[" + getName() + "]";
+  }
+}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountDashboardInfo.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountDashboardInfo.java
index abcfe28..e24900b 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountDashboardInfo.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountDashboardInfo.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.common.data;
 
-import com.google.gerrit.reviewdb.Account;
+import com.google.gerrit.reviewdb.client.Account;
 
 import java.util.List;
 
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountInfo.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountInfo.java
index 39c5d9c..9a4d9fb 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountInfo.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountInfo.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.common.data;
 
-import com.google.gerrit.reviewdb.Account;
+import com.google.gerrit.reviewdb.client.Account;
 
 /** Summary information about an {@link Account}, for simple tabular displays. */
 public class AccountInfo {
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountInfoCache.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountInfoCache.java
index cf5f20b..bc028e8 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountInfoCache.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountInfoCache.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.common.data;
 
-import com.google.gerrit.reviewdb.Account;
+import com.google.gerrit.reviewdb.client.Account;
 
 import java.util.Collections;
 import java.util.HashMap;
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountProjectWatchInfo.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountProjectWatchInfo.java
index 89a50f1..581a09b 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountProjectWatchInfo.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountProjectWatchInfo.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.common.data;
 
-import com.google.gerrit.reviewdb.AccountProjectWatch;
-import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch;
+import com.google.gerrit.reviewdb.client.Project;
 
 public final class AccountProjectWatchInfo {
   protected AccountProjectWatch watch;
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountSecurity.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountSecurity.java
index 1117455..21aca69 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountSecurity.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountSecurity.java
@@ -15,17 +15,16 @@
 package com.google.gerrit.common.data;
 
 import com.google.gerrit.common.auth.SignInRequired;
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.AccountExternalId;
-import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.AccountSshKey;
-import com.google.gerrit.reviewdb.ContactInformation;
-import com.google.gerrit.reviewdb.ContributorAgreement;
-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 com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountExternalId;
+import com.google.gerrit.reviewdb.client.AccountSshKey;
+import com.google.gerrit.reviewdb.client.ContactInformation;
+import com.google.gerrit.reviewdb.client.ContributorAgreement;
+import com.google.gwtjsonrpc.common.AsyncCallback;
+import com.google.gwtjsonrpc.common.RemoteJsonService;
+import com.google.gwtjsonrpc.common.RpcImpl;
+import com.google.gwtjsonrpc.common.VoidResult;
+import com.google.gwtjsonrpc.common.RpcImpl.Version;
 
 import java.util.List;
 import java.util.Set;
@@ -57,7 +56,7 @@
   void myExternalIds(AsyncCallback<List<AccountExternalId>> callback);
 
   @SignInRequired
-  void myGroups(AsyncCallback<List<AccountGroup>> callback);
+  void myGroups(AsyncCallback<List<GroupDetail>> callback);
 
   @SignInRequired
   void deleteExternalIds(Set<AccountExternalId.Key> keys,
@@ -72,7 +71,7 @@
       AsyncCallback<VoidResult> callback);
 
   @SignInRequired
-  void registerEmail(String address, AsyncCallback<VoidResult> callback);
+  void registerEmail(String address, AsyncCallback<Account> callback);
 
   @SignInRequired
   void validateEmail(String token, AsyncCallback<VoidResult> callback);
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountService.java
index e219074..7377d7e 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountService.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountService.java
@@ -15,15 +15,15 @@
 package com.google.gerrit.common.data;
 
 import com.google.gerrit.common.auth.SignInRequired;
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.AccountDiffPreference;
-import com.google.gerrit.reviewdb.AccountGeneralPreferences;
-import com.google.gerrit.reviewdb.AccountProjectWatch;
-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 com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountDiffPreference;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch;
+import com.google.gwtjsonrpc.common.AsyncCallback;
+import com.google.gwtjsonrpc.common.RemoteJsonService;
+import com.google.gwtjsonrpc.common.RpcImpl;
+import com.google.gwtjsonrpc.common.VoidResult;
+import com.google.gwtjsonrpc.common.RpcImpl.Version;
 
 import java.util.List;
 import java.util.Set;
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/AgreementInfo.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/AgreementInfo.java
index 7ae651e..0c6f6b7 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/AgreementInfo.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/AgreementInfo.java
@@ -14,9 +14,9 @@
 
 package com.google.gerrit.common.data;
 
-import com.google.gerrit.reviewdb.AccountAgreement;
-import com.google.gerrit.reviewdb.AccountGroupAgreement;
-import com.google.gerrit.reviewdb.ContributorAgreement;
+import com.google.gerrit.reviewdb.client.AccountAgreement;
+import com.google.gerrit.reviewdb.client.AccountGroupAgreement;
+import com.google.gerrit.reviewdb.client.ContributorAgreement;
 
 import java.util.List;
 import java.util.Map;
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ApprovalDetail.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ApprovalDetail.java
index 9834693..3d438f2 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ApprovalDetail.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ApprovalDetail.java
@@ -14,16 +14,16 @@
 
 package com.google.gerrit.common.data;
 
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.ApprovalCategory;
-import com.google.gerrit.reviewdb.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.ApprovalCategory;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
 
 import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.Comparator;
-import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
-import java.util.Map;
+import java.util.Set;
 
 public class ApprovalDetail {
   public static final Comparator<ApprovalDetail> SORT =
@@ -43,6 +43,8 @@
   protected List<PatchSetApproval> approvals;
   protected boolean canRemove;
 
+  private transient Set<String> approved;
+  private transient Set<String> rejected;
   private transient int hasNonZero;
   private transient Timestamp sortOrder = EG_D;
 
@@ -66,13 +68,17 @@
     canRemove = removeable;
   }
 
-  public Map<ApprovalCategory.Id, PatchSetApproval> getApprovalMap() {
-    final HashMap<ApprovalCategory.Id, PatchSetApproval> r;
-    r = new HashMap<ApprovalCategory.Id, PatchSetApproval>();
-    for (final PatchSetApproval ca : approvals) {
-      r.put(ca.getCategoryId(), ca);
+  public List<PatchSetApproval> getPatchSetApprovals() {
+    return approvals;
+  }
+
+  public PatchSetApproval getPatchSetApproval(ApprovalCategory.Id category) {
+    for (PatchSetApproval psa : approvals) {
+      if (psa.getCategoryId().equals(category)) {
+        return psa;
+      }
     }
-    return r;
+    return null;
   }
 
   public void sortFirst() {
@@ -91,4 +97,26 @@
       hasNonZero = 1;
     }
   }
+
+  public void approved(String label) {
+    if (approved == null) {
+      approved = new HashSet<String>();
+    }
+    approved.add(label);
+  }
+
+  public void rejected(String label) {
+    if (rejected == null) {
+      rejected = new HashSet<String>();
+    }
+    rejected.add(label);
+  }
+
+  public boolean isApproved(String label) {
+    return approved != null && approved.contains(label);
+  }
+
+  public boolean isRejected(String label) {
+    return rejected != null && rejected.contains(label);
+  }
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ApprovalSummary.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ApprovalSummary.java
index 57e8b71..19c9d67 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ApprovalSummary.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ApprovalSummary.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.common.data;
 
-import com.google.gerrit.reviewdb.ApprovalCategory;
-import com.google.gerrit.reviewdb.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.ApprovalCategory;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
 
 import java.util.Collections;
 import java.util.HashMap;
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ApprovalSummarySet.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ApprovalSummarySet.java
index 57ee928..2b11085 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ApprovalSummarySet.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ApprovalSummarySet.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.common.data;
 
-import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.reviewdb.client.Change;
 
 import java.util.Collections;
 import java.util.HashMap;
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ApprovalType.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ApprovalType.java
index ea9aedb..333b91c 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ApprovalType.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ApprovalType.java
@@ -14,9 +14,9 @@
 
 package com.google.gerrit.common.data;
 
-import com.google.gerrit.reviewdb.ApprovalCategory;
-import com.google.gerrit.reviewdb.ApprovalCategoryValue;
-import com.google.gerrit.reviewdb.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.ApprovalCategory;
+import com.google.gerrit.reviewdb.client.ApprovalCategoryValue;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -31,6 +31,7 @@
   protected short maxNegative;
   protected short maxPositive;
 
+  private transient List<Integer> intList;
   private transient Map<Short, ApprovalCategoryValue> byValue;
 
   protected ApprovalType() {
@@ -56,6 +57,9 @@
         maxPositive = values.get(values.size() - 1).getValue();
       }
     }
+
+    // Force the label name to pre-compute so we don't have data race conditions.
+    getCategory().getLabelName();
   }
 
   public ApprovalCategory getCategory() {
@@ -107,4 +111,16 @@
       }
     }
   }
+
+  public List<Integer> getValuesAsList() {
+    if (intList == null) {
+      intList = new ArrayList<Integer>(values.size());
+      for (ApprovalCategoryValue acv : values) {
+        intList.add(Integer.valueOf(acv.getValue()));
+      }
+      Collections.sort(intList);
+      Collections.reverse(intList);
+    }
+    return intList;
+  }
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ApprovalTypes.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ApprovalTypes.java
index 1b6d4a3..b1e32d1 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ApprovalTypes.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ApprovalTypes.java
@@ -14,25 +14,22 @@
 
 package com.google.gerrit.common.data;
 
-import com.google.gerrit.reviewdb.ApprovalCategory;
+import com.google.gerrit.reviewdb.client.ApprovalCategory;
 
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
 
 public class ApprovalTypes {
   protected List<ApprovalType> approvalTypes;
-  protected List<ApprovalType> actionTypes;
-  private transient Map<ApprovalCategory.Id, ApprovalType> byCategoryId;
+  private transient Map<ApprovalCategory.Id, ApprovalType> byId;
+  private transient Map<String, ApprovalType> byLabel;
 
   protected ApprovalTypes() {
   }
 
-  public ApprovalTypes(final List<ApprovalType> approvals,
-      final List<ApprovalType> actions) {
+  public ApprovalTypes(final List<ApprovalType> approvals) {
     approvalTypes = approvals;
-    actionTypes = actions;
     byCategory();
   }
 
@@ -40,33 +37,35 @@
     return approvalTypes;
   }
 
-  public List<ApprovalType> getActionTypes() {
-    return actionTypes;
-  }
-
-  public ApprovalType getApprovalType(final ApprovalCategory.Id id) {
+  public ApprovalType byId(final ApprovalCategory.Id id) {
     return byCategory().get(id);
   }
 
-  public Set<ApprovalCategory.Id> getApprovalCategories() {
-    return byCategory().keySet();
-  }
-
   private Map<ApprovalCategory.Id, ApprovalType> byCategory() {
-    if (byCategoryId == null) {
-      byCategoryId = new HashMap<ApprovalCategory.Id, ApprovalType>();
-      if (actionTypes != null) {
-        for (final ApprovalType t : actionTypes) {
-          byCategoryId.put(t.getCategory().getId(), t);
-        }
-      }
-
+    if (byId == null) {
+      byId = new HashMap<ApprovalCategory.Id, ApprovalType>();
       if (approvalTypes != null) {
         for (final ApprovalType t : approvalTypes) {
-          byCategoryId.put(t.getCategory().getId(), t);
+          byId.put(t.getCategory().getId(), t);
         }
       }
     }
-    return byCategoryId;
+    return byId;
+  }
+
+  public ApprovalType byLabel(String labelName) {
+    return byLabel().get(labelName.toLowerCase());
+  }
+
+  private Map<String, ApprovalType> byLabel() {
+    if (byLabel == null) {
+      byLabel = new HashMap<String, ApprovalType>();
+      if (approvalTypes != null) {
+        for (ApprovalType t : approvalTypes) {
+          byLabel.put(t.getCategory().getLabelName().toLowerCase(), t);
+        }
+      }
+    }
+    return byLabel;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_42.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/Capable.java
similarity index 64%
copy from gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_42.java
copy to gerrit-common/src/main/java/com/google/gerrit/common/data/Capable.java
index 83bca7b..0be4b76 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_42.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/Capable.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2010 The Android Open Source Project
+// 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.
@@ -12,14 +12,18 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.schema;
+package com.google.gerrit.common.data;
 
-import com.google.inject.Inject;
-import com.google.inject.Provider;
+public class Capable {
+  public static final Capable OK = new Capable("OK");
 
-public class Schema_42 extends SchemaVersion {
-  @Inject
-  Schema_42(Provider<Schema_41> prior) {
-    super(prior);
+  private final String message;
+
+  public Capable(String msg) {
+    message = msg;
+  }
+
+  public String getMessage() {
+    return message;
   }
 }
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 572fd7a..74d1962 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
@@ -14,35 +14,37 @@
 
 package com.google.gerrit.common.data;
 
-import com.google.gerrit.reviewdb.ApprovalCategory;
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.ChangeMessage;
-import com.google.gerrit.reviewdb.PatchSet;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.PatchSet;
 
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
-import java.util.Set;
 
 /** Detail necessary to display a change. */
 public class ChangeDetail {
   protected AccountInfoCache accounts;
   protected boolean allowsAnonymous;
   protected boolean canAbandon;
+  protected boolean canPublish;
+  protected boolean canRebase;
   protected boolean canRestore;
   protected boolean canRevert;
+  protected boolean canDeleteDraft;
   protected Change change;
   protected boolean starred;
   protected List<ChangeInfo> dependsOn;
   protected List<ChangeInfo> neededBy;
   protected List<PatchSet> patchSets;
   protected List<ApprovalDetail> approvals;
-  protected Set<ApprovalCategory.Id> missingApprovals;
+  protected List<SubmitRecord> submitRecords;
+  protected boolean canSubmit;
   protected List<ChangeMessage> messages;
   protected PatchSet.Id currentPatchSetId;
   protected PatchSetDetail currentDetail;
-  protected Set<ApprovalCategory.Id> currentActions;
+  protected boolean canEdit;
 
   public ChangeDetail() {
   }
@@ -71,6 +73,22 @@
     canAbandon = a;
   }
 
+  public boolean canPublish() {
+    return canPublish;
+  }
+
+  public void setCanPublish(final boolean a) {
+    canPublish = a;
+  }
+
+  public boolean canRebase() {
+    return canRebase;
+  }
+
+  public void setCanRebase(final boolean a) {
+    canRebase = a;
+  }
+
   public boolean canRestore() {
     return canRestore;
   }
@@ -87,6 +105,22 @@
       canRevert = a;
   }
 
+  public boolean canSubmit() {
+    return canSubmit;
+  }
+
+  public void setCanSubmit(boolean a) {
+    canSubmit = a;
+  }
+
+  public boolean canDeleteDraft() {
+    return canDeleteDraft;
+  }
+
+  public void setCanDeleteDraft(boolean a) {
+    canDeleteDraft = a;
+  }
+
   public Change getChange() {
     return change;
   }
@@ -145,20 +179,12 @@
     Collections.sort(approvals, ApprovalDetail.SORT);
   }
 
-  public Set<ApprovalCategory.Id> getMissingApprovals() {
-    return missingApprovals;
+  public void setSubmitRecords(List<SubmitRecord> all) {
+    submitRecords = all;
   }
 
-  public void setMissingApprovals(Set<ApprovalCategory.Id> a) {
-    missingApprovals = a;
-  }
-
-  public Set<ApprovalCategory.Id> getCurrentActions() {
-    return currentActions;
-  }
-
-  public void setCurrentActions(Set<ApprovalCategory.Id> a) {
-    currentActions = a;
+  public List<SubmitRecord> getSubmitRecords() {
+    return submitRecords;
   }
 
   public boolean isCurrentPatchSet(final PatchSetDetail detail) {
@@ -189,7 +215,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/ChangeDetailService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetailService.java
index 2fbda21..8b43624 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetailService.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeDetailService.java
@@ -15,13 +15,13 @@
 package com.google.gerrit.common.data;
 
 import com.google.gerrit.common.auth.SignInRequired;
-import com.google.gerrit.reviewdb.AccountDiffPreference;
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.PatchSet;
-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.RpcImpl.Version;
+import com.google.gerrit.reviewdb.client.AccountDiffPreference;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gwtjsonrpc.common.AsyncCallback;
+import com.google.gwtjsonrpc.common.RemoteJsonService;
+import com.google.gwtjsonrpc.common.RpcImpl;
+import com.google.gwtjsonrpc.common.RpcImpl.Version;
 
 @RpcImpl(version = Version.V2_0)
 public interface ChangeDetailService extends RemoteJsonService {
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeInfo.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeInfo.java
index b5271ec..391d615 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeInfo.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeInfo.java
@@ -14,8 +14,9 @@
 
 package com.google.gerrit.common.data;
 
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
 
 import java.sql.Timestamp;
 
@@ -31,11 +32,13 @@
   protected boolean starred;
   protected Timestamp lastUpdatedOn;
   protected String sortKey;
+  protected PatchSet.Id patchSetId;
+  protected boolean latest;
 
   protected ChangeInfo() {
   }
 
-  public ChangeInfo(final Change c) {
+  public ChangeInfo(final Change c, final PatchSet.Id patchId) {
     id = c.getId();
     key = c.getKey();
     owner = c.getOwner();
@@ -46,6 +49,12 @@
     topic = c.getTopic();
     lastUpdatedOn = c.getLastUpdatedOn();
     sortKey = c.getSortKey();
+    patchSetId = patchId;
+    latest = patchSetId == null || c.currPatchSetId().equals(patchSetId);
+  }
+
+  public ChangeInfo(final Change c) {
+    this(c, null);
   }
 
   public Change.Id getId() {
@@ -88,6 +97,14 @@
     starred = s;
   }
 
+  public PatchSet.Id getPatchSetId() {
+    return patchSetId;
+  }
+
+  public boolean isLatest() {
+    return latest;
+  }
+
   public java.sql.Timestamp getLastUpdatedOn() {
     return lastUpdatedOn;
   }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeListService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeListService.java
index 5ff85e3..f646bc6 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeListService.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ChangeListService.java
@@ -15,13 +15,13 @@
 package com.google.gerrit.common.data;
 
 import com.google.gerrit.common.auth.SignInRequired;
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.Change;
-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 com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gwtjsonrpc.common.AsyncCallback;
+import com.google.gwtjsonrpc.common.RemoteJsonService;
+import com.google.gwtjsonrpc.common.RpcImpl;
+import com.google.gwtjsonrpc.common.VoidResult;
+import com.google.gwtjsonrpc.common.RpcImpl.Version;
 
 import java.util.Set;
 
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..872bddc 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
@@ -15,11 +15,12 @@
 package com.google.gerrit.common.data;
 
 import com.google.gerrit.common.auth.SignInRequired;
-import com.google.gerrit.reviewdb.PatchSet;
-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.RpcImpl.Version;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gwtjsonrpc.common.AsyncCallback;
+import com.google.gwtjsonrpc.common.RemoteJsonService;
+import com.google.gwtjsonrpc.common.RpcImpl;
+import com.google.gwtjsonrpc.common.VoidResult;
+import com.google.gwtjsonrpc.common.RpcImpl.Version;
 
 @RpcImpl(version = Version.V2_0)
 public interface ChangeManageService extends RemoteJsonService {
@@ -37,4 +38,13 @@
   @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);
+
+  @SignInRequired
+  void rebaseChange(PatchSet.Id patchSetId, AsyncCallback<ChangeDetail> callback);
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/CommentDetail.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/CommentDetail.java
index 576e077..0e079b3 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/CommentDetail.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/CommentDetail.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.common.data;
 
-import com.google.gerrit.reviewdb.PatchLineComment;
-import com.google.gerrit.reviewdb.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.PatchSet;
 
 import java.util.ArrayList;
 import java.util.Collections;
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 9bb87ab..456ffb4 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
@@ -15,10 +15,10 @@
 package com.google.gerrit.common.data;
 
 import com.google.gerrit.common.auth.openid.OpenIdProviderPattern;
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.AuthType;
-import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.AccountGeneralPreferences.DownloadScheme;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AuthType;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme;
 import com.google.gwtexpui.safehtml.client.RegexFindReplace;
 
 import java.util.List;
@@ -26,21 +26,25 @@
 
 public class GerritConfig implements Cloneable {
   protected String registerUrl;
+  protected String httpPasswordUrl;
   protected List<OpenIdProviderPattern> allowedOpenIDs;
 
-  protected GitwebLink gitweb;
+  protected GitwebConfig gitweb;
   protected boolean useContributorAgreements;
   protected boolean useContactInfo;
   protected boolean allowRegisterNewEmail;
   protected AuthType authType;
   protected Set<DownloadScheme> downloadSchemes;
   protected String gitDaemonUrl;
+  protected String gitHttpUrl;
   protected String sshdAddress;
   protected Project.NameKey wildProject;
   protected ApprovalTypes approvalTypes;
   protected Set<Account.FieldName> editableAccountFields;
   protected List<RegexFindReplace> commentLinks;
   protected boolean documentationAvailable;
+  protected boolean testChangeMerge;
+  protected String anonymousCowardName;
 
   public String getRegisterUrl() {
     return registerUrl;
@@ -50,6 +54,14 @@
     registerUrl = u;
   }
 
+  public String getHttpPasswordUrl() {
+    return httpPasswordUrl;
+  }
+
+  public void setHttpPasswordUrl(String url) {
+    httpPasswordUrl = url;
+  }
+
   public List<OpenIdProviderPattern> getAllowedOpenIDs() {
     return allowedOpenIDs;
   }
@@ -74,11 +86,11 @@
     downloadSchemes = s;
   }
 
-  public GitwebLink getGitwebLink() {
+  public GitwebConfig getGitwebLink() {
     return gitweb;
   }
 
-  public void setGitwebLink(final GitwebLink w) {
+  public void setGitwebLink(final GitwebConfig w) {
     gitweb = w;
   }
 
@@ -109,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;
   }
@@ -160,4 +183,20 @@
   public void setDocumentationAvailable(final boolean available) {
     documentationAvailable = available;
   }
+
+  public boolean testChangeMerge() {
+    return testChangeMerge;
+  }
+
+  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-server/src/main/java/com/google/gerrit/server/schema/Schema_42.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GitwebConfig.java
similarity index 60%
copy from gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_42.java
copy to gerrit-common/src/main/java/com/google/gerrit/common/data/GitwebConfig.java
index 83bca7b..0503c60 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_42.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GitwebConfig.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2010 The Android Open Source Project
+// 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.
@@ -12,14 +12,18 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.schema;
+package com.google.gerrit.common.data;
 
-import com.google.inject.Inject;
-import com.google.inject.Provider;
+/** Link to an external gitweb server. */
+public class GitwebConfig {
+  public String baseUrl;
+  public GitWebType type;
 
-public class Schema_42 extends SchemaVersion {
-  @Inject
-  Schema_42(Provider<Schema_41> prior) {
-    super(prior);
+  protected GitwebConfig() {
+  }
+
+  public GitwebConfig(final String base, final GitWebType gitWebType) {
+    baseUrl = base;
+    type = gitWebType;
   }
 }
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
deleted file mode 100644
index 937bbd4..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GitwebLink.java
+++ /dev/null
@@ -1,64 +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.common.data;
-
-import com.google.gerrit.reviewdb.Branch;
-import com.google.gerrit.reviewdb.PatchSet;
-import com.google.gerrit.reviewdb.Project;
-import com.google.gwt.http.client.URL;
-
-import java.util.HashMap;
-import java.util.Map;
-
-/** Link to an external gitweb server. */
-public class GitwebLink {
-  protected String baseUrl;
-
-  protected GitWebType type;
-
-  protected GitwebLink() {
-  }
-
-  public GitwebLink(final String base, final GitWebType gitWebType) {
-    baseUrl = base;
-    type = gitWebType;
-  }
-
-  public String toRevision(final Project.NameKey project, final PatchSet ps) {
-    ParamertizedString pattern = new ParamertizedString(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()));
-    return baseUrl + pattern.replace(p);
-  }
-
-  public String toProject(final Project.NameKey project) {
-    ParamertizedString pattern = new ParamertizedString(type.getProject());
-
-    final Map<String, String> p = new HashMap<String, String>();
-    p.put("project", URL.encodeQueryString(project.get()));
-    return baseUrl + pattern.replace(p);
-  }
-
-  public String toBranch(final Branch.NameKey branch) {
-    ParamertizedString pattern = new ParamertizedString(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()));
-    return baseUrl + pattern.replace(p);
-  }
-}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java
new file mode 100644
index 0000000..d3d2a4d
--- /dev/null
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java
@@ -0,0 +1,119 @@
+// 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 java.util.ArrayList;
+import java.util.List;
+
+/** Server wide capabilities. Represented as {@link Permission} objects. */
+public class GlobalCapability {
+  /**
+   * Denotes the server's administrators.
+   * <p>
+   * This is similar to UNIX root, or Windows SYSTEM account. Any user that
+   * has this capability can perform almost any other action, or can grant
+   * themselves the power to perform any other action on the site. Most of
+   * the other capabilities and permissions fall-back to the predicate
+   * "OR user has capablity ADMINISTRATE_SERVER".
+   */
+  public static final String ADMINISTRATE_SERVER = "administrateServer";
+
+  /** Can create any account on the server. */
+  public static final String CREATE_ACCOUNT = "createAccount";
+
+  /** Can create any group on the server. */
+  public static final String CREATE_GROUP = "createGroup";
+
+  /** Can create any project on the server. */
+  public static final String CREATE_PROJECT = "createProject";
+
+  /**
+   * Denotes who may email change reviewers.
+   * <p>
+   * This can be used to deny build bots from emailing reviewers and people who
+   * have starred the changed. Instead, only the authors of the change will be
+   * emailed. The allow rules are evaluated before deny rules, however the
+   * default is to allow emailing, if no explicit rule is matched.
+   */
+  public static final String EMAIL_REVIEWERS = "emailReviewers";
+
+  /** Can flush any cache except the active web_sessions cache. */
+  public static final String FLUSH_CACHES = "flushCaches";
+
+  /** Can terminate any task using the kill command. */
+  public static final String KILL_TASK = "killTask";
+
+  /** Queue a user can access to submit their tasks to. */
+  public static final String PRIORITY = "priority";
+
+  /** Maximum result limit per executed query. */
+  public static final String QUERY_LIMIT = "queryLimit";
+
+  /** Forcefully restart replication to any configured destination. */
+  public static final String START_REPLICATION = "startReplication";
+
+  /** Can view the server's current cache states. */
+  public static final String VIEW_CACHES = "viewCaches";
+
+  /** Can view open connections to the server's SSH port. */
+  public static final String VIEW_CONNECTIONS = "viewConnections";
+
+  /** Can view all pending tasks in the queue (not just the filtered set). */
+  public static final String VIEW_QUEUE = "viewQueue";
+
+  private static final List<String> NAMES_LC;
+
+  static {
+    NAMES_LC = new ArrayList<String>();
+    NAMES_LC.add(ADMINISTRATE_SERVER.toLowerCase());
+    NAMES_LC.add(CREATE_ACCOUNT.toLowerCase());
+    NAMES_LC.add(CREATE_GROUP.toLowerCase());
+    NAMES_LC.add(CREATE_PROJECT.toLowerCase());
+    NAMES_LC.add(EMAIL_REVIEWERS.toLowerCase());
+    NAMES_LC.add(FLUSH_CACHES.toLowerCase());
+    NAMES_LC.add(KILL_TASK.toLowerCase());
+    NAMES_LC.add(PRIORITY.toLowerCase());
+    NAMES_LC.add(QUERY_LIMIT.toLowerCase());
+    NAMES_LC.add(START_REPLICATION.toLowerCase());
+    NAMES_LC.add(VIEW_CACHES.toLowerCase());
+    NAMES_LC.add(VIEW_CONNECTIONS.toLowerCase());
+    NAMES_LC.add(VIEW_QUEUE.toLowerCase());
+  }
+
+  /** @return true if the name is recognized as a capability name. */
+  public static boolean isCapability(String varName) {
+    return NAMES_LC.contains(varName.toLowerCase());
+  }
+
+  /** @return true if the capability should have a range attached. */
+  public static boolean hasRange(String varName) {
+    return QUERY_LIMIT.equalsIgnoreCase(varName);
+  }
+
+  /** @return the valid range for the capability if it has one, otherwise null. */
+  public static PermissionRange.WithDefaults getRange(String varName) {
+    if (QUERY_LIMIT.equalsIgnoreCase(varName)) {
+      return new PermissionRange.WithDefaults(
+          varName,
+          0, Integer.MAX_VALUE,
+          0, 500);
+    }
+    return null;
+  }
+
+  private GlobalCapability() {
+    // Utility class, do not create instances.
+  }
+}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupAdminService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupAdminService.java
index ce508cc..f385e27 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupAdminService.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupAdminService.java
@@ -15,14 +15,14 @@
 package com.google.gerrit.common.data;
 
 import com.google.gerrit.common.auth.SignInRequired;
-import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.AccountGroupInclude;
-import com.google.gerrit.reviewdb.AccountGroupMember;
-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 com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupInclude;
+import com.google.gerrit.reviewdb.client.AccountGroupMember;
+import com.google.gwtjsonrpc.common.AsyncCallback;
+import com.google.gwtjsonrpc.common.RemoteJsonService;
+import com.google.gwtjsonrpc.common.RpcImpl;
+import com.google.gwtjsonrpc.common.VoidResult;
+import com.google.gwtjsonrpc.common.RpcImpl.Version;
 
 import java.util.List;
 import java.util.Set;
@@ -30,13 +30,14 @@
 @RpcImpl(version = Version.V2_0)
 public interface GroupAdminService extends RemoteJsonService {
   @SignInRequired
-  void visibleGroups(AsyncCallback<List<AccountGroup>> callback);
+  void visibleGroups(AsyncCallback<GroupList> callback);
 
   @SignInRequired
   void createGroup(String newName, AsyncCallback<AccountGroup.Id> callback);
 
   @SignInRequired
-  void groupDetail(AccountGroup.Id groupId, AsyncCallback<GroupDetail> callback);
+  void groupDetail(AccountGroup.Id groupId, AccountGroup.UUID uuid,
+      AsyncCallback<GroupDetail> callback);
 
   @SignInRequired
   void changeGroupDescription(AccountGroup.Id groupId, String description,
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDetail.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDetail.java
index 69e535c..65723f7 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDetail.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDetail.java
@@ -14,9 +14,9 @@
 
 package com.google.gerrit.common.data;
 
-import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.AccountGroupInclude;
-import com.google.gerrit.reviewdb.AccountGroupMember;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupInclude;
+import com.google.gerrit.reviewdb.client.AccountGroupMember;
 
 import java.util.List;
 
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupInfo.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupInfo.java
index 55577f2..547a5f4 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupInfo.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupInfo.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.common.data;
 
-import com.google.gerrit.reviewdb.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroup;
 
 /** Summary information about an {@link AccountGroup}, for simple tabular displays. */
 public class GroupInfo {
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupInfoCache.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupInfoCache.java
index 80d8756..6a5dd5c 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupInfoCache.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupInfoCache.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.common.data;
 
-import com.google.gerrit.reviewdb.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroup;
 
 import java.util.Collections;
 import java.util.HashMap;
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
new file mode 100644
index 0000000..6352461
--- /dev/null
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupList.java
@@ -0,0 +1,46 @@
+// 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 java.util.List;
+
+public class GroupList {
+  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;
+  }
+
+  public void setGroups(List<GroupDetail> groups) {
+    this.groups = groups;
+  }
+
+  public boolean isCanCreateGroup() {
+    return canCreateGroup;
+  }
+
+  public void setCanCreateGroup(boolean set) {
+    canCreateGroup = set;
+  }
+}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupOptions.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupOptions.java
index 82e0618..c6ed781 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupOptions.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupOptions.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.common.data;
 
-import com.google.gerrit.reviewdb.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroup;
 
 /**
  * Options for an {@link AccountGroup}.
@@ -22,22 +22,15 @@
 public class GroupOptions {
 
   private boolean visibleToAll;
-  private boolean emailOnlyAuthors;
 
   protected GroupOptions() {
   }
 
-  public GroupOptions(final boolean visibleToAll,
-       final boolean emailOnlyAuthors) {
+  public GroupOptions(final boolean visibleToAll) {
     this.visibleToAll = visibleToAll;
-    this.emailOnlyAuthors = emailOnlyAuthors;
   }
 
   public boolean isVisibleToAll() {
     return visibleToAll;
   }
-
-  public boolean isEmailOnlyAuthors() {
-    return emailOnlyAuthors;
-  }
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupReference.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupReference.java
new file mode 100644
index 0000000..f05d1b9
--- /dev/null
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupReference.java
@@ -0,0 +1,76 @@
+// 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.common.data;
+
+import com.google.gerrit.reviewdb.client.AccountGroup;
+
+/** Describes a group within a projects {@link AccessSection}s. */
+public class GroupReference implements Comparable<GroupReference> {
+  /** @return a new reference to the given group description. */
+  public static GroupReference forGroup(AccountGroup group) {
+    return new GroupReference(group.getGroupUUID(), group.getName());
+  }
+
+  protected String uuid;
+  protected String name;
+
+  protected GroupReference() {
+  }
+
+  public GroupReference(AccountGroup.UUID uuid, String name) {
+    setUUID(uuid);
+    setName(name);
+  }
+
+  public AccountGroup.UUID getUUID() {
+    return uuid != null ? new AccountGroup.UUID(uuid) : null;
+  }
+
+  public void setUUID(AccountGroup.UUID newUUID) {
+    uuid = newUUID != null ? newUUID.get() : null;
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  public void setName(String newName) {
+    this.name = newName;
+  }
+
+  @Override
+  public int compareTo(GroupReference o) {
+    return uuid(this).compareTo(uuid(o));
+  }
+
+  private static String uuid(GroupReference a) {
+    return a.getUUID() != null ? a.getUUID().get() : "?";
+  }
+
+  @Override
+  public int hashCode() {
+    return uuid(this).hashCode();
+  }
+
+  @Override
+  public boolean equals(Object o) {
+    return o instanceof GroupReference && compareTo((GroupReference) o) == 0;
+  }
+
+  @Override
+  public String toString() {
+    return "Group[" + getName() + " / " + getUUID() + "]";
+  }
+}
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..c3d3f1e 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
@@ -14,13 +14,14 @@
 
 package com.google.gerrit.common.data;
 
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.AccountDiffPreference;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountDiffPreference;
 
 /** Data sent as part of the host page, to bootstrap the UI. */
 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/InheritedRefRight.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/InheritedRefRight.java
deleted file mode 100644
index 4dc998b..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/InheritedRefRight.java
+++ /dev/null
@@ -1,74 +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.common.data;
-
-import com.google.gerrit.reviewdb.RefRight;
-
-/**
- * Additional data about a {@link RefRight} not normally loaded: defines if a
- * right is inherited from a parent structure (e.g. a parent project).
- */
-public class InheritedRefRight {
-  private RefRight right;
-  private boolean inherited;
-  private boolean owner;
-
-  /**
-   * Creates a instance of a {@link RefRight} with data about inheritance
-   */
-  protected InheritedRefRight() {
-  }
-
-  /**
-   * Creates a instance of a {@link RefRight} with data about inheritance
-   *
-   * @param right the right
-   * @param inherited true if the right is inherited, false otherwise
-   * @param owner true if right is owned by current user, false otherwise
-   */
-  public InheritedRefRight(RefRight right, boolean inherited, boolean owner) {
-    this.right = right;
-    this.inherited = inherited;
-    this.owner = owner;
-  }
-
-  public RefRight getRight() {
-    return right;
-  }
-
-  public boolean isInherited() {
-    return inherited;
-  }
-
-  public boolean isOwner() {
-    return owner;
-  }
-
-  @Override
-  public boolean equals(Object o) {
-    if (o instanceof InheritedRefRight) {
-      InheritedRefRight a = this;
-      InheritedRefRight b = (InheritedRefRight) o;
-      return a.getRight().equals(b.getRight())
-          && a.isInherited() == b.isInherited();
-    }
-    return false;
-  }
-
-  @Override
-  public int hashCode() {
-    return getRight().hashCode();
-  }
-}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ListBranchesResult.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ListBranchesResult.java
index 7341c47..1c830e9 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ListBranchesResult.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ListBranchesResult.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.common.data;
 
-import com.google.gerrit.reviewdb.Branch;
+import com.google.gerrit.reviewdb.client.Branch;
 
 import java.util.List;
 
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ParamertizedString.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ParameterizedString.java
similarity index 81%
rename from gerrit-common/src/main/java/com/google/gerrit/common/data/ParamertizedString.java
rename to gerrit-common/src/main/java/com/google/gerrit/common/data/ParameterizedString.java
index ae696b5..68676cf 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ParamertizedString.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ParameterizedString.java
@@ -21,10 +21,10 @@
 import java.util.Map;
 
 /** Performs replacements on strings such as <code>Hello ${user}</code>. */
-public class ParamertizedString {
+public class ParameterizedString {
   /** Obtain a string which has no parameters and always produces the value. */
-  public static ParamertizedString asis(final String constant) {
-    return new ParamertizedString(new Constant(constant));
+  public static ParameterizedString asis(final String constant) {
+    return new ParameterizedString(new Constant(constant));
   }
 
   private final String pattern;
@@ -32,18 +32,18 @@
   private final List<Format> patternOps;
   private final List<Parameter> parameters;
 
-  protected ParamertizedString() {
+  protected ParameterizedString() {
     this(new Constant(""));
   }
 
-  private ParamertizedString(final Constant c) {
+  private ParameterizedString(final Constant c) {
     pattern = c.text;
     rawPattern = c.text;
     patternOps = Collections.<Format> singletonList(c);
     parameters = Collections.emptyList();
   }
 
-  public ParamertizedString(final String pattern) {
+  public ParameterizedString(final String pattern) {
     final StringBuilder raw = new StringBuilder();
     final List<Parameter> prs = new ArrayList<Parameter>(4);
     final List<Format> ops = new ArrayList<Format>(4);
@@ -63,20 +63,27 @@
       ops.add(new Constant(pattern.substring(i, b)));
 
       String expr = pattern.substring(b + 2, e);
-      Function function;
-      int lastDot = expr.lastIndexOf('.');
-      if (lastDot < 0) {
-        function = NOOP;
+      String parameterName = "";
+      List<Function> functions = new ArrayList<Function>();
+      if (!expr.contains(".")) {
+        parameterName = expr;
       } else {
-        function = FUNCTIONS.get(expr.substring(lastDot + 1));
-        if (function == null) {
-          function = NOOP;
-        } else {
-          expr = expr.substring(0, lastDot);
+        int firstDot = expr.indexOf('.');
+        parameterName = expr.substring(0, firstDot);
+        String actionsStr = expr.substring(firstDot + 1);
+        String[] actions = actionsStr.split("\\.");
+
+        for (String action : actions) {
+          Function function = FUNCTIONS.get(action);
+          if (function == null) {
+            function = NOOP;
+          }
+          functions.add(function);
         }
       }
 
-      final Parameter p = new Parameter(expr, function);
+      final Parameter p =
+          new Parameter(parameterName, Collections.unmodifiableList(functions));
       raw.append("{" + prs.size() + "}");
       prs.add(p);
       ops.add(p);
@@ -152,7 +159,7 @@
 
     @Override
     public String toString() {
-      return ParamertizedString.this.replace(params);
+      return ParameterizedString.this.replace(params);
     }
   }
 
@@ -175,11 +182,11 @@
 
   private static class Parameter extends Format {
     private final String name;
-    private final Function function;
+    private final List<Function> functions;
 
-    Parameter(final String name, final Function function) {
+    Parameter(final String name, final List<Function> functions) {
       this.name = name;
-      this.function = function;
+      this.functions = functions;
     }
 
     @Override
@@ -188,7 +195,10 @@
       if (v == null) {
         v = "";
       }
-      b.append(function.apply(v));
+      for (Function function : functions) {
+        v = function.apply(v);
+      }
+      b.append(v);
     }
   }
 
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 5aec0c7..0191544 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
@@ -15,19 +15,19 @@
 package com.google.gerrit.common.data;
 
 import com.google.gerrit.common.auth.SignInRequired;
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.AccountDiffPreference;
-import com.google.gerrit.reviewdb.ApprovalCategoryValue;
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.Patch;
-import com.google.gerrit.reviewdb.PatchLineComment;
-import com.google.gerrit.reviewdb.PatchSet;
-import com.google.gerrit.reviewdb.Patch.Key;
-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 com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountDiffPreference;
+import com.google.gerrit.reviewdb.client.ApprovalCategoryValue;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Patch.Key;
+import com.google.gwtjsonrpc.common.AsyncCallback;
+import com.google.gwtjsonrpc.common.RemoteJsonService;
+import com.google.gwtjsonrpc.common.RpcImpl;
+import com.google.gwtjsonrpc.common.VoidResult;
+import com.google.gwtjsonrpc.common.RpcImpl.Version;
 
 import java.util.List;
 import java.util.Set;
@@ -44,13 +44,29 @@
   @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,
       AsyncCallback<VoidResult> callback);
 
   @SignInRequired
-  void addReviewers(Change.Id id, List<String> reviewers,
+  void addReviewers(Change.Id id, List<String> reviewers, boolean confirmed,
       AsyncCallback<ReviewerResult> callback);
 
   @SignInRequired
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java
index 24cdee4..2308b77 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java
@@ -14,16 +14,13 @@
 
 package com.google.gerrit.common.data;
 
-import com.google.gerrit.prettify.client.ClientSideFormatter;
 import com.google.gerrit.prettify.common.EditList;
-import com.google.gerrit.prettify.common.PrettyFormatter;
 import com.google.gerrit.prettify.common.SparseFileContent;
-import com.google.gerrit.prettify.common.SparseHtmlFile;
-import com.google.gerrit.reviewdb.AccountDiffPreference;
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.Patch;
-import com.google.gerrit.reviewdb.AccountDiffPreference.Whitespace;
-import com.google.gerrit.reviewdb.Patch.ChangeType;
+import com.google.gerrit.reviewdb.client.AccountDiffPreference;
+import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.Patch.ChangeType;
 
 import org.eclipse.jgit.diff.Edit;
 
@@ -155,6 +152,10 @@
     return intralineFailure;
   }
 
+  public boolean isExpandAllComments() {
+    return diffPrefs.isExpandAllComments();
+  }
+
   public SparseFileContent getA() {
     return a;
   }
@@ -163,36 +164,6 @@
     return b;
   }
 
-  public SparseHtmlFile getSparseHtmlFileA() {
-    AccountDiffPreference dp = new AccountDiffPreference(diffPrefs);
-    dp.setShowWhitespaceErrors(false);
-
-    PrettyFormatter f = ClientSideFormatter.FACTORY.get();
-    f.setDiffPrefs(dp);
-    f.setFileName(a.getPath());
-    f.setEditFilter(PrettyFormatter.A);
-    f.setEditList(edits);
-    f.format(a);
-    return f;
-  }
-
-  public SparseHtmlFile getSparseHtmlFileB() {
-    AccountDiffPreference dp = new AccountDiffPreference(diffPrefs);
-
-    PrettyFormatter f = ClientSideFormatter.FACTORY.get();
-    f.setDiffPrefs(dp);
-    f.setFileName(b.getPath());
-    f.setEditFilter(PrettyFormatter.B);
-    f.setEditList(edits);
-
-    if (dp.isSyntaxHighlighting() && a.isWholeFile() && !b.isWholeFile()) {
-      f.format(b.apply(a, edits));
-    } else {
-      f.format(b);
-    }
-    return f;
-  }
-
   public List<Edit> getEdits() {
     return edits;
   }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchSetDetail.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchSetDetail.java
index 64e666e..a2debf2 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchSetDetail.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchSetDetail.java
@@ -14,9 +14,9 @@
 
 package com.google.gerrit.common.data;
 
-import com.google.gerrit.reviewdb.Patch;
-import com.google.gerrit.reviewdb.PatchSet;
-import com.google.gerrit.reviewdb.PatchSetInfo;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetInfo;
 
 import java.util.List;
 
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchSetPublishDetail.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchSetPublishDetail.java
index 7a0ada3..075d558 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchSetPublishDetail.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchSetPublishDetail.java
@@ -14,40 +14,36 @@
 
 package com.google.gerrit.common.data;
 
-import com.google.gerrit.reviewdb.ApprovalCategory;
-import com.google.gerrit.reviewdb.ApprovalCategoryValue;
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.PatchLineComment;
-import com.google.gerrit.reviewdb.PatchSetApproval;
-import com.google.gerrit.reviewdb.PatchSetInfo;
+import com.google.gerrit.reviewdb.client.ApprovalCategory;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.PatchSetInfo;
 
 import java.util.List;
-import java.util.Map;
-import java.util.Set;
 
 public class PatchSetPublishDetail {
   protected AccountInfoCache accounts;
   protected PatchSetInfo patchSetInfo;
   protected Change change;
   protected List<PatchLineComment> drafts;
-  protected Map<ApprovalCategory.Id, Set<ApprovalCategoryValue.Id>> allowed;
-  protected Map<ApprovalCategory.Id, PatchSetApproval> given;
-  protected boolean isSubmitAllowed;
+  protected List<PermissionRange> labels;
+  protected List<PatchSetApproval> given;
+  protected boolean canSubmit;
 
-  public Map<ApprovalCategory.Id, Set<ApprovalCategoryValue.Id>> getAllowed() {
-    return allowed;
+  public List<PermissionRange> getLabels() {
+    return labels;
   }
 
-  public void setAllowed(
-      Map<ApprovalCategory.Id, Set<ApprovalCategoryValue.Id>> allowed) {
-    this.allowed = allowed;
+  public void setLabels(List<PermissionRange> labels) {
+    this.labels = labels;
   }
 
-  public Map<ApprovalCategory.Id, PatchSetApproval> getGiven() {
+  public List<PatchSetApproval> getGiven() {
     return given;
   }
 
-  public void setGiven(Map<ApprovalCategory.Id, PatchSetApproval> given) {
+  public void setGiven(List<PatchSetApproval> given) {
     this.given = given;
   }
 
@@ -67,8 +63,8 @@
     this.drafts = drafts;
   }
 
-  public void setSubmitAllowed(boolean allowed) {
-    isSubmitAllowed = allowed;
+  public void setCanSubmit(boolean allowed) {
+    canSubmit = allowed;
   }
 
   public AccountInfoCache getAccounts() {
@@ -87,20 +83,25 @@
     return drafts;
   }
 
-  public boolean isAllowed(final ApprovalCategory.Id id) {
-    final Set<ApprovalCategoryValue.Id> s = getAllowed(id);
-    return s != null && !s.isEmpty();
+  public PermissionRange getRange(final String permissionName) {
+    for (PermissionRange s : labels) {
+      if (s.getName().equals(permissionName)) {
+        return s;
+      }
+    }
+    return null;
   }
 
-  public Set<ApprovalCategoryValue.Id> getAllowed(final ApprovalCategory.Id id) {
-    return allowed.get(id);
+  public PatchSetApproval getChangeApproval(ApprovalCategory.Id id) {
+    for (PatchSetApproval a : given) {
+      if (a.getCategoryId().equals(id)) {
+        return a;
+      }
+    }
+    return null;
   }
 
-  public PatchSetApproval getChangeApproval(final ApprovalCategory.Id id) {
-    return given.get(id);
-  }
-
-  public boolean isSubmitAllowed() {
-    return isSubmitAllowed;
+  public boolean canSubmit() {
+    return canSubmit;
   }
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/Permission.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/Permission.java
new file mode 100644
index 0000000..20261de
--- /dev/null
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/Permission.java
@@ -0,0 +1,211 @@
+// 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.common.data;
+
+import java.util.ArrayList;
+import java.util.Iterator;
+import java.util.List;
+
+/** A single permission within an {@link AccessSection} of a project. */
+public class Permission implements Comparable<Permission> {
+  public static final String CREATE = "create";
+  public static final String FORGE_AUTHOR = "forgeAuthor";
+  public static final String FORGE_COMMITTER = "forgeCommitter";
+  public static final String FORGE_SERVER = "forgeServerAsCommitter";
+  public static final String LABEL = "label-";
+  public static final String OWNER = "owner";
+  public static final String PUSH = "push";
+  public static final String PUSH_MERGE = "pushMerge";
+  public static final String PUSH_TAG = "pushTag";
+  public static final String READ = "read";
+  public static final String REBASE = "rebase";
+  public static final String SUBMIT = "submit";
+
+  private static final List<String> NAMES_LC;
+  private static final int labelIndex;
+
+  static {
+    NAMES_LC = new ArrayList<String>();
+    NAMES_LC.add(OWNER.toLowerCase());
+    NAMES_LC.add(READ.toLowerCase());
+    NAMES_LC.add(CREATE.toLowerCase());
+    NAMES_LC.add(FORGE_AUTHOR.toLowerCase());
+    NAMES_LC.add(FORGE_COMMITTER.toLowerCase());
+    NAMES_LC.add(FORGE_SERVER.toLowerCase());
+    NAMES_LC.add(PUSH.toLowerCase());
+    NAMES_LC.add(PUSH_MERGE.toLowerCase());
+    NAMES_LC.add(PUSH_TAG.toLowerCase());
+    NAMES_LC.add(LABEL.toLowerCase());
+    NAMES_LC.add(REBASE.toLowerCase());
+    NAMES_LC.add(SUBMIT.toLowerCase());
+
+    labelIndex = NAMES_LC.indexOf(Permission.LABEL);
+  }
+
+  /** @return true if the name is recognized as a permission name. */
+  public static boolean isPermission(String varName) {
+    String lc = varName.toLowerCase();
+    if (lc.startsWith(LABEL)) {
+      return LABEL.length() < lc.length();
+    }
+    return NAMES_LC.contains(lc);
+  }
+
+  /** @return true if the permission name is actually for a review label. */
+  public static boolean isLabel(String varName) {
+    return varName.startsWith(LABEL) && LABEL.length() < varName.length();
+  }
+
+  /** @return permission name for the given review label. */
+  public static String forLabel(String labelName) {
+    return LABEL + labelName;
+  }
+
+  protected String name;
+  protected boolean exclusiveGroup;
+  protected List<PermissionRule> rules;
+
+  protected Permission() {
+  }
+
+  public Permission(String name) {
+    this.name = name;
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  public boolean isLabel() {
+    return isLabel(getName());
+  }
+
+  public String getLabel() {
+    if (isLabel()) {
+      return getName().substring(LABEL.length());
+    }
+    return null;
+  }
+
+  public Boolean getExclusiveGroup() {
+    // Only permit exclusive group behavior on non OWNER permissions,
+    // otherwise an owner might lose access to a delegated subspace.
+    //
+    return exclusiveGroup && !OWNER.equals(getName());
+  }
+
+  public void setExclusiveGroup(Boolean newExclusiveGroup) {
+    exclusiveGroup = newExclusiveGroup;
+  }
+
+  public List<PermissionRule> getRules() {
+    initRules();
+    return rules;
+  }
+
+  public void setRules(List<PermissionRule> list) {
+    rules = list;
+  }
+
+  public void add(PermissionRule rule) {
+    initRules();
+    rules.add(rule);
+  }
+
+  public void remove(PermissionRule rule) {
+    if (rule != null) {
+      removeRule(rule.getGroup());
+    }
+  }
+
+  public void removeRule(GroupReference group) {
+    if (rules != null) {
+      for (Iterator<PermissionRule> itr = rules.iterator(); itr.hasNext();) {
+        if (sameGroup(itr.next(), group)) {
+          itr.remove();
+        }
+      }
+    }
+  }
+
+  public PermissionRule getRule(GroupReference group) {
+    return getRule(group, false);
+  }
+
+  public PermissionRule getRule(GroupReference group, boolean create) {
+    initRules();
+
+    for (PermissionRule r : rules) {
+      if (sameGroup(r, group)) {
+        return r;
+      }
+    }
+
+    if (create) {
+      PermissionRule r = new PermissionRule(group);
+      rules.add(r);
+      return r;
+    } else {
+      return null;
+    }
+  }
+
+  void mergeFrom(Permission src) {
+    for (PermissionRule srcRule : src.getRules()) {
+      PermissionRule dstRule = getRule(srcRule.getGroup());
+      if (dstRule != null) {
+        dstRule.mergeFrom(srcRule);
+      } else {
+        add(srcRule);
+      }
+    }
+  }
+
+  private static boolean sameGroup(PermissionRule rule, GroupReference group) {
+    if (group.getUUID() != null) {
+      return group.getUUID().equals(rule.getGroup().getUUID());
+
+    } else if (group.getName() != null) {
+      return group.getName().equals(rule.getGroup().getName());
+
+    } else {
+      return false;
+    }
+  }
+
+  private void initRules() {
+    if (rules == null) {
+      rules = new ArrayList<PermissionRule>(4);
+    }
+  }
+
+  @Override
+  public int compareTo(Permission b) {
+    int cmp = index(this) - index(b);
+    if (cmp == 0) {
+      cmp = getName().compareTo(b.getName());
+    }
+    return cmp;
+  }
+
+  private static int index(Permission a) {
+    if (a.isLabel()) {
+      return labelIndex;
+    }
+
+    int index = NAMES_LC.indexOf(a.getName().toLowerCase());
+    return 0 <= index ? index : NAMES_LC.size();
+  }
+}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/PermissionRange.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/PermissionRange.java
new file mode 100644
index 0000000..3490dd7
--- /dev/null
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/PermissionRange.java
@@ -0,0 +1,138 @@
+// 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.common.data;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class PermissionRange implements Comparable<PermissionRange> {
+  public static class WithDefaults extends PermissionRange {
+    protected int defaultMin;
+    protected int defaultMax;
+
+    protected WithDefaults() {
+    }
+
+    public WithDefaults(String name, int min, int max, int defMin, int defMax) {
+      super(name, min, max);
+      setDefaultRange(defMin, defMax);
+    }
+
+    public int getDefaultMin() {
+      return defaultMin;
+    }
+
+    public int getDefaultMax() {
+      return defaultMax;
+    }
+
+    public void setDefaultRange(int min, int max) {
+      defaultMin = min;
+      defaultMax = max;
+    }
+
+    /** @return all values between {@link #getMin()} and {@link #getMax()} */
+    public List<Integer> getValuesAsList() {
+      ArrayList<Integer> r = new ArrayList<Integer>(getRangeSize());
+      for (int i = min; i <= max; i++) {
+        r.add(i);
+      }
+      return r;
+    }
+
+    /** @return number of values between {@link #getMin()} and {@link #getMax()} */
+    public int getRangeSize() {
+      return max - min;
+    }
+  }
+
+  protected String name;
+  protected int min;
+  protected int max;
+
+  protected PermissionRange() {
+  }
+
+  public PermissionRange(String name, int min, int max) {
+    this.name = name;
+
+    if (min <= max) {
+      this.min = min;
+      this.max = max;
+    } else {
+      this.min = max;
+      this.max = min;
+    }
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  public boolean isLabel() {
+    return Permission.isLabel(getName());
+  }
+
+  public String getLabel() {
+    return isLabel() ? getName().substring(Permission.LABEL.length()) : null;
+  }
+
+  public int getMin() {
+    return min;
+  }
+
+  public int getMax() {
+    return max;
+  }
+
+  /** True if the value is within the range. */
+  public boolean contains(int value) {
+    return getMin() <= value && value <= getMax();
+  }
+
+  /** Normalize the value to fit within the bounds of the range. */
+  public int squash(int value) {
+    return Math.min(Math.max(getMin(), value), getMax());
+  }
+
+  /** True both {@link #getMin()} and {@link #getMax()} are 0. */
+  public boolean isEmpty() {
+    return getMin() == 0 && getMax() == 0;
+  }
+
+  @Override
+  public int compareTo(PermissionRange o) {
+    return getName().compareTo(o.getName());
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder r = new StringBuilder();
+    if (getMin() < 0 && getMax() == 0) {
+      r.append(getMin());
+      r.append(' ');
+    } else {
+      if (getMin() != getMax()) {
+        if (0 <= getMin()) r.append('+');
+        r.append(getMin());
+        r.append("..");
+      }
+      if (0 <= getMax()) r.append('+');
+      r.append(getMax());
+      r.append(' ');
+    }
+    return r.toString();
+  }
+}
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
new file mode 100644
index 0000000..9b6695e
--- /dev/null
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/PermissionRule.java
@@ -0,0 +1,260 @@
+// 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.common.data;
+
+public class PermissionRule implements Comparable<PermissionRule> {
+  public static final String FORCE_PUSH = "Force Push";
+  public static enum Action {
+    ALLOW, DENY, BLOCK,
+
+    INTERACTIVE, BATCH;
+  }
+
+  protected Action action = Action.ALLOW;
+  protected boolean force;
+  protected int min;
+  protected int max;
+  protected GroupReference group;
+
+  public PermissionRule() {
+  }
+
+  public PermissionRule(GroupReference group) {
+    this.group = group;
+  }
+
+  public Action getAction() {
+    return action;
+  }
+
+  public void setAction(Action action) {
+    if (action == null) {
+      throw new NullPointerException("action");
+    }
+    this.action = action;
+  }
+
+  public boolean isDeny() {
+    return action == Action.DENY;
+  }
+
+  public void setDeny() {
+    action = Action.DENY;
+  }
+
+  public boolean isBlock() {
+    return action == Action.BLOCK;
+  }
+
+  public void setBlock() {
+    action = Action.BLOCK;
+  }
+
+  public Boolean getForce() {
+    return force;
+  }
+
+  public void setForce(Boolean newForce) {
+    force = newForce;
+  }
+
+  public Integer getMin() {
+    return min;
+  }
+
+  public void setMin(Integer min) {
+    this.min = min;
+  }
+
+  public void setMax(Integer max) {
+    this.max = max;
+  }
+
+  public Integer getMax() {
+    return max;
+  }
+
+  public void setRange(int newMin, int newMax) {
+    if (newMax < newMin) {
+      min = newMax;
+      max = newMin;
+    } else {
+      min = newMin;
+      max = newMax;
+    }
+  }
+
+  public GroupReference getGroup() {
+    return group;
+  }
+
+  public void setGroup(GroupReference newGroup) {
+    group = newGroup;
+  }
+
+  void mergeFrom(PermissionRule src) {
+    if (getAction() != src.getAction()) {
+      if (getAction() == Action.BLOCK || src.getAction() == Action.BLOCK) {
+        setAction(Action.BLOCK);
+
+      } else if (getAction() == Action.DENY || src.getAction() == Action.DENY) {
+        setAction(Action.DENY);
+
+      } else if (getAction() == Action.BATCH || src.getAction() == Action.BATCH) {
+        setAction(Action.BATCH);
+
+      }
+    }
+
+    setForce(getForce() || src.getForce());
+    setRange(Math.min(getMin(), src.getMin()), Math.max(getMax(), src.getMax()));
+  }
+
+  @Override
+  public int compareTo(PermissionRule o) {
+    int cmp = action(this) - action(o);
+    if (cmp == 0) cmp = range(o) - range(this);
+    if (cmp == 0) cmp = group(this).compareTo(group(o));
+    return cmp;
+  }
+
+  private static int action(PermissionRule a) {
+    switch (a.getAction()) {
+      case DENY:
+        return 0;
+      default:
+        return 1 + a.getAction().ordinal();
+    }
+  }
+
+  private static int range(PermissionRule a) {
+    return Math.abs(a.getMin()) + Math.abs(a.getMax());
+  }
+
+  private static String group(PermissionRule a) {
+    return a.getGroup().getName() != null ? a.getGroup().getName() : "";
+  }
+
+  @Override
+  public String toString() {
+    return asString(true);
+  }
+
+  public String asString(boolean canUseRange) {
+    StringBuilder r = new StringBuilder();
+
+    switch (getAction()) {
+      case ALLOW:
+        break;
+
+      case DENY:
+        r.append("deny ");
+        break;
+
+      case BLOCK:
+        r.append("block ");
+        break;
+
+      case INTERACTIVE:
+        r.append("interactive ");
+        break;
+
+      case BATCH:
+        r.append("batch ");
+        break;
+    }
+
+    if (getForce()) {
+      r.append("+force ");
+    }
+
+    if (canUseRange && (getMin() != 0 || getMax() != 0)) {
+      if (0 <= getMin()) r.append('+');
+      r.append(getMin());
+      r.append("..");
+      if (0 <= getMax()) r.append('+');
+      r.append(getMax());
+      r.append(' ');
+    }
+
+    r.append("group ");
+    r.append(getGroup().getName());
+
+    return r.toString();
+  }
+
+  public static PermissionRule fromString(String src, boolean mightUseRange) {
+    final String orig = src;
+    final PermissionRule rule = new PermissionRule();
+
+    src = src.trim();
+
+    if (src.startsWith("deny ")) {
+      rule.setAction(Action.DENY);
+      src = src.substring("deny ".length()).trim();
+
+    } else if (src.startsWith("block ")) {
+      rule.setAction(Action.BLOCK);
+      src = src.substring("block ".length()).trim();
+
+    } else if (src.startsWith("interactive ")) {
+      rule.setAction(Action.INTERACTIVE);
+      src = src.substring("interactive ".length()).trim();
+
+    } else if (src.startsWith("batch ")) {
+      rule.setAction(Action.BATCH);
+      src = src.substring("batch ".length()).trim();
+    }
+
+    if (src.startsWith("+force ")) {
+      rule.setForce(true);
+      src = src.substring("+force ".length()).trim();
+    }
+
+    if (mightUseRange && !src.startsWith("group ")) {
+      int sp = src.indexOf(' ');
+      String range = src.substring(0, sp);
+
+      if (range.matches("^([+-]?\\d+)\\.\\.([+-]?\\d+)$")) {
+        int dotdot = range.indexOf("..");
+        int min = parseInt(range.substring(0, dotdot));
+        int max = parseInt(range.substring(dotdot + 2));
+        rule.setRange(min, max);
+      } else {
+        throw new IllegalArgumentException("Invalid range in rule: " + orig);
+      }
+
+      src = src.substring(sp + 1).trim();
+    }
+
+    if (src.startsWith("group ")) {
+      src = src.substring(6).trim();
+      GroupReference group = new GroupReference();
+      group.setName(src);
+      rule.setGroup(group);
+    } else {
+      throw new IllegalArgumentException("Rule must include group: " + orig);
+    }
+
+    return rule;
+  }
+
+  private static int parseInt(String value) {
+    if (value.startsWith("+")) {
+      value = value.substring(1);
+    }
+    return Integer.parseInt(value);
+  }
+}
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
new file mode 100644
index 0000000..f935c03
--- /dev/null
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectAccess.java
@@ -0,0 +1,97 @@
+// Copyright (C) 2011 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.common.data;
+
+import com.google.gerrit.reviewdb.client.Project;
+
+import java.util.List;
+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;
+  }
+
+  public void setRevision(String name) {
+    revision = name;
+  }
+
+  public Project.NameKey getInheritsFrom() {
+    return inheritsFrom;
+  }
+
+  public void setInheritsFrom(Project.NameKey name) {
+    inheritsFrom = name;
+  }
+
+  public List<AccessSection> getLocal() {
+    return local;
+  }
+
+  public void setLocal(List<AccessSection> as) {
+    local = as;
+  }
+
+  public AccessSection getLocal(String name) {
+    for (AccessSection s : local) {
+      if (s.getName().equals(name)) {
+        return s;
+      }
+    }
+    return null;
+  }
+
+  public boolean isOwnerOf(AccessSection section) {
+    return isOwnerOf(section.getName());
+  }
+
+  public boolean isOwnerOf(String name) {
+    return ownerOf.contains(name);
+  }
+
+  public Set<String> getOwnerOf() {
+    return ownerOf;
+  }
+
+  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 b5a986f..1b504b0 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
@@ -15,37 +15,43 @@
 package com.google.gerrit.common.data;
 
 import com.google.gerrit.common.auth.SignInRequired;
-import com.google.gerrit.reviewdb.ApprovalCategory;
-import com.google.gerrit.reviewdb.Branch;
-import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.RefRight;
-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.RpcImpl.Version;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gwtjsonrpc.common.AsyncCallback;
+import com.google.gwtjsonrpc.common.RemoteJsonService;
+import com.google.gwtjsonrpc.common.RpcImpl;
+import com.google.gwtjsonrpc.common.VoidResult;
+import com.google.gwtjsonrpc.common.RpcImpl.Version;
 
 import java.util.List;
 import java.util.Set;
 
 @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);
+
+  @SignInRequired
   void changeProjectSettings(Project update,
       AsyncCallback<ProjectDetail> callback);
 
   @SignInRequired
-  void deleteRight(Project.NameKey projectName, Set<RefRight.Key> ids,
-      AsyncCallback<ProjectDetail> callback);
-
-  @SignInRequired
-  void addRight(Project.NameKey projectName, ApprovalCategory.Id categoryId,
-      String groupName, String refName, short min, short max,
-      AsyncCallback<ProjectDetail> callback);
+  void changeProjectAccess(Project.NameKey projectName, String baseRevision,
+      String message, List<AccessSection> sections,
+      AsyncCallback<ProjectAccess> callback);
 
   void listBranches(Project.NameKey projectName,
       AsyncCallback<ListBranchesResult> 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 2aa8c62..1d88cf8 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
@@ -14,20 +14,16 @@
 
 package com.google.gerrit.common.data;
 
-import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.Project;
-
-import java.util.List;
-import java.util.Map;
+import com.google.gerrit.reviewdb.client.Project;
 
 public class ProjectDetail {
   public Project project;
-  public Map<AccountGroup.Id, AccountGroup> groups;
-  public List<InheritedRefRight> rights;
   public boolean canModifyDescription;
   public boolean canModifyMergeType;
   public boolean canModifyAgreements;
   public boolean canModifyAccess;
+  public boolean canModifyState;
+  public boolean isPermissionOnly;
 
   public ProjectDetail() {
   }
@@ -36,14 +32,6 @@
     project = p;
   }
 
-  public void setGroups(final Map<AccountGroup.Id, AccountGroup> g) {
-    groups = g;
-  }
-
-  public void setRights(final List<InheritedRefRight> r) {
-    rights = r;
-  }
-
   public void setCanModifyDescription(final boolean cmd) {
     canModifyDescription = cmd;
   }
@@ -52,6 +40,10 @@
     canModifyMergeType = cmmt;
   }
 
+  public void setCanModifyState(final boolean cms) {
+    canModifyState = cms;
+  }
+
   public void setCanModifyAgreements(final boolean cma) {
     canModifyAgreements = cma;
   }
@@ -59,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/ProjectInfo.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectInfo.java
index 8f22936..dfef806 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectInfo.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectInfo.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.common.data;
 
-import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.reviewdb.client.Project;
 
 public class ProjectInfo {
   protected Project.NameKey key;
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..8511460
--- /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.client.Project;
+
+import java.util.List;
+
+public class ProjectList {
+  protected List<Project> projects;
+  protected boolean canCreateProject;
+
+  public ProjectList() {
+  }
+
+  public List<Project> getProjects() {
+    return projects;
+  }
+
+  public void setProjects(List<Project> projects) {
+    this.projects = projects;
+  }
+
+  public boolean canCreateProject() {
+    return canCreateProject;
+  }
+
+  public void setCanCreateProject(boolean canCreateProject) {
+    this.canCreateProject = canCreateProject;
+  }
+}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/RefConfigSection.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/RefConfigSection.java
new file mode 100644
index 0000000..490378e
--- /dev/null
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/RefConfigSection.java
@@ -0,0 +1,48 @@
+// 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;
+
+public abstract class RefConfigSection {
+  /** Pattern that matches all references in a project. */
+  public static final String ALL = "refs/*";
+
+  /** Pattern that matches all branches in a project. */
+  public static final String HEADS = "refs/heads/*";
+
+  /** Prefix that triggers a regular expression pattern. */
+  public static final String REGEX_PREFIX = "^";
+
+  /** @return true if the name is likely to be a valid reference section name. */
+  public static boolean isValid(String name) {
+    return name.startsWith("refs/") || name.startsWith("^refs/");
+  }
+
+  protected String name;
+
+  public RefConfigSection() {
+  }
+
+  public RefConfigSection(String name) {
+    setName(name);
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  public void setName(String name) {
+    this.name = name;
+  }
+}
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..001f9b4
--- /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.client.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/data/ReviewerInfo.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ReviewerInfo.java
new file mode 100644
index 0000000..063f13b
--- /dev/null
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ReviewerInfo.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.common.data;
+
+/**
+ * Suggested reviewer for a change. Can be a user ({@link AccountInfo}) or a
+ * group ({@link GroupReference}).
+ */
+public class ReviewerInfo implements Comparable<ReviewerInfo> {
+  private AccountInfo accountInfo;
+  private GroupReference groupReference;
+
+  protected ReviewerInfo() {
+  }
+
+  public ReviewerInfo(final AccountInfo accountInfo) {
+    this.accountInfo = accountInfo;
+  }
+
+  public ReviewerInfo(final GroupReference groupReference) {
+    this.groupReference = groupReference;
+  }
+
+  public AccountInfo getAccountInfo() {
+    return accountInfo;
+  }
+
+  public GroupReference getGroup() {
+    return groupReference;
+  }
+
+  @Override
+  public int compareTo(final ReviewerInfo o) {
+    return getSortValue().compareTo(o.getSortValue());
+  }
+
+  private String getSortValue() {
+    if (accountInfo != null) {
+      if (accountInfo.getPreferredEmail() != null) {
+        return accountInfo.getPreferredEmail();
+      }
+      return accountInfo.getFullName().toString();
+    }
+    return groupReference.getName();
+  }
+}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ReviewerResult.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ReviewerResult.java
index 678ec79..d85095a 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ReviewerResult.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ReviewerResult.java
@@ -24,6 +24,8 @@
 public class ReviewerResult {
   protected List<Error> errors;
   protected ChangeDetail change;
+  protected int memberCount;
+  protected boolean askForConfirmation;
 
   public ReviewerResult() {
     errors = new ArrayList<Error>();
@@ -45,10 +47,26 @@
     change = d;
   }
 
+  public int getMemberCount() {
+    return memberCount;
+  }
+
+  public void setMemberCount(final int memberCount) {
+    this.memberCount = memberCount;
+  }
+
+  public boolean askForConfirmation() {
+    return askForConfirmation;
+  }
+
+  public void setAskForConfirmation(final boolean askForConfirmation) {
+    this.askForConfirmation = askForConfirmation;
+  }
+
   public static class Error {
     public static enum Type {
-      /** Name supplied does not match to a registered account. */
-      ACCOUNT_NOT_FOUND,
+      /** Name supplied does not match to a registered account or account group. */
+      REVIEWER_NOT_FOUND,
 
       /** The account is inactive. */
       ACCOUNT_INACTIVE,
@@ -56,8 +74,20 @@
       /** The account is not permitted to see the change. */
       CHANGE_NOT_VISIBLE,
 
-      /** Could not remove this reviewer from the change. */
-      COULD_NOT_REMOVE
+      /** The groups has no members. */
+      GROUP_EMPTY,
+
+      /** The groups has too many members. */
+      GROUP_HAS_TOO_MANY_MEMBERS,
+
+      /** The group is not allowed to be added as reviewer. */
+      GROUP_NOT_ALLOWED,
+
+      /** Could not remove this reviewer from the change due to ORMException. */
+      COULD_NOT_REMOVE,
+
+      /** Not permitted to remove this reviewer from the change. */
+      REMOVE_NOT_PERMITTED
     }
 
     protected Type type;
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/SubmitRecord.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/SubmitRecord.java
new file mode 100644
index 0000000..5049ba4
--- /dev/null
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/SubmitRecord.java
@@ -0,0 +1,82 @@
+// Copyright (C) 2011 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.common.data;
+
+import com.google.gerrit.reviewdb.client.Account;
+
+import java.util.List;
+
+/**
+ * Describes the state required to submit a change.
+ */
+public class SubmitRecord {
+  public static enum Status {
+    /** The change is ready for submission. */
+    OK,
+
+    /** The change is missing a required label. */
+    NOT_READY,
+
+    /** The change has been closed. */
+    CLOSED,
+
+    /**
+     * An internal server error occurred preventing computation.
+     * <p>
+     * Additional detail may be available in {@link SubmitRecord#errorMessage}.
+     */
+    RULE_ERROR;
+  }
+
+  public Status status;
+  public List<Label> labels;
+  public String errorMessage;
+
+  public static class Label {
+    public static enum Status {
+      /**
+       * This label provides what is necessary for submission.
+       * <p>
+       * If provided, {@link Label#appliedBy} describes the user account
+       * that applied this label to the change.
+       */
+      OK,
+
+      /**
+       * This label prevents the change from being submitted.
+       * <p>
+       * If provided, {@link Label#appliedBy} describes the user account
+       * that applied this label to the change.
+       */
+      REJECT,
+
+      /**
+       * The label is required for submission, but has not been satisfied.
+       */
+      NEED,
+
+      /**
+       * The label is required for submission, but is impossible to complete.
+       * The likely cause is access has not been granted correctly by the
+       * project owner or site administrator.
+       */
+      IMPOSSIBLE;
+    }
+
+    public String label;
+    public Status status;
+    public Account.Id appliedBy;
+  }
+}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/SuggestService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/SuggestService.java
index 9dae169..85518b2 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/SuggestService.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/SuggestService.java
@@ -14,12 +14,13 @@
 
 package com.google.gerrit.common.data;
 
-import com.google.gerrit.reviewdb.AccountGroupName;
-import com.google.gerrit.reviewdb.Project;
-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.RpcImpl.Version;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gwtjsonrpc.common.AsyncCallback;
+import com.google.gwtjsonrpc.common.RemoteJsonService;
+import com.google.gwtjsonrpc.common.RpcImpl;
+import com.google.gwtjsonrpc.common.RpcImpl.Version;
 
 import java.util.List;
 
@@ -32,5 +33,23 @@
       AsyncCallback<List<AccountInfo>> callback);
 
   void suggestAccountGroup(String query, int limit,
-      AsyncCallback<List<AccountGroupName>> callback);
+      AsyncCallback<List<GroupReference>> callback);
+
+  /**
+   * @see #suggestChangeReviewer(com.google.gerrit.reviewdb.client.Change.Id, String, int, AsyncCallback)
+   */
+  @Deprecated
+  void suggestReviewer(Project.NameKey project, String query, int limit,
+      AsyncCallback<List<ReviewerInfo>> callback);
+
+  /**
+   * Suggests reviewers. A reviewer can be a user or a group. Inactive users,
+   * the system groups {@link AccountGroup#ANONYMOUS_USERS} and
+   * {@link AccountGroup#REGISTERED_USERS} and groups that have more than the
+   * configured <code>addReviewer.maxAllowed</code> members are not suggested as
+   * reviewers.
+   * @param changeId the change for which reviewers should be suggested
+   */
+  void suggestChangeReviewer(Change.Id changeId, String query, int limit,
+      AsyncCallback<List<ReviewerInfo>> callback);
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/SystemInfoService.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/SystemInfoService.java
index 3c16018..78ccca1 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/SystemInfoService.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/SystemInfoService.java
@@ -15,13 +15,13 @@
 package com.google.gerrit.common.data;
 
 import com.google.gerrit.common.auth.SignInRequired;
-import com.google.gerrit.reviewdb.ContributorAgreement;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import com.google.gwtjsonrpc.client.AllowCrossSiteRequest;
-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 com.google.gerrit.reviewdb.client.ContributorAgreement;
+import com.google.gwtjsonrpc.common.AsyncCallback;
+import com.google.gwtjsonrpc.common.AllowCrossSiteRequest;
+import com.google.gwtjsonrpc.common.RemoteJsonService;
+import com.google.gwtjsonrpc.common.RpcImpl;
+import com.google.gwtjsonrpc.common.VoidResult;
+import com.google.gwtjsonrpc.common.RpcImpl.Version;
 
 import java.util.List;
 
@@ -34,4 +34,6 @@
   void contributorAgreements(AsyncCallback<List<ContributorAgreement>> callback);
 
   void clientError(String message, AsyncCallback<VoidResult> callback);
+
+  public void gerritConfig(final AsyncCallback<GerritConfig> callback);
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ToggleStarRequest.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ToggleStarRequest.java
index 32178fb..c9ba72f 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ToggleStarRequest.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ToggleStarRequest.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.common.data;
 
-import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.reviewdb.client.Change;
 
 import java.util.HashSet;
 import java.util.Set;
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/errors/InvalidNameException.java b/gerrit-common/src/main/java/com/google/gerrit/common/errors/InvalidNameException.java
index 5eb6e3f..d975aef 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/errors/InvalidNameException.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/errors/InvalidNameException.java
@@ -23,4 +23,8 @@
   public InvalidNameException() {
     super(MESSAGE);
   }
+
+  public InvalidNameException(String invalidName) {
+    super(MESSAGE + ": " + invalidName);
+  }
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/errors/InvalidQueryException.java b/gerrit-common/src/main/java/com/google/gerrit/common/errors/InvalidQueryException.java
index 4a66a416a..53792e4 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/errors/InvalidQueryException.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/errors/InvalidQueryException.java
@@ -18,6 +18,10 @@
 public class InvalidQueryException extends Exception {
   private static final long serialVersionUID = 1L;
 
+  public InvalidQueryException(String message) {
+    super(message);
+  }
+
   public InvalidQueryException(String message, String query) {
     super("Invalid query: " + query + "\n\n" + message);
   }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/errors/InvalidUserNameException.java b/gerrit-common/src/main/java/com/google/gerrit/common/errors/InvalidUserNameException.java
index 55d490c..f1c35a8 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/errors/InvalidUserNameException.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/errors/InvalidUserNameException.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.common.errors;
 
-import com.google.gerrit.reviewdb.Account;
+import com.google.gerrit.reviewdb.client.Account;
 
 /** Error indicating the SSH user name does not match {@link Account#USER_NAME_PATTERN} pattern. */
 public class InvalidUserNameException extends Exception {
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/errors/NoSuchGroupException.java b/gerrit-common/src/main/java/com/google/gerrit/common/errors/NoSuchGroupException.java
index 4e117b1..ade0d98 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/errors/NoSuchGroupException.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/errors/NoSuchGroupException.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.common.errors;
 
-import com.google.gerrit.reviewdb.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroup;
 
 /** Indicates the account group does not exist. */
 public class NoSuchGroupException extends Exception {
@@ -26,10 +26,18 @@
     this(key, null);
   }
 
+  public NoSuchGroupException(final AccountGroup.UUID key) {
+    this(key, null);
+  }
+
   public NoSuchGroupException(final AccountGroup.Id key, final Throwable why) {
     super(MESSAGE + key.toString(), why);
   }
 
+  public NoSuchGroupException(final AccountGroup.UUID key, final Throwable why) {
+    super(MESSAGE + key.toString(), why);
+  }
+
   public NoSuchGroupException(final AccountGroup.NameKey k) {
     this(k, null);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_50.java b/gerrit-common/src/main/java/com/google/gerrit/common/errors/PermissionDeniedException.java
similarity index 69%
copy from gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_50.java
copy to gerrit-common/src/main/java/com/google/gerrit/common/errors/PermissionDeniedException.java
index 4a90c1f..76520aa 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_50.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/errors/PermissionDeniedException.java
@@ -12,14 +12,13 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.schema;
+package com.google.gerrit.common.errors;
 
-import com.google.inject.Inject;
-import com.google.inject.Provider;
+/** Indicats the user cannot perform this task. */
+public class PermissionDeniedException extends Exception {
+  private static final long serialVersionUID = 1L;
 
-public class Schema_50 extends SchemaVersion {
-  @Inject
-  Schema_50(Provider<Schema_49> prior) {
-    super(prior);
+  public PermissionDeniedException(String msg) {
+    super(msg);
   }
-}
\ No newline at end of file
+}
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-common/src/test/java/com/google/gerrit/common/data/ParamertizedStringTest.java b/gerrit-common/src/test/java/com/google/gerrit/common/data/ParamertizedStringTest.java
deleted file mode 100644
index af8301f..0000000
--- a/gerrit-common/src/test/java/com/google/gerrit/common/data/ParamertizedStringTest.java
+++ /dev/null
@@ -1,172 +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.common.data;
-
-import junit.framework.TestCase;
-
-import java.util.HashMap;
-import java.util.Map;
-
-public class ParamertizedStringTest extends TestCase {
-  public void testEmptyString() {
-    final ParamertizedString p = new ParamertizedString("");
-    assertEquals("", p.getPattern());
-    assertEquals("", p.getRawPattern());
-    assertTrue(p.getParameterNames().isEmpty());
-
-    final Map<String, String> a = new HashMap<String, String>();
-    assertNotNull(p.bind(a));
-    assertEquals(0, p.bind(a).length);
-    assertEquals("", p.replace(a));
-  }
-
-  public void testAsis1() {
-    final ParamertizedString p = ParamertizedString.asis("${bar}c");
-    assertEquals("${bar}c", p.getPattern());
-    assertEquals("${bar}c", p.getRawPattern());
-    assertTrue(p.getParameterNames().isEmpty());
-
-    final Map<String, String> a = new HashMap<String, String>();
-    a.put("bar", "frobinator");
-    assertNotNull(p.bind(a));
-    assertEquals(0, p.bind(a).length);
-    assertEquals("${bar}c", p.replace(a));
-  }
-
-  public void testReplace1() {
-    final ParamertizedString p = new ParamertizedString("${bar}c");
-    assertEquals("${bar}c", p.getPattern());
-    assertEquals("{0}c", p.getRawPattern());
-    assertEquals(1, p.getParameterNames().size());
-    assertTrue(p.getParameterNames().contains("bar"));
-
-    final Map<String, String> a = new HashMap<String, String>();
-    a.put("bar", "frobinator");
-    assertNotNull(p.bind(a));
-    assertEquals(1, p.bind(a).length);
-    assertEquals("frobinator", p.bind(a)[0]);
-    assertEquals("frobinatorc", p.replace(a));
-  }
-
-  public void testReplace2() {
-    final ParamertizedString p = new ParamertizedString("a${bar}c");
-    assertEquals("a${bar}c", p.getPattern());
-    assertEquals("a{0}c", p.getRawPattern());
-    assertEquals(1, p.getParameterNames().size());
-    assertTrue(p.getParameterNames().contains("bar"));
-
-    final Map<String, String> a = new HashMap<String, String>();
-    a.put("bar", "frobinator");
-    assertNotNull(p.bind(a));
-    assertEquals(1, p.bind(a).length);
-    assertEquals("frobinator", p.bind(a)[0]);
-    assertEquals("afrobinatorc", p.replace(a));
-  }
-
-  public void testReplace3() {
-    final ParamertizedString p = new ParamertizedString("a${bar}");
-    assertEquals("a${bar}", p.getPattern());
-    assertEquals("a{0}", p.getRawPattern());
-    assertEquals(1, p.getParameterNames().size());
-    assertTrue(p.getParameterNames().contains("bar"));
-
-    final Map<String, String> a = new HashMap<String, String>();
-    a.put("bar", "frobinator");
-    assertNotNull(p.bind(a));
-    assertEquals(1, p.bind(a).length);
-    assertEquals("frobinator", p.bind(a)[0]);
-    assertEquals("afrobinator", p.replace(a));
-  }
-
-  public void testReplace4() {
-    final ParamertizedString p = new ParamertizedString("a${bar}c");
-    assertEquals("a${bar}c", p.getPattern());
-    assertEquals("a{0}c", p.getRawPattern());
-    assertEquals(1, p.getParameterNames().size());
-    assertTrue(p.getParameterNames().contains("bar"));
-
-    final Map<String, String> a = new HashMap<String, String>();
-    assertNotNull(p.bind(a));
-    assertEquals(1, p.bind(a).length);
-    assertEquals("", p.bind(a)[0]);
-    assertEquals("ac", p.replace(a));
-  }
-
-  public void testReplaceToLowerCase() {
-    final ParamertizedString p = new ParamertizedString("${a.toLowerCase}");
-    assertEquals(1, p.getParameterNames().size());
-    assertTrue(p.getParameterNames().contains("a"));
-
-    final Map<String, String> a = new HashMap<String, String>();
-
-    a.put("a", "foo");
-    assertNotNull(p.bind(a));
-    assertEquals(1, p.bind(a).length);
-    assertEquals("foo", p.bind(a)[0]);
-    assertEquals("foo", p.replace(a));
-
-    a.put("a", "FOO");
-    assertNotNull(p.bind(a));
-    assertEquals(1, p.bind(a).length);
-    assertEquals("foo", p.bind(a)[0]);
-    assertEquals("foo", p.replace(a));
-  }
-
-  public void testReplaceToUpperCase() {
-    final ParamertizedString p = new ParamertizedString("${a.toUpperCase}");
-    assertEquals(1, p.getParameterNames().size());
-    assertTrue(p.getParameterNames().contains("a"));
-
-    final Map<String, String> a = new HashMap<String, String>();
-
-    a.put("a", "foo");
-    assertNotNull(p.bind(a));
-    assertEquals(1, p.bind(a).length);
-    assertEquals("FOO", p.bind(a)[0]);
-    assertEquals("FOO", p.replace(a));
-
-    a.put("a", "FOO");
-    assertNotNull(p.bind(a));
-    assertEquals(1, p.bind(a).length);
-    assertEquals("FOO", p.bind(a)[0]);
-    assertEquals("FOO", p.replace(a));
-  }
-
-  public void testReplaceLocalName() {
-    final ParamertizedString p = new ParamertizedString("${a.localPart}");
-    assertEquals(1, p.getParameterNames().size());
-    assertTrue(p.getParameterNames().contains("a"));
-
-    final Map<String, String> a = new HashMap<String, String>();
-
-    a.put("a", "foo@example.com");
-    assertNotNull(p.bind(a));
-    assertEquals(1, p.bind(a).length);
-    assertEquals("foo", p.bind(a)[0]);
-    assertEquals("foo", p.replace(a));
-
-    a.put("a", "foo");
-    assertNotNull(p.bind(a));
-    assertEquals(1, p.bind(a).length);
-    assertEquals("foo", p.bind(a)[0]);
-    assertEquals("foo", p.replace(a));
-  }
-
-  public void testUndefinedFunctionName() {
-    ParamertizedString p = new ParamertizedString("${a.anUndefinedMethod}");
-    assertEquals(1, p.getParameterNames().size());
-    assertTrue(p.getParameterNames().contains("a.anUndefinedMethod"));
-  }
-}
diff --git a/gerrit-common/src/test/java/com/google/gerrit/common/data/ParameterizedStringTest.java b/gerrit-common/src/test/java/com/google/gerrit/common/data/ParameterizedStringTest.java
new file mode 100644
index 0000000..a2d1279
--- /dev/null
+++ b/gerrit-common/src/test/java/com/google/gerrit/common/data/ParameterizedStringTest.java
@@ -0,0 +1,374 @@
+// 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.common.data;
+
+import junit.framework.TestCase;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class ParameterizedStringTest extends TestCase {
+  public void testEmptyString() {
+    final ParameterizedString p = new ParameterizedString("");
+    assertEquals("", p.getPattern());
+    assertEquals("", p.getRawPattern());
+    assertTrue(p.getParameterNames().isEmpty());
+
+    final Map<String, String> a = new HashMap<String, String>();
+    assertNotNull(p.bind(a));
+    assertEquals(0, p.bind(a).length);
+    assertEquals("", p.replace(a));
+  }
+
+  public void testAsis1() {
+    final ParameterizedString p = ParameterizedString.asis("${bar}c");
+    assertEquals("${bar}c", p.getPattern());
+    assertEquals("${bar}c", p.getRawPattern());
+    assertTrue(p.getParameterNames().isEmpty());
+
+    final Map<String, String> a = new HashMap<String, String>();
+    a.put("bar", "frobinator");
+    assertNotNull(p.bind(a));
+    assertEquals(0, p.bind(a).length);
+    assertEquals("${bar}c", p.replace(a));
+  }
+
+  public void testReplace1() {
+    final ParameterizedString p = new ParameterizedString("${bar}c");
+    assertEquals("${bar}c", p.getPattern());
+    assertEquals("{0}c", p.getRawPattern());
+    assertEquals(1, p.getParameterNames().size());
+    assertTrue(p.getParameterNames().contains("bar"));
+
+    final Map<String, String> a = new HashMap<String, String>();
+    a.put("bar", "frobinator");
+    assertNotNull(p.bind(a));
+    assertEquals(1, p.bind(a).length);
+    assertEquals("frobinator", p.bind(a)[0]);
+    assertEquals("frobinatorc", p.replace(a));
+  }
+
+  public void testReplace2() {
+    final ParameterizedString p = new ParameterizedString("a${bar}c");
+    assertEquals("a${bar}c", p.getPattern());
+    assertEquals("a{0}c", p.getRawPattern());
+    assertEquals(1, p.getParameterNames().size());
+    assertTrue(p.getParameterNames().contains("bar"));
+
+    final Map<String, String> a = new HashMap<String, String>();
+    a.put("bar", "frobinator");
+    assertNotNull(p.bind(a));
+    assertEquals(1, p.bind(a).length);
+    assertEquals("frobinator", p.bind(a)[0]);
+    assertEquals("afrobinatorc", p.replace(a));
+  }
+
+  public void testReplace3() {
+    final ParameterizedString p = new ParameterizedString("a${bar}");
+    assertEquals("a${bar}", p.getPattern());
+    assertEquals("a{0}", p.getRawPattern());
+    assertEquals(1, p.getParameterNames().size());
+    assertTrue(p.getParameterNames().contains("bar"));
+
+    final Map<String, String> a = new HashMap<String, String>();
+    a.put("bar", "frobinator");
+    assertNotNull(p.bind(a));
+    assertEquals(1, p.bind(a).length);
+    assertEquals("frobinator", p.bind(a)[0]);
+    assertEquals("afrobinator", p.replace(a));
+  }
+
+  public void testReplace4() {
+    final ParameterizedString p = new ParameterizedString("a${bar}c");
+    assertEquals("a${bar}c", p.getPattern());
+    assertEquals("a{0}c", p.getRawPattern());
+    assertEquals(1, p.getParameterNames().size());
+    assertTrue(p.getParameterNames().contains("bar"));
+
+    final Map<String, String> a = new HashMap<String, String>();
+    assertNotNull(p.bind(a));
+    assertEquals(1, p.bind(a).length);
+    assertEquals("", p.bind(a)[0]);
+    assertEquals("ac", p.replace(a));
+  }
+
+  public void testReplaceToLowerCase() {
+    final ParameterizedString p = new ParameterizedString("${a.toLowerCase}");
+    assertEquals(1, p.getParameterNames().size());
+    assertTrue(p.getParameterNames().contains("a"));
+
+    final Map<String, String> a = new HashMap<String, String>();
+
+    a.put("a", "foo");
+    assertNotNull(p.bind(a));
+    assertEquals(1, p.bind(a).length);
+    assertEquals("foo", p.bind(a)[0]);
+    assertEquals("foo", p.replace(a));
+
+    a.put("a", "FOO");
+    assertNotNull(p.bind(a));
+    assertEquals(1, p.bind(a).length);
+    assertEquals("foo", p.bind(a)[0]);
+    assertEquals("foo", p.replace(a));
+  }
+
+  public void testReplaceToUpperCase() {
+    final ParameterizedString p = new ParameterizedString("${a.toUpperCase}");
+    assertEquals(1, p.getParameterNames().size());
+    assertTrue(p.getParameterNames().contains("a"));
+
+    final Map<String, String> a = new HashMap<String, String>();
+
+    a.put("a", "foo");
+    assertNotNull(p.bind(a));
+    assertEquals(1, p.bind(a).length);
+    assertEquals("FOO", p.bind(a)[0]);
+    assertEquals("FOO", p.replace(a));
+
+    a.put("a", "FOO");
+    assertNotNull(p.bind(a));
+    assertEquals(1, p.bind(a).length);
+    assertEquals("FOO", p.bind(a)[0]);
+    assertEquals("FOO", p.replace(a));
+  }
+
+  public void testReplaceLocalName() {
+    final ParameterizedString p = new ParameterizedString("${a.localPart}");
+    assertEquals(1, p.getParameterNames().size());
+    assertTrue(p.getParameterNames().contains("a"));
+
+    final Map<String, String> a = new HashMap<String, String>();
+
+    a.put("a", "foo@example.com");
+    assertNotNull(p.bind(a));
+    assertEquals(1, p.bind(a).length);
+    assertEquals("foo", p.bind(a)[0]);
+    assertEquals("foo", p.replace(a));
+
+    a.put("a", "foo");
+    assertNotNull(p.bind(a));
+    assertEquals(1, p.bind(a).length);
+    assertEquals("foo", p.bind(a)[0]);
+    assertEquals("foo", p.replace(a));
+  }
+
+  public void testUndefinedFunctionName() {
+    ParameterizedString p =
+        new ParameterizedString(
+            "hi, ${userName.toUpperCase},your eamil address is '${email.toLowerCase.localPart}'.right?");
+    assertEquals(2, p.getParameterNames().size());
+    assertTrue(p.getParameterNames().contains("userName"));
+    assertTrue(p.getParameterNames().contains("email"));
+
+    final Map<String, String> a = new HashMap<String, String>();
+    a.put("userName", "firstName lastName");
+    a.put("email", "FIRSTNAME.LASTNAME@EXAMPLE.COM");
+    assertNotNull(p.bind(a));
+    assertEquals(2, p.bind(a).length);
+
+    assertEquals("FIRSTNAME LASTNAME", p.bind(a)[0]);
+    assertEquals("firstname.lastname", p.bind(a)[1]);
+    assertEquals("hi, FIRSTNAME LASTNAME,your eamil address is 'firstname.lastname'.right?", p.replace(a));
+  }
+
+  public void testReplaceToUpperCaseToLowerCase() {
+    final ParameterizedString p =
+        new ParameterizedString("${a.toUpperCase.toLowerCase}");
+    assertEquals(1, p.getParameterNames().size());
+    assertTrue(p.getParameterNames().contains("a"));
+
+    final Map<String, String> a = new HashMap<String, String>();
+
+    a.put("a", "FOO@EXAMPLE.COM");
+    assertNotNull(p.bind(a));
+    assertEquals(1, p.bind(a).length);
+    assertEquals("foo@example.com", p.bind(a)[0]);
+    assertEquals("foo@example.com", p.replace(a));
+
+    a.put("a", "foo@example.com");
+    assertNotNull(p.bind(a));
+    assertEquals(1, p.bind(a).length);
+    assertEquals("foo@example.com", p.bind(a)[0]);
+    assertEquals("foo@example.com", p.replace(a));
+  }
+
+  public void testReplaceToUpperCaseLocalName() {
+    final ParameterizedString p =
+        new ParameterizedString("${a.toUpperCase.localPart}");
+    assertEquals(1, p.getParameterNames().size());
+    assertTrue(p.getParameterNames().contains("a"));
+
+    final Map<String, String> a = new HashMap<String, String>();
+
+    a.put("a", "foo@example.com");
+    assertNotNull(p.bind(a));
+    assertEquals(1, p.bind(a).length);
+    assertEquals("FOO", p.bind(a)[0]);
+    assertEquals("FOO", p.replace(a));
+
+    a.put("a", "FOO@EXAMPLE.COM");
+    assertNotNull(p.bind(a));
+    assertEquals(1, p.bind(a).length);
+    assertEquals("FOO", p.bind(a)[0]);
+    assertEquals("FOO", p.replace(a));
+  }
+
+  public void testReplaceToUpperCaseAnUndefinedMethod() {
+    final ParameterizedString p =
+        new ParameterizedString("${a.toUpperCase.anUndefinedMethod}");
+    assertEquals(1, p.getParameterNames().size());
+    assertTrue(p.getParameterNames().contains("a"));
+
+    final Map<String, String> a = new HashMap<String, String>();
+
+    a.put("a", "foo@example.com");
+    assertNotNull(p.bind(a));
+    assertEquals(1, p.bind(a).length);
+    assertEquals("FOO@EXAMPLE.COM", p.bind(a)[0]);
+    assertEquals("FOO@EXAMPLE.COM", p.replace(a));
+
+    a.put("a", "FOO@EXAMPLE.COM");
+    assertNotNull(p.bind(a));
+    assertEquals(1, p.bind(a).length);
+    assertEquals("FOO@EXAMPLE.COM", p.bind(a)[0]);
+    assertEquals("FOO@EXAMPLE.COM", p.replace(a));
+  }
+
+  public void testReplaceLocalNameToUpperCase() {
+    final ParameterizedString p =
+        new ParameterizedString("${a.localPart.toUpperCase}");
+    assertEquals(1, p.getParameterNames().size());
+    assertTrue(p.getParameterNames().contains("a"));
+
+    final Map<String, String> a = new HashMap<String, String>();
+
+    a.put("a", "foo@example.com");
+    assertNotNull(p.bind(a));
+    assertEquals(1, p.bind(a).length);
+    assertEquals("FOO", p.bind(a)[0]);
+    assertEquals("FOO", p.replace(a));
+
+    a.put("a", "FOO@EXAMPLE.COM");
+    assertNotNull(p.bind(a));
+    assertEquals(1, p.bind(a).length);
+    assertEquals("FOO", p.bind(a)[0]);
+    assertEquals("FOO", p.replace(a));
+  }
+
+  public void testReplaceLocalNameToLowerCase() {
+    final ParameterizedString p =
+        new ParameterizedString("${a.localPart.toLowerCase}");
+    assertEquals(1, p.getParameterNames().size());
+    assertTrue(p.getParameterNames().contains("a"));
+
+    final Map<String, String> a = new HashMap<String, String>();
+
+    a.put("a", "FOO@EXAMPLE.COM");
+    assertNotNull(p.bind(a));
+    assertEquals(1, p.bind(a).length);
+    assertEquals("foo", p.bind(a)[0]);
+    assertEquals("foo", p.replace(a));
+
+    a.put("a", "foo@example.com");
+    assertNotNull(p.bind(a));
+    assertEquals(1, p.bind(a).length);
+    assertEquals("foo", p.bind(a)[0]);
+    assertEquals("foo", p.replace(a));
+  }
+
+  public void testReplaceLocalNameAnUndefinedMethod() {
+    final ParameterizedString p =
+        new ParameterizedString("${a.localPart.anUndefinedMethod}");
+    assertEquals(1, p.getParameterNames().size());
+    assertTrue(p.getParameterNames().contains("a"));
+
+    final Map<String, String> a = new HashMap<String, String>();
+
+    a.put("a", "FOO@EXAMPLE.COM");
+    assertNotNull(p.bind(a));
+    assertEquals(1, p.bind(a).length);
+    assertEquals("FOO", p.bind(a)[0]);
+    assertEquals("FOO", p.replace(a));
+
+    a.put("a", "foo@example.com");
+    assertNotNull(p.bind(a));
+    assertEquals(1, p.bind(a).length);
+    assertEquals("foo", p.bind(a)[0]);
+    assertEquals("foo", p.replace(a));
+  }
+
+  public void testReplaceToLowerCaseToUpperCase() {
+    final ParameterizedString p =
+        new ParameterizedString("${a.toLowerCase.toUpperCase}");
+    assertEquals(1, p.getParameterNames().size());
+    assertTrue(p.getParameterNames().contains("a"));
+
+    final Map<String, String> a = new HashMap<String, String>();
+
+    a.put("a", "FOO@EXAMPLE.COM");
+    assertNotNull(p.bind(a));
+    assertEquals(1, p.bind(a).length);
+    assertEquals("FOO@EXAMPLE.COM", p.bind(a)[0]);
+    assertEquals("FOO@EXAMPLE.COM", p.replace(a));
+
+    a.put("a", "foo@example.com");
+    assertNotNull(p.bind(a));
+    assertEquals(1, p.bind(a).length);
+    assertEquals("FOO@EXAMPLE.COM", p.bind(a)[0]);
+    assertEquals("FOO@EXAMPLE.COM", p.replace(a));
+  }
+
+  public void testReplaceToLowerCaseLocalName() {
+    final ParameterizedString p =
+        new ParameterizedString("${a.toLowerCase.localPart}");
+    assertEquals(1, p.getParameterNames().size());
+    assertTrue(p.getParameterNames().contains("a"));
+
+    final Map<String, String> a = new HashMap<String, String>();
+
+    a.put("a", "FOO@EXAMPLE.COM");
+    assertNotNull(p.bind(a));
+    assertEquals(1, p.bind(a).length);
+    assertEquals("foo", p.bind(a)[0]);
+    assertEquals("foo", p.replace(a));
+
+    a.put("a", "foo@example.com");
+    assertNotNull(p.bind(a));
+    assertEquals(1, p.bind(a).length);
+    assertEquals("foo", p.bind(a)[0]);
+    assertEquals("foo", p.replace(a));
+  }
+
+  public void testReplaceToLowerCaseAnUndefinedMethod() {
+    final ParameterizedString p =
+        new ParameterizedString("${a.toLowerCase.anUndefinedMethod}");
+    assertEquals(1, p.getParameterNames().size());
+    assertTrue(p.getParameterNames().contains("a"));
+
+    final Map<String, String> a = new HashMap<String, String>();
+
+    a.put("a", "foo@example.com");
+    assertNotNull(p.bind(a));
+    assertEquals(1, p.bind(a).length);
+    assertEquals("foo@example.com", p.bind(a)[0]);
+    assertEquals("foo@example.com", p.replace(a));
+
+    a.put("a", "FOO@EXAMPLE.COM");
+    assertNotNull(p.bind(a));
+    assertEquals(1, p.bind(a).length);
+    assertEquals("foo@example.com", p.bind(a)[0]);
+    assertEquals("foo@example.com", p.replace(a));
+  }
+}
diff --git a/gerrit-ehcache/.gitignore b/gerrit-ehcache/.gitignore
new file mode 100644
index 0000000..20251d4
--- /dev/null
+++ b/gerrit-ehcache/.gitignore
@@ -0,0 +1,5 @@
+/target
+/.classpath
+/.project
+/.settings/org.eclipse.m2e.core.prefs
+/.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..839c52b0
--- /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.4-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/.settings/org.eclipse.core.resources.prefs b/gerrit-gwtdebug/.settings/org.eclipse.core.resources.prefs
index 82eb859..36e1448 100644
--- a/gerrit-gwtdebug/.settings/org.eclipse.core.resources.prefs
+++ b/gerrit-gwtdebug/.settings/org.eclipse.core.resources.prefs
@@ -1,3 +1,4 @@
-#Tue Sep 02 16:59:24 PDT 2008
+#Thu Jul 28 11:02:38 PDT 2011
 eclipse.preferences.version=1
+encoding//src/main/java=UTF-8
 encoding/<project>=UTF-8
diff --git a/gerrit-gwtdebug/.settings/org.eclipse.jdt.core.prefs b/gerrit-gwtdebug/.settings/org.eclipse.jdt.core.prefs
index 04afc7f..d2b5901 100644
--- a/gerrit-gwtdebug/.settings/org.eclipse.jdt.core.prefs
+++ b/gerrit-gwtdebug/.settings/org.eclipse.jdt.core.prefs
@@ -1,4 +1,4 @@
-#Tue May 12 17:44:13 PDT 2009
+#Thu Jul 28 11:02:38 PDT 2011
 eclipse.preferences.version=1
 org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
 org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
@@ -9,6 +9,7 @@
 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
diff --git a/gerrit-gwtdebug/pom.xml b/gerrit-gwtdebug/pom.xml
index 88246ac..734f645 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.1-SNAPSHOT</version>
+    <version>2.4-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-gwtdebug</artifactId>
@@ -83,5 +83,18 @@
       <version>140</version>
       <scope>provided</scope>
     </dependency>
+
+    <!-- GWT should require these itself, but doesn't. -->
+    <dependency>
+      <groupId>javax.validation</groupId>
+      <artifactId>validation-api</artifactId>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>javax.validation</groupId>
+      <artifactId>validation-api</artifactId>
+      <classifier>sources</classifier>
+      <scope>provided</scope>
+    </dependency>
   </dependencies>
 </project>
diff --git a/gerrit-gwtdebug/src/main/java/com/google/gerrit/gwtdebug/GerritDebugLauncher.java b/gerrit-gwtdebug/src/main/java/com/google/gerrit/gwtdebug/GerritDebugLauncher.java
index 1834cdf..d23aa35 100644
--- a/gerrit-gwtdebug/src/main/java/com/google/gerrit/gwtdebug/GerritDebugLauncher.java
+++ b/gerrit-gwtdebug/src/main/java/com/google/gerrit/gwtdebug/GerritDebugLauncher.java
@@ -23,11 +23,11 @@
 
 import org.mortbay.component.AbstractLifeCycle;
 import org.mortbay.jetty.AbstractConnector;
+import org.mortbay.jetty.HttpFields.Field;
 import org.mortbay.jetty.Request;
 import org.mortbay.jetty.RequestLog;
 import org.mortbay.jetty.Response;
 import org.mortbay.jetty.Server;
-import org.mortbay.jetty.HttpFields.Field;
 import org.mortbay.jetty.handler.RequestLogHandler;
 import org.mortbay.jetty.nio.SelectChannelConnector;
 import org.mortbay.jetty.webapp.WebAppClassLoader;
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/.settings/org.eclipse.core.resources.prefs b/gerrit-gwtui/.settings/org.eclipse.core.resources.prefs
index 82eb859..c780f44 100644
--- a/gerrit-gwtui/.settings/org.eclipse.core.resources.prefs
+++ b/gerrit-gwtui/.settings/org.eclipse.core.resources.prefs
@@ -1,3 +1,4 @@
-#Tue Sep 02 16:59:24 PDT 2008
+#Thu Jul 28 11:02:36 PDT 2011
 eclipse.preferences.version=1
+encoding//src/main/java=UTF-8
 encoding/<project>=UTF-8
diff --git a/gerrit-gwtui/.settings/org.eclipse.jdt.core.prefs b/gerrit-gwtui/.settings/org.eclipse.jdt.core.prefs
index 04afc7f..470942d 100644
--- a/gerrit-gwtui/.settings/org.eclipse.jdt.core.prefs
+++ b/gerrit-gwtui/.settings/org.eclipse.jdt.core.prefs
@@ -1,4 +1,4 @@
-#Tue May 12 17:44:13 PDT 2009
+#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
@@ -9,6 +9,7 @@
 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
diff --git a/gerrit-gwtui/pom.xml b/gerrit-gwtui/pom.xml
index f1e7438..f14daf6 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.1-SNAPSHOT</version>
+    <version>2.4-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-gwtui</artifactId>
@@ -37,7 +37,6 @@
     <dependency>
       <groupId>com.google.gwt</groupId>
       <artifactId>gwt-user</artifactId>
-      <version>${gwtVersion}</version>
     </dependency>
 
     <dependency>
@@ -135,6 +134,19 @@
       <classifier>sources</classifier>
       <type>jar</type>
     </dependency>
+
+    <!-- GWT should require these itself, but doesn't. -->
+    <dependency>
+      <groupId>javax.validation</groupId>
+      <artifactId>validation-api</artifactId>
+      <scope>provided</scope>
+    </dependency>
+    <dependency>
+      <groupId>javax.validation</groupId>
+      <artifactId>validation-api</artifactId>
+      <classifier>sources</classifier>
+      <scope>provided</scope>
+    </dependency>
   </dependencies>
 
   <profiles>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUI.gwt.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUI.gwt.xml
index ec0f74f..8555c75 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUI.gwt.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUI.gwt.xml
@@ -14,6 +14,7 @@
  limitations under the License.
 -->
 <module rename-to="gerrit">
+  <inherits name='com.google.gwt.editor.Editor'/>
   <inherits name='com.google.gwt.user.User'/>
   <inherits name='com.google.gwt.resources.Resources'/>
   <inherits name='com.google.gwt.user.theme.chrome.Chrome'/>
@@ -31,6 +32,7 @@
   <extend-property name='locale' values='en'/>
   <set-property-fallback name='locale' value='en'/>
   <set-property name='locale' value='en'/>
+  <set-configuration-property name='UiBinder.useSafeHtmlTemplates' value='true'/>
 
   <entry-point class='com.google.gerrit.client.Gerrit'/>
 </module>
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 2b3ace4..3aee0e2 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;
@@ -42,13 +43,17 @@
 import com.google.gerrit.client.account.NewAgreementScreen;
 import com.google.gerrit.client.account.RegisterScreen;
 import com.google.gerrit.client.account.ValidateEmailScreen;
+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;
 import com.google.gerrit.client.admin.ProjectInfoScreen;
 import com.google.gerrit.client.admin.ProjectListScreen;
 import com.google.gerrit.client.admin.ProjectScreen;
+import com.google.gerrit.client.admin.Util;
 import com.google.gerrit.client.auth.openid.OpenIdSignInDialog;
 import com.google.gerrit.client.auth.userpass.UserPassSignInDialog;
 import com.google.gerrit.client.changes.AccountDashboardScreen;
@@ -57,50 +62,101 @@
 import com.google.gerrit.client.changes.PublishCommentScreen;
 import com.google.gerrit.client.changes.QueryScreen;
 import com.google.gerrit.client.patches.PatchScreen;
+import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.ui.Screen;
+import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.auth.SignInMode;
+import com.google.gerrit.common.data.GroupDetail;
 import com.google.gerrit.common.data.PatchSetDetail;
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.Patch;
-import com.google.gerrit.reviewdb.PatchSet;
-import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.core.client.RunAsyncCallback;
+import com.google.gwt.user.client.Window;
 import com.google.gwtorm.client.KeyUtil;
 
 public class Dispatcher {
   public static String toPatchSideBySide(final Patch.Key id) {
-    return toPatch("sidebyside", id);
+    return toPatch("", null, id);
+  }
+
+  public static String toPatchSideBySide(PatchSet.Id diffBase, Patch.Key id) {
+    return toPatch("", diffBase, id);
   }
 
   public static String toPatchUnified(final Patch.Key id) {
-    return toPatch("unified", id);
+    return toPatch("unified", null, id);
   }
 
-  public static String toPatch(final String type, final Patch.Key id) {
-    return "patch," + type + "," + id.toString();
+  public static String toPatchUnified(PatchSet.Id diffBase, Patch.Key id) {
+    return toPatch("unified", diffBase, id);
   }
 
-  public static String toAccountGroup(final AccountGroup.Id id) {
-    return "admin,group," + id.toString();
+  private static String toPatch(String type, PatchSet.Id diffBase, Patch.Key id) {
+    PatchSet.Id ps = id.getParentKey();
+    Change.Id c = ps.getParentKey();
+    StringBuilder p = new StringBuilder();
+    p.append("/c/").append(c).append("/");
+    if (diffBase != null) {
+      p.append(diffBase.get()).append("..");
+    }
+    p.append(ps.get()).append("/").append(KeyUtil.encode(id.get()));
+    if (type != null && !type.isEmpty()) {
+      p.append(",").append(type);
+    }
+    return p.toString();
   }
 
-  public static String toProjectAdmin(final Project.NameKey n, final String tab) {
-    return "admin,project," + n.toString() + "," + tab;
+  public static String toPatch(final PatchScreen.Type type, final Patch.Key id) {
+    if (type == PatchScreen.Type.SIDE_BY_SIDE) {
+      return toPatchSideBySide(id);
+    } else {
+      return toPatchUnified(id);
+    }
   }
 
-  static final String RELOAD_UI = "reload-ui,";
+  public static String toPublish(PatchSet.Id ps) {
+    Change.Id c = ps.getParentKey();
+    return "/c/" + c + "/" + ps.get() + ",publish";
+  }
+
+  public static String toGroup(final AccountGroup.Id id) {
+    return "/admin/groups/" + id.toString();
+  }
+
+  public static String toGroup(AccountGroup.Id id, String panel) {
+    return "/admin/groups/" + id.toString() + "," + panel;
+  }
+
+  public static String toGroup(final AccountGroup.UUID uuid) {
+    return "/admin/groups/uuid-" + uuid.toString();
+  }
+
+  public static String toGroup(AccountGroup.UUID uuid, String panel) {
+    return "/admin/groups/uuid-" + uuid.toString() + "," + panel;
+  }
+
+  public static String toProjectAdmin(Project.NameKey n, String panel) {
+    if (ProjectScreen.INFO.equals(panel)) {
+      return "/admin/projects/" + n.toString();
+    }
+    return "/admin/projects/" + n.toString() + "," + panel;
+  }
+
+  static final String RELOAD_UI = "/reload-ui/";
   private static boolean wasStartedByReloadUI;
 
   void display(String token) {
     assert token != null;
     try {
       try {
-        if (token.startsWith(RELOAD_UI)) {
+        if (matchPrefix(RELOAD_UI, token)) {
           wasStartedByReloadUI = true;
-          token = skip(RELOAD_UI, token);
+          token = skip(token);
         }
         select(token);
       } finally {
@@ -113,193 +169,328 @@
   }
 
   private static void select(final String token) {
-    if (token.startsWith("patch,")) {
-      patch(token, null, 0, null, null);
+    if (matchPrefix("/q/", token)) {
+      query(token);
 
-    } else if (token.startsWith("change,publish,")) {
-      publish(token);
+    } else if (matchPrefix("/c/", token)) {
+      change(token);
 
-    } else if (MINE.equals(token) || token.startsWith("mine,")) {
+    } else if (matchExact(MINE, token)) {
       Gerrit.display(token, mine(token));
 
-    } else if (token.startsWith("all,")) {
-      Gerrit.display(token, all(token));
+    } else if (matchPrefix("/dashboard/", token)) {
+      dashboard(token);
 
-    } else if (token.startsWith("project,")) {
-      Gerrit.display(token, project(token));
-
-    } else if (SETTINGS.equals(token) //
-        || REGISTER.equals(token) //
-        || token.startsWith("settings,") //
-        || token.startsWith("register,") //
-        || token.startsWith("VE,") //
-        || token.startsWith("SignInFailure,")) {
+    } else if (matchExact(SETTINGS, token) //
+        || matchPrefix("/settings/", token) //
+        || matchExact("register", token) //
+        || matchExact(REGISTER, token) //
+        || matchPrefix("/register/", token) //
+        || matchPrefix("/VE/", token) || matchPrefix("VE,", token) //
+        || matchPrefix("/SignInFailure,", token)) {
       settings(token);
 
-    } else if (token.startsWith("admin,")) {
+    } else if (matchPrefix("/admin/", token)) {
       admin(token);
 
-    } else {
-      Gerrit.display(token, core(token));
-    }
-  }
-
-  private static Screen mine(final String token) {
-    if (MINE.equals(token)) {
-      if (Gerrit.isSignedIn()) {
-        return new AccountDashboardScreen(Gerrit.getUserAccount().getId());
-
-      } else {
-        final Screen r = new AccountDashboardScreen(null);
-        r.setRequiresSignIn(true);
-        return r;
-      }
-
-    } else if ("mine,starred".equals(token)) {
-      return QueryScreen.forQuery("is:starred");
-
-    } else if ("mine,drafts".equals(token)) {
-      return QueryScreen.forQuery("has:draft");
+    } else if (/* LEGACY URL */matchPrefix("all,", token)) {
+      redirectFromLegacyToken(token, legacyAll(token));
+    } else if (/* LEGACY URL */matchPrefix("mine,", token)
+        || matchExact("mine", token)) {
+      redirectFromLegacyToken(token, legacyMine(token));
+    } else if (/* LEGACY URL */matchPrefix("project,", token)) {
+      redirectFromLegacyToken(token, legacyProject(token));
+    } else if (/* LEGACY URL */matchPrefix("change,", token)) {
+      redirectFromLegacyToken(token, legacyChange(token));
+    } else if (/* LEGACY URL */matchPrefix("patch,", token)) {
+      redirectFromLegacyToken(token, legacyPatch(token));
+    } else if (/* LEGACY URL */matchPrefix("admin,", token)) {
+      redirectFromLegacyToken(token, legacyAdmin(token));
+    } else if (/* LEGACY URL */matchPrefix("settings,", token)
+        || matchPrefix("register,", token)
+        || matchPrefix("q,", token)) {
+      redirectFromLegacyToken(token, legacySettings(token));
 
     } else {
-      String p = "mine,watched,";
-      if (token.startsWith(p)) {
-        return QueryScreen.forQuery("is:watched status:open", skip(p, token));
-      }
-
-      return new NotFoundScreen();
+      Gerrit.display(token, new NotFoundScreen());
     }
   }
 
-  private static Screen all(final String token) {
-    String p;
-
-    p = "all,abandoned,";
-    if (token.startsWith(p)) {
-      return QueryScreen.forQuery("status:abandoned", skip(p, token));
+  private static void redirectFromLegacyToken(String oldToken, String newToken) {
+    if (newToken != null) {
+      Window.Location.replace(Window.Location.getPath() + "#" + newToken);
+    } else {
+      Gerrit.display(oldToken, new NotFoundScreen());
     }
-
-    p = "all,merged,";
-    if (token.startsWith(p)) {
-      return QueryScreen.forQuery("status:merged", skip(p, token));
-    }
-
-    p = "all,open,";
-    if (token.startsWith(p)) {
-      return QueryScreen.forQuery("status:open", skip(p, token));
-    }
-
-    return new NotFoundScreen();
   }
 
-  private static Screen project(final String token) {
-    String p;
+  private static String legacyMine(final String token) {
+    if (matchExact("mine", token)) {
+      return MINE;
+    }
 
-    p = "project,open,";
-    if (token.startsWith(p)) {
-      final String s = skip(p, token);
+    if (matchExact("mine,starred", token)) {
+      return PageLinks.toChangeQuery("is:starred");
+    }
+
+    if (matchExact("mine,drafts", token)) {
+      return PageLinks.toChangeQuery("has:draft");
+    }
+
+    if (matchPrefix("mine,watched,", token)) {
+      return PageLinks.toChangeQuery("is:watched status:open", skip(token));
+    }
+
+    return null;
+  }
+
+  private static String legacyAll(final String token) {
+    if (matchPrefix("all,abandoned,", token)) {
+      return PageLinks.toChangeQuery("status:abandoned", skip(token));
+    }
+
+    if (matchPrefix("all,merged,", token)) {
+      return PageLinks.toChangeQuery("status:merged", skip(token));
+    }
+
+    if (matchPrefix("all,open,", token)) {
+      return PageLinks.toChangeQuery("status:open", skip(token));
+    }
+
+    return null;
+  }
+
+  private static String legacyProject(final String token) {
+    if (matchPrefix("project,open,", token)) {
+      final String s = skip(token);
       final int c = s.indexOf(',');
       Project.NameKey proj = Project.NameKey.parse(s.substring(0, c));
-      return QueryScreen.forQuery( //
+      return PageLinks.toChangeQuery( //
           "status:open " + op("project", proj.get()), //
           s.substring(c + 1));
     }
 
-    p = "project,merged,";
-    if (token.startsWith(p)) {
-      final String s = skip(p, token);
+    if (matchPrefix("project,merged,", token)) {
+      final String s = skip(token);
       final int c = s.indexOf(',');
       Project.NameKey proj = Project.NameKey.parse(s.substring(0, c));
-      return QueryScreen.forQuery( //
+      return PageLinks.toChangeQuery( //
           "status:merged " + op("project", proj.get()), //
           s.substring(c + 1));
     }
 
-    p = "project,abandoned,";
-    if (token.startsWith(p)) {
-      final String s = skip(p, token);
+    if (matchPrefix("project,abandoned,", token)) {
+      final String s = skip(token);
       final int c = s.indexOf(',');
       Project.NameKey proj = Project.NameKey.parse(s.substring(0, c));
-      return QueryScreen.forQuery( //
+      return PageLinks.toChangeQuery( //
           "status:abandoned " + op("project", proj.get()), //
           s.substring(c + 1));
     }
 
-    return new NotFoundScreen();
+    return null;
   }
 
-  private static Screen core(final String token) {
-    String p;
+  private static String legacyChange(final String token) {
+    final String s = skip(token);
+    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])));
+    }
+    return PageLinks.toChange(Change.Id.parse(t[0]));
+  }
 
-    p = "change,";
-    if (token.startsWith(p)) {
-      final String s = skip(p, token);
-      final String q = "patchset=";
-      final String t[] = s.split(",", 2);
-      if (t.length > 1 && t[1].startsWith(q)) {
-        return new ChangeScreen(PatchSet.Id.parse(t[0] + "," + skip(q, t[1])));
+  private static String legacyPatch(String token) {
+    if (/* LEGACY URL */matchPrefix("patch,sidebyside,", token)) {
+      return toPatchSideBySide(Patch.Key.parse(skip(token)));
+    }
+
+    if (/* LEGACY URL */matchPrefix("patch,unified,", token)) {
+      return toPatchUnified(Patch.Key.parse(skip(token)));
+    }
+
+    return null;
+  }
+
+  private static String legacyAdmin(String token) {
+    if (matchPrefix("admin,group,", token)) {
+      return "/admin/groups/" + skip(token);
+    }
+
+    if (matchPrefix("admin,project,", token)) {
+      String rest = skip(token);
+      int c = rest.indexOf(',');
+      String panel;
+      Project.NameKey k;
+      if (0 < c) {
+        panel = rest.substring(c + 1);
+        k = Project.NameKey.parse(rest.substring(0, c));
+      } else {
+        panel = ProjectScreen.INFO;
+        k = Project.NameKey.parse(rest);
       }
-      return new ChangeScreen(Change.Id.parse(t[0]));
+      return toProjectAdmin(k, panel);
     }
 
-    p = "dashboard,";
-    if (token.startsWith(p))
-      return new AccountDashboardScreen(Account.Id.parse(skip(p, token)));
-
-    p = "q,";
-    if (token.startsWith(p)) {
-      final String s = skip(p, token);
-      final int c = s.indexOf(',');
-      return new QueryScreen(s.substring(0, c), s.substring(c + 1));
-    }
-
-    return new NotFoundScreen();
+    return null;
   }
 
-  private static void publish(String token) {
+  private static String legacySettings(String token) {
+    int c = token.indexOf(',');
+    if (0 < c) {
+      return "/" + token.substring(0, c) + "/" + token.substring(c + 1);
+    }
+    return null;
+  }
+
+  private static void query(final String token) {
+    final String s = skip(token);
+    final int c = s.indexOf(',');
+    Gerrit.display(token, new QueryScreen(s.substring(0, c), s.substring(c + 1)));
+  }
+
+  private static Screen mine(final String token) {
+    if (Gerrit.isSignedIn()) {
+      return new AccountDashboardScreen(Gerrit.getUserAccount().getId());
+
+    } else {
+      Screen r = new AccountDashboardScreen(null);
+      r.setRequiresSignIn(true);
+      return r;
+    }
+  }
+
+  private static void dashboard(final String token) {
+    Gerrit.display(token, //
+        new AccountDashboardScreen(Account.Id.parse(skip(token))));
+  }
+
+  private static void change(final String token) {
+    String rest = skip(token);
+    int c = rest.lastIndexOf(',');
+    String panel = null;
+    if (0 <= c) {
+      panel = rest.substring(c + 1);
+      rest = rest.substring(0, c);
+    }
+
+    Change.Id id;
+    int s = rest.indexOf('/');
+    if (0 <= s) {
+      id = Change.Id.parse(rest.substring(0, s));
+      rest = rest.substring(s + 1);
+    } else {
+      id = Change.Id.parse(rest);
+      rest = "";
+    }
+
+    if (rest.isEmpty()) {
+      Gerrit.display(token, panel== null //
+          ? new ChangeScreen(id) //
+          : new NotFoundScreen());
+      return;
+    }
+
+    String psIdStr;
+    s = rest.indexOf('/');
+    if (0 <= s) {
+      psIdStr = rest.substring(0, s);
+      rest = rest.substring(s + 1);
+    } else {
+      psIdStr = rest;
+      rest = "";
+    }
+
+    PatchSet.Id base;
+    PatchSet.Id ps;
+    int dotdot = psIdStr.indexOf("..");
+    if (1 <= dotdot) {
+      base = new PatchSet.Id(id, Integer.parseInt(psIdStr.substring(0, dotdot)));
+      ps = new PatchSet.Id(id, Integer.parseInt(psIdStr.substring(dotdot + 2)));
+    } else {
+      base = null;
+      ps = new PatchSet.Id(id, Integer.parseInt(psIdStr));
+    }
+
+    if (!rest.isEmpty()) {
+      Patch.Key p = new Patch.Key(ps, rest);
+      patch(token, base, p, 0, null, null, panel);
+    } else {
+      if (panel == null) {
+        Gerrit.display(token, new ChangeScreen(ps));
+      } else if ("publish".equals(panel)) {
+        publish(ps);
+      } else {
+        Gerrit.display(token, new NotFoundScreen());
+      }
+    }
+  }
+
+  private static void publish(final PatchSet.Id ps) {
+    String token = toPublish(ps);
     new AsyncSplit(token) {
       public void onSuccess() {
         Gerrit.display(token, select());
       }
 
       private Screen select() {
-        String p = "change,publish,";
-        if (token.startsWith(p))
-          return new PublishCommentScreen(PatchSet.Id.parse(skip(p, token)));
-        return new NotFoundScreen();
+        return new PublishCommentScreen(ps);
       }
     }.onSuccess();
   }
 
-  public static void patch(String token, final Patch.Key id,
+  public static void patch(String token, PatchSet.Id base, Patch.Key id,
+      int patchIndex, PatchSetDetail patchSetDetail,
+      PatchTable patchTable, PatchScreen.TopView topView) {
+    patch(token, base, id, patchIndex, patchSetDetail, patchTable, topView, null);
+  }
+
+  public static void patch(String token, PatchSet.Id base, Patch.Key id,
+      int patchIndex, PatchSetDetail patchSetDetail,
+      PatchTable patchTable, String panelType) {
+    patch(token, base, id, patchIndex, patchSetDetail, patchTable,
+        null, panelType);
+  }
+
+  public static void patch(String token, final PatchSet.Id baseId, final Patch.Key id,
       final int patchIndex, final PatchSetDetail patchSetDetail,
-      final PatchTable patchTable) {
+      final PatchTable patchTable, final PatchScreen.TopView topView,
+      final String panelType) {
+    final PatchScreen.TopView top =  topView == null ?
+        Gerrit.getPatchScreenTopView() : topView;
+
     GWT.runAsync(new AsyncSplit(token) {
       public void onSuccess() {
         Gerrit.display(token, select());
       }
 
       private Screen select() {
-        String p;
+        if (id != null) {
+          String panel = panelType;
+          if (panel == null) {
+            int c = token.lastIndexOf(',');
+            panel = 0 <= c ? token.substring(c + 1) : "";
+          }
 
-        p = "patch,sidebyside,";
-        if (token.startsWith(p)) {
-          return new PatchScreen.SideBySide( //
-              id != null ? id : Patch.Key.parse(skip(p, token)), //
-              patchIndex, //
-              patchSetDetail, //
-              patchTable //
-          );
-        }
-
-        p = "patch,unified,";
-        if (token.startsWith(p)) {
-          return new PatchScreen.Unified( //
-              id != null ? id : Patch.Key.parse(skip(p, token)), //
-              patchIndex, //
-              patchSetDetail, //
-              patchTable //
-          );
+          if ("".equals(panel)) {
+            return new PatchScreen.SideBySide( //
+                id, //
+                patchIndex, //
+                patchSetDetail, //
+                patchTable, //
+                top, //
+                baseId //
+            );
+          } else if ("unified".equals(panel)) {
+            return new PatchScreen.Unified( //
+                id, //
+                patchIndex, //
+                patchSetDetail, //
+                patchTable, //
+                top, //
+                baseId //
+            );
+          }
         }
 
         return new NotFoundScreen();
@@ -314,59 +505,56 @@
       }
 
       private Screen select() {
-        String p;
-
-        if (token.equals(SETTINGS)) {
+        if (matchExact(SETTINGS, token)) {
           return new MyProfileScreen();
         }
 
-        if (token.equals(SETTINGS_PREFERENCES)) {
+        if (matchExact(SETTINGS_PREFERENCES, token)) {
           return new MyPreferencesScreen();
         }
 
-        if (token.equals(SETTINGS_PROJECTS)) {
+        if (matchExact(SETTINGS_PROJECTS, token)) {
           return new MyWatchedProjectsScreen();
         }
 
-        if (token.equals(SETTINGS_CONTACT)) {
+        if (matchExact(SETTINGS_CONTACT, token)) {
           return new MyContactInformationScreen();
         }
 
-        if (token.equals(SETTINGS_SSHKEYS)) {
+        if (matchExact(SETTINGS_SSHKEYS, token)) {
           return new MySshKeysScreen();
         }
 
-        if (token.equals(SETTINGS_WEBIDENT)) {
+        if (matchExact(SETTINGS_WEBIDENT, token)) {
           return new MyIdentitiesScreen();
         }
 
-        if (token.equals(SETTINGS_HTTP_PASSWORD)) {
+        if (matchExact(SETTINGS_HTTP_PASSWORD, token)) {
           return new MyPasswordScreen();
         }
 
-        if (token.equals(SETTINGS_MYGROUPS)) {
+        if (matchExact(SETTINGS_MYGROUPS, token)) {
           return new MyGroupsScreen();
         }
 
-        if (token.equals(SETTINGS_AGREEMENTS)
+        if (matchExact(SETTINGS_AGREEMENTS, token)
             && Gerrit.getConfig().isUseContributorAgreements()) {
           return new MyAgreementsScreen();
         }
 
-        p = "register,";
-        if (token.startsWith(p)) {
-          return new RegisterScreen(skip(p, token));
-        } else if (REGISTER.equals(token)) {
+        if (matchExact(REGISTER, token)
+            || matchExact("/register/", token)
+            || matchExact("register", token)) {
           return new RegisterScreen(MINE);
+        } else if (matchPrefix("/register/", token)) {
+          return new RegisterScreen("/" + skip(token));
         }
 
-        p = "VE,";
-        if (token.startsWith(p))
-          return new ValidateEmailScreen(skip(p, token));
+        if (matchPrefix("/VE/", token) || matchPrefix("VE,", token))
+          return new ValidateEmailScreen(skip(token));
 
-        p = "SignInFailure,";
-        if (token.startsWith(p)) {
-          final String[] args = skip(p, token).split(",");
+        if (matchPrefix("/SignInFailure,", token)) {
+          final String[] args = skip(token).split(",");
           final SignInMode mode = SignInMode.valueOf(args[0]);
           final String msg = KeyUtil.decode(args[1]);
           final String to = MINE;
@@ -389,12 +577,11 @@
           }
         }
 
-        if (SETTINGS_NEW_AGREEMENT.equals(token))
+        if (matchExact(SETTINGS_NEW_AGREEMENT, token))
           return new NewAgreementScreen();
 
-        p = SETTINGS_NEW_AGREEMENT + ",";
-        if (token.startsWith(p)) {
-          return new NewAgreementScreen(skip(p, token));
+        if (matchPrefix(SETTINGS_NEW_AGREEMENT + "/", token)) {
+          return new NewAgreementScreen(skip(token));
         }
 
         return new NotFoundScreen();
@@ -405,54 +592,140 @@
   private static void admin(String token) {
     GWT.runAsync(new AsyncSplit(token) {
       public void onSuccess() {
-        Gerrit.display(token, select());
+        if (matchExact(ADMIN_GROUPS, token)
+            || matchExact("/admin/groups", token)) {
+          Gerrit.display(token, new GroupListScreen());
+
+        } else if (matchPrefix("/admin/groups/", token)) {
+          group();
+
+        } else if (matchExact(ADMIN_PROJECTS, token)
+            || matchExact("/admin/projects", token)) {
+          Gerrit.display(token, new ProjectListScreen());
+
+        } 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());
+        }
       }
 
-      private Screen select() {
-        String p;
+      private void group() {
+        final String panel;
+        AccountGroup.Id groupId = null;
+        AccountGroup.UUID groupUUID = null;
 
-        p = "admin,group,";
-        if (token.startsWith(p))
-          return new AccountGroupScreen(AccountGroup.Id.parse(skip(p, token)));
+        if (matchPrefix("/admin/groups/uuid-", token)) {
+          String p = skip(token);
+          int c = p.indexOf(',');
+          if (c < 0) {
+            groupUUID = AccountGroup.UUID.parse(p);
+            panel = null;
+          } else {
+            groupUUID = AccountGroup.UUID.parse(p.substring(0, c));
+            panel = p.substring(c + 1);
+          }
+        } else if (matchPrefix("/admin/groups/", token)) {
+          String p = skip(token);
+          int c = p.indexOf(',');
+          if (c < 0) {
+            groupId = AccountGroup.Id.parse(p);
+            panel = null;
+          } else {
+            groupId = AccountGroup.Id.parse(p.substring(0, c));
+            panel = p.substring(c + 1);
+          }
+        } else {
+          Gerrit.display(token, new NotFoundScreen());
+          return;
+        }
 
-        p = "admin,project,";
-        if (token.startsWith(p)) {
-          p = skip(p, token);
-          final int c = p.indexOf(',');
-          final Project.NameKey k = Project.NameKey.parse(p.substring(0, c));
-          final boolean isWild = k.equals(Gerrit.getConfig().getWildProject());
-          p = p.substring(c + 1);
+        Util.GROUP_SVC.groupDetail(groupId, groupUUID,
+            new GerritCallback<GroupDetail>() {
+              @Override
+              public void onSuccess(GroupDetail groupDetail) {
+                if (panel == null || panel.isEmpty()) {
+                  // The token does not say which group screen should be shown,
+                  // as default for internal groups show the members, as default
+                  // for external and system groups show the info screen (since
+                  // for external and system groups the members cannot be
+                  // shown in the web UI).
+                  //
+                  if (groupDetail.group.getType() == AccountGroup.Type.INTERNAL) {
+                    Gerrit.display(toGroup(groupDetail.group.getId(),
+                        AccountGroupScreen.MEMBERS),
+                        new AccountGroupMembersScreen(groupDetail, token));
+                  } else {
+                    Gerrit.display(toGroup(groupDetail.group.getId(),
+                        AccountGroupScreen.INFO),
+                        new AccountGroupInfoScreen(groupDetail, token));
+                  }
+                } else if (AccountGroupScreen.INFO.equals(panel)) {
+                  Gerrit.display(token,
+                      new AccountGroupInfoScreen(groupDetail, token));
+                } else if (AccountGroupScreen.MEMBERS.equals(panel)) {
+                  Gerrit.display(token,
+                      new AccountGroupMembersScreen(groupDetail, token));
+                } else {
+                  Gerrit.display(token,new NotFoundScreen());
+                }
+              }
+            });
+      }
 
-          if (ProjectScreen.INFO.equals(p)) {
+      private Screen selectProject() {
+        if (matchPrefix("/admin/projects/", token)) {
+          String rest = skip(token);
+          int c = rest.lastIndexOf(',');
+          if (c < 0) {
+            return new ProjectInfoScreen(Project.NameKey.parse(rest));
+          } else if (c == 0) {
+            return new NotFoundScreen();
+          }
+
+          Project.NameKey k = Project.NameKey.parse(rest.substring(0, c));
+          String panel = rest.substring(c + 1);
+
+          if (ProjectScreen.INFO.equals(panel)) {
             return new ProjectInfoScreen(k);
           }
 
-          if (!isWild && ProjectScreen.BRANCH.equals(p)) {
+          if (ProjectScreen.BRANCH.equals(panel)
+              && !k.equals(Gerrit.getConfig().getWildProject())) {
             return new ProjectBranchesScreen(k);
           }
 
-          if (ProjectScreen.ACCESS.equals(p)) {
+          if (ProjectScreen.ACCESS.equals(panel)) {
             return new ProjectAccessScreen(k);
           }
-
-          return new NotFoundScreen();
         }
-
-        if (ADMIN_GROUPS.equals(token)) {
-          return new GroupListScreen();
-        }
-
-        if (ADMIN_PROJECTS.equals(token)) {
-          return new ProjectListScreen();
-        }
-
         return new NotFoundScreen();
       }
     });
   }
 
-  private static String skip(final String prefix, final String in) {
-    return in.substring(prefix.length());
+  private static boolean matchExact(String want, String token) {
+    return token.equals(want);
+  }
+
+  private static int prefixlen;
+
+  private static boolean matchPrefix(String want, String token) {
+    if (token.startsWith(want)) {
+      prefixlen = want.length();
+      return true;
+    } else {
+      return false;
+    }
+  }
+
+  private static String skip(String token) {
+    return token.substring(prefixlen);
   }
 
   private static abstract class AsyncSplit implements RunAsyncCallback {
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..e578eae 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
@@ -15,8 +15,8 @@
 package com.google.gerrit.client;
 
 import com.google.gerrit.common.data.AccountInfo;
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.AccountGeneralPreferences;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
 import com.google.gwt.i18n.client.DateTimeFormat;
 
 import java.util.Date;
@@ -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 d0e5192..6dbfeee 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
@@ -16,27 +16,34 @@
 
 import com.google.gerrit.client.auth.openid.OpenIdSignInDialog;
 import com.google.gerrit.client.auth.userpass.UserPassSignInDialog;
+import com.google.gerrit.client.changes.ChangeConstants;
 import com.google.gerrit.client.changes.ChangeListScreen;
+import com.google.gerrit.client.patches.PatchScreen;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.ui.LinkMenuBar;
 import com.google.gerrit.client.ui.LinkMenuItem;
+import com.google.gerrit.client.ui.MorphingTabPanel;
+import com.google.gerrit.client.ui.PatchLink;
 import com.google.gerrit.client.ui.Screen;
 import com.google.gerrit.common.ClientVersion;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.auth.SignInMode;
 import com.google.gerrit.common.data.GerritConfig;
+import com.google.gerrit.common.data.GitwebConfig;
 import com.google.gerrit.common.data.HostPageData;
 import com.google.gerrit.common.data.SystemInfoService;
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.AccountDiffPreference;
-import com.google.gerrit.reviewdb.AccountGeneralPreferences;
-import com.google.gerrit.reviewdb.AuthType;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountDiffPreference;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
+import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gwt.core.client.EntryPoint;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.dom.client.AnchorElement;
 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;
@@ -47,11 +54,10 @@
 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.InlineLabel;
 import com.google.gwt.user.client.ui.Label;
 import com.google.gwt.user.client.ui.RootPanel;
-import com.google.gwt.user.client.ui.TabPanel;
-import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
 import com.google.gwtexpui.clippy.client.CopyableLabel;
 import com.google.gwtexpui.user.client.UserAgent;
 import com.google.gwtexpui.user.client.ViewSite;
@@ -64,26 +70,28 @@
 
 public class Gerrit implements EntryPoint {
   public static final GerritConstants C = GWT.create(GerritConstants.class);
+  public static final ChangeConstants CC = GWT.create(ChangeConstants.class);
   public static final GerritMessages M = GWT.create(GerritMessages.class);
   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 TabPanel menuLeft;
+  private static MorphingTabPanel menuLeft;
   private static LinkMenuBar menuRight;
+  private static LinkMenuBar diffBar;
   private static RootPanel siteHeader;
   private static RootPanel siteFooter;
   private static SearchPanel searchPanel;
   private static final Dispatcher dispatcher = new Dispatcher();
   private static ViewSite<Screen> body;
-
+  private static PatchScreen patchScreen;
   private static String lastChangeListToken;
 
   static {
@@ -96,6 +104,13 @@
     Window.Location.reload();
   }
 
+  public static PatchScreen.TopView getPatchScreenTopView() {
+    if (patchScreen == null) {
+      return null;
+    }
+    return patchScreen.getTopView();
+  }
+
   public static void displayLastChangeList() {
     if (lastChangeListToken != null) {
       display(lastChangeListToken);
@@ -145,6 +160,25 @@
   }
 
   /**
+   * Update any top level menus which can vary based on the view which was
+   * loaded.
+   * @param view the loaded view.
+   */
+  public static void updateMenus(Screen view) {
+    if (view instanceof PatchScreen) {
+      patchScreen = (PatchScreen) view;
+      menuLeft.setVisible(diffBar, true);
+      menuLeft.selectTab(menuLeft.getWidgetIndex(diffBar));
+    } else {
+      if (patchScreen != null && menuLeft.getSelectedWidget() == diffBar) {
+        menuLeft.selectTab(isSignedIn() ? 1 : 0);
+      }
+      patchScreen = null;
+      menuLeft.setVisible(diffBar, false);
+    }
+  }
+
+  /**
    * Update the current history token after a screen change.
    * <p>
    * The caller has already updated the UI, but wants to publish a different
@@ -156,13 +190,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) {
@@ -184,6 +212,11 @@
     return myConfig;
   }
 
+  public static GitwebLink getGitwebLink() {
+    GitwebConfig gw = getConfig().getGitwebLink();
+    return gw != null ? new GitwebLink(gw) : null;
+  }
+
   /** Site theme information (site specific colors)/ */
   public static HostPageData.Theme getTheme() {
     return myTheme;
@@ -209,16 +242,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:
@@ -232,11 +266,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() {
@@ -273,9 +357,11 @@
         myTheme = result.theme;
         if (result.account != null) {
           myAccount = result.account;
+          xsrfToken = result.xsrfToken;
         }
         if (result.accountDiffPref != null) {
           myAccountDiffPref = result.accountDiffPref;
+          applyUserPreferences();
         }
         onModuleLoad2();
       }
@@ -295,6 +381,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); }; }-*/;
@@ -316,9 +403,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);
+      }
     }
   }
 
@@ -356,7 +448,7 @@
     gBody.setStyleName(RESOURCES.css().gerritBody());
 
     final Grid menuLine = new Grid(1, 3);
-    menuLeft = new TabPanel();
+    menuLeft = new MorphingTabPanel();
     menuRight = new LinkMenuBar();
     searchPanel = new SearchPanel();
     menuLeft.setStyleName(RESOURCES.css().topmenuMenuLeft());
@@ -383,9 +475,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) {
@@ -404,7 +494,7 @@
     JsonUtil.setDefaultXsrfManager(new XsrfManager() {
       @Override
       public String getToken(JsonDefTarget proxy) {
-        return Cookies.getCookie(SESSION_COOKIE);
+        return xsrfToken;
       }
 
       @Override
@@ -429,15 +519,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() {
@@ -466,6 +557,16 @@
       menuLeft.selectTab(0);
     }
 
+    patchScreen = null;
+    diffBar = new LinkMenuBar();
+    menuLeft.addInvisible(diffBar, C.menuDiff());
+    addDiffLink(diffBar, CC.patchTableDiffSideBySide(), PatchScreen.Type.SIDE_BY_SIDE);
+    addDiffLink(diffBar, CC.patchTableDiffUnified(), PatchScreen.Type.UNIFIED);
+    addDiffLink(diffBar, C.menuDiffCommit(), PatchScreen.TopView.COMMIT);
+    addDiffLink(diffBar, C.menuDiffPreferences(), PatchScreen.TopView.PREFERENCES);
+    addDiffLink(diffBar, C.menuDiffPatchSets(), PatchScreen.TopView.PATCH_SETS);
+    addDiffLink(diffBar, C.menuDiffFiles(), PatchScreen.TopView.FILES);
+
     if (signedIn) {
       m = new LinkMenuBar();
       addLink(m, C.menuGroups(), PageLinks.ADMIN_GROUPS);
@@ -486,7 +587,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()) {
@@ -511,18 +612,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;
       }
     }
@@ -561,9 +660,39 @@
     m.addItem(new LinkMenuItem(text, historyToken));
   }
 
+  private static void addDiffLink(final LinkMenuBar m, final String text,
+      final PatchScreen.TopView tv) {
+    m.addItem(new LinkMenuItem(text, "") {
+        @Override
+        public void go() {
+          if (patchScreen != null) {
+            patchScreen.setTopView(tv);
+          }
+          AnchorElement.as(getElement()).blur();
+        }
+      });
+  }
+
+  private static void addDiffLink(final LinkMenuBar m, final String text,
+      final PatchScreen.Type type) {
+    m.addItem(new LinkMenuItem(text, "") {
+        @Override
+        public void go() {
+          if (patchScreen != null) {
+            patchScreen.setTopView(PatchScreen.TopView.MAIN);
+            if (type == patchScreen.getPatchScreenType()) {
+              AnchorElement.as(getElement()).blur();
+            } else {
+              new PatchLink("", type, patchScreen).go();
+            }
+          }
+        }
+      });
+  }
+
   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 fbec6aa..ee107d0 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
@@ -61,6 +61,12 @@
   String menuMyWatchedChanges();
   String menuMyStarredChanges();
 
+  String menuDiff();
+  String menuDiffCommit();
+  String menuDiffPreferences();
+  String menuDiffPatchSets();
+  String menuDiffFiles();
+
   String menuAdmin();
   String menuPeople();
   String menuGroups();
@@ -75,8 +81,7 @@
   String searchHint();
   String searchButton();
 
-  String rpcStatusLoading();
-  String anonymousCoward();
+  String rpcStatusWorking();
 
   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 617ab6b..41db3d5 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
@@ -25,7 +25,7 @@
 <p>To continue, please sign-in again.</p>
 
 notFoundTitle = Not Found
-notFoundBody = The page you requested was not found.
+notFoundBody = The page you requested was not found, or you do not have permission to view this page.
 nameAlreadyUsedBody = The name is already in use.
 noSuchAccountTitle = Code Review - Unknown User
 
@@ -44,6 +44,12 @@
 menuMyStarredChanges = Starred Changes
 menuMyWatchedChanges = Watched Changes
 
+menuDiff = Differences
+menuDiffCommit = Commit Message
+menuDiffPreferences = Preferences
+menuDiffPatchSets = Patch Sets
+menuDiffFiles = Files
+
 menuAdmin = Admin
 menuPeople = People
 menuGroups = Groups
@@ -58,8 +64,7 @@
 searchHint = Change #, SHA-1, tr:id, owner:email or reviewer:email
 searchButton = Search
 
-rpcStatusLoading = Loading ...
-anonymousCoward = Anonymous Coward
+rpcStatusWorking = Working ...
 
 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 993d06a..a8315d8 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
@@ -18,10 +18,6 @@
 
 public interface GerritCss extends CssResource {
   String greenCheckClass();
-  String abandonChangeDialog();
-  String abandonMessage();
-  String revertChangeDialog();
-  String revertMessage();
   String accountContactOnFile();
   String accountContactPrivacyDetails();
   String accountDashboard();
@@ -30,6 +26,7 @@
   String accountUsername();
   String accountPassword();
   String activeRow();
+  String addMemberTextBox();
   String addReviewer();
   String removeReviewer();
   String removeReviewerCell();
@@ -57,6 +54,8 @@
   String changeTypeCell();
   String changeid();
   String closedstate();
+  String commentedActionDialog();
+  String commentedActionMessage();
   String commentCell();
   String commentEditorPanel();
   String commentHolder();
@@ -79,6 +78,8 @@
   String contributorAgreementLegal();
   String contributorAgreementShortDescription();
   String coverMessage();
+  String createProjectLink();
+  String createProjectPanel();
   String dataCell();
   String dataHeader();
   String diffLinkCell();
@@ -113,7 +114,19 @@
   String filePathCell();
   String gerritTopMenu();
   String gerritBody();
+  String groupDescriptionPanel();
+  String groupExternalNameFilterTextBox();
+  String groupIncludesTable();
+  String groupMembersTable();
   String groupName();
+  String groupNamePanel();
+  String groupNameTextBox();
+  String groupOptionsPanel();
+  String groupOwnerPanel();
+  String groupOwnerTextBox();
+  String groupTypePanel();
+  String groupTypeSelectListBox();
+  String groupUUIDPanel();
   String header();
   String hyperlink();
   String iconCell();
@@ -138,12 +151,14 @@
   String negscore();
   String noLineLineNumber();
   String noborder();
+  String outdated();
   String parentsTable();
   String patchBrowserPopup();
   String patchBrowserPopupBody();
   String patchComments();
   String patchContentTable();
   String patchHistoryTable();
+  String patchHistoryTablePatchSetHeader();
   String patchNoDifference();
   String patchScreenDisplayControls();
   String patchSetActions();
@@ -174,6 +189,8 @@
   String skipLine();
   String smallHeading();
   String sourceFilePath();
+  String specialBranchDataCell();
+  String specialBranchIconCell();
   String sshHostKeyPanel();
   String sshHostKeyPanelFingerprintData();
   String sshHostKeyPanelHeading();
@@ -186,6 +203,7 @@
   String topmenuTDglue();
   String topmenuTDmenu();
   String topmost();
+  String topMostCell();
   String useridentity();
   String usernameField();
   String version();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GitwebLink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GitwebLink.java
new file mode 100644
index 0000000..5f62a52
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GitwebLink.java
@@ -0,0 +1,81 @@
+// Copyright (C) 2008 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client;
+
+import com.google.gerrit.common.data.GitWebType;
+import com.google.gerrit.common.data.ParameterizedString;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gwt.http.client.URL;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/** Link to an external gitweb server. */
+public class GitwebLink {
+  protected String baseUrl;
+
+  protected GitWebType type;
+
+  public GitwebLink(com.google.gerrit.common.data.GitwebConfig link) {
+    baseUrl = link.baseUrl;
+    type = link.type;
+  }
+
+  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", encode(project.get()));
+    p.put("commit", encode(ps.getRevision().get()));
+    return baseUrl + pattern.replace(p);
+  }
+
+  public String toProject(final Project.NameKey project) {
+    ParameterizedString pattern = new ParameterizedString(type.getProject());
+
+    final Map<String, String> p = new HashMap<String, String>();
+    p.put("project", encode(project.get()));
+    return baseUrl + pattern.replace(p);
+  }
+
+  public String toBranch(final Branch.NameKey branch) {
+    ParameterizedString pattern = new ParameterizedString(type.getBranch());
+
+    final Map<String, String> p = new HashMap<String, String>();
+    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-gwtui/src/main/java/com/google/gerrit/client/HostPageDataService.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/HostPageDataService.java
index accec0a..bd3d483 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/HostPageDataService.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/HostPageDataService.java
@@ -15,11 +15,11 @@
 package com.google.gerrit.client;
 
 import com.google.gerrit.common.data.HostPageData;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import com.google.gwtjsonrpc.client.HostPageCache;
-import com.google.gwtjsonrpc.client.RemoteJsonService;
-import com.google.gwtjsonrpc.client.RpcImpl;
-import com.google.gwtjsonrpc.client.RpcImpl.Version;
+import com.google.gwtjsonrpc.common.AsyncCallback;
+import com.google.gwtjsonrpc.common.HostPageCache;
+import com.google.gwtjsonrpc.common.RemoteJsonService;
+import com.google.gwtjsonrpc.common.RpcImpl;
+import com.google.gwtjsonrpc.common.RpcImpl.Version;
 
 @RpcImpl(version = Version.V2_0)
 interface HostPageDataService extends RemoteJsonService {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/RpcStatus.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/RpcStatus.java
index 21c5154..76ce384 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/RpcStatus.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/RpcStatus.java
@@ -47,7 +47,7 @@
     p.add(r);
 
     loading = new InlineLabel();
-    loading.setText(Gerrit.C.rpcStatusLoading());
+    loading.setText(Gerrit.C.rpcStatusWorking());
     loading.setStyleName(Gerrit.RESOURCES.css().rpcStatus());
     loading.addStyleName(Gerrit.RESOURCES.css().rpcStatusLoading());
     loading.setVisible(false);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchPanel.java
index d1f1296..e3d8468 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchPanel.java
@@ -17,7 +17,7 @@
 import com.google.gerrit.client.changes.QueryScreen;
 import com.google.gerrit.client.ui.HintTextBox;
 import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.reviewdb.client.Change;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.event.dom.client.KeyCodes;
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 1d4aeca..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,7 +56,9 @@
   String buttonChangeUserName();
   String buttonClearPassword();
   String buttonGeneratePassword();
+  String linkObtainPassword();
   String invalidUserName();
+  String invalidUserEmail();
 
   String sshKeyInvalid();
   String sshKeyAlgorithm();
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 a1c96c7..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,8 +37,9 @@
 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
 sshKeyAlgorithm = Algorithm
 sshKeyKey = Key
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelFull.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelFull.java
index b7a00ad..4b8f0e2 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelFull.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ContactPanelFull.java
@@ -16,8 +16,8 @@
 
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.ui.OnEditEnabler;
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.ContactInformation;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.ContactInformation;
 import com.google.gwt.user.client.ui.Grid;
 import com.google.gwt.user.client.ui.HTML;
 import com.google.gwt.user.client.ui.Label;
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 8fe4d5f..4e0b3b2 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
@@ -14,31 +14,33 @@
 
 package com.google.gerrit.client.account;
 
+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.OnEditEnabler;
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.AccountExternalId;
-import com.google.gerrit.reviewdb.ContactInformation;
-import com.google.gerrit.reviewdb.Account.FieldName;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountExternalId;
+import com.google.gerrit.reviewdb.client.AuthType;
+import com.google.gerrit.reviewdb.client.ContactInformation;
+import com.google.gerrit.reviewdb.client.Account.FieldName;
 import com.google.gwt.event.dom.client.ChangeEvent;
 import com.google.gwt.event.dom.client.ChangeHandler;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.i18n.client.LocaleInfo;
+import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.Composite;
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.FormPanel;
+import com.google.gwt.user.client.ui.FormPanel.SubmitEvent;
 import com.google.gwt.user.client.ui.Grid;
 import com.google.gwt.user.client.ui.HTML;
 import com.google.gwt.user.client.ui.ListBox;
 import com.google.gwt.user.client.ui.VerticalPanel;
 import com.google.gwt.user.client.ui.Widget;
-import com.google.gwt.user.client.ui.FormPanel.SubmitEvent;
 import com.google.gwtexpui.globalkey.client.NpTextBox;
 import com.google.gwtexpui.user.client.AutoCenterDialogBox;
-import com.google.gwtjsonrpc.client.VoidResult;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -117,7 +119,7 @@
     save.addClickHandler(new ClickHandler() {
       @Override
       public void onClick(final ClickEvent event) {
-        doSave();
+        doSave(null);
       }
     });
     new OnEditEnabler(save, nameTxt);
@@ -205,30 +207,7 @@
 
   private void postLoad() {
     if (haveAccount && haveEmails) {
-      if (currentEmail != null) {
-        boolean found = false;
-        for (int i = 0; i < emailPick.getItemCount(); i++) {
-          if (currentEmail.equals(emailPick.getValue(i))) {
-            emailPick.setSelectedIndex(i);
-            found = true;
-            break;
-          }
-        }
-        if (!found) {
-          emailPick.addItem(currentEmail);
-          emailPick.setSelectedIndex(emailPick.getItemCount() - 1);
-        }
-      }
-      if (emailPick.getItemCount() > 0) {
-        emailPick.setVisible(true);
-        emailPick.setEnabled(true);
-        if (canRegisterNewEmail()) {
-          final String t = Util.C.buttonOpenRegisterNewEmail();
-          emailPick.addItem("... " + t + "  ", t);
-        }
-      } else {
-        emailPick.setVisible(false);
-      }
+      updateEmailList();
       registerNewEmail.setEnabled(true);
     }
     display();
@@ -270,14 +249,24 @@
         event.cancel();
         final String addr = inEmail.getText().trim();
         if (!addr.contains("@")) {
+          new ErrorDialog(Util.C.invalidUserEmail()).center();
           return;
         }
 
         inEmail.setEnabled(false);
         register.setEnabled(false);
-        Util.ACCOUNT_SEC.registerEmail(addr, new GerritCallback<VoidResult>() {
-          public void onSuccess(VoidResult result) {
+        Util.ACCOUNT_SEC.registerEmail(addr, new GerritCallback<Account>() {
+          public void onSuccess(Account currentUser) {
             box.hide();
+            if (Gerrit.getConfig().getAuthType() == AuthType.DEVELOPMENT_BECOME_ANY_ACCOUNT) {
+              currentEmail = addr;
+              if (emailPick.getItemCount() == 0) {
+                onSaveSuccess(currentUser);
+              } else {
+                save.setEnabled(true);
+              }
+              updateEmailList();
+            }
           }
 
           @Override
@@ -309,7 +298,9 @@
     buttons.add(register);
     buttons.add(cancel);
 
-    body.add(new HTML(Util.C.descRegisterNewEmail()));
+    if (Gerrit.getConfig().getAuthType() != AuthType.DEVELOPMENT_BECOME_ANY_ACCOUNT) {
+      body.add(new HTML(Util.C.descRegisterNewEmail()));
+    }
     body.add(inEmail);
     body.add(buttons);
 
@@ -319,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;
@@ -346,6 +337,9 @@
           public void onSuccess(final Account result) {
             registerNewEmail.setEnabled(true);
             onSaveSuccess(result);
+            if (onSave != null) {
+              onSave.onSuccess(result);
+            }
           }
 
           @Override
@@ -367,4 +361,39 @@
   ContactInformation toContactInformation() {
     return null;
   }
+
+  private int emailListIndexOf(String value) {
+    for (int i = 0; i < emailPick.getItemCount(); i++) {
+      if (value.equalsIgnoreCase(emailPick.getValue(i))) {
+        return i;
+      }
+    }
+    return -1;
+  }
+
+  private void updateEmailList() {
+    if (currentEmail != null) {
+      int index = emailListIndexOf(currentEmail);
+      if (index == -1) {
+        emailPick.addItem(currentEmail);
+        emailPick.setSelectedIndex(emailPick.getItemCount() - 1);
+      } else {
+        emailPick.setSelectedIndex(index);
+      }
+    }
+    if (emailPick.getItemCount() > 0) {
+      emailPick.setVisible(true);
+      emailPick.setEnabled(true);
+      if (canRegisterNewEmail()) {
+        final String t = Util.C.buttonOpenRegisterNewEmail();
+        int index = emailListIndexOf(t);
+        if (index != -1) {
+          emailPick.removeItem(index);
+        }
+        emailPick.addItem("... " + t + "  ", t);
+      }
+    } else {
+      emailPick.setVisible(false);
+    }
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyAgreementsScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyAgreementsScreen.java
index 54de472..af619a8 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyAgreementsScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyAgreementsScreen.java
@@ -21,10 +21,10 @@
 import com.google.gerrit.client.ui.Hyperlink;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.data.AgreementInfo;
-import com.google.gerrit.reviewdb.AbstractAgreement;
-import com.google.gerrit.reviewdb.AccountAgreement;
-import com.google.gerrit.reviewdb.AccountGroupAgreement;
-import com.google.gerrit.reviewdb.ContributorAgreement;
+import com.google.gerrit.reviewdb.client.AbstractAgreement;
+import com.google.gerrit.reviewdb.client.AccountAgreement;
+import com.google.gerrit.reviewdb.client.AccountGroupAgreement;
+import com.google.gerrit.reviewdb.client.ContributorAgreement;
 import com.google.gwt.user.client.ui.Anchor;
 import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
 import com.google.gwtexpui.safehtml.client.SafeHtml;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGroupsScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGroupsScreen.java
index 437d438..719c395 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGroupsScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyGroupsScreen.java
@@ -16,7 +16,7 @@
 
 import com.google.gerrit.client.admin.GroupTable;
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
-import com.google.gerrit.reviewdb.AccountGroup;
+import com.google.gerrit.common.data.GroupDetail;
 
 import java.util.List;
 
@@ -33,8 +33,8 @@
   @Override
   protected void onLoad() {
     super.onLoad();
-    Util.ACCOUNT_SEC.myGroups(new ScreenLoadCallback<List<AccountGroup>>(this) {
-      public void preDisplay(final List<AccountGroup> result) {
+    Util.ACCOUNT_SEC.myGroups(new ScreenLoadCallback<List<GroupDetail>>(this) {
+      public void preDisplay(final List<GroupDetail> result) {
         groups.display(result);
       }
     });
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyIdentitiesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyIdentitiesScreen.java
index f3816f2..899aa03 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyIdentitiesScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyIdentitiesScreen.java
@@ -22,7 +22,7 @@
 import com.google.gerrit.client.ui.FancyFlexTable;
 import com.google.gerrit.common.auth.SignInMode;
 import com.google.gerrit.common.auth.openid.OpenIdUrls;
-import com.google.gerrit.reviewdb.AccountExternalId;
+import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.event.logical.shared.ValueChangeEvent;
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..abb1f4d 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
@@ -14,20 +14,21 @@
 
 package com.google.gerrit.client.account;
 
-import static com.google.gerrit.reviewdb.AccountExternalId.SCHEME_USERNAME;
+import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_USERNAME;
 
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
-import com.google.gerrit.reviewdb.AccountExternalId;
+import com.google.gerrit.reviewdb.client.AccountExternalId;
 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/MyPreferencesScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java
index 1415167..84e87aa 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyPreferencesScreen.java
@@ -14,14 +14,14 @@
 
 package com.google.gerrit.client.account;
 
-import static com.google.gerrit.reviewdb.AccountGeneralPreferences.DEFAULT_PAGESIZE;
-import static com.google.gerrit.reviewdb.AccountGeneralPreferences.PAGESIZE_CHOICES;
+import static com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DEFAULT_PAGESIZE;
+import static com.google.gerrit.reviewdb.client.AccountGeneralPreferences.PAGESIZE_CHOICES;
 
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.AccountGeneralPreferences;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
 import com.google.gwt.event.dom.client.ChangeEvent;
 import com.google.gwt.event.dom.client.ChangeHandler;
 import com.google.gwt.event.dom.client.ClickEvent;
@@ -33,7 +33,7 @@
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.Grid;
 import com.google.gwt.user.client.ui.ListBox;
-import com.google.gwtjsonrpc.client.VoidResult;
+import com.google.gwtjsonrpc.common.VoidResult;
 
 import java.util.Date;
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyProfileScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyProfileScreen.java
index 65c2590..165168d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyProfileScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyProfileScreen.java
@@ -17,7 +17,7 @@
 import static com.google.gerrit.client.FormatUtil.mediumFormat;
 
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.reviewdb.Account;
+import com.google.gerrit.reviewdb.client.Account;
 import com.google.gwt.i18n.client.LocaleInfo;
 import com.google.gwt.user.client.ui.Grid;
 import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
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/MyWatchesTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchesTable.java
index b3bd0cb..c158c2c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchesTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/MyWatchesTable.java
@@ -19,16 +19,16 @@
 import com.google.gerrit.client.ui.FancyFlexTable;
 import com.google.gerrit.client.ui.ProjectLink;
 import com.google.gerrit.common.data.AccountProjectWatchInfo;
-import com.google.gerrit.reviewdb.AccountProjectWatch;
-import com.google.gerrit.reviewdb.Change.Status;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch;
+import com.google.gerrit.reviewdb.client.Change.Status;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.user.client.DOM;
 import com.google.gwt.user.client.ui.CheckBox;
+import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.Label;
-import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
-import com.google.gwtjsonrpc.client.VoidResult;
+import com.google.gwtjsonrpc.common.VoidResult;
 
 import java.util.HashSet;
 import java.util.List;
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..154e2ce 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,9 +22,10 @@
 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.AccountAgreement;
-import com.google.gerrit.reviewdb.AccountGroupAgreement;
-import com.google.gerrit.reviewdb.ContributorAgreement;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountAgreement;
+import com.google.gerrit.reviewdb.client.AccountGroupAgreement;
+import com.google.gerrit.reviewdb.client.ContributorAgreement;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
@@ -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.gwtjsonrpc.common.AsyncCallback;
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.FormPanel;
@@ -43,7 +45,7 @@
 import com.google.gwt.user.client.ui.RadioButton;
 import com.google.gwt.user.client.ui.VerticalPanel;
 import com.google.gwtexpui.globalkey.client.NpTextBox;
-import com.google.gwtjsonrpc.client.VoidResult;
+import com.google.gwtjsonrpc.common.VoidResult;
 
 import java.util.HashSet;
 import java.util.List;
@@ -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) {
@@ -234,7 +250,7 @@
     String url = cla.getAgreementUrl();
     if (url != null && url.length() > 0) {
       agreementGroup.setVisible(true);
-      agreementHtml.setText(Gerrit.C.rpcStatusLoading());
+      agreementHtml.setText(Gerrit.C.rpcStatusWorking());
       if (!url.startsWith("http:") && !url.startsWith("https:")) {
         url = GWT.getHostPageBaseURL() + url;
       }
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..084ea6f 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
@@ -19,8 +19,8 @@
 import com.google.gerrit.client.ui.InlineHyperlink;
 import com.google.gerrit.client.ui.SmallHeading;
 import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.Account.FieldName;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Account.FieldName;
 import com.google.gwt.i18n.client.LocaleInfo;
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.FormPanel;
@@ -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/account/SshPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SshPanel.java
index c8a1485..73127f0 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SshPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/SshPanel.java
@@ -21,7 +21,7 @@
 import com.google.gerrit.client.ui.SmallHeading;
 import com.google.gerrit.common.data.SshHostKey;
 import com.google.gerrit.common.errors.InvalidSshKeyException;
-import com.google.gerrit.reviewdb.AccountSshKey;
+import com.google.gerrit.reviewdb.client.AccountSshKey;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.event.logical.shared.ValueChangeEvent;
@@ -39,7 +39,7 @@
 import com.google.gwtexpui.clippy.client.CopyableLabel;
 import com.google.gwtexpui.globalkey.client.NpTextArea;
 import com.google.gwtjsonrpc.client.RemoteJsonException;
-import com.google.gwtjsonrpc.client.VoidResult;
+import com.google.gwtjsonrpc.common.VoidResult;
 
 import java.util.Collections;
 import java.util.HashSet;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/UsernameField.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/UsernameField.java
index bee1636..0666bb3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/UsernameField.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/UsernameField.java
@@ -19,7 +19,7 @@
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.ui.OnEditEnabler;
 import com.google.gerrit.common.errors.InvalidUserNameException;
-import com.google.gerrit.reviewdb.Account;
+import com.google.gerrit.reviewdb.client.Account;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.event.dom.client.KeyCodes;
@@ -31,7 +31,7 @@
 import com.google.gwt.user.client.ui.TextBox;
 import com.google.gwtexpui.clippy.client.CopyableLabel;
 import com.google.gwtexpui.globalkey.client.NpTextBox;
-import com.google.gwtjsonrpc.client.VoidResult;
+import com.google.gwtjsonrpc.common.VoidResult;
 
 class UsernameField extends Composite {
   private CopyableLabel userNameLbl;
@@ -138,7 +138,8 @@
     @Override
     public void onKeyPress(final KeyPressEvent event) {
       final char code = event.getCharCode();
-      switch (code) {
+      final int nativeCode = event.getNativeEvent().getKeyCode();
+      switch (nativeCode) {
         case KeyCodes.KEY_ALT:
         case KeyCodes.KEY_BACKSPACE:
         case KeyCodes.KEY_CTRL:
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ValidateEmailScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ValidateEmailScreen.java
index 4e54e85..1164efc 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ValidateEmailScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/ValidateEmailScreen.java
@@ -18,7 +18,7 @@
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
 import com.google.gerrit.client.ui.AccountScreen;
 import com.google.gerrit.common.PageLinks;
-import com.google.gwtjsonrpc.client.VoidResult;
+import com.google.gwtjsonrpc.common.VoidResult;
 
 public class ValidateEmailScreen extends AccountScreen {
   private final String magicToken;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessRightEditor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessRightEditor.java
deleted file mode 100644
index 8f6831b..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessRightEditor.java
+++ /dev/null
@@ -1,404 +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.client.admin;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.ui.AccountGroupSuggestOracle;
-import com.google.gerrit.client.ui.HintTextBox;
-import com.google.gerrit.client.ui.RPCSuggestOracle;
-import com.google.gerrit.common.data.ApprovalType;
-import com.google.gerrit.common.data.ProjectDetail;
-import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.ApprovalCategory;
-import com.google.gerrit.reviewdb.ApprovalCategoryValue;
-import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.RefRight;
-import com.google.gwt.event.dom.client.ChangeEvent;
-import com.google.gwt.event.dom.client.ChangeHandler;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.dom.client.KeyCodes;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.event.dom.client.KeyPressHandler;
-import com.google.gwt.event.logical.shared.HasValueChangeHandlers;
-import com.google.gwt.event.logical.shared.ValueChangeEvent;
-import com.google.gwt.event.logical.shared.ValueChangeHandler;
-import com.google.gwt.event.shared.HandlerRegistration;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.Grid;
-import com.google.gwt.user.client.ui.ListBox;
-import com.google.gwt.user.client.ui.SuggestBox;
-
-public class AccessRightEditor extends Composite
-    implements HasValueChangeHandlers<ProjectDetail> {
-  private Project.NameKey projectKey;
-  private ListBox catBox;
-  private HintTextBox nameTxt;
-  private SuggestBox nameSug;
-  private HintTextBox referenceTxt;
-  private ListBox topBox;
-  private ListBox botBox;
-  private Button addBut;
-  private Button clearBut;
-
-  public AccessRightEditor(final Project.NameKey key) {
-    projectKey = key;
-
-    initWidgets();
-    initCategories();
-
-    final Grid grid = new Grid(5, 2);
-    grid.setText(0, 0, Util.C.columnApprovalCategory() + ":");
-    grid.setWidget(0, 1, catBox);
-
-    grid.setText(1, 0, Util.C.columnGroupName() + ":");
-    grid.setWidget(1, 1, nameSug);
-
-    grid.setText(2, 0, Util.C.columnRefName() + ":");
-    grid.setWidget(2, 1, referenceTxt);
-
-    grid.setText(3, 0, Util.C.columnRightRange() + ":");
-    grid.setWidget(3, 1, topBox);
-
-    grid.setText(4, 0, "");
-    grid.setWidget(4, 1, botBox);
-
-    FlowPanel fp = new FlowPanel();
-    fp.setStyleName(Gerrit.RESOURCES.css().addSshKeyPanel());
-
-    fp.add(grid);
-    fp.add(addBut);
-    fp.add(clearBut);
-    initWidget(fp);
-  }
-
-  protected void initWidgets() {
-    catBox = new ListBox();
-    catBox.addChangeHandler(new ChangeHandler() {
-      @Override
-      public void onChange(final ChangeEvent event) {
-        updateCategorySelection();
-      }
-    });
-
-    nameTxt = new HintTextBox();
-    nameSug = new SuggestBox(new RPCSuggestOracle(
-        new AccountGroupSuggestOracle()), nameTxt);
-    nameTxt.setVisibleLength(50);
-    nameTxt.setHintText(Util.C.defaultAccountGroupName());
-
-    referenceTxt = new HintTextBox();
-    referenceTxt.setVisibleLength(50);
-    referenceTxt.setText("");
-    referenceTxt.addKeyPressHandler(new KeyPressHandler() {
-      @Override
-      public void onKeyPress(KeyPressEvent event) {
-        if (event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER) {
-          doAddNewRight();
-        }
-      }
-    });
-
-    topBox = new ListBox();
-    botBox = new ListBox();
-
-    addBut = new Button(Util.C.buttonAddProjectRight());
-    addBut.addClickHandler(new ClickHandler() {
-      @Override
-      public void onClick(final ClickEvent event) {
-        doAddNewRight();
-      }
-    });
-
-    clearBut = new Button(Util.C.buttonClearProjectRight());
-    clearBut.addClickHandler(new ClickHandler() {
-      @Override
-      public void onClick(final ClickEvent event) {
-        clear();
-      }
-    });
-  }
-
-  protected void initCategories() {
-    for (final ApprovalType at : Gerrit.getConfig().getApprovalTypes()
-        .getApprovalTypes()) {
-      final ApprovalCategory c = at.getCategory();
-      catBox.addItem(c.getName(), c.getId().get());
-    }
-    for (final ApprovalType at : Gerrit.getConfig().getApprovalTypes()
-        .getActionTypes()) {
-      final ApprovalCategory c = at.getCategory();
-      if (Gerrit.getConfig().getWildProject().equals(projectKey)
-          && !c.getId().canBeOnWildProject()) {
-        // Giving out control of the WILD_PROJECT to other groups beyond
-        // Administrators is dangerous. Having control over WILD_PROJECT
-        // is about the same as having Administrator access as users are
-        // able to affect grants in all projects on the system.
-        //
-        continue;
-      }
-      catBox.addItem(c.getName(), c.getId().get());
-    }
-
-    if (catBox.getItemCount() > 0) {
-      catBox.setSelectedIndex(0);
-      updateCategorySelection();
-    }
-  }
-
-  public void enableForm(final boolean on) {
-    final boolean canAdd = on && catBox.getItemCount() > 0;
-    addBut.setEnabled(canAdd);
-    clearBut.setEnabled(canAdd);
-    nameTxt.setEnabled(canAdd);
-    referenceTxt.setEnabled(canAdd);
-    catBox.setEnabled(canAdd);
-    topBox.setEnabled(canAdd);
-    botBox.setEnabled(canAdd);
-  }
-
-  public void clear() {
-    setCat(null);
-    setName("");
-    setReference("");
-  }
-
-  public void load(final RefRight right, final AccountGroup group) {
-    final ApprovalType atype =
-       Gerrit.getConfig().getApprovalTypes().getApprovalType(
-          right.getApprovalCategoryId());
-
-    setCat(atype != null ? atype.getCategory().getName()
-                         : right.getApprovalCategoryId().get() );
-
-    setName(group.getName());
-    setReference(right.getRefPatternForDisplay());
-
-    setRange(atype.getCategory().isRange() ? atype.getValue(right.getMinValue())
-             : null, atype.getValue(right.getMaxValue()) );
-  }
-
-  protected void doAddNewRight() {
-    final ApprovalType at = getApprovalType();
-    ApprovalCategoryValue min = getMin(at);
-    ApprovalCategoryValue max = getMax(at);
-
-    if (at == null || min == null || max == null) {
-      return;
-    }
-
-    final String groupName = nameSug.getText();
-    if ("".equals(groupName)
-        || Util.C.defaultAccountGroupName().equals(groupName)) {
-      return;
-    }
-
-    final String refPattern = referenceTxt.getText();
-
-    addBut.setEnabled(false);
-    Util.PROJECT_SVC.addRight(projectKey, at.getCategory().getId(),
-        groupName, refPattern, min.getValue(), max.getValue(),
-        new GerritCallback<ProjectDetail>() {
-          public void onSuccess(final ProjectDetail result) {
-            addBut.setEnabled(true);
-            nameSug.setText("");
-            referenceTxt.setText("");
-            ValueChangeEvent.fire(AccessRightEditor.this, result);
-          }
-
-          @Override
-          public void onFailure(final Throwable caught) {
-            addBut.setEnabled(true);
-            super.onFailure(caught);
-          }
-        });
-  }
-
-  protected void updateCategorySelection() {
-    final ApprovalType at = getApprovalType();
-
-    if (at == null || at.getValues().isEmpty()) {
-      topBox.setEnabled(false);
-      botBox.setEnabled(false);
-      referenceTxt.setEnabled(false);
-      addBut.setEnabled(false);
-      clearBut.setEnabled(false);
-      return;
-    }
-
-    updateRanges(at);
-  }
-
-  protected void updateRanges(final ApprovalType at) {
-    ApprovalCategoryValue min = null, max = null, last = null;
-
-    topBox.clear();
-    botBox.clear();
-
-    for(final ApprovalCategoryValue v : at.getValues()) {
-      final int nval = v.getValue();
-      final String vStr = String.valueOf(nval);
-
-      String nStr = vStr + ": " + v.getName();
-      if (nval > 0) {
-        nStr = "+" + nStr;
-      }
-
-      topBox.addItem(nStr, vStr);
-      botBox.addItem(nStr, vStr);
-
-      if (min == null || nval < 0) {
-        min = v;
-      } else if (max == null && nval > 0) {
-        max = v;
-      }
-      last = v;
-    }
-
-    if (max == null) {
-      max = last;
-    }
-
-    if (ApprovalCategory.READ.equals(at.getCategory().getId())) {
-      // Special case; for READ the most logical range is just
-      // +1 READ, so assume that as the default for both.
-      min = max;
-    }
-
-    if (! at.getCategory().isRange()) {
-      max = null;
-    }
-
-    setRange(min, max);
-  }
-
-  protected void setCat(final String cat) {
-    if (cat == null) {
-      catBox.setSelectedIndex(0);
-    } else {
-      setSelectedText(catBox, cat);
-    }
-    updateCategorySelection();
-  }
-
-  protected void setName(final String name) {
-    nameTxt.setText(name);
-  }
-
-  protected void setReference(final String ref) {
-    referenceTxt.setText(ref);
-  }
-
-  protected void setRange(final ApprovalCategoryValue min,
-                          final ApprovalCategoryValue max) {
-    if (min == null || max == null) {
-      botBox.setVisible(false);
-      if (max != null) {
-        setSelectedValue(topBox, "" + max.getValue());
-        return;
-      }
-    } else {
-      botBox.setVisible(true);
-      setSelectedValue(botBox, "" + max.getValue());
-    }
-    setSelectedValue(topBox, "" + min.getValue());
-  }
-
-  private ApprovalType getApprovalType() {
-    int idx = catBox.getSelectedIndex();
-    if (idx < 0) {
-      return null;
-    }
-    return Gerrit.getConfig().getApprovalTypes().getApprovalType(
-             new ApprovalCategory.Id(catBox.getValue(idx)));
-  }
-
-  public ApprovalCategoryValue getMin(ApprovalType at) {
-    final ApprovalCategoryValue top = getTop(at);
-    final ApprovalCategoryValue bot = getBot(at);
-    if (bot == null) {
-      for (final ApprovalCategoryValue v : at.getValues()) {
-        if (0 <= v.getValue() && v.getValue() <= top.getValue()) {
-          return v;
-        }
-      }
-      return at.getMin();
-    }
-
-    if (top.getValue() > bot.getValue()) {
-      return bot;
-    }
-    return top;
-  }
-
-  public ApprovalCategoryValue getMax(ApprovalType at) {
-    final ApprovalCategoryValue top = getTop(at);
-    final ApprovalCategoryValue bot = getBot(at);
-    if (bot == null || bot.getValue() < top.getValue()) {
-      return top;
-    }
-    return bot;
-  }
-
-  protected ApprovalCategoryValue getTop(ApprovalType at) {
-    int idx = topBox.getSelectedIndex();
-    if (idx < 0) {
-      return null;
-    }
-    return at.getValue(Short.parseShort(topBox.getValue(idx)));
-  }
-
-  protected ApprovalCategoryValue getBot(ApprovalType at) {
-    int idx = botBox.getSelectedIndex();
-    if (idx < 0 || ! botBox.isVisible()) {
-      return null;
-    }
-    return at.getValue(Short.parseShort(botBox.getValue(idx)));
-  }
-
-  public HandlerRegistration addValueChangeHandler(
-      final ValueChangeHandler<ProjectDetail> handler) {
-    return addHandler(handler, ValueChangeEvent.getType());
-  }
-
-  public static boolean setSelectedText(ListBox box, String text) {
-    if (text == null) {
-      return false;
-    }
-    for (int i =0 ; i < box.getItemCount(); i++) {
-      if (text.equals(box.getItemText(i))) {
-        box.setSelectedIndex(i);
-        return true;
-      }
-    }
-    return false;
-  }
-
-  public static boolean setSelectedValue(ListBox box, String value) {
-    if (value == null) {
-      return false;
-    }
-    for (int i =0 ; i < box.getItemCount(); i++) {
-      if (value.equals(box.getValue(i))) {
-        box.setSelectedIndex(i);
-        return true;
-      }
-    }
-    return false;
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.java
new file mode 100644
index 0000000..72ac1a4
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.java
@@ -0,0 +1,294 @@
+// 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.Gerrit;
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.ApprovalType;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.common.data.ProjectAccess;
+import com.google.gerrit.common.data.RefConfigSection;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.dom.client.DivElement;
+import com.google.gwt.dom.client.SpanElement;
+import com.google.gwt.dom.client.Style.Display;
+import com.google.gwt.editor.client.Editor;
+import com.google.gwt.editor.client.EditorDelegate;
+import com.google.gwt.editor.client.ValueAwareEditor;
+import com.google.gwt.editor.client.adapters.EditorSource;
+import com.google.gwt.editor.client.adapters.ListEditor;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.MouseOutEvent;
+import com.google.gwt.event.dom.client.MouseOverEvent;
+import com.google.gwt.event.logical.shared.ValueChangeEvent;
+import com.google.gwt.event.logical.shared.ValueChangeHandler;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.uibinder.client.UiHandler;
+import com.google.gwt.user.client.ui.Anchor;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwt.user.client.ui.ValueListBox;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+public class AccessSectionEditor extends Composite implements
+    Editor<AccessSection>, ValueAwareEditor<AccessSection> {
+  interface Binder extends UiBinder<HTMLPanel, AccessSectionEditor> {
+  }
+
+  private static final Binder uiBinder = GWT.create(Binder.class);
+
+  @UiField
+  ValueEditor<String> name;
+
+  @UiField
+  FlowPanel permissionContainer;
+  ListEditor<Permission, PermissionEditor> permissions;
+
+  @UiField
+  DivElement addContainer;
+  @UiField(provided = true)
+  @Editor.Ignore
+  ValueListBox<String> permissionSelector;
+
+  @UiField
+  SpanElement deletedName;
+
+  @UiField
+  Anchor deleteSection;
+
+  @UiField
+  DivElement normal;
+  @UiField
+  DivElement deleted;
+
+  @UiField
+  SpanElement sectionType;
+  @UiField
+  SpanElement sectionName;
+
+  private final ProjectAccess projectAccess;
+  private AccessSection value;
+  private boolean editing;
+  private boolean readOnly;
+  private boolean isDeleted;
+
+  public AccessSectionEditor(ProjectAccess access) {
+    projectAccess = access;
+
+    permissionSelector =
+        new ValueListBox<String>(PermissionNameRenderer.INSTANCE);
+    permissionSelector.addValueChangeHandler(new ValueChangeHandler<String>() {
+      @Override
+      public void onValueChange(ValueChangeEvent<String> event) {
+        if (!Util.C.addPermission().equals(event.getValue())) {
+          onAddPermission(event.getValue());
+        }
+      }
+    });
+
+    initWidget(uiBinder.createAndBindUi(this));
+    permissions = ListEditor.of(new PermissionEditorSource());
+  }
+
+  @UiHandler("deleteSection")
+  void onDeleteHover(MouseOverEvent event) {
+    normal.addClassName(AdminResources.I.css().deleteSectionHover());
+  }
+
+  @UiHandler("deleteSection")
+  void onDeleteNonHover(MouseOutEvent event) {
+    normal.removeClassName(AdminResources.I.css().deleteSectionHover());
+  }
+
+  @UiHandler("deleteSection")
+  void onDeleteSection(ClickEvent event) {
+    isDeleted = true;
+
+    if (name.isVisible()
+        && RefConfigSection.isValid(name.getValue())) {
+      deletedName.setInnerText(Util.M.deletedReference(name.getValue()));
+
+    } else {
+      String name = Util.C.sectionNames().get(value.getName());
+      if (name == null) {
+        name = value.getName();
+      }
+      deletedName.setInnerText(Util.M.deletedSection(name));
+    }
+
+    normal.getStyle().setDisplay(Display.NONE);
+    deleted.getStyle().setDisplay(Display.BLOCK);
+  }
+
+  @UiHandler("undoDelete")
+  void onUndoDelete(ClickEvent event) {
+    isDeleted = false;
+    deleted.getStyle().setDisplay(Display.NONE);
+    normal.getStyle().setDisplay(Display.BLOCK);
+  }
+
+  void onAddPermission(String varName) {
+    int idx = permissions.getList().size();
+
+    Permission p = value.getPermission(varName, true);
+    permissions.getList().add(p);
+
+    PermissionEditor e = permissions.getEditors().get(idx);
+    e.beginAddRule();
+
+    rebuildPermissionSelector();
+  }
+
+  void editRefPattern() {
+    name.edit();
+    Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+      @Override
+      public void execute() {
+        name.setFocus(true);
+      }});
+  }
+
+  void enableEditing() {
+    readOnly = false;
+    addContainer.getStyle().setDisplay(Display.BLOCK);
+    rebuildPermissionSelector();
+  }
+
+  boolean isDeleted() {
+    return isDeleted;
+  }
+
+  @Override
+  public void setValue(AccessSection value) {
+    Collections.sort(value.getPermissions());
+
+    this.value = value;
+    this.readOnly = !editing || !projectAccess.isOwnerOf(value);
+
+    name.setEnabled(!readOnly);
+    deleteSection.setVisible(!readOnly);
+
+    if (RefConfigSection.isValid(value.getName())) {
+      name.setVisible(true);
+      name.setIgnoreEditorValue(false);
+      sectionType.setInnerText(Util.C.sectionTypeReference());
+
+    } else {
+      name.setVisible(false);
+      name.setIgnoreEditorValue(true);
+
+      String name = Util.C.sectionNames().get(value.getName());
+      if (name != null) {
+        sectionType.setInnerText(name);
+        sectionName.getStyle().setDisplay(Display.NONE);
+      } else {
+        sectionType.setInnerText(Util.C.sectionTypeSection());
+        sectionName.setInnerText(value.getName());
+        sectionName.getStyle().clearDisplay();
+      }
+    }
+
+    if (readOnly) {
+      addContainer.getStyle().setDisplay(Display.NONE);
+    } else {
+      enableEditing();
+    }
+  }
+
+  void setEditing(final boolean editing) {
+    this.editing = editing;
+  }
+
+  private void rebuildPermissionSelector() {
+    List<String> perms = new ArrayList<String>();
+
+    if (AccessSection.GLOBAL_CAPABILITIES.equals(value.getName())) {
+      for (String varName : Util.C.capabilityNames().keySet()) {
+        if (value.getPermission(varName) == null) {
+          perms.add(varName);
+        }
+      }
+    } else if (RefConfigSection.isValid(value.getName())) {
+      for (ApprovalType t : Gerrit.getConfig().getApprovalTypes()
+          .getApprovalTypes()) {
+        String varName = Permission.LABEL + t.getCategory().getLabelName();
+        if (value.getPermission(varName) == null) {
+          perms.add(varName);
+        }
+      }
+      for (String varName : Util.C.permissionNames().keySet()) {
+        if (value.getPermission(varName) == null) {
+          perms.add(varName);
+        }
+      }
+    }
+    if (perms.isEmpty()) {
+      addContainer.getStyle().setDisplay(Display.NONE);
+    } else {
+      addContainer.getStyle().setDisplay(Display.BLOCK);
+      perms.add(0, Util.C.addPermission());
+      permissionSelector.setValue(Util.C.addPermission());
+      permissionSelector.setAcceptableValues(perms);
+    }
+  }
+
+  @Override
+  public void flush() {
+    List<Permission> src = permissions.getList();
+    List<Permission> keep = new ArrayList<Permission>(src.size());
+
+    for (int i = 0; i < src.size(); i++) {
+      PermissionEditor e = (PermissionEditor) permissionContainer.getWidget(i);
+      if (!e.isDeleted()) {
+        keep.add(src.get(i));
+      }
+    }
+    value.setPermissions(keep);
+  }
+
+  @Override
+  public void onPropertyChange(String... paths) {
+  }
+
+  @Override
+  public void setDelegate(EditorDelegate<AccessSection> delegate) {
+  }
+
+  private class PermissionEditorSource extends EditorSource<PermissionEditor> {
+    @Override
+    public PermissionEditor create(int index) {
+      PermissionEditor subEditor = new PermissionEditor(readOnly, value);
+      permissionContainer.insert(subEditor, index);
+      return subEditor;
+    }
+
+    @Override
+    public void dispose(PermissionEditor subEditor) {
+      subEditor.removeFromParent();
+    }
+
+    @Override
+    public void setIndex(PermissionEditor subEditor, int index) {
+      permissionContainer.insert(subEditor, index);
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.ui.xml
new file mode 100644
index 0000000..501c3fc
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.ui.xml
@@ -0,0 +1,161 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<ui:UiBinder
+  xmlns:ui='urn:ui:com.google.gwt.uibinder'
+  xmlns:g='urn:import:com.google.gwt.user.client.ui'
+  xmlns:e='urn:import:com.google.gwt.editor.ui.client'
+  xmlns:my='urn:import:com.google.gerrit.client.admin'
+  ui:generateFormat='com.google.gwt.i18n.rebind.format.PropertiesFormat'
+  ui:generateKeys='com.google.gwt.i18n.rebind.keygen.MD5KeyGenerator'
+  ui:generateLocales='default,en'
+  >
+<ui:with field='res' type='com.google.gerrit.client.admin.AdminResources'/>
+<ui:style>
+  @eval selectionColor com.google.gerrit.client.Gerrit.getTheme().selectionColor;
+  @eval trimColor com.google.gerrit.client.Gerrit.getTheme().trimColor;
+
+  .panel {
+    position: relative;
+  }
+
+  .content {
+    margin-top: 4px;
+    margin-bottom: 4px;
+    padding-bottom: 2px;
+  }
+
+  .normal {
+    background-color: trimColor;
+  }
+
+  .deleted {
+    padding-left: 7px;
+    padding-bottom: 2px;
+  }
+
+  .header {
+    padding-left: 5px;
+    padding-right: 5px;
+  }
+  .headerText {
+    vertical-align: top;
+    white-space: nowrap;
+    font-weight: bold;
+  }
+  .headerTable {
+    border: 0;
+    width: 100%;
+    padding-right: 40px;
+  }
+
+  .header:hover {
+    background-color: selectionColor;
+  }
+
+  .name {
+    width: 100%;
+  }
+  .nameEdit {
+    width: 100%;
+  }
+
+  .permissionList {
+    margin-left: 5px;
+    margin-right: 5px;
+    margin-bottom: 5px;
+  }
+
+  .addContainer {
+    padding-left: 16px;
+    padding-right: 16px;
+    font-size: 80%;
+  }
+  .addContainer:hover {
+    background-color: selectionColor;
+  }
+  .addSelector {
+    font-size: 80%;
+  }
+
+  .deleteIcon {
+    position: absolute;
+    top: 5px;
+    right: 17px;
+  }
+
+  .undoIcon {
+    position: absolute;
+    top: 2px;
+    right: 17px;
+  }
+</ui:style>
+
+<g:HTMLPanel styleName='{style.panel}'>
+<div ui:field='normal' class='{style.normal} {style.content}'>
+  <div class='{style.header}'>
+    <table class='{style.headerTable}'><tr>
+      <td class='{style.headerText}'>
+        <span ui:field='sectionType'/>
+      </td>
+      <td width='100%'>
+        <my:ValueEditor
+            ui:field='name'
+            addStyleNames='{style.name}'
+            editTitle='Edit reference pattern'>
+          <ui:attribute name='editTitle'/>
+          <my:editor>
+            <my:RefPatternBox styleName='{style.nameEdit}'/>
+          </my:editor>
+        </my:ValueEditor>
+        <span ui:field='sectionName' class='{style.name}'/>
+      </td>
+    </tr></table>
+
+    <g:Anchor
+        ui:field='deleteSection'
+        href='javascript:void'
+        styleName='{style.deleteIcon} {res.css.deleteIcon}'
+        title='Delete this section (and nested rules)'>
+      <ui:attribute name='title'/>
+    </g:Anchor>
+  </div>
+
+  <g:FlowPanel
+      ui:field='permissionContainer'
+      styleName='{style.permissionList}'/>
+  <div ui:field='addContainer' class='{style.addContainer}'>
+    <g:ValueListBox
+        ui:field='permissionSelector'
+        styleName='{style.addSelector}' />
+  </div>
+</div>
+
+<div
+    ui:field='deleted'
+    class='{style.deleted} {res.css.deleted}'
+    style='display: none'>
+  <span ui:field='deletedName'/>
+  <g:Anchor
+      ui:field='undoDelete'
+      href='javascript:void'
+      styleName='{style.undoIcon} {res.css.undoIcon}'
+      title='Undo deletion'>
+    <ui:attribute name='title'/>
+  </g:Anchor>
+</div>
+</g:HTMLPanel>
+</ui:UiBinder>
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
new file mode 100644
index 0000000..936bfe5
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupInfoScreen.java
@@ -0,0 +1,469 @@
+// Copyright (C) 2008 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.admin;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.ui.AccountGroupSuggestOracle;
+import com.google.gerrit.client.ui.OnEditEnabler;
+import com.google.gerrit.client.ui.RPCSuggestOracle;
+import com.google.gerrit.client.ui.SmallHeading;
+import com.google.gerrit.common.data.GroupDetail;
+import com.google.gerrit.common.data.GroupOptions;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gwt.event.dom.client.ChangeEvent;
+import com.google.gwt.event.dom.client.ChangeHandler;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.dom.client.KeyPressHandler;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.CheckBox;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.Grid;
+import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
+import com.google.gwt.user.client.ui.Label;
+import com.google.gwt.user.client.ui.ListBox;
+import com.google.gwt.user.client.ui.Panel;
+import com.google.gwt.user.client.ui.SuggestBox;
+import com.google.gwt.user.client.ui.VerticalPanel;
+import com.google.gwtexpui.clippy.client.CopyableLabel;
+import com.google.gwtexpui.globalkey.client.NpTextArea;
+import com.google.gwtexpui.globalkey.client.NpTextBox;
+import com.google.gwtjsonrpc.common.VoidResult;
+
+import java.util.List;
+
+public class AccountGroupInfoScreen extends AccountGroupScreen {
+  private CopyableLabel groupUUIDLabel;
+
+  private NpTextBox groupNameTxt;
+  private Button saveName;
+
+  private NpTextBox ownerTxtBox;
+  private SuggestBox ownerTxt;
+  private Button saveOwner;
+
+  private NpTextArea descTxt;
+  private Button saveDesc;
+
+  private Label typeSystem;
+  private ListBox typeSelect;
+  private Button saveType;
+
+  private Panel externalPanel;
+  private Label externalName;
+  private NpTextBox externalNameFilter;
+  private Button externalNameSearch;
+  private Grid externalMatches;
+
+  private CheckBox visibleToAllCheckBox;
+  private Button saveGroupOptions;
+
+  public AccountGroupInfoScreen(final GroupDetail toShow, final String token) {
+    super(toShow, token);
+  }
+
+  @Override
+  protected void onInitUI() {
+    super.onInitUI();
+    initUUID();
+    initName();
+    initOwner();
+    initDescription();
+    initGroupOptions();
+    initGroupType();
+
+    initExternal();
+  }
+
+  private void enableForm(final boolean canModify) {
+    groupNameTxt.setEnabled(canModify);
+    ownerTxtBox.setEnabled(canModify);
+    descTxt.setEnabled(canModify);
+    typeSelect.setEnabled(canModify);
+    externalNameFilter.setEnabled(canModify);
+    externalNameSearch.setEnabled(canModify);
+    visibleToAllCheckBox.setEnabled(canModify);
+  }
+
+  private void initUUID() {
+    final VerticalPanel groupUUIDPanel = new VerticalPanel();
+    groupUUIDPanel.setStyleName(Gerrit.RESOURCES.css().groupUUIDPanel());
+    groupUUIDPanel.add(new SmallHeading(Util.C.headingGroupUUID()));
+    groupUUIDLabel = new CopyableLabel("");
+    groupUUIDPanel.add(groupUUIDLabel);
+    add(groupUUIDPanel);
+  }
+
+  private void initName() {
+    final VerticalPanel groupNamePanel = new VerticalPanel();
+    groupNamePanel.setStyleName(Gerrit.RESOURCES.css().groupNamePanel());
+    groupNameTxt = new NpTextBox();
+    groupNameTxt.setStyleName(Gerrit.RESOURCES.css().groupNameTextBox());
+    groupNameTxt.setVisibleLength(60);
+    groupNamePanel.add(groupNameTxt);
+
+    saveName = new Button(Util.C.buttonRenameGroup());
+    saveName.setEnabled(false);
+    saveName.addClickHandler(new ClickHandler() {
+      @Override
+      public void onClick(final ClickEvent event) {
+        final String newName = groupNameTxt.getText().trim();
+        Util.GROUP_SVC.renameGroup(getGroupId(), newName,
+            new GerritCallback<GroupDetail>() {
+              public void onSuccess(final GroupDetail groupDetail) {
+                saveName.setEnabled(false);
+                setPageTitle(Util.M.group(groupDetail.group.getName()));
+                display(groupDetail);
+              }
+            });
+      }
+    });
+    groupNamePanel.add(saveName);
+    add(groupNamePanel);
+
+    new OnEditEnabler(saveName, groupNameTxt);
+  }
+
+  private void initOwner() {
+    final VerticalPanel ownerPanel = new VerticalPanel();
+    ownerPanel.setStyleName(Gerrit.RESOURCES.css().groupOwnerPanel());
+    ownerPanel.add(new SmallHeading(Util.C.headingOwner()));
+
+    ownerTxtBox = new NpTextBox();
+    ownerTxtBox.setVisibleLength(60);
+    ownerTxt = new SuggestBox(new RPCSuggestOracle(
+        new AccountGroupSuggestOracle()), ownerTxtBox);
+    ownerTxt.setStyleName(Gerrit.RESOURCES.css().groupOwnerTextBox());
+    ownerPanel.add(ownerTxt);
+
+    saveOwner = new Button(Util.C.buttonChangeGroupOwner());
+    saveOwner.setEnabled(false);
+    saveOwner.addClickHandler(new ClickHandler() {
+      @Override
+      public void onClick(final ClickEvent event) {
+        final String newOwner = ownerTxt.getText().trim();
+        if (newOwner.length() > 0) {
+          Util.GROUP_SVC.changeGroupOwner(getGroupId(), newOwner,
+              new GerritCallback<VoidResult>() {
+                public void onSuccess(final VoidResult result) {
+                  saveOwner.setEnabled(false);
+                }
+              });
+        }
+      }
+    });
+    ownerPanel.add(saveOwner);
+    add(ownerPanel);
+
+    new OnEditEnabler(saveOwner, ownerTxtBox);
+  }
+
+  private void initDescription() {
+    final VerticalPanel vp = new VerticalPanel();
+    vp.setStyleName(Gerrit.RESOURCES.css().groupDescriptionPanel());
+    vp.add(new SmallHeading(Util.C.headingDescription()));
+
+    descTxt = new NpTextArea();
+    descTxt.setVisibleLines(6);
+    descTxt.setCharacterWidth(60);
+    vp.add(descTxt);
+
+    saveDesc = new Button(Util.C.buttonSaveDescription());
+    saveDesc.setEnabled(false);
+    saveDesc.addClickHandler(new ClickHandler() {
+      @Override
+      public void onClick(final ClickEvent event) {
+        final String txt = descTxt.getText().trim();
+        Util.GROUP_SVC.changeGroupDescription(getGroupId(), txt,
+            new GerritCallback<VoidResult>() {
+              public void onSuccess(final VoidResult result) {
+                saveDesc.setEnabled(false);
+              }
+            });
+      }
+    });
+    vp.add(saveDesc);
+    add(vp);
+
+    new OnEditEnabler(saveDesc, descTxt);
+  }
+
+  private void initGroupOptions() {
+    final VerticalPanel groupOptionsPanel = new VerticalPanel();
+
+    final VerticalPanel vp = new VerticalPanel();
+    vp.setStyleName(Gerrit.RESOURCES.css().groupOptionsPanel());
+    vp.add(new SmallHeading(Util.C.headingGroupOptions()));
+
+    visibleToAllCheckBox = new CheckBox(Util.C.isVisibleToAll());
+    vp.add(visibleToAllCheckBox);
+    groupOptionsPanel.add(vp);
+
+    saveGroupOptions = new Button(Util.C.buttonSaveGroupOptions());
+    saveGroupOptions.setEnabled(false);
+    saveGroupOptions.addClickHandler(new ClickHandler() {
+      @Override
+      public void onClick(final ClickEvent event) {
+        final GroupOptions groupOptions =
+            new GroupOptions(visibleToAllCheckBox.getValue());
+        Util.GROUP_SVC.changeGroupOptions(getGroupId(), groupOptions,
+            new GerritCallback<VoidResult>() {
+              public void onSuccess(final VoidResult result) {
+                saveGroupOptions.setEnabled(false);
+              }
+            });
+      }
+    });
+    groupOptionsPanel.add(saveGroupOptions);
+
+    add(groupOptionsPanel);
+
+    final OnEditEnabler enabler = new OnEditEnabler(saveGroupOptions);
+    enabler.listenTo(visibleToAllCheckBox);
+  }
+
+  private void initGroupType() {
+    typeSystem = new Label(Util.C.groupType_SYSTEM());
+
+    typeSelect = new ListBox();
+    typeSelect.setStyleName(Gerrit.RESOURCES.css().groupTypeSelectListBox());
+    typeSelect.addItem(Util.C.groupType_INTERNAL(), AccountGroup.Type.INTERNAL.name());
+    typeSelect.addItem(Util.C.groupType_LDAP(), AccountGroup.Type.LDAP.name());
+    typeSelect.addChangeHandler(new ChangeHandler() {
+      @Override
+      public void onChange(ChangeEvent event) {
+        saveType.setEnabled(true);
+      }
+    });
+
+    saveType = new Button(Util.C.buttonChangeGroupType());
+    saveType.setEnabled(false);
+    saveType.addClickHandler(new ClickHandler() {
+      @Override
+      public void onClick(ClickEvent event) {
+        onSaveType();
+      }
+    });
+
+    switch (Gerrit.getConfig().getAuthType()) {
+      case HTTP_LDAP:
+      case LDAP:
+      case LDAP_BIND:
+      case CLIENT_SSL_CERT_LDAP:
+        break;
+      default:
+        return;
+    }
+
+    final VerticalPanel fp = new VerticalPanel();
+    fp.setStyleName(Gerrit.RESOURCES.css().groupTypePanel());
+    fp.add(new SmallHeading(Util.C.headingGroupType()));
+    fp.add(typeSystem);
+    fp.add(typeSelect);
+    fp.add(saveType);
+    add(fp);
+  }
+
+  private void initExternal() {
+    externalName = new Label();
+
+    externalNameFilter = new NpTextBox();
+    externalNameFilter.setStyleName(Gerrit.RESOURCES.css()
+        .groupExternalNameFilterTextBox());
+    externalNameFilter.setVisibleLength(30);
+    externalNameFilter.addKeyPressHandler(new KeyPressHandler() {
+      @Override
+      public void onKeyPress(final KeyPressEvent event) {
+        if (event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER) {
+          doExternalSearch();
+        }
+      }
+    });
+
+    externalNameSearch = new Button(Gerrit.C.searchButton());
+    externalNameSearch.addClickHandler(new ClickHandler() {
+      @Override
+      public void onClick(ClickEvent event) {
+        doExternalSearch();
+      }
+    });
+
+    externalMatches = new Grid();
+    externalMatches.setStyleName(Gerrit.RESOURCES.css().infoTable());
+    externalMatches.setVisible(false);
+
+    final FlowPanel searchLine = new FlowPanel();
+    searchLine.add(externalNameFilter);
+    searchLine.add(externalNameSearch);
+
+    externalPanel = new VerticalPanel();
+    externalPanel.add(new SmallHeading(Util.C.headingExternalGroup()));
+    externalPanel.add(externalName);
+    externalPanel.add(searchLine);
+    externalPanel.add(externalMatches);
+    add(externalPanel);
+  }
+
+  private void setType(final AccountGroup.Type newType) {
+    final boolean system = newType == AccountGroup.Type.SYSTEM;
+
+    typeSystem.setVisible(system);
+    typeSelect.setVisible(!system);
+    saveType.setVisible(!system);
+    externalPanel.setVisible(newType == AccountGroup.Type.LDAP);
+    externalNameFilter.setText(groupNameTxt.getText());
+
+    if (!system) {
+      for (int i = 0; i < typeSelect.getItemCount(); i++) {
+        if (newType.name().equals(typeSelect.getValue(i))) {
+          typeSelect.setSelectedIndex(i);
+          break;
+        }
+      }
+    }
+
+    saveType.setEnabled(false);
+
+    setMembersTabVisible(newType == AccountGroup.Type.INTERNAL);
+  }
+
+  private void onSaveType() {
+    final int idx = typeSelect.getSelectedIndex();
+    final AccountGroup.Type newType =
+        AccountGroup.Type.valueOf(typeSelect.getValue(idx));
+
+    typeSelect.setEnabled(false);
+    saveType.setEnabled(false);
+
+    Util.GROUP_SVC.changeGroupType(getGroupId(), newType,
+        new GerritCallback<VoidResult>() {
+          @Override
+          public void onSuccess(VoidResult result) {
+            typeSelect.setEnabled(true);
+            setType(newType);
+          }
+
+          @Override
+          public void onFailure(Throwable caught) {
+            typeSelect.setEnabled(true);
+            saveType.setEnabled(true);
+            super.onFailure(caught);
+          }
+        });
+  }
+
+  private void doExternalSearch() {
+    externalNameFilter.setEnabled(false);
+    externalNameSearch.setEnabled(false);
+    Util.GROUP_SVC.searchExternalGroups(externalNameFilter.getText(),
+        new GerritCallback<List<AccountGroup.ExternalNameKey>>() {
+          @Override
+          public void onSuccess(List<AccountGroup.ExternalNameKey> result) {
+            final CellFormatter fmt = externalMatches.getCellFormatter();
+
+            if (result.isEmpty()) {
+              externalMatches.resize(1, 1);
+              externalMatches.setText(0, 0, Util.C.errorNoMatchingGroups());
+              fmt.setStyleName(0, 0, Gerrit.RESOURCES.css().header());
+              return;
+            }
+
+            externalMatches.resize(1 + result.size(), 2);
+
+            externalMatches.setText(0, 0, Util.C.columnGroupName());
+            externalMatches.setText(0, 1, "");
+            fmt.setStyleName(0, 0, Gerrit.RESOURCES.css().header());
+            fmt.setStyleName(0, 1, Gerrit.RESOURCES.css().header());
+
+            for (int row = 0; row < result.size(); row++) {
+              final AccountGroup.ExternalNameKey key = result.get(row);
+              final Button b = new Button(Util.C.buttonSelectGroup());
+              b.addClickHandler(new ClickHandler() {
+                @Override
+                public void onClick(ClickEvent event) {
+                  setExternalGroup(key);
+                }
+              });
+              externalMatches.setText(1 + row, 0, key.get());
+              externalMatches.setWidget(1 + row, 1, b);
+              fmt.setStyleName(1 + row, 1, Gerrit.RESOURCES.css().rightmost());
+            }
+            externalMatches.setVisible(true);
+
+            externalNameFilter.setEnabled(true);
+            externalNameSearch.setEnabled(true);
+          }
+
+          @Override
+          public void onFailure(Throwable caught) {
+            externalNameFilter.setEnabled(true);
+            externalNameSearch.setEnabled(true);
+            super.onFailure(caught);
+          }
+        });
+  }
+
+  private void setExternalGroup(final AccountGroup.ExternalNameKey key) {
+    externalMatches.setVisible(false);
+
+    Util.GROUP_SVC.changeExternalGroup(getGroupId(), key,
+        new GerritCallback<VoidResult>() {
+          @Override
+          public void onSuccess(VoidResult result) {
+            externalName.setText(key.get());
+          }
+
+          @Override
+          public void onFailure(Throwable caught) {
+            externalMatches.setVisible(true);
+            super.onFailure(caught);
+          }
+        });
+  }
+
+  @Override
+  protected void display(final GroupDetail groupDetail) {
+    final AccountGroup group = groupDetail.group;
+    groupUUIDLabel.setText(group.getGroupUUID().get());
+    groupNameTxt.setText(group.getName());
+    if (groupDetail.ownerGroup != null) {
+      ownerTxt.setText(groupDetail.ownerGroup.getName());
+    } else {
+      ownerTxt.setText(Util.M.deletedGroup(group.getOwnerGroupId().get()));
+    }
+    descTxt.setText(group.getDescription());
+
+    visibleToAllCheckBox.setValue(group.isVisibleToAll());
+
+    switch (group.getType()) {
+      case LDAP:
+        externalName.setText(group.getExternalNameKey() != null ? group
+            .getExternalNameKey().get() : Util.C.noGroupSelected());
+        break;
+    }
+
+    setType(group.getType());
+
+    enableForm(groupDetail.canModify);
+    saveName.setVisible(groupDetail.canModify);
+    saveOwner.setVisible(groupDetail.canModify);
+    saveDesc.setVisible(groupDetail.canModify);
+    saveGroupOptions.setVisible(groupDetail.canModify);
+    saveType.setVisible(groupDetail.canModify);
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java
new file mode 100644
index 0000000..b5cca86
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java
@@ -0,0 +1,380 @@
+// Copyright (C) 2008 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.admin;
+
+import com.google.gerrit.client.Dispatcher;
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.ui.AccountDashboardLink;
+import com.google.gerrit.client.ui.AccountGroupSuggestOracle;
+import com.google.gerrit.client.ui.AddMemberBox;
+import com.google.gerrit.client.ui.FancyFlexTable;
+import com.google.gerrit.client.ui.Hyperlink;
+import com.google.gerrit.client.ui.SmallHeading;
+import com.google.gerrit.common.data.AccountInfoCache;
+import com.google.gerrit.common.data.GroupDetail;
+import com.google.gerrit.common.data.GroupInfo;
+import com.google.gerrit.common.data.GroupInfoCache;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupInclude;
+import com.google.gerrit.reviewdb.client.AccountGroupMember;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.CheckBox;
+import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.Panel;
+import com.google.gwtjsonrpc.common.VoidResult;
+
+import java.util.HashSet;
+import java.util.List;
+
+public class AccountGroupMembersScreen extends AccountGroupScreen {
+
+  private AccountInfoCache accounts = AccountInfoCache.empty();
+  private GroupInfoCache groups = GroupInfoCache.empty();
+  private MemberTable members;
+  private IncludeTable includes;
+
+  private Panel memberPanel;
+  private AddMemberBox addMemberBox;
+  private Button delMember;
+
+  private Panel includePanel;
+  private AddMemberBox addIncludeBox;
+  private Button delInclude;
+
+  private FlowPanel noMembersInfo;
+
+  public AccountGroupMembersScreen(final GroupDetail toShow, final String token) {
+    super(toShow, token);
+  }
+
+  @Override
+  protected void onInitUI() {
+    super.onInitUI();
+    initMemberList();
+    initIncludeList();
+    initNoMembersInfo();
+  }
+
+  private void enableForm(final boolean canModify) {
+    addMemberBox.setEnabled(canModify);
+    members.setEnabled(canModify);
+    addIncludeBox.setEnabled(canModify);
+    includes.setEnabled(canModify);
+  }
+
+
+  private void initMemberList() {
+    addMemberBox = new AddMemberBox();
+
+    addMemberBox.addClickHandler(new ClickHandler() {
+      @Override
+      public void onClick(final ClickEvent event) {
+        doAddNewMember();
+      }
+    });
+
+    members = new MemberTable();
+    members.addStyleName(Gerrit.RESOURCES.css().groupMembersTable());
+
+    delMember = new Button(Util.C.buttonDeleteGroupMembers());
+    delMember.addClickHandler(new ClickHandler() {
+      @Override
+      public void onClick(final ClickEvent event) {
+        members.deleteChecked();
+      }
+    });
+
+    memberPanel = new FlowPanel();
+    memberPanel.add(new SmallHeading(Util.C.headingMembers()));
+    memberPanel.add(addMemberBox);
+    memberPanel.add(members);
+    memberPanel.add(delMember);
+    add(memberPanel);
+  }
+
+  private void initIncludeList() {
+    addIncludeBox =
+      new AddMemberBox(Util.C.buttonAddIncludedGroup(),
+          Util.C.defaultAccountGroupName(), new AccountGroupSuggestOracle());
+
+    addIncludeBox.addClickHandler(new ClickHandler() {
+      @Override
+      public void onClick(final ClickEvent event) {
+        doAddNewInclude();
+      }
+    });
+
+    includes = new IncludeTable();
+    includes.addStyleName(Gerrit.RESOURCES.css().groupIncludesTable());
+
+    delInclude = new Button(Util.C.buttonDeleteIncludedGroup());
+    delInclude.addClickHandler(new ClickHandler() {
+      @Override
+      public void onClick(final ClickEvent event) {
+        includes.deleteChecked();
+      }
+    });
+
+    includePanel = new FlowPanel();
+    includePanel.add(new SmallHeading(Util.C.headingIncludedGroups()));
+    includePanel.add(addIncludeBox);
+    includePanel.add(includes);
+    includePanel.add(delInclude);
+    add(includePanel);
+  }
+
+  private void initNoMembersInfo() {
+    noMembersInfo = new FlowPanel();
+    noMembersInfo.setVisible(false);
+    noMembersInfo.add(new SmallHeading(Util.C.noMembersInfo()));
+    add(noMembersInfo);
+  }
+
+  @Override
+  protected void display(final GroupDetail groupDetail) {
+    switch (groupDetail.group.getType()) {
+      case INTERNAL:
+        accounts = groupDetail.accounts;
+        groups = groupDetail.groups;
+        members.display(groupDetail.members);
+        includes.display(groupDetail.includes);
+        break;
+      default:
+        memberPanel.setVisible(false);
+        includePanel.setVisible(false);
+        noMembersInfo.setVisible(true);
+        break;
+    }
+
+    enableForm(groupDetail.canModify);
+    delMember.setVisible(groupDetail.canModify);
+    delInclude.setVisible(groupDetail.canModify);
+  }
+
+  void doAddNewMember() {
+    final String nameEmail = addMemberBox.getText();
+    if (nameEmail.length() == 0) {
+      return;
+    }
+
+    addMemberBox.setEnabled(false);
+    Util.GROUP_SVC.addGroupMember(getGroupId(), nameEmail,
+        new GerritCallback<GroupDetail>() {
+          public void onSuccess(final GroupDetail result) {
+            addMemberBox.setEnabled(true);
+            addMemberBox.setText("");
+            if (result.accounts != null && result.members != null) {
+              accounts.merge(result.accounts);
+              members.display(result.members);
+            }
+          }
+
+          @Override
+          public void onFailure(final Throwable caught) {
+            addMemberBox.setEnabled(true);
+            super.onFailure(caught);
+          }
+        });
+  }
+
+  void doAddNewInclude() {
+    final String groupName = addIncludeBox.getText();
+    if (groupName.length() == 0) {
+      return;
+    }
+
+    addIncludeBox.setEnabled(false);
+    Util.GROUP_SVC.addGroupInclude(getGroupId(), groupName,
+        new GerritCallback<GroupDetail>() {
+          public void onSuccess(final GroupDetail result) {
+            addIncludeBox.setEnabled(true);
+            addIncludeBox.setText("");
+            if (result.groups != null && result.includes != null) {
+              groups.merge(result.groups);
+              includes.display(result.includes);
+            }
+          }
+
+          @Override
+          public void onFailure(final Throwable caught) {
+            addIncludeBox.setEnabled(true);
+            super.onFailure(caught);
+          }
+        });
+  }
+
+  private class MemberTable extends FancyFlexTable<AccountGroupMember> {
+    private boolean enabled = true;
+
+    MemberTable() {
+      table.setText(0, 2, Util.C.columnMember());
+      table.setText(0, 3, Util.C.columnEmailAddress());
+
+      final FlexCellFormatter fmt = table.getFlexCellFormatter();
+      fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().iconHeader());
+      fmt.addStyleName(0, 2, Gerrit.RESOURCES.css().dataHeader());
+      fmt.addStyleName(0, 3, Gerrit.RESOURCES.css().dataHeader());
+    }
+
+    void setEnabled(final boolean enabled) {
+      this.enabled = enabled;
+      for (int row = 1; row < table.getRowCount(); row++) {
+        final AccountGroupMember k = getRowItem(row);
+        if (k != null) {
+          ((CheckBox) table.getWidget(row, 1)).setEnabled(enabled);
+        }
+      }
+    }
+
+    void deleteChecked() {
+      final HashSet<AccountGroupMember.Key> ids =
+          new HashSet<AccountGroupMember.Key>();
+      for (int row = 1; row < table.getRowCount(); row++) {
+        final AccountGroupMember k = getRowItem(row);
+        if (k != null && ((CheckBox) table.getWidget(row, 1)).getValue()) {
+          ids.add(k.getKey());
+        }
+      }
+      if (!ids.isEmpty()) {
+        Util.GROUP_SVC.deleteGroupMembers(getGroupId(), ids,
+            new GerritCallback<VoidResult>() {
+              public void onSuccess(final VoidResult result) {
+                for (int row = 1; row < table.getRowCount();) {
+                  final AccountGroupMember k = getRowItem(row);
+                  if (k != null && ids.contains(k.getKey())) {
+                    table.removeRow(row);
+                  } else {
+                    row++;
+                  }
+                }
+              }
+            });
+      }
+    }
+
+    void display(final List<AccountGroupMember> result) {
+      while (1 < table.getRowCount())
+        table.removeRow(table.getRowCount() - 1);
+
+      for (final AccountGroupMember k : result) {
+        final int row = table.getRowCount();
+        table.insertRow(row);
+        applyDataRowStyle(row);
+        populate(row, k);
+      }
+    }
+
+    void populate(final int row, final AccountGroupMember k) {
+      final Account.Id accountId = k.getAccountId();
+      CheckBox checkBox = new CheckBox();
+      table.setWidget(row, 1, checkBox);
+      checkBox.setEnabled(enabled);
+      table.setWidget(row, 2, AccountDashboardLink.link(accounts, accountId));
+      table.setText(row, 3, accounts.get(accountId).getPreferredEmail());
+
+      final FlexCellFormatter fmt = table.getFlexCellFormatter();
+      fmt.addStyleName(row, 1, Gerrit.RESOURCES.css().iconCell());
+      fmt.addStyleName(row, 2, Gerrit.RESOURCES.css().dataCell());
+      fmt.addStyleName(row, 3, Gerrit.RESOURCES.css().dataCell());
+
+      setRowItem(row, k);
+    }
+  }
+
+  private class IncludeTable extends FancyFlexTable<AccountGroupInclude> {
+    private boolean enabled = true;
+
+    IncludeTable() {
+      table.setText(0, 2, Util.C.columnGroupName());
+      table.setText(0, 3, Util.C.columnGroupDescription());
+
+      final FlexCellFormatter fmt = table.getFlexCellFormatter();
+      fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().iconHeader());
+      fmt.addStyleName(0, 2, Gerrit.RESOURCES.css().dataHeader());
+      fmt.addStyleName(0, 3, Gerrit.RESOURCES.css().dataHeader());
+    }
+
+    void setEnabled(final boolean enabled) {
+      this.enabled = enabled;
+      for (int row = 1; row < table.getRowCount(); row++) {
+        final AccountGroupInclude k = getRowItem(row);
+        if (k != null) {
+          ((CheckBox) table.getWidget(row, 1)).setEnabled(enabled);
+        }
+      }
+    }
+
+    void deleteChecked() {
+      final HashSet<AccountGroupInclude.Key> keys =
+          new HashSet<AccountGroupInclude.Key>();
+      for (int row = 1; row < table.getRowCount(); row++) {
+        final AccountGroupInclude k = getRowItem(row);
+        if (k != null && ((CheckBox) table.getWidget(row, 1)).getValue()) {
+          keys.add(k.getKey());
+        }
+      }
+      if (!keys.isEmpty()) {
+        Util.GROUP_SVC.deleteGroupIncludes(getGroupId(), keys,
+            new GerritCallback<VoidResult>() {
+              public void onSuccess(final VoidResult result) {
+                for (int row = 1; row < table.getRowCount();) {
+                  final AccountGroupInclude k = getRowItem(row);
+                  if (k != null && keys.contains(k.getKey())) {
+                    table.removeRow(row);
+                  } else {
+                    row++;
+                  }
+                }
+              }
+            });
+      }
+    }
+
+    void display(final List<AccountGroupInclude> result) {
+      while (1 < table.getRowCount())
+        table.removeRow(table.getRowCount() - 1);
+
+      for (final AccountGroupInclude k : result) {
+        final int row = table.getRowCount();
+        table.insertRow(row);
+        applyDataRowStyle(row);
+        populate(row, k);
+      }
+    }
+
+    void populate(final int row, final AccountGroupInclude k) {
+      AccountGroup.Id id = k.getIncludeId();
+      GroupInfo group = groups.get(id);
+      CheckBox checkBox = new CheckBox();
+      table.setWidget(row, 1, checkBox);
+      checkBox.setEnabled(enabled);
+      table.setWidget(row, 2,
+          new Hyperlink(group.getName(), Dispatcher.toGroup(id)));
+      table.setText(row, 3, groups.get(id).getDescription());
+
+      final FlexCellFormatter fmt = table.getFlexCellFormatter();
+      fmt.addStyleName(row, 1, Gerrit.RESOURCES.css().iconCell());
+      fmt.addStyleName(row, 2, Gerrit.RESOURCES.css().dataCell());
+      fmt.addStyleName(row, 3, Gerrit.RESOURCES.css().dataCell());
+
+      setRowItem(row, k);
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupScreen.java
index 593ab34..9ac6c9a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupScreen.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2008 The Android Open Source Project
+// 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.
@@ -14,763 +14,53 @@
 
 package com.google.gerrit.client.admin;
 
-import com.google.gerrit.client.Dispatcher;
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.rpc.ScreenLoadCallback;
-import com.google.gerrit.client.ui.AccountDashboardLink;
-import com.google.gerrit.client.ui.AccountGroupSuggestOracle;
-import com.google.gerrit.client.ui.AccountScreen;
-import com.google.gerrit.client.ui.AddMemberBox;
-import com.google.gerrit.client.ui.AddIncludedGroupBox;
-import com.google.gerrit.client.ui.FancyFlexTable;
-import com.google.gerrit.client.ui.Hyperlink;
-import com.google.gerrit.client.ui.OnEditEnabler;
-import com.google.gerrit.client.ui.RPCSuggestOracle;
-import com.google.gerrit.client.ui.SmallHeading;
-import com.google.gerrit.common.data.AccountInfoCache;
+import static com.google.gerrit.client.Dispatcher.toGroup;
+
+import com.google.gerrit.client.ui.MenuScreen;
 import com.google.gerrit.common.data.GroupDetail;
-import com.google.gerrit.common.data.GroupOptions;
-import com.google.gerrit.common.data.GroupInfo;
-import com.google.gerrit.common.data.GroupInfoCache;
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.AccountGroupInclude;
-import com.google.gerrit.reviewdb.AccountGroupMember;
-import com.google.gwt.event.dom.client.ChangeEvent;
-import com.google.gwt.event.dom.client.ChangeHandler;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.dom.client.KeyCodes;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.event.dom.client.KeyPressHandler;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.CheckBox;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.Grid;
-import com.google.gwt.user.client.ui.Label;
-import com.google.gwt.user.client.ui.ListBox;
-import com.google.gwt.user.client.ui.Panel;
-import com.google.gwt.user.client.ui.SuggestBox;
-import com.google.gwt.user.client.ui.VerticalPanel;
-import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
-import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
-import com.google.gwtexpui.globalkey.client.NpTextArea;
-import com.google.gwtexpui.globalkey.client.NpTextBox;
-import com.google.gwtjsonrpc.client.VoidResult;
+import com.google.gerrit.reviewdb.client.AccountGroup;
 
-import java.util.HashSet;
-import java.util.List;
+public abstract class AccountGroupScreen extends MenuScreen {
+  public static final String INFO = "info";
+  public static final String MEMBERS = "members";
 
-public class AccountGroupScreen extends AccountScreen {
-  private final AccountGroup.Id groupId;
-  private AccountInfoCache accounts = AccountInfoCache.empty();
-  private GroupInfoCache groups = GroupInfoCache.empty();
-  private MemberTable members;
-  private IncludeTable includes;
+  private final GroupDetail groupDetail;
+  private final String membersTabToken;
 
-  private NpTextBox groupNameTxt;
-  private Button saveName;
+  public AccountGroupScreen(final GroupDetail toShow, final String token) {
+    setRequiresSignIn(true);
 
-  private NpTextBox ownerTxtBox;
-  private SuggestBox ownerTxt;
-  private Button saveOwner;
+    this.groupDetail = toShow;
+    this.membersTabToken = getTabToken(token, MEMBERS);
 
-  private NpTextArea descTxt;
-  private Button saveDesc;
+    link(Util.C.groupTabGeneral(), getTabToken(token, INFO));
+    link(Util.C.groupTabMembers(), membersTabToken,
+        groupDetail.group.getType() == AccountGroup.Type.INTERNAL);
+  }
 
-  private Label typeSystem;
-  private ListBox typeSelect;
-  private Button saveType;
-
-  private Panel memberPanel;
-  private AddMemberBox addMemberBox;
-  private Button delMember;
-
-  private Panel includePanel;
-  private AddIncludedGroupBox addIncludeBox;
-  private Button delInclude;
-
-  private Panel externalPanel;
-  private Label externalName;
-  private NpTextBox externalNameFilter;
-  private Button externalNameSearch;
-  private Grid externalMatches;
-
-  private Panel groupOptionsPanel;
-  private CheckBox visibleToAllCheckBox;
-  private CheckBox emailOnlyAuthors;
-  private Button saveGroupOptions;
-
-  public AccountGroupScreen(final AccountGroup.Id toShow) {
-    groupId = toShow;
+  private String getTabToken(final String token, final String tab) {
+    if (token.startsWith("/admin/groups/uuid-")) {
+      return toGroup(groupDetail.group.getGroupUUID(), tab);
+    } else {
+      return toGroup(groupDetail.group.getId(), tab);
+    }
   }
 
   @Override
   protected void onLoad() {
     super.onLoad();
-    Util.GROUP_SVC.groupDetail(groupId, new ScreenLoadCallback<GroupDetail>(
-        this) {
-      @Override
-      protected void preDisplay(final GroupDetail result) {
-        enableForm(result.canModify);
-        saveName.setVisible(result.canModify);
-        saveOwner.setVisible(result.canModify);
-        saveDesc.setVisible(result.canModify);
-        saveGroupOptions.setVisible(result.canModify);
-        delMember.setVisible(result.canModify);
-        saveType.setVisible(result.canModify);
-        delInclude.setVisible(result.canModify);
-        display(result);
-      }
-    });
+    setPageTitle(Util.M.group(groupDetail.group.getName()));
+    display();
+    display(groupDetail);
   }
 
-  @Override
-  protected void onInitUI() {
-    super.onInitUI();
-    initName();
-    initOwner();
-    initDescription();
-    initGroupOptions();
-    initGroupType();
+  protected abstract void display(final GroupDetail groupDetail);
 
-    initMemberList();
-    initIncludeList();
-    initExternal();
+  protected AccountGroup.Id getGroupId() {
+    return groupDetail.group.getId();
   }
 
-  private void enableForm(final boolean canModify) {
-    groupNameTxt.setEnabled(canModify);
-    ownerTxtBox.setEnabled(canModify);
-    descTxt.setEnabled(canModify);
-    typeSelect.setEnabled(canModify);
-    addMemberBox.setEnabled(canModify);
-    members.setEnabled(canModify);
-    externalNameFilter.setEnabled(canModify);
-    externalNameSearch.setEnabled(canModify);
-    visibleToAllCheckBox.setEnabled(canModify);
-    emailOnlyAuthors.setEnabled(canModify);
-    addIncludeBox.setEnabled(canModify);
-    includes.setEnabled(canModify);
-  }
-
-  private void initName() {
-    final VerticalPanel groupNamePanel = new VerticalPanel();
-    groupNameTxt = new NpTextBox();
-    groupNameTxt.setVisibleLength(60);
-    groupNamePanel.add(groupNameTxt);
-
-    saveName = new Button(Util.C.buttonRenameGroup());
-    saveName.setEnabled(false);
-    saveName.addClickHandler(new ClickHandler() {
-      @Override
-      public void onClick(final ClickEvent event) {
-        final String newName = groupNameTxt.getText().trim();
-        Util.GROUP_SVC.renameGroup(groupId, newName,
-            new GerritCallback<GroupDetail>() {
-              public void onSuccess(final GroupDetail groupDetail) {
-                saveName.setEnabled(false);
-                display(groupDetail);
-              }
-            });
-      }
-    });
-    groupNamePanel.add(saveName);
-    add(groupNamePanel);
-
-    new OnEditEnabler(saveName, groupNameTxt);
-  }
-
-  private void initOwner() {
-    final VerticalPanel ownerPanel = new VerticalPanel();
-    ownerPanel.add(new SmallHeading(Util.C.headingOwner()));
-
-    ownerTxtBox = new NpTextBox();
-    ownerTxtBox.setVisibleLength(60);
-    ownerTxt = new SuggestBox(new RPCSuggestOracle(
-        new AccountGroupSuggestOracle()), ownerTxtBox);
-    ownerPanel.add(ownerTxt);
-
-    saveOwner = new Button(Util.C.buttonChangeGroupOwner());
-    saveOwner.setEnabled(false);
-    saveOwner.addClickHandler(new ClickHandler() {
-      @Override
-      public void onClick(final ClickEvent event) {
-        final String newOwner = ownerTxt.getText().trim();
-        if (newOwner.length() > 0) {
-          Util.GROUP_SVC.changeGroupOwner(groupId, newOwner,
-              new GerritCallback<VoidResult>() {
-                public void onSuccess(final VoidResult result) {
-                  saveOwner.setEnabled(false);
-                }
-              });
-        }
-      }
-    });
-    ownerPanel.add(saveOwner);
-    add(ownerPanel);
-
-    new OnEditEnabler(saveOwner, ownerTxtBox);
-  }
-
-  private void initDescription() {
-    final VerticalPanel vp = new VerticalPanel();
-    vp.add(new SmallHeading(Util.C.headingDescription()));
-
-    descTxt = new NpTextArea();
-    descTxt.setVisibleLines(6);
-    descTxt.setCharacterWidth(60);
-    vp.add(descTxt);
-
-    saveDesc = new Button(Util.C.buttonSaveDescription());
-    saveDesc.setEnabled(false);
-    saveDesc.addClickHandler(new ClickHandler() {
-      @Override
-      public void onClick(final ClickEvent event) {
-        final String txt = descTxt.getText().trim();
-        Util.GROUP_SVC.changeGroupDescription(groupId, txt,
-            new GerritCallback<VoidResult>() {
-              public void onSuccess(final VoidResult result) {
-                saveDesc.setEnabled(false);
-              }
-            });
-      }
-    });
-    vp.add(saveDesc);
-    add(vp);
-
-    new OnEditEnabler(saveDesc, descTxt);
-  }
-
-  private void initGroupOptions() {
-    groupOptionsPanel = new VerticalPanel();
-    groupOptionsPanel.add(new SmallHeading(Util.C.headingGroupOptions()));
-
-    visibleToAllCheckBox = new CheckBox(Util.C.isVisibleToAll());
-    groupOptionsPanel.add(visibleToAllCheckBox);
-
-    emailOnlyAuthors = new CheckBox(Util.C.emailOnlyAuthors());
-
-    final VerticalPanel vp = new VerticalPanel();
-    vp.add(new Label(Util.C.descriptionNotifications()));
-    vp.add(emailOnlyAuthors);
-    groupOptionsPanel.add(vp);
-
-    saveGroupOptions = new Button(Util.C.buttonSaveGroupOptions());
-    saveGroupOptions.setEnabled(false);
-    saveGroupOptions.addClickHandler(new ClickHandler() {
-      @Override
-      public void onClick(final ClickEvent event) {
-        final GroupOptions groupOptions =
-            new GroupOptions(visibleToAllCheckBox.getValue(),
-              emailOnlyAuthors.getValue());
-        Util.GROUP_SVC.changeGroupOptions(groupId, groupOptions,
-            new GerritCallback<VoidResult>() {
-              public void onSuccess(final VoidResult result) {
-                saveGroupOptions.setEnabled(false);
-              }
-            });
-      }
-    });
-    groupOptionsPanel.add(saveGroupOptions);
-
-    add(groupOptionsPanel);
-
-    final OnEditEnabler enabler = new OnEditEnabler(saveGroupOptions);
-    enabler.listenTo(visibleToAllCheckBox);
-    enabler.listenTo(emailOnlyAuthors);
-  }
-
-  private void initGroupType() {
-    typeSystem = new Label(Util.C.groupType_SYSTEM());
-
-    typeSelect = new ListBox();
-    typeSelect.addItem(Util.C.groupType_INTERNAL(), AccountGroup.Type.INTERNAL.name());
-    typeSelect.addItem(Util.C.groupType_LDAP(), AccountGroup.Type.LDAP.name());
-    typeSelect.addChangeHandler(new ChangeHandler() {
-      @Override
-      public void onChange(ChangeEvent event) {
-        saveType.setEnabled(true);
-      }
-    });
-
-    saveType = new Button(Util.C.buttonChangeGroupType());
-    saveType.setEnabled(false);
-    saveType.addClickHandler(new ClickHandler() {
-      @Override
-      public void onClick(ClickEvent event) {
-        onSaveType();
-      }
-    });
-
-    switch (Gerrit.getConfig().getAuthType()) {
-      case HTTP_LDAP:
-      case LDAP:
-      case LDAP_BIND:
-      case CLIENT_SSL_CERT_LDAP:
-        break;
-      default:
-        return;
-    }
-
-    final VerticalPanel fp = new VerticalPanel();
-    fp.add(new SmallHeading(Util.C.headingGroupType()));
-    fp.add(typeSystem);
-    fp.add(typeSelect);
-    fp.add(saveType);
-    add(fp);
-  }
-
-  private void initMemberList() {
-    addMemberBox = new AddMemberBox();
-
-    addMemberBox.addClickHandler(new ClickHandler() {
-      @Override
-      public void onClick(final ClickEvent event) {
-        doAddNewMember();
-      }
-    });
-
-    members = new MemberTable();
-
-    delMember = new Button(Util.C.buttonDeleteGroupMembers());
-    delMember.addClickHandler(new ClickHandler() {
-      @Override
-      public void onClick(final ClickEvent event) {
-        members.deleteChecked();
-      }
-    });
-
-    memberPanel = new FlowPanel();
-    memberPanel.add(new SmallHeading(Util.C.headingMembers()));
-    memberPanel.add(addMemberBox);
-    memberPanel.add(members);
-    memberPanel.add(delMember);
-    add(memberPanel);
-  }
-
-  private void initIncludeList() {
-    addIncludeBox = new AddIncludedGroupBox();
-
-    addIncludeBox.addClickHandler(new ClickHandler() {
-      @Override
-      public void onClick(final ClickEvent event) {
-        doAddNewInclude();
-      }
-    });
-
-    includes = new IncludeTable();
-
-    delInclude = new Button(Util.C.buttonDeleteIncludedGroup());
-    delInclude.addClickHandler(new ClickHandler() {
-      @Override
-      public void onClick(final ClickEvent event) {
-        includes.deleteChecked();
-      }
-    });
-
-    includePanel = new FlowPanel();
-    includePanel.add(new SmallHeading(Util.C.headingIncludedGroups()));
-    includePanel.add(addIncludeBox);
-    includePanel.add(includes);
-    includePanel.add(delInclude);
-    add(includePanel);
-  }
-
-  private void initExternal() {
-    externalName = new Label();
-
-    externalNameFilter = new NpTextBox();
-    externalNameFilter.setVisibleLength(30);
-    externalNameFilter.addKeyPressHandler(new KeyPressHandler() {
-      @Override
-      public void onKeyPress(final KeyPressEvent event) {
-        if (event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER) {
-          doExternalSearch();
-        }
-      }
-    });
-
-    externalNameSearch = new Button(Gerrit.C.searchButton());
-    externalNameSearch.addClickHandler(new ClickHandler() {
-      @Override
-      public void onClick(ClickEvent event) {
-        doExternalSearch();
-      }
-    });
-
-    externalMatches = new Grid();
-    externalMatches.setStyleName(Gerrit.RESOURCES.css().infoTable());
-    externalMatches.setVisible(false);
-
-    final FlowPanel searchLine = new FlowPanel();
-    searchLine.add(externalNameFilter);
-    searchLine.add(externalNameSearch);
-
-    externalPanel = new VerticalPanel();
-    externalPanel.add(new SmallHeading(Util.C.headingExternalGroup()));
-    externalPanel.add(externalName);
-    externalPanel.add(searchLine);
-    externalPanel.add(externalMatches);
-    add(externalPanel);
-  }
-
-  private void setType(final AccountGroup.Type newType) {
-    final boolean system = newType == AccountGroup.Type.SYSTEM;
-
-    typeSystem.setVisible(system);
-    typeSelect.setVisible(!system);
-    saveType.setVisible(!system);
-    memberPanel.setVisible(newType == AccountGroup.Type.INTERNAL);
-    includePanel.setVisible(newType == AccountGroup.Type.INTERNAL);
-    externalPanel.setVisible(newType == AccountGroup.Type.LDAP);
-    externalNameFilter.setText(groupNameTxt.getText());
-
-    if (!system) {
-      for (int i = 0; i < typeSelect.getItemCount(); i++) {
-        if (newType.name().equals(typeSelect.getValue(i))) {
-          typeSelect.setSelectedIndex(i);
-          break;
-        }
-      }
-    }
-
-    saveType.setEnabled(false);
-  }
-
-  private void onSaveType() {
-    final int idx = typeSelect.getSelectedIndex();
-    final AccountGroup.Type newType =
-        AccountGroup.Type.valueOf(typeSelect.getValue(idx));
-
-    typeSelect.setEnabled(false);
-    saveType.setEnabled(false);
-
-    Util.GROUP_SVC.changeGroupType(groupId, newType,
-        new GerritCallback<VoidResult>() {
-          @Override
-          public void onSuccess(VoidResult result) {
-            typeSelect.setEnabled(true);
-            setType(newType);
-          }
-
-          @Override
-          public void onFailure(Throwable caught) {
-            typeSelect.setEnabled(true);
-            saveType.setEnabled(true);
-            super.onFailure(caught);
-          }
-        });
-  }
-
-  private void doExternalSearch() {
-    externalNameFilter.setEnabled(false);
-    externalNameSearch.setEnabled(false);
-    Util.GROUP_SVC.searchExternalGroups(externalNameFilter.getText(),
-        new GerritCallback<List<AccountGroup.ExternalNameKey>>() {
-          @Override
-          public void onSuccess(List<AccountGroup.ExternalNameKey> result) {
-            final CellFormatter fmt = externalMatches.getCellFormatter();
-
-            if (result.isEmpty()) {
-              externalMatches.resize(1, 1);
-              externalMatches.setText(0, 0, Util.C.errorNoMatchingGroups());
-              fmt.setStyleName(0, 0, Gerrit.RESOURCES.css().header());
-              return;
-            }
-
-            externalMatches.resize(1 + result.size(), 2);
-
-            externalMatches.setText(0, 0, Util.C.columnGroupName());
-            externalMatches.setText(0, 1, "");
-            fmt.setStyleName(0, 0, Gerrit.RESOURCES.css().header());
-            fmt.setStyleName(0, 1, Gerrit.RESOURCES.css().header());
-
-            for (int row = 0; row < result.size(); row++) {
-              final AccountGroup.ExternalNameKey key = result.get(row);
-              final Button b = new Button(Util.C.buttonSelectGroup());
-              b.addClickHandler(new ClickHandler() {
-                @Override
-                public void onClick(ClickEvent event) {
-                  setExternalGroup(key);
-                }
-              });
-              externalMatches.setText(1 + row, 0, key.get());
-              externalMatches.setWidget(1 + row, 1, b);
-              fmt.setStyleName(1 + row, 1, Gerrit.RESOURCES.css().rightmost());
-            }
-            externalMatches.setVisible(true);
-
-            externalNameFilter.setEnabled(true);
-            externalNameSearch.setEnabled(true);
-          }
-
-          @Override
-          public void onFailure(Throwable caught) {
-            externalNameFilter.setEnabled(true);
-            externalNameSearch.setEnabled(true);
-            super.onFailure(caught);
-          }
-        });
-  }
-
-  private void setExternalGroup(final AccountGroup.ExternalNameKey key) {
-    externalMatches.setVisible(false);
-
-    Util.GROUP_SVC.changeExternalGroup(groupId, key,
-        new GerritCallback<VoidResult>() {
-          @Override
-          public void onSuccess(VoidResult result) {
-            externalName.setText(key.get());
-          }
-
-          @Override
-          public void onFailure(Throwable caught) {
-            externalMatches.setVisible(true);
-            super.onFailure(caught);
-          }
-        });
-  }
-
-  private void display(final GroupDetail result) {
-    final AccountGroup group = result.group;
-    setPageTitle(Util.M.group(group.getName()));
-    groupNameTxt.setText(group.getName());
-    if (result.ownerGroup != null) {
-      ownerTxt.setText(result.ownerGroup.getName());
-    } else {
-      ownerTxt.setText(Util.M.deletedGroup(group.getOwnerGroupId().get()));
-    }
-    descTxt.setText(group.getDescription());
-
-    visibleToAllCheckBox.setValue(group.isVisibleToAll());
-    emailOnlyAuthors.setValue(group.isEmailOnlyAuthors());
-
-    switch (group.getType()) {
-      case INTERNAL:
-        accounts = result.accounts;
-        groups = result.groups;
-        members.display(result.members);
-        includes.display(result.includes);
-        break;
-
-      case LDAP:
-        externalName.setText(group.getExternalNameKey() != null ? group
-            .getExternalNameKey().get() : Util.C.noGroupSelected());
-        break;
-    }
-
-    setType(group.getType());
-  }
-
-  void doAddNewMember() {
-    final String nameEmail = addMemberBox.getText();
-    if (nameEmail.length() == 0) {
-      return;
-    }
-
-    addMemberBox.setEnabled(false);
-    Util.GROUP_SVC.addGroupMember(groupId, nameEmail,
-        new GerritCallback<GroupDetail>() {
-          public void onSuccess(final GroupDetail result) {
-            addMemberBox.setEnabled(true);
-            addMemberBox.setText("");
-            if (result.accounts != null && result.members != null) {
-              accounts.merge(result.accounts);
-              members.display(result.members);
-            }
-          }
-
-          @Override
-          public void onFailure(final Throwable caught) {
-            addMemberBox.setEnabled(true);
-            super.onFailure(caught);
-          }
-        });
-  }
-
-  void doAddNewInclude() {
-    final String groupName = addIncludeBox.getText();
-    if (groupName.length() == 0) {
-      return;
-    }
-
-    addIncludeBox.setEnabled(false);
-    Util.GROUP_SVC.addGroupInclude(groupId, groupName,
-        new GerritCallback<GroupDetail>() {
-          public void onSuccess(final GroupDetail result) {
-            addIncludeBox.setEnabled(true);
-            addIncludeBox.setText("");
-            if (result.groups != null && result.includes != null) {
-              groups.merge(result.groups);
-              includes.display(result.includes);
-            }
-          }
-
-          @Override
-          public void onFailure(final Throwable caught) {
-            addIncludeBox.setEnabled(true);
-            super.onFailure(caught);
-          }
-        });
-  }
-
-  private class MemberTable extends FancyFlexTable<AccountGroupMember> {
-    private boolean enabled = true;
-
-    MemberTable() {
-      table.setText(0, 2, Util.C.columnMember());
-      table.setText(0, 3, Util.C.columnEmailAddress());
-
-      final FlexCellFormatter fmt = table.getFlexCellFormatter();
-      fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().iconHeader());
-      fmt.addStyleName(0, 2, Gerrit.RESOURCES.css().dataHeader());
-      fmt.addStyleName(0, 3, Gerrit.RESOURCES.css().dataHeader());
-    }
-
-    void setEnabled(final boolean enabled) {
-      this.enabled = enabled;
-      for (int row = 1; row < table.getRowCount(); row++) {
-        final AccountGroupMember k = getRowItem(row);
-        if (k != null) {
-          ((CheckBox) table.getWidget(row, 1)).setEnabled(enabled);
-        }
-      }
-    }
-
-    void deleteChecked() {
-      final HashSet<AccountGroupMember.Key> ids =
-          new HashSet<AccountGroupMember.Key>();
-      for (int row = 1; row < table.getRowCount(); row++) {
-        final AccountGroupMember k = getRowItem(row);
-        if (k != null && ((CheckBox) table.getWidget(row, 1)).getValue()) {
-          ids.add(k.getKey());
-        }
-      }
-      if (!ids.isEmpty()) {
-        Util.GROUP_SVC.deleteGroupMembers(groupId, ids,
-            new GerritCallback<VoidResult>() {
-              public void onSuccess(final VoidResult result) {
-                for (int row = 1; row < table.getRowCount();) {
-                  final AccountGroupMember k = getRowItem(row);
-                  if (k != null && ids.contains(k.getKey())) {
-                    table.removeRow(row);
-                  } else {
-                    row++;
-                  }
-                }
-              }
-            });
-      }
-    }
-
-    void display(final List<AccountGroupMember> result) {
-      while (1 < table.getRowCount())
-        table.removeRow(table.getRowCount() - 1);
-
-      for (final AccountGroupMember k : result) {
-        final int row = table.getRowCount();
-        table.insertRow(row);
-        applyDataRowStyle(row);
-        populate(row, k);
-      }
-    }
-
-    void populate(final int row, final AccountGroupMember k) {
-      final Account.Id accountId = k.getAccountId();
-      CheckBox checkBox = new CheckBox();
-      table.setWidget(row, 1, checkBox);
-      checkBox.setEnabled(enabled);
-      table.setWidget(row, 2, AccountDashboardLink.link(accounts, accountId));
-      table.setText(row, 3, accounts.get(accountId).getPreferredEmail());
-
-      final FlexCellFormatter fmt = table.getFlexCellFormatter();
-      fmt.addStyleName(row, 1, Gerrit.RESOURCES.css().iconCell());
-      fmt.addStyleName(row, 2, Gerrit.RESOURCES.css().dataCell());
-      fmt.addStyleName(row, 3, Gerrit.RESOURCES.css().dataCell());
-
-      setRowItem(row, k);
-    }
-  }
-
-  private class IncludeTable extends FancyFlexTable<AccountGroupInclude> {
-    private boolean enabled = true;
-
-    IncludeTable() {
-      table.setText(0, 2, Util.C.columnGroupName());
-      table.setText(0, 3, Util.C.columnGroupDescription());
-
-      final FlexCellFormatter fmt = table.getFlexCellFormatter();
-      fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().iconHeader());
-      fmt.addStyleName(0, 2, Gerrit.RESOURCES.css().dataHeader());
-      fmt.addStyleName(0, 3, Gerrit.RESOURCES.css().dataHeader());
-    }
-
-    void setEnabled(final boolean enabled) {
-      this.enabled = enabled;
-      for (int row = 1; row < table.getRowCount(); row++) {
-        final AccountGroupInclude k = getRowItem(row);
-        if (k != null) {
-          ((CheckBox) table.getWidget(row, 1)).setEnabled(enabled);
-        }
-      }
-    }
-
-    void deleteChecked() {
-      final HashSet<AccountGroupInclude.Key> keys =
-          new HashSet<AccountGroupInclude.Key>();
-      for (int row = 1; row < table.getRowCount(); row++) {
-        final AccountGroupInclude k = getRowItem(row);
-        if (k != null && ((CheckBox) table.getWidget(row, 1)).getValue()) {
-          keys.add(k.getKey());
-        }
-      }
-      if (!keys.isEmpty()) {
-        Util.GROUP_SVC.deleteGroupIncludes(groupId, keys,
-            new GerritCallback<VoidResult>() {
-              public void onSuccess(final VoidResult result) {
-                for (int row = 1; row < table.getRowCount();) {
-                  final AccountGroupInclude k = getRowItem(row);
-                  if (k != null && keys.contains(k.getKey())) {
-                    table.removeRow(row);
-                  } else {
-                    row++;
-                  }
-                }
-              }
-            });
-      }
-    }
-
-    void display(final List<AccountGroupInclude> result) {
-      while (1 < table.getRowCount())
-        table.removeRow(table.getRowCount() - 1);
-
-      for (final AccountGroupInclude k : result) {
-        final int row = table.getRowCount();
-        table.insertRow(row);
-        applyDataRowStyle(row);
-        populate(row, k);
-      }
-    }
-
-    void populate(final int row, final AccountGroupInclude k) {
-      AccountGroup.Id id = k.getIncludeId();
-      GroupInfo group = groups.get(id);
-      CheckBox checkBox = new CheckBox();
-      table.setWidget(row, 1, checkBox);
-      checkBox.setEnabled(enabled);
-      table.setWidget(row, 2, new Hyperlink(group.getName(), Dispatcher
-          .toAccountGroup(id)));
-      table.setText(row, 3, groups.get(id).getDescription());
-
-      final FlexCellFormatter fmt = table.getFlexCellFormatter();
-      fmt.addStyleName(row, 1, Gerrit.RESOURCES.css().iconCell());
-      fmt.addStyleName(row, 2, Gerrit.RESOURCES.css().dataCell());
-      fmt.addStyleName(row, 3, Gerrit.RESOURCES.css().dataCell());
-
-      setRowItem(row, k);
-    }
+  protected void setMembersTabVisible(final boolean visible) {
+    setLinkVisible(membersTabToken, visible);
   }
 }
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 4631a29..9250ca3 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
@@ -16,6 +16,8 @@
 
 import com.google.gwt.i18n.client.Constants;
 
+import java.util.Map;
+
 public interface AdminConstants extends Constants {
   String defaultAccountName();
   String defaultAccountGroupName();
@@ -29,40 +31,47 @@
   String buttonSaveDescription();
   String buttonRenameGroup();
   String buttonCreateGroup();
+  String buttonCreateProject();
   String buttonChangeGroupOwner();
   String buttonChangeGroupType();
   String buttonSelectGroup();
-  String buttonAddProjectRight();
-  String buttonClearProjectRight();
   String buttonSaveChanges();
+  String checkBoxEmptyCommit();
+  String checkBoxPermissionsOnly();
   String useContentMerge();
   String useContributorAgreements();
   String useSignedOffBy();
   String requireChangeID();
   String headingGroupOptions();
   String isVisibleToAll();
-  String emailOnlyAuthors();
-  String descriptionNotifications();
   String buttonSaveGroupOptions();
+  String suggestedGroupLabel();
+  String parentSuggestions();
 
+  String headingGroupUUID();
   String headingOwner();
-  String headingParentProjectName();
   String headingDescription();
   String headingProjectOptions();
   String headingGroupType();
   String headingMembers();
   String headingIncludedGroups();
+  String noMembersInfo();
   String headingExternalGroup();
   String headingCreateGroup();
-  String headingAccessRights();
+  String headingCreateProject();
+  String headingParentProjectName();
+  String columnProjectName();
   String headingAgreements();
-  String headingShowInherited();
 
   String projectSubmitType_FAST_FORWARD_ONLY();
   String projectSubmitType_MERGE_ALWAYS();
   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();
@@ -70,13 +79,10 @@
   String columnMember();
   String columnEmailAddress();
   String columnGroupName();
-  String columnProjectName();
   String columnGroupDescription();
-  String columnProjectDescription();
-  String columnRightOrigin();
-  String columnApprovalCategory();
-  String columnRightRange();
-  String columnRefName();
+  String columnGroupType();
+  String columnGroupNotifications();
+  String columnGroupVisibleToAll();
 
   String columnBranchName();
   String columnBranchRevision();
@@ -84,16 +90,15 @@
   String buttonAddBranch();
   String buttonDeleteBranch();
 
-  String projectListPrev();
-  String projectListNext();
-  String projectListOpen();
-
   String groupListPrev();
   String groupListNext();
   String groupListOpen();
 
   String groupListTitle();
+  String groupTabGeneral();
+  String groupTabMembers();
   String projectListTitle();
+  String createProjectTitle();
   String projectAdminTabGeneral();
   String projectAdminTabBranches();
   String projectAdminTabAccess();
@@ -101,4 +106,20 @@
   String noGroupSelected();
   String errorNoMatchingGroups();
   String errorNoGitRepository();
+
+  String addPermission();
+  Map<String,String> permissionNames();
+
+  String refErrorEmpty();
+  String refErrorBeginSlash();
+  String refErrorDoubleSlash();
+  String refErrorNoSpace();
+  String refErrorPrintable();
+  String errorsMustBeFixed();
+
+  Map<String, String> capabilityNames();
+
+  String sectionTypeReference();
+  String sectionTypeSection();
+  Map<String, String> sectionNames();
 }
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 4bad368..ef7b73d 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,12 +10,13 @@
 buttonRenameGroup = Rename Group
 buttonSaveDescription = Save Description
 buttonCreateGroup = Create Group
+buttonCreateProject = Create Project
 buttonChangeGroupOwner = Change Owner
 buttonChangeGroupType = Change Type
 buttonSelectGroup = Select
-buttonAddProjectRight = Add Access Right
-buttonClearProjectRight = Clear Form
 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
@@ -23,28 +24,33 @@
 headingGroupOptions = Group Options
 isVisibleToAll = Make group visible to all registered users.
 buttonSaveGroupOptions = Save Group Options
-
-emailOnlyAuthors = Authors
-descriptionNotifications = Send email notifications about comments and actions by users in this group only to:
-
-headingOwner = Owners
+suggestedGroupLabel = group
 headingParentProjectName = Rights Inherit From
+parentSuggestions = Parent Suggestion
+columnProjectName = Project Name
+
+headingGroupUUID = Group UUID
+headingOwner = Owners
 headingDescription = Description
 headingProjectOptions = Project Options
 headingGroupType = Group Type
 headingMembers = Members
 headingIncludedGroups = Included Groups
+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
-headingAccessRights = Access Rights
+headingCreateProject = Create New Project
 headingAgreements = Contributor Agreements
-headingShowInherited = Show Inherited Rights
 
 projectSubmitType_FAST_FORWARD_ONLY = Fast Forward Only
 projectSubmitType_MERGE_IF_NECESSARY = Merge If Necessary
 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
@@ -52,13 +58,10 @@
 columnMember = Member
 columnEmailAddress = Email Address
 columnGroupName = Group Name
-columnProjectName = Project Name
 columnGroupDescription = Description
-columnProjectDescription = Description
-columnApprovalCategory = Category
-columnRightOrigin = Origin
-columnRightRange = Permitted Range
-columnRefName = Reference Name
+columnGroupType = Group Type
+columnGroupNotifications = Email Only Authors
+columnGroupVisibleToAll = Visible To All
 
 columnBranchName = Branch Name
 columnBranchRevision = Revision
@@ -66,16 +69,15 @@
 buttonAddBranch = Create Branch
 buttonDeleteBranch = Delete
 
-projectListPrev = Previous project
-projectListNext = Next project
-projectListOpen = Open project
-
 groupListPrev = Previous group
 groupListNext = Next group
 groupListOpen = Open group
 
 groupListTitle = Groups
+groupTabGeneral = General
+groupTabMembers = Members
 projectListTitle = Projects
+createProjectTitle = Create Project
 projectAdminTabGeneral = General
 projectAdminTabBranches = Branches
 projectAdminTabAccess = Access
@@ -83,3 +85,74 @@
 noGroupSelected = (No group selected)
 errorNoMatchingGroups = No Matching Groups
 errorNoGitRepository = No Git Repository
+
+
+addPermission = Add Permission ...
+
+# Permission Names
+permissionNames = \
+	create, \
+	forgeAuthor, \
+	forgeCommitter, \
+	forgeServerAsCommitter, \
+	owner, \
+	push, \
+	pushMerge, \
+	pushTag, \
+	read, \
+	rebase, \
+	submit
+create = Create Reference
+forgeAuthor = Forge Author Identity
+forgeCommitter = Forge Committer Identity
+forgeServerAsCommitter = Forge Server Identity
+owner = Owner
+push = Push
+pushMerge = Push Merge Commit
+pushTag = Push Annotated Tag
+read = Read
+rebase = Rebase
+submit = Submit
+
+refErrorEmpty = Reference must be supplied
+refErrorBeginSlash = Reference must not start with '/'
+refErrorDoubleSlash = References cannot contain '//'
+refErrorNoSpace = References cannot contain spaces
+refErrorPrintable = References may contain only printable characters
+errorsMustBeFixed = Errors must be fixed before committing changes.
+
+# Capability Names
+capabilityNames = \
+  administrateServer, \
+  createAccount, \
+  createGroup, \
+  createProject, \
+  emailReviewers, \
+  flushCaches, \
+  killTask, \
+  priority, \
+  queryLimit, \
+  startReplication, \
+  viewCaches, \
+  viewConnections, \
+  viewQueue
+administrateServer = Administrate Server 
+createAccount = Create Account
+createGroup = Create Group
+createProject = Create Project
+emailReviewers = Email Reviewers
+flushCaches = Flush Caches
+killTask = Kill Task
+priority = Priority
+queryLimit = Query Limit
+startReplication = Start Replication
+viewCaches = View Caches
+viewConnections = View Connections
+viewQueue = View Queue
+
+# Section Names
+sectionTypeReference = Reference:
+sectionTypeSection = Section:
+sectionNames = \
+  GLOBAL_CAPABILITIES
+GLOBAL_CAPABILITIES = Global Capabilities
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_50.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminCss.java
similarity index 69%
copy from gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_50.java
copy to gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminCss.java
index 4a90c1f..55a9bf3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_50.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminCss.java
@@ -12,14 +12,16 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.schema;
+package com.google.gerrit.client.admin;
 
-import com.google.inject.Inject;
-import com.google.inject.Provider;
+import com.google.gwt.resources.client.CssResource;
 
-public class Schema_50 extends SchemaVersion {
-  @Inject
-  Schema_50(Provider<Schema_49> prior) {
-    super(prior);
-  }
-}
\ No newline at end of file
+public interface AdminCss extends CssResource {
+  String deleteIcon();
+  String undoIcon();
+
+  String deleted();
+  String deletedBorder();
+
+  String deleteSectionHover();
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminMessages.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminMessages.java
index ab3541e..3b4d7d4 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminMessages.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminMessages.java
@@ -18,6 +18,10 @@
 
 public interface AdminMessages extends Messages {
   String group(String name);
+  String label(String name);
   String project(String name);
   String deletedGroup(int id);
+
+  String deletedReference(String name);
+  String deletedSection(String name);
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminMessages.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminMessages.properties
index 6feb69a..7f8cd56 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminMessages.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminMessages.properties
@@ -1,3 +1,6 @@
 group = Group {0}
+label = Label {0}
 project = Project {0}
 deletedGroup = Deleted Group {0}
+deletedReference = Reference {0} was deleted
+deletedSection = Section {0} was deleted
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminResources.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminResources.java
new file mode 100644
index 0000000..cd366f3
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AdminResources.java
@@ -0,0 +1,38 @@
+// 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.gwt.core.client.GWT;
+import com.google.gwt.resources.client.ClientBundle;
+import com.google.gwt.resources.client.ImageResource;
+
+public interface AdminResources extends ClientBundle {
+  public static final AdminResources I = GWT.create(AdminResources.class);
+
+  @Source("admin.css")
+  AdminCss css();
+
+  @Source("editText.png")
+  public ImageResource editText();
+
+  @Source("deleteNormal.png")
+  public ImageResource deleteNormal();
+
+  @Source("deleteHover.png")
+  public ImageResource deleteHover();
+
+  @Source("undoNormal.png")
+  public ImageResource undoNormal();
+}
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..0ce2d77
--- /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.client.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.common.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/GroupListScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java
index a6dbedc..9d62d34 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java
@@ -22,7 +22,8 @@
 import com.google.gerrit.client.ui.OnEditEnabler;
 import com.google.gerrit.client.ui.SmallHeading;
 import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.reviewdb.AccountGroup;
+import com.google.gerrit.common.data.GroupList;
+import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gwt.event.dom.client.BlurEvent;
 import com.google.gwt.event.dom.client.BlurHandler;
 import com.google.gwt.event.dom.client.ClickEvent;
@@ -37,11 +38,10 @@
 import com.google.gwt.user.client.ui.VerticalPanel;
 import com.google.gwtexpui.globalkey.client.NpTextBox;
 
-import java.util.List;
-
 public class GroupListScreen extends AccountScreen {
   private GroupTable groups;
 
+  private VerticalPanel addPanel;
   private NpTextBox addTxt;
   private Button addNew;
 
@@ -49,10 +49,11 @@
   protected void onLoad() {
     super.onLoad();
     Util.GROUP_SVC
-        .visibleGroups(new ScreenLoadCallback<List<AccountGroup>>(this) {
+        .visibleGroups(new ScreenLoadCallback<GroupList>(this) {
           @Override
-          protected void preDisplay(final List<AccountGroup> result) {
-            groups.display(result);
+          protected void preDisplay(GroupList result) {
+            addPanel.setVisible(result.isCanCreateGroup());
+            groups.display(result.getGroups());
             groups.finishDisplay();
           }
         });
@@ -66,9 +67,9 @@
     groups = new GroupTable(true /* hyperlink to admin */, PageLinks.ADMIN_GROUPS);
     add(groups);
 
-    final VerticalPanel fp = new VerticalPanel();
-    fp.setStyleName(Gerrit.RESOURCES.css().addSshKeyPanel());
-    fp.add(new SmallHeading(Util.C.headingCreateGroup()));
+    addPanel = new VerticalPanel();
+    addPanel.setStyleName(Gerrit.RESOURCES.css().addSshKeyPanel());
+    addPanel.add(new SmallHeading(Util.C.headingCreateGroup()));
 
     addTxt = new NpTextBox();
     addTxt.setVisibleLength(60);
@@ -80,7 +81,7 @@
         }
       }
     });
-    fp.add(addTxt);
+    addPanel.add(addTxt);
 
     addNew = new Button(Util.C.buttonCreateGroup());
     addNew.setEnabled(false);
@@ -109,8 +110,8 @@
         groups.setRegisterKeys(true);
       }
     });
-    fp.add(addNew);
-    add(fp);
+    addPanel.add(addNew);
+    add(addPanel);
 
     new OnEditEnabler(addNew, addTxt);
   }
@@ -130,7 +131,7 @@
     addNew.setEnabled(false);
     Util.GROUP_SVC.createGroup(newName, new GerritCallback<AccountGroup.Id>() {
       public void onSuccess(final AccountGroup.Id result) {
-        History.newItem(Dispatcher.toAccountGroup(result));
+        History.newItem(Dispatcher.toGroup(result));
       }
 
       @Override
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
new file mode 100644
index 0000000..9da9c22
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupReferenceBox.java
@@ -0,0 +1,143 @@
+// 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.ui.AccountGroupSuggestOracle;
+import com.google.gerrit.client.ui.RPCSuggestOracle;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gwt.editor.client.LeafValueEditor;
+import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.dom.client.KeyPressHandler;
+import com.google.gwt.event.dom.client.KeyUpEvent;
+import com.google.gwt.event.dom.client.KeyUpHandler;
+import com.google.gwt.event.logical.shared.CloseEvent;
+import com.google.gwt.event.logical.shared.CloseHandler;
+import com.google.gwt.event.logical.shared.HasCloseHandlers;
+import com.google.gwt.event.logical.shared.HasSelectionHandlers;
+import com.google.gwt.event.logical.shared.SelectionEvent;
+import com.google.gwt.event.logical.shared.SelectionHandler;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.Focusable;
+import com.google.gwt.user.client.ui.SuggestBox;
+import com.google.gwt.user.client.ui.SuggestBox.DefaultSuggestionDisplay;
+import com.google.gwt.user.client.ui.SuggestOracle.Suggestion;
+import com.google.gwtexpui.globalkey.client.NpTextBox;
+
+public class GroupReferenceBox extends Composite implements
+    LeafValueEditor<GroupReference>, HasSelectionHandlers<GroupReference>,
+    HasCloseHandlers<GroupReferenceBox>, Focusable {
+  private final DefaultSuggestionDisplay suggestions;
+  private final NpTextBox textBox;
+  private final AccountGroupSuggestOracle oracle;
+  private final SuggestBox suggestBox;
+
+  private boolean submitOnSelection;
+
+  public GroupReferenceBox() {
+    suggestions = new DefaultSuggestionDisplay();
+    textBox = new NpTextBox();
+    oracle = new AccountGroupSuggestOracle();
+    suggestBox = new SuggestBox( //
+        new RPCSuggestOracle(oracle), //
+        textBox, //
+        suggestions);
+    initWidget(suggestBox);
+
+    textBox.addKeyPressHandler(new KeyPressHandler() {
+      @Override
+      public void onKeyPress(KeyPressEvent event) {
+        submitOnSelection = false;
+
+        if (event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER) {
+          if (suggestions.isSuggestionListShowing()) {
+            submitOnSelection = true;
+          } else {
+            SelectionEvent.fire(GroupReferenceBox.this, getValue());
+          }
+        }
+      }
+    });
+    suggestBox.addKeyUpHandler(new KeyUpHandler() {
+      @Override
+      public void onKeyUp(KeyUpEvent event) {
+        if (event.getNativeKeyCode() == KeyCodes.KEY_ESCAPE) {
+          suggestBox.setText("");
+          CloseEvent.fire(GroupReferenceBox.this, GroupReferenceBox.this);
+        }
+      }
+    });
+    suggestBox.addSelectionHandler(new SelectionHandler<Suggestion>() {
+      @Override
+      public void onSelection(SelectionEvent<Suggestion> event) {
+        if (submitOnSelection) {
+          submitOnSelection = false;
+          SelectionEvent.fire(GroupReferenceBox.this, getValue());
+        }
+      }
+    });
+  }
+
+  public void setVisibleLength(int len) {
+    textBox.setVisibleLength(len);
+  }
+
+  @Override
+  public HandlerRegistration addSelectionHandler(
+      SelectionHandler<GroupReference> handler) {
+    return addHandler(handler, SelectionEvent.getType());
+  }
+
+  @Override
+  public HandlerRegistration addCloseHandler(
+      CloseHandler<GroupReferenceBox> handler) {
+    return addHandler(handler, CloseEvent.getType());
+  }
+
+  @Override
+  public GroupReference getValue() {
+    String name = suggestBox.getText();
+    if (name != null && !name.isEmpty()) {
+      return new GroupReference(oracle.getUUID(name), name);
+    } else {
+      return null;
+    }
+  }
+
+  @Override
+  public void setValue(GroupReference value) {
+    suggestBox.setText(value != null ? value.getName() : "");
+  }
+
+  @Override
+  public int getTabIndex() {
+    return suggestBox.getTabIndex();
+  }
+
+  @Override
+  public void setTabIndex(int index) {
+    suggestBox.setTabIndex(index);
+  }
+
+  public void setFocus(boolean focused) {
+    suggestBox.setFocus(focused);
+  }
+
+  @Override
+  public void setAccessKey(char key) {
+    suggestBox.setAccessKey(key);
+  }
+}
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 18a3761..7f73226 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupTable.java
@@ -18,18 +18,22 @@
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.ui.Hyperlink;
 import com.google.gerrit.client.ui.NavigationTable;
-import com.google.gerrit.reviewdb.AccountGroup;
+import com.google.gerrit.common.data.GroupDetail;
+import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.event.dom.client.KeyCodes;
 import com.google.gwt.user.client.History;
 import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
 import com.google.gwt.user.client.ui.HTMLTable.Cell;
+import com.google.gwt.user.client.ui.Image;
 
 import java.util.List;
 
 
 public class GroupTable extends NavigationTable<AccountGroup> {
+  private static final int NUM_COLS = 5;
+
   private final boolean enableLink;
 
   public GroupTable(final boolean enableLink) {
@@ -48,6 +52,9 @@
 
     table.setText(0, 1, Util.C.columnGroupName());
     table.setText(0, 2, Util.C.columnGroupDescription());
+    table.setText(0, 3, Util.C.headingOwner());
+    table.setText(0, 4, Util.C.columnGroupType());
+    table.setText(0, 5, Util.C.columnGroupVisibleToAll());
     table.addClickHandler(new ClickHandler() {
       @Override
       public void onClick(ClickEvent event) {
@@ -60,8 +67,9 @@
     });
 
     final FlexCellFormatter fmt = table.getFlexCellFormatter();
-    fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().dataHeader());
-    fmt.addStyleName(0, 2, Gerrit.RESOURCES.css().dataHeader());
+    for (int i = 1; i <= NUM_COLS; i++) {
+      fmt.addStyleName(0, i, Gerrit.RESOURCES.css().dataHeader());
+    }
   }
 
   @Override
@@ -71,34 +79,41 @@
 
   @Override
   protected void onOpenRow(final int row) {
-    History.newItem(Dispatcher.toAccountGroup(getRowItem(row).getId()));
+    History.newItem(Dispatcher.toGroup(getRowItem(row).getId()));
   }
 
-  public void display(final List<AccountGroup> result) {
+  public void display(final List<GroupDetail> result) {
     while (1 < table.getRowCount())
       table.removeRow(table.getRowCount() - 1);
 
-    for (final AccountGroup k : result) {
+    for(GroupDetail detail : result) {
       final int row = table.getRowCount();
       table.insertRow(row);
       applyDataRowStyle(row);
-      populate(row, k);
+      populate(row, detail);
     }
   }
 
-  void populate(final int row, final AccountGroup k) {
+  void populate(final int row, final GroupDetail detail) {
+    AccountGroup k = detail.group;
     if (enableLink) {
-      table.setWidget(row, 1, new Hyperlink(k.getName(), Dispatcher.toAccountGroup(k
-          .getId())));
+      table.setWidget(row, 1, new Hyperlink(k.getName(),
+          Dispatcher.toGroup(k.getId())));
     } else {
       table.setText(row, 1, k.getName());
     }
     table.setText(row, 2, k.getDescription());
+    table.setText(row, 3, detail.ownerGroup.getName());
+    table.setText(row, 4, k.getType().toString());
+    if (k.isVisibleToAll()) {
+      table.setWidget(row, 5, new Image(Gerrit.RESOURCES.greenCheck()));
+    }
 
     final FlexCellFormatter fmt = table.getFlexCellFormatter();
-    fmt.addStyleName(row, 1, Gerrit.RESOURCES.css().dataCell());
     fmt.addStyleName(row, 1, Gerrit.RESOURCES.css().groupName());
-    fmt.addStyleName(row, 2, Gerrit.RESOURCES.css().dataCell());
+    for (int i = 1; i <= NUM_COLS; i++) {
+      fmt.addStyleName(row, i, Gerrit.RESOURCES.css().dataCell());
+    }
 
     setRowItem(row, k);
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.java
new file mode 100644
index 0000000..d10afd1
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.java
@@ -0,0 +1,319 @@
+// 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.Gerrit;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.ui.SuggestUtil;
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.ApprovalType;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.common.data.GroupReference;
+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.common.data.RefConfigSection;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.dom.client.DivElement;
+import com.google.gwt.dom.client.Style.Display;
+import com.google.gwt.editor.client.Editor;
+import com.google.gwt.editor.client.EditorDelegate;
+import com.google.gwt.editor.client.ValueAwareEditor;
+import com.google.gwt.editor.client.adapters.EditorSource;
+import com.google.gwt.editor.client.adapters.ListEditor;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.MouseOutEvent;
+import com.google.gwt.event.dom.client.MouseOverEvent;
+import com.google.gwt.event.logical.shared.CloseEvent;
+import com.google.gwt.event.logical.shared.SelectionEvent;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.uibinder.client.UiHandler;
+import com.google.gwt.user.client.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.Composite;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwt.user.client.ui.ValueLabel;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class PermissionEditor extends Composite implements Editor<Permission>,
+    ValueAwareEditor<Permission> {
+  interface Binder extends UiBinder<HTMLPanel, PermissionEditor> {
+  }
+
+  private static final Binder uiBinder = GWT.create(Binder.class);
+
+  @UiField(provided = true)
+  @Path("name")
+  ValueLabel<String> normalName;
+
+  @UiField(provided = true)
+  @Path("name")
+  ValueLabel<String> deletedName;
+
+  @UiField
+  CheckBox exclusiveGroup;
+
+  @UiField
+  FlowPanel ruleContainer;
+  ListEditor<PermissionRule, PermissionRuleEditor> rules;
+
+  @UiField
+  DivElement addContainer;
+  @UiField
+  DivElement addStage1;
+  @UiField
+  DivElement addStage2;
+  @UiField
+  Anchor beginAddRule;
+  @UiField
+  @Editor.Ignore
+  GroupReferenceBox groupToAdd;
+  @UiField
+  Button addRule;
+
+  @UiField
+  Anchor deletePermission;
+
+  @UiField
+  DivElement normal;
+  @UiField
+  DivElement deleted;
+
+  private final boolean readOnly;
+  private final AccessSection section;
+  private Permission value;
+  private PermissionRange.WithDefaults validRange;
+  private boolean isDeleted;
+
+  public PermissionEditor(boolean readOnly, AccessSection section) {
+    this.readOnly = readOnly;
+    this.section = section;
+
+    normalName = new ValueLabel<String>(PermissionNameRenderer.INSTANCE);
+    deletedName = new ValueLabel<String>(PermissionNameRenderer.INSTANCE);
+
+    initWidget(uiBinder.createAndBindUi(this));
+    rules = ListEditor.of(new RuleEditorSource());
+
+    exclusiveGroup.setEnabled(!readOnly);
+    exclusiveGroup.setVisible(RefConfigSection
+        .isValid(section.getName()));
+
+    if (readOnly) {
+      addContainer.removeFromParent();
+      addContainer = null;
+
+      deletePermission.removeFromParent();
+      deletePermission = null;
+    }
+  }
+
+  @UiHandler("deletePermission")
+  void onDeleteHover(MouseOverEvent event) {
+    addStyleName(AdminResources.I.css().deleteSectionHover());
+  }
+
+  @UiHandler("deletePermission")
+  void onDeleteNonHover(MouseOutEvent event) {
+    removeStyleName(AdminResources.I.css().deleteSectionHover());
+  }
+
+  @UiHandler("deletePermission")
+  void onDeletePermission(ClickEvent event) {
+    isDeleted = true;
+    normal.getStyle().setDisplay(Display.NONE);
+    deleted.getStyle().setDisplay(Display.BLOCK);
+  }
+
+  @UiHandler("undoDelete")
+  void onUndoDelete(ClickEvent event) {
+    isDeleted = false;
+    deleted.getStyle().setDisplay(Display.NONE);
+    normal.getStyle().setDisplay(Display.BLOCK);
+  }
+
+  @UiHandler("beginAddRule")
+  void onBeginAddRule(ClickEvent event) {
+    beginAddRule();
+  }
+
+  void beginAddRule() {
+    addStage1.getStyle().setDisplay(Display.NONE);
+    addStage2.getStyle().setDisplay(Display.BLOCK);
+
+    Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+      @Override
+      public void execute() {
+        groupToAdd.setFocus(true);
+      }
+    });
+  }
+
+  @UiHandler("addRule")
+  void onAddGroupByClick(ClickEvent event) {
+    GroupReference ref = groupToAdd.getValue();
+    if (ref != null) {
+      addGroup(ref);
+    } else {
+      groupToAdd.setFocus(true);
+    }
+  }
+
+  @UiHandler("groupToAdd")
+  void onAddGroupByEnter(SelectionEvent<GroupReference> event) {
+    GroupReference ref = event.getSelectedItem();
+    if (ref != null) {
+      addGroup(ref);
+    }
+  }
+
+  @UiHandler("groupToAdd")
+  void onAbortAddGroup(CloseEvent<GroupReferenceBox> event) {
+    hideAddGroup();
+  }
+
+  @UiHandler("hideAddGroup")
+  void hideAddGroup(ClickEvent event) {
+    hideAddGroup();
+  }
+
+  private void hideAddGroup() {
+    addStage1.getStyle().setDisplay(Display.BLOCK);
+    addStage2.getStyle().setDisplay(Display.NONE);
+  }
+
+  private void addGroup(GroupReference ref) {
+    if (ref.getUUID() != null) {
+      if (value.getRule(ref) == null) {
+        PermissionRule newRule = value.getRule(ref, true);
+        if (validRange != null) {
+          int min = validRange.getDefaultMin();
+          int max = validRange.getDefaultMax();
+          newRule.setRange(min, max);
+
+        } else if (GlobalCapability.PRIORITY.equals(value.getName())) {
+          newRule.setAction(PermissionRule.Action.BATCH);
+        }
+
+        rules.getList().add(newRule);
+      }
+      groupToAdd.setValue(null);
+      groupToAdd.setFocus(true);
+
+    } else {
+      // If the oracle didn't get to complete a UUID, resolve it now.
+      //
+      addRule.setEnabled(false);
+      SuggestUtil.SVC.suggestAccountGroup(ref.getName(), 1,
+          new GerritCallback<List<GroupReference>>() {
+            @Override
+            public void onSuccess(List<GroupReference> result) {
+              addRule.setEnabled(true);
+              if (result.size() == 1) {
+                addGroup(result.get(0));
+              } else {
+                groupToAdd.setFocus(true);
+              }
+            }
+
+            @Override
+            public void onFailure(Throwable caught) {
+              addRule.setEnabled(true);
+              super.onFailure(caught);
+            }
+          });
+    }
+  }
+
+  boolean isDeleted() {
+    return isDeleted;
+  }
+
+  @Override
+  public void setValue(Permission value) {
+    this.value = value;
+
+    if (value.isLabel()) {
+      ApprovalType at = Gerrit.getConfig().getApprovalTypes().byLabel(value.getLabel());
+      if (at != null) {
+        validRange = new PermissionRange.WithDefaults(
+            value.getName(),
+            at.getMin().getValue(), at.getMax().getValue(),
+            at.getMin().getValue(), at.getMax().getValue());
+      }
+    } else if (GlobalCapability.isCapability(value.getName())) {
+      validRange = GlobalCapability.getRange(value.getName());
+
+    } else {
+      validRange = null;
+    }
+
+    if (value != null && Permission.OWNER.equals(value.getName())) {
+      exclusiveGroup.setEnabled(false);
+    } else {
+      exclusiveGroup.setEnabled(!readOnly);
+    }
+  }
+
+  @Override
+  public void flush() {
+    List<PermissionRule> src = rules.getList();
+    List<PermissionRule> keep = new ArrayList<PermissionRule>(src.size());
+
+    for (int i = 0; i < src.size(); i++) {
+      PermissionRuleEditor e =
+          (PermissionRuleEditor) ruleContainer.getWidget(i);
+      if (!e.isDeleted()) {
+        keep.add(src.get(i));
+      }
+    }
+    value.setRules(keep);
+  }
+
+  @Override
+  public void onPropertyChange(String... paths) {
+  }
+
+  @Override
+  public void setDelegate(EditorDelegate<Permission> delegate) {
+  }
+
+  private class RuleEditorSource extends EditorSource<PermissionRuleEditor> {
+    @Override
+    public PermissionRuleEditor create(int index) {
+      PermissionRuleEditor subEditor =
+          new PermissionRuleEditor(readOnly, section, value, validRange);
+      ruleContainer.insert(subEditor, index);
+      return subEditor;
+    }
+
+    @Override
+    public void dispose(PermissionRuleEditor subEditor) {
+      subEditor.removeFromParent();
+    }
+
+    @Override
+    public void setIndex(PermissionRuleEditor subEditor, int index) {
+      ruleContainer.insert(subEditor, index);
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.ui.xml
new file mode 100644
index 0000000..25995d9
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.ui.xml
@@ -0,0 +1,145 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<ui:UiBinder
+  xmlns:ui='urn:ui:com.google.gwt.uibinder'
+  xmlns:g='urn:import:com.google.gwt.user.client.ui'
+  xmlns:e='urn:import:com.google.gwt.editor.ui.client'
+  xmlns:my='urn:import:com.google.gerrit.client.admin'
+  ui:generateFormat='com.google.gwt.i18n.rebind.format.PropertiesFormat'
+  ui:generateKeys='com.google.gwt.i18n.rebind.keygen.MD5KeyGenerator'
+  ui:generateLocales='default,en'
+  >
+<ui:with field='res' type='com.google.gerrit.client.admin.AdminResources'/>
+<ui:style>
+  @eval selectionColor com.google.gerrit.client.Gerrit.getTheme().selectionColor;
+  @eval backgroundColor com.google.gerrit.client.Gerrit.getTheme().backgroundColor;
+
+  .panel {
+    position: relative;
+  }
+
+  .normal {
+    border: 1px solid backgroundColor;
+    margin-top: -1px;
+    margin-bottom: -1px;
+  }
+
+  .header {
+    padding-left: 5px;
+    padding-right: 5px;
+    padding-bottom: 1px;
+    white-space: nowrap;
+  }
+
+  .header:hover {
+    background-color: selectionColor;
+  }
+
+  .name {
+    font-style: italic;
+  }
+
+  .exclusiveGroup {
+    position: absolute;
+    top: 0;
+    right: 36px;
+    width: 7em;
+    font-size: 80%;
+  }
+
+  .addContainer {
+    padding-left: 10px;
+    position: relative;
+  }
+  .addContainer:hover {
+    background-color: selectionColor;
+  }
+  .addLink {
+    font-size: 80%;
+  }
+
+  .deleteIcon {
+    position: absolute;
+    top: 1px;
+    right: 12px;
+  }
+</ui:style>
+
+<g:HTMLPanel stylePrimaryName='{style.panel}'>
+<div ui:field='normal' class='{style.normal}'>
+  <div class='{style.header}'>
+    <g:ValueLabel styleName='{style.name}' ui:field='normalName'/>
+    <g:CheckBox
+        ui:field='exclusiveGroup'
+        addStyleNames='{style.exclusiveGroup}'
+        text='Exclusive'>
+      <ui:attribute name='text'/>
+    </g:CheckBox>
+  <g:Anchor
+      ui:field='deletePermission'
+      href='javascript:void'
+      styleName='{style.deleteIcon} {res.css.deleteIcon}'
+      title='Delete this permission (and nested rules)'>
+    <ui:attribute name='title'/>
+  </g:Anchor>
+  </div>
+  <g:FlowPanel ui:field='ruleContainer'/>
+  <div ui:field='addContainer' class='{style.addContainer}'>
+    <div ui:field='addStage1'>
+      <g:Anchor
+          ui:field='beginAddRule'
+          styleName='{style.addLink}'
+          href='javascript:void'
+          text='Add Group'>
+        <ui:attribute name='text'/>
+      </g:Anchor>
+    </div>
+    <div ui:field='addStage2' style='display: none'>
+      <ui:msg>Group Name: <my:GroupReferenceBox
+                                            ui:field='groupToAdd'
+                                            visibleLength='45'/></ui:msg>
+      <g:Button
+          ui:field='addRule'
+          text='Add'>
+        <ui:attribute name='text'/>
+      </g:Button>
+      <g:Anchor
+          ui:field='hideAddGroup'
+          href='javascript:void'
+          styleName='{style.deleteIcon} {res.css.deleteIcon}'
+          title='Cancel additional group'>
+        <ui:attribute name='title'/>
+      </g:Anchor>
+    </div>
+  </div>
+</div>
+
+<div
+    ui:field='deleted'
+    class='{res.css.deleted} {res.css.deletedBorder}'
+    style='display: none'>
+  <ui:msg>Permission <g:ValueLabel styleName='{style.name}' ui:field='deletedName'/> was deleted</ui:msg>
+  <g:Anchor
+      ui:field='undoDelete'
+      href='javascript:void'
+      styleName='{style.deleteIcon} {res.css.undoIcon}'
+      title='Undo deletion'>
+    <ui:attribute name='title'/>
+  </g:Anchor>
+</div>
+</g:HTMLPanel>
+</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionNameRenderer.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionNameRenderer.java
new file mode 100644
index 0000000..ad3473c
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionNameRenderer.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.client.admin;
+
+import com.google.gerrit.common.data.Permission;
+import com.google.gwt.text.shared.Renderer;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+class PermissionNameRenderer implements Renderer<String> {
+  static final PermissionNameRenderer INSTANCE = new PermissionNameRenderer();
+
+  private static final Map<String, String> all;
+
+  static {
+    all = new HashMap<String, String>();
+    for (Map.Entry<String, String> e : Util.C.capabilityNames().entrySet()) {
+      all.put(e.getKey(), e.getValue());
+      all.put(e.getKey().toLowerCase(), e.getValue());
+    }
+    for (Map.Entry<String, String> e : Util.C.permissionNames().entrySet()) {
+      all.put(e.getKey(), e.getValue());
+      all.put(e.getKey().toLowerCase(), e.getValue());
+    }
+  }
+
+  @Override
+  public String render(String varName) {
+    if (Permission.isLabel(varName)) {
+      return Util.M.label(new Permission(varName).getLabel());
+    }
+
+    String desc = all.get(varName);
+    if (desc == null) {
+      desc = all.get(varName.toLowerCase());
+    }
+    return desc != null ? desc : varName;
+  }
+
+  @Override
+  public void render(String object, Appendable appendable) throws IOException {
+    appendable.append(render(object));
+  }
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..e4cced7
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionRuleEditor.java
@@ -0,0 +1,220 @@
+// 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 static com.google.gerrit.common.data.Permission.PUSH;
+import static com.google.gerrit.common.data.Permission.PUSH_TAG;
+
+import com.google.gerrit.client.Dispatcher;
+import com.google.gerrit.client.ui.Hyperlink;
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.common.data.PermissionRange;
+import com.google.gerrit.common.data.PermissionRule;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.dom.client.DivElement;
+import com.google.gwt.dom.client.SpanElement;
+import com.google.gwt.dom.client.Style.Display;
+import com.google.gwt.editor.client.Editor;
+import com.google.gwt.editor.client.EditorDelegate;
+import com.google.gwt.editor.client.ValueAwareEditor;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.text.shared.Renderer;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.uibinder.client.UiHandler;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.ui.Anchor;
+import com.google.gwt.user.client.ui.CheckBox;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwt.user.client.ui.UIObject;
+import com.google.gwt.user.client.ui.ValueListBox;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+
+public class PermissionRuleEditor extends Composite implements
+    Editor<PermissionRule>, ValueAwareEditor<PermissionRule> {
+  interface Binder extends UiBinder<HTMLPanel, PermissionRuleEditor> {
+  }
+
+  private static final Binder uiBinder = GWT.create(Binder.class);
+
+  @UiField(provided = true)
+  ValueListBox<PermissionRule.Action> action;
+
+  @UiField(provided = true)
+  RangeBox min;
+
+  @UiField(provided = true)
+  RangeBox max;
+
+  @UiField
+  CheckBox force;
+
+  @UiField
+  Hyperlink groupNameLink;
+  @UiField
+  SpanElement groupNameSpan;
+  @UiField
+  SpanElement deletedGroupName;
+
+  @UiField
+  Anchor deleteRule;
+
+  @UiField
+  DivElement normal;
+  @UiField
+  DivElement deleted;
+
+  @UiField
+  SpanElement rangeEditor;
+
+  private boolean isDeleted;
+
+  public PermissionRuleEditor(boolean readOnly, AccessSection section,
+      Permission permission, PermissionRange.WithDefaults validRange) {
+    action = new ValueListBox<PermissionRule.Action>(actionRenderer);
+
+    if (validRange != null && 10 < validRange.getRangeSize()) {
+        min = new RangeBox.Box();
+        max = new RangeBox.Box();
+
+    } else if (validRange != null) {
+      RangeBox.List minList = new RangeBox.List();
+      RangeBox.List maxList = new RangeBox.List();
+      List<Integer> valueList = validRange.getValuesAsList();
+
+      minList.list.setValue(validRange.getMin());
+      maxList.list.setValue(validRange.getMax());
+
+      minList.list.setAcceptableValues(valueList);
+      maxList.list.setAcceptableValues(valueList);
+
+      min = minList;
+      max = maxList;
+
+    } else {
+      min = new RangeBox.Box();
+      max = new RangeBox.Box();
+
+      if (GlobalCapability.PRIORITY.equals(permission.getName())) {
+        action.setValue(PermissionRule.Action.INTERACTIVE);
+        action.setAcceptableValues(Arrays.asList(
+            PermissionRule.Action.INTERACTIVE,
+            PermissionRule.Action.BATCH));
+
+      } else {
+        action.setValue(PermissionRule.Action.ALLOW);
+        action.setAcceptableValues(Arrays.asList(
+            PermissionRule.Action.ALLOW,
+            PermissionRule.Action.DENY,
+            PermissionRule.Action.BLOCK));
+      }
+    }
+
+    initWidget(uiBinder.createAndBindUi(this));
+
+    String name = permission.getName();
+    boolean canForce = PUSH.equals(name) || PUSH_TAG.equals(name);
+    if (canForce) {
+      String ref = section.getName();
+      canForce = !ref.startsWith("refs/for/") && !ref.startsWith("^refs/for/");
+    }
+    force.setText(PermissionRule.FORCE_PUSH);
+    force.setVisible(canForce);
+    force.setEnabled(!readOnly);
+
+    if (validRange != null) {
+      min.setEnabled(!readOnly);
+      max.setEnabled(!readOnly);
+      action.getElement().getStyle().setDisplay(Display.NONE);
+
+    } else {
+      rangeEditor.getStyle().setDisplay(Display.NONE);
+      DOM.setElementPropertyBoolean(action.getElement(), "disabled", readOnly);
+    }
+
+    if (readOnly) {
+      deleteRule.removeFromParent();
+      deleteRule = null;
+    }
+  }
+
+  boolean isDeleted() {
+    return isDeleted;
+  }
+
+  @UiHandler("deleteRule")
+  void onDeleteRule(ClickEvent event) {
+    isDeleted = true;
+    normal.getStyle().setDisplay(Display.NONE);
+    deleted.getStyle().setDisplay(Display.BLOCK);
+  }
+
+  @UiHandler("undoDelete")
+  void onUndoDelete(ClickEvent event) {
+    isDeleted = false;
+    deleted.getStyle().setDisplay(Display.NONE);
+    normal.getStyle().setDisplay(Display.BLOCK);
+  }
+
+  @Override
+  public void setValue(PermissionRule value) {
+    GroupReference ref = value.getGroup();
+    if (ref.getUUID() != null) {
+      groupNameLink.setTargetHistoryToken(Dispatcher.toGroup(ref.getUUID()));
+    }
+
+    groupNameLink.setText(ref.getName());
+    groupNameSpan.setInnerText(ref.getName());
+    deletedGroupName.setInnerText(ref.getName());
+
+    groupNameLink.setVisible(ref.getUUID() != null);
+    UIObject.setVisible(groupNameSpan, ref.getUUID() == null);
+  }
+
+  @Override
+  public void setDelegate(EditorDelegate<PermissionRule> delegate) {
+  }
+
+  @Override
+  public void flush() {
+  }
+
+  @Override
+  public void onPropertyChange(String... paths) {
+  }
+
+  private static class ActionRenderer implements
+      Renderer<PermissionRule.Action> {
+    @Override
+    public String render(PermissionRule.Action object) {
+      return object != null ? object.toString() : "";
+    }
+
+    @Override
+    public void render(PermissionRule.Action object, Appendable appendable)
+        throws IOException {
+      appendable.append(render(object));
+    }
+  }
+
+  private static final ActionRenderer actionRenderer = new ActionRenderer();
+}
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
new file mode 100644
index 0000000..26fc229
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionRuleEditor.ui.xml
@@ -0,0 +1,107 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<ui:UiBinder
+  xmlns:ui='urn:ui:com.google.gwt.uibinder'
+  xmlns:g='urn:import:com.google.gwt.user.client.ui'
+  xmlns:e='urn:import:com.google.gwt.editor.ui.client'
+  xmlns:my='urn:import:com.google.gerrit.client.admin'
+  xmlns:q='urn:import:com.google.gerrit.client.ui'
+  ui:generateFormat='com.google.gwt.i18n.rebind.format.PropertiesFormat'
+  ui:generateKeys='com.google.gwt.i18n.rebind.keygen.MD5KeyGenerator'
+  ui:generateLocales='default,en'
+  >
+<ui:with field='res' type='com.google.gerrit.client.admin.AdminResources'/>
+<ui:style>
+  @eval selectionColor com.google.gerrit.client.Gerrit.getTheme().selectionColor;
+
+  .panel {
+    position: relative;
+    height: 1.5em;
+  }
+
+  .panel:hover {
+    background-color: selectionColor;
+  }
+
+  .normal {
+    padding-left: 10px;
+    white-space: nowrap;
+    height: 100%;
+  }
+
+  .deleted {
+    height: 100%;
+  }
+
+  .actionList, .minmax {
+    font-size: 80%;
+  }
+
+  .forcePush {
+    position: absolute;
+    top: 0;
+    right: 36px;
+    width: 7em;
+    font-size: 80%;
+  }
+
+  .deleteIcon {
+    position: absolute;
+    top: 2px;
+    right: 11px;
+  }
+
+  .groupName {
+    display: inline;
+  }
+</ui:style>
+
+<g:HTMLPanel styleName='{style.panel}'>
+<div ui:field='normal' class='{style.normal}'>
+  <g:ValueListBox ui:field='action' styleName='{style.actionList}'/>
+  <span ui:field='rangeEditor'>
+    <g:Widget ui:field='min' styleName='{style.minmax}'/>
+    <g:Widget ui:field='max' styleName='{style.minmax}'/>
+  </span>
+
+  <q:Hyperlink ui:field='groupNameLink' styleName='{style.groupName}'/>
+  <span ui:field='groupNameSpan' styleName='{style.groupName}'/>
+  <g:CheckBox ui:field='force' addStyleNames='{style.forcePush}'/>
+  <g:Anchor
+      ui:field='deleteRule'
+      href='javascript:void'
+      styleName='{style.deleteIcon} {res.css.deleteIcon}'
+      title='Delete this rule'>
+    <ui:attribute name='title'/>
+  </g:Anchor>
+</div>
+
+<div
+    ui:field='deleted'
+    class='{res.css.deleted} {style.deleted}'
+    style='display: none'>
+  <ui:msg>Group <span ui:field='deletedGroupName'/> was deleted</ui:msg>
+  <g:Anchor
+      ui:field='undoDelete'
+      href='javascript:void'
+      styleName='{style.deleteIcon} {res.css.undoIcon}'
+      title='Undo deletion'>
+    <ui:attribute name='title'/>
+  </g:Anchor>
+</div>
+</g:HTMLPanel>
+</ui:UiBinder>
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
new file mode 100644
index 0000000..e3bf555
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.java
@@ -0,0 +1,178 @@
+// 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.Gerrit;
+import com.google.gerrit.client.GitwebLink;
+import com.google.gerrit.client.ui.Hyperlink;
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.ProjectAccess;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.dom.client.DivElement;
+import com.google.gwt.dom.client.Style.Display;
+import com.google.gwt.editor.client.Editor;
+import com.google.gwt.editor.client.EditorDelegate;
+import com.google.gwt.editor.client.ValueAwareEditor;
+import com.google.gwt.editor.client.adapters.EditorSource;
+import com.google.gwt.editor.client.adapters.ListEditor;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.uibinder.client.UiHandler;
+import com.google.gwt.user.client.ui.Anchor;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.HTMLPanel;
+
+import java.util.ArrayList;
+import java.util.List;
+
+public class ProjectAccessEditor extends Composite implements
+    Editor<ProjectAccess>, ValueAwareEditor<ProjectAccess> {
+  interface Binder extends UiBinder<HTMLPanel, ProjectAccessEditor> {
+  }
+
+  private static final Binder uiBinder = GWT.create(Binder.class);
+
+  @UiField
+  DivElement inheritsFrom;
+
+  @UiField
+  Hyperlink parentProject;
+
+  @UiField
+  DivElement history;
+
+  @UiField
+  Anchor gitweb;
+
+  @UiField
+  FlowPanel localContainer;
+  ListEditor<AccessSection, AccessSectionEditor> local;
+
+  @UiField
+  Anchor addSection;
+
+  private ProjectAccess value;
+
+  private boolean editing;
+
+  public ProjectAccessEditor() {
+    initWidget(uiBinder.createAndBindUi(this));
+    local = ListEditor.of(new Source(localContainer));
+  }
+
+  @UiHandler("addSection")
+  void onAddSection(ClickEvent event) {
+    int index = local.getList().size();
+    local.getList().add(new AccessSection("refs/heads/*"));
+
+    AccessSectionEditor editor = local.getEditors().get(index);
+    editor.enableEditing();
+    editor.editRefPattern();
+  }
+
+  @Override
+  public void setValue(ProjectAccess value) {
+    // If the owner can edit the Global Capabilities but they don't exist in this
+    // project, create an empty one at the beginning of the list making it
+    // possible to add permissions to it.
+    if (editing
+        && value.isOwnerOf(AccessSection.GLOBAL_CAPABILITIES)
+        && value.getLocal(AccessSection.GLOBAL_CAPABILITIES) == null) {
+      value.getLocal().add(0, new AccessSection(AccessSection.GLOBAL_CAPABILITIES));
+    }
+
+    this.value = value;
+
+    Project.NameKey parent = value.getInheritsFrom();
+    if (parent != null) {
+      inheritsFrom.getStyle().setDisplay(Display.BLOCK);
+      parentProject.setText(parent.get());
+      parentProject.setTargetHistoryToken( //
+          Dispatcher.toProjectAdmin(parent, ProjectScreen.ACCESS));
+    } else {
+      inheritsFrom.getStyle().setDisplay(Display.NONE);
+    }
+
+    final GitwebLink c = Gerrit.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());
+  }
+
+  @Override
+  public void flush() {
+    List<AccessSection> src = local.getList();
+    List<AccessSection> keep = new ArrayList<AccessSection>(src.size());
+
+    for (int i = 0; i < src.size(); i++) {
+      AccessSectionEditor e = (AccessSectionEditor) localContainer.getWidget(i);
+      if (!e.isDeleted() && !src.get(i).getPermissions().isEmpty()) {
+        keep.add(src.get(i));
+      }
+    }
+    value.setLocal(keep);
+  }
+
+  @Override
+  public void onPropertyChange(String... paths) {
+  }
+
+  @Override
+  public void setDelegate(EditorDelegate<ProjectAccess> delegate) {
+  }
+
+  void setEditing(final boolean editing) {
+    this.editing = editing;
+    addSection.setVisible(editing);
+  }
+
+  private class Source extends EditorSource<AccessSectionEditor> {
+    private final FlowPanel container;
+
+    Source(FlowPanel container) {
+      this.container = container;
+    }
+
+    @Override
+    public AccessSectionEditor create(int index) {
+      AccessSectionEditor subEditor = new AccessSectionEditor(value);
+      subEditor.setEditing(editing);
+      container.insert(subEditor, index);
+      return subEditor;
+    }
+
+    @Override
+    public void dispose(AccessSectionEditor subEditor) {
+      subEditor.removeFromParent();
+    }
+
+    @Override
+    public void setIndex(AccessSectionEditor subEditor, int index) {
+      container.insert(subEditor, index);
+    }
+  }
+}
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
new file mode 100644
index 0000000..4942b83
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessEditor.ui.xml
@@ -0,0 +1,75 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<ui:UiBinder
+  xmlns:ui='urn:ui:com.google.gwt.uibinder'
+  xmlns:g='urn:import:com.google.gwt.user.client.ui'
+  xmlns:q='urn:import:com.google.gerrit.client.ui'
+  ui:generateFormat='com.google.gwt.i18n.rebind.format.PropertiesFormat'
+  ui:generateKeys='com.google.gwt.i18n.rebind.keygen.MD5KeyGenerator'
+  ui:generateLocales='default,en'
+  >
+<ui:style>
+  .inheritsFrom {
+    margin-bottom: 0.5em;
+  }
+  .parentTitle {
+    font-weight: bold;
+  }
+  .parentLink {
+    display: inline;
+  }
+
+  .history {
+    margin-bottom: 0.5em;
+  }
+  .historyTitle {
+    font-weight: bold;
+  }
+  .gitwebLink {
+    display: inline;
+  }
+
+  .addContainer {
+    margin-top: 5px;
+    font-size: 80%;
+  }
+  .addContainer:hover {
+    background-color: selectionColor;
+  }
+</ui:style>
+
+<g:HTMLPanel>
+  <div ui:field='inheritsFrom' class='{style.inheritsFrom}'>
+    <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}'>
+    <g:Anchor
+        ui:field='addSection'
+        href='javascript:void'
+        text='Add Reference'>
+      <ui:attribute name='text'/>
+    </g:Anchor>
+  </div>
+</g:HTMLPanel>
+</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.java
index b87f6f1..1ed919b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2008 The Android Open Source Project
+// 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.
@@ -14,46 +14,63 @@
 
 package com.google.gerrit.client.admin;
 
-import com.google.gerrit.client.Dispatcher;
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
-import com.google.gerrit.client.ui.FancyFlexTable;
-import com.google.gerrit.client.ui.Hyperlink;
-import com.google.gerrit.client.ui.SmallHeading;
-import com.google.gerrit.common.data.ApprovalType;
-import com.google.gerrit.common.data.GerritConfig;
-import com.google.gerrit.common.data.InheritedRefRight;
-import com.google.gerrit.common.data.ProjectDetail;
-import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.ApprovalCategoryValue;
-import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.RefRight;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.common.data.ProjectAccess;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.dom.client.DivElement;
+import com.google.gwt.editor.client.SimpleBeanEditorDriver;
 import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.logical.shared.ValueChangeEvent;
-import com.google.gwt.event.logical.shared.ValueChangeHandler;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.uibinder.client.UiHandler;
+import com.google.gwt.user.client.Window;
 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.Panel;
-import com.google.gwt.user.client.ui.VerticalPanel;
-import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
-import com.google.gwtexpui.safehtml.client.SafeHtml;
-import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
-
-import java.util.HashSet;
-import java.util.List;
-import java.util.Map;
+import com.google.gwt.user.client.ui.HTMLPanel;
+import com.google.gwt.user.client.ui.UIObject;
+import com.google.gwtexpui.globalkey.client.NpTextArea;
 
 public class ProjectAccessScreen extends ProjectScreen {
-  private Panel parentPanel;
-  private Hyperlink parentName;
+  interface Binder extends UiBinder<HTMLPanel, ProjectAccessScreen> {
+  }
 
-  private RightsTable rights;
-  private Button delRight;
-  private AccessRightEditor rightEditor;
-  private CheckBox showInherited;
+  private static final Binder uiBinder = GWT.create(Binder.class);
+
+  interface Driver extends SimpleBeanEditorDriver< //
+      ProjectAccess, //
+      ProjectAccessEditor> {
+  }
+
+  @UiField
+  DivElement editTools;
+
+  @UiField
+  Button edit;
+
+  @UiField
+  Button cancel1;
+
+  @UiField
+  Button cancel2;
+
+  @UiField
+  ProjectAccessEditor accessEditor;
+
+  @UiField
+  DivElement commitTools;
+
+  @UiField
+  NpTextArea commitMessage;
+
+  @UiField
+  Button commit;
+
+  private Driver driver;
+
+  private ProjectAccess access;
 
   public ProjectAccessScreen(final Project.NameKey toShow) {
     super(toShow);
@@ -62,266 +79,89 @@
   @Override
   protected void onInitUI() {
     super.onInitUI();
-    initParent();
-    initRights();
+    add(uiBinder.createAndBindUi(this));
+
+    driver = GWT.create(Driver.class);
+    accessEditor.setEditing(false);
+    driver.initialize(accessEditor);
   }
 
   @Override
   protected void onLoad() {
     super.onLoad();
-    Util.PROJECT_SVC.projectDetail(getProjectKey(),
-        new ScreenLoadCallback<ProjectDetail>(this) {
-          public void preDisplay(final ProjectDetail result) {
-            enableForm(true);
-            display(result);
+    Util.PROJECT_SVC.projectAccess(getProjectKey(),
+        new ScreenLoadCallback<ProjectAccess>(this) {
+          @Override
+          public void preDisplay(ProjectAccess access) {
+            displayReadOnly(access);
           }
         });
   }
 
-  private void enableForm(final boolean on) {
-    delRight.setEnabled(on);
-    rightEditor.enableForm(on);
+  private void displayReadOnly(ProjectAccess access) {
+    this.access = access;
+    accessEditor.setEditing(false);
+    UIObject.setVisible(editTools, !access.getOwnerOf().isEmpty());
+    edit.setEnabled(!access.getOwnerOf().isEmpty());
+    cancel1.setVisible(false);
+    UIObject.setVisible(commitTools, false);
+    driver.edit(access);
   }
 
-  private void initParent() {
-    parentName = new Hyperlink("", "");
-
-    showInherited = new CheckBox();
-    showInherited.setValue(true);
-    showInherited.addClickHandler(new ClickHandler() {
-      public void onClick(ClickEvent event) {
-        rights.showInherited(showInherited.getValue());
-      }
-    });
-
-    Grid g = new Grid(2, 3);
-    g.setWidget(0, 0, new SmallHeading(Util.C.headingParentProjectName()));
-    g.setWidget(1, 0, parentName);
-    g.setWidget(1, 1, showInherited);
-    g.setText(1, 2, Util.C.headingShowInherited());
-
-    parentPanel = new VerticalPanel();
-    parentPanel.add(g);
-    add(parentPanel);
+  @UiHandler("edit")
+  void onEdit(ClickEvent event) {
+    edit.setEnabled(false);
+    cancel1.setVisible(true);
+    UIObject.setVisible(commitTools, true);
+    accessEditor.setEditing(true);
+    driver.edit(access);
   }
 
-  private void initRights() {
-    rights = new RightsTable();
-
-    delRight = new Button(Util.C.buttonDeleteGroupMembers());
-    delRight.addClickHandler(new ClickHandler() {
-      @Override
-      public void onClick(final ClickEvent event) {
-        final HashSet<RefRight.Key> refRightIds = rights.getRefRightIdsChecked();
-        doDeleteRefRights(refRightIds);
-      }
-    });
-
-    rightEditor = new AccessRightEditor(getProjectKey());
-    rightEditor.addValueChangeHandler(new ValueChangeHandler<ProjectDetail>() {
-        @Override
-        public void onValueChange(ValueChangeEvent<ProjectDetail> event) {
-          display(event.getValue());
-        }
-      });
-
-    add(new SmallHeading(Util.C.headingAccessRights()));
-    add(rights);
-    add(delRight);
-    add(rightEditor);
+  @UiHandler(value={"cancel1", "cancel2"})
+  void onCancel(ClickEvent event) {
+    Gerrit.display(PageLinks.toProjectAcceess(getProjectKey()));
   }
 
-  void display(final ProjectDetail result) {
-    final Project project = result.project;
+  @UiHandler("commit")
+  void onCommit(ClickEvent event) {
+    ProjectAccess access = driver.flush();
 
-    final Project.NameKey wildKey = Gerrit.getConfig().getWildProject();
-    final boolean isWild = wildKey.equals(project.getNameKey());
-    Project.NameKey parent = project.getParent();
-    if (parent == null) {
-      parent = wildKey;
+    if (driver.hasErrors()) {
+      Window.alert(Util.C.errorsMustBeFixed());
+      return;
     }
 
-    parentPanel.setVisible(!isWild);
-    parentName.setTargetHistoryToken(Dispatcher.toProjectAdmin(parent, ACCESS));
-    parentName.setText(parent.get());
+    String message = commitMessage.getText().trim();
+    if ("".equals(message)) {
+      message = null;
+    }
 
-    rights.display(result.groups, result.rights);
+    enable(false);
+    Util.PROJECT_SVC.changeProjectAccess( //
+        getProjectKey(), //
+        access.getRevision(), //
+        message, //
+        access.getLocal(), //
+        new GerritCallback<ProjectAccess>() {
+          @Override
+          public void onSuccess(ProjectAccess access) {
+            enable(true);
+            commitMessage.setText("");
+            displayReadOnly(access);
+          }
 
-    rightEditor.setVisible(result.canModifyAccess);
-    delRight.setVisible(rights.getCanDelete());
+          @Override
+          public void onFailure(Throwable caught) {
+            enable(true);
+            super.onFailure(caught);
+          }
+        });
   }
 
-  private void doDeleteRefRights(final HashSet<RefRight.Key> refRightIds) {
-    if (!refRightIds.isEmpty()) {
-      Util.PROJECT_SVC.deleteRight(getProjectKey(), refRightIds,
-          new GerritCallback<ProjectDetail>() {
-        @Override
-        public void onSuccess(final ProjectDetail result) {
-          //The user could no longer modify access after deleting a ref right.
-          display(result);
-        }
-      });
-    }
-  }
-
-  private class RightsTable extends FancyFlexTable<InheritedRefRight> {
-    boolean canDelete;
-    Map<AccountGroup.Id, AccountGroup> groups;
-
-    RightsTable() {
-      table.setWidth("");
-      table.setText(0, 2, Util.C.columnRightOrigin());
-      table.setText(0, 3, Util.C.columnApprovalCategory());
-      table.setText(0, 4, Util.C.columnGroupName());
-      table.setText(0, 5, Util.C.columnRefName());
-      table.setText(0, 6, Util.C.columnRightRange());
-
-      final FlexCellFormatter fmt = table.getFlexCellFormatter();
-      fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().iconHeader());
-      fmt.addStyleName(0, 2, Gerrit.RESOURCES.css().dataHeader());
-      fmt.addStyleName(0, 3, Gerrit.RESOURCES.css().dataHeader());
-      fmt.addStyleName(0, 4, Gerrit.RESOURCES.css().dataHeader());
-      fmt.addStyleName(0, 5, Gerrit.RESOURCES.css().dataHeader());
-      fmt.addStyleName(0, 6, Gerrit.RESOURCES.css().dataHeader());
-
-      table.addClickHandler(new ClickHandler() {
-        @Override
-        public void onClick(final ClickEvent event) {
-          onOpenRow(table.getCellForEvent(event).getRowIndex());
-        }
-      });
-    }
-
-    HashSet<RefRight.Key> getRefRightIdsChecked() {
-      final HashSet<RefRight.Key> refRightIds = new HashSet<RefRight.Key>();
-      for (int row = 1; row < table.getRowCount(); row++) {
-        RefRight r = getRowItem(row).getRight();
-        if (r != null && table.getWidget(row, 1) instanceof CheckBox
-            && ((CheckBox) table.getWidget(row, 1)).getValue()) {
-          refRightIds.add(r.getKey());
-        }
-      }
-      return refRightIds;
-    }
-
-    void display(final Map<AccountGroup.Id, AccountGroup> grps,
-        final List<InheritedRefRight> refRights) {
-      groups = grps;
-      canDelete = false;
-
-      while (1 < table.getRowCount())
-        table.removeRow(table.getRowCount() - 1);
-
-      for (final InheritedRefRight r : refRights) {
-        final int row = table.getRowCount();
-        table.insertRow(row);
-        if (! showInherited.getValue() && r.isInherited()) {
-          table.getRowFormatter().setVisible(row, false);
-        }
-        applyDataRowStyle(row);
-        populate(row, r);
-      }
-    }
-
-    protected void onOpenRow(final int row) {
-      if (row > 0) {
-        RefRight right = getRowItem(row).getRight();
-        rightEditor.load(right, groups.get(right.getAccountGroupId()));
-      }
-    }
-
-    void populate(final int row, final InheritedRefRight r) {
-      final GerritConfig config = Gerrit.getConfig();
-      final RefRight right = r.getRight();
-      final ApprovalType ar =
-          config.getApprovalTypes().getApprovalType(
-              right.getApprovalCategoryId());
-      final AccountGroup group = groups.get(right.getAccountGroupId());
-
-      if (r.isInherited() || !r.isOwner()) {
-        table.setText(row, 1, "");
-      } else {
-        table.setWidget(row, 1, new CheckBox());
-        canDelete = true;
-      }
-
-      if (r.isInherited()) {
-        Project.NameKey fromProject = right.getKey().getProjectNameKey();
-        table.setWidget(row, 2, new Hyperlink(fromProject.get(), Dispatcher
-            .toProjectAdmin(fromProject, ACCESS)));
-      } else {
-        table.setText(row, 2, "");
-      }
-
-      table.setText(row, 3, ar != null ? ar.getCategory().getName()
-                                       : right.getApprovalCategoryId().get() );
-
-      if (group != null) {
-        table.setWidget(row, 4, new Hyperlink(group.getName(), Dispatcher
-            .toAccountGroup(group.getId())));
-      } else {
-        table.setText(row, 4, Util.M.deletedGroup(right.getAccountGroupId()
-            .get()));
-      }
-
-      table.setText(row, 5, right.getRefPatternForDisplay());
-
-      {
-        final SafeHtmlBuilder m = new SafeHtmlBuilder();
-        final ApprovalCategoryValue min, max;
-        min = ar != null ? ar.getValue(right.getMinValue()) : null;
-        max = ar != null ? ar.getValue(right.getMaxValue()) : null;
-
-        if (ar != null && ar.getCategory().isRange()) {
-          formatValue(m, right.getMinValue(), min);
-          m.br();
-        }
-        formatValue(m, right.getMaxValue(), max);
-        SafeHtml.set(table, row, 6, m);
-      }
-
-      final FlexCellFormatter fmt = table.getFlexCellFormatter();
-      fmt.addStyleName(row, 1, Gerrit.RESOURCES.css().iconCell());
-      fmt.addStyleName(row, 2, Gerrit.RESOURCES.css().dataCell());
-      fmt.addStyleName(row, 3, Gerrit.RESOURCES.css().dataCell());
-      fmt.addStyleName(row, 4, Gerrit.RESOURCES.css().dataCell());
-      fmt.addStyleName(row, 5, Gerrit.RESOURCES.css().dataCell());
-      fmt.addStyleName(row, 6, Gerrit.RESOURCES.css().dataCell());
-      fmt.addStyleName(row, 6, Gerrit.RESOURCES.css()
-          .projectAdminApprovalCategoryRangeLine());
-
-      setRowItem(row, r);
-    }
-
-    public void showInherited(boolean visible) {
-      for (int r = 0; r < table.getRowCount(); r++) {
-        if (getRowItem(r) != null && getRowItem(r).isInherited()) {
-          table.getRowFormatter().setVisible(r, visible);
-        }
-      }
-    }
-
-    private void formatValue(final SafeHtmlBuilder m, final short v,
-        final ApprovalCategoryValue e) {
-      m.openSpan();
-      m
-          .setStyleName(Gerrit.RESOURCES.css()
-              .projectAdminApprovalCategoryValue());
-      if (v == 0) {
-        m.append(' ');
-      } else if (v > 0) {
-        m.append('+');
-      }
-      m.append(v);
-      m.closeSpan();
-      if (e != null) {
-        m.append(": ");
-        m.append(e.getName());
-      }
-    }
-
-    private boolean getCanDelete() {
-      return canDelete;
-    }
+  private void enable(boolean enabled) {
+    commitMessage.setEnabled(enabled);
+    commit.setEnabled(enabled);
+    cancel1.setEnabled(enabled);
+    cancel2.setEnabled(enabled);
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.ui.xml
new file mode 100644
index 0000000..2536159
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.ui.xml
@@ -0,0 +1,74 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<ui:UiBinder
+  xmlns:ui='urn:ui:com.google.gwt.uibinder'
+  xmlns:g='urn:import:com.google.gwt.user.client.ui'
+  xmlns:my='urn:import:com.google.gerrit.client.admin'
+  xmlns:expui='urn:import:com.google.gwtexpui.globalkey.client'
+  ui:generateFormat='com.google.gwt.i18n.rebind.format.PropertiesFormat'
+  ui:generateKeys='com.google.gwt.i18n.rebind.keygen.MD5KeyGenerator'
+  ui:generateLocales='default,en'
+  >
+<ui:style>
+  @external .gwt-TextArea;
+
+  .commitMessage {
+    margin-top: 2em;
+  }
+  .commitMessage .gwt-TextArea {
+    margin: 5px 5px 5px 5px;
+  }
+</ui:style>
+
+<g:HTMLPanel>
+  <div ui:field='editTools'>
+    <g:Button
+        ui:field='edit'
+        text='Edit'>
+      <ui:attribute name='text'/>
+    </g:Button>
+    <g:Button
+        ui:field='cancel1'
+        text='Cancel'>
+      <ui:attribute name='text'/>
+    </g:Button>
+  </div>
+  <my:ProjectAccessEditor ui:field='accessEditor'/>
+  <div ui:field='commitTools'>
+    <div class='{style.commitMessage}'>
+      <ui:msg>Commit Message (optional):</ui:msg><br/>
+      <expui:NpTextArea
+          ui:field='commitMessage'
+          visibleLines='4'
+          characterWidth='60'
+          spellCheck='true'
+          />
+    </div>
+    <g:Button
+        ui:field='commit'
+        text='Save Changes'>
+      <ui:attribute name='text'/>
+    </g:Button>
+    <g:Button
+        ui:field='cancel2'
+        text='Cancel'>
+      <ui:attribute name='text'/>
+    </g:Button>
+  </div>
+  <div style='width: 35em; visibility: hidden;' />
+</g:HTMLPanel>
+</ui:UiBinder>
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 8c8f2f0..f3ecbb3 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
@@ -17,16 +17,16 @@
 import com.google.gerrit.client.ConfirmationCallback;
 import com.google.gerrit.client.ConfirmationDialog;
 import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.GitwebLink;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
 import com.google.gerrit.client.ui.FancyFlexTable;
 import com.google.gerrit.client.ui.HintTextBox;
-import com.google.gerrit.common.data.GitwebLink;
 import com.google.gerrit.common.data.ListBranchesResult;
 import com.google.gerrit.common.errors.InvalidNameException;
 import com.google.gerrit.common.errors.InvalidRevisionException;
-import com.google.gerrit.reviewdb.Branch;
-import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gwt.core.client.Scheduler;
 import com.google.gwt.core.client.Scheduler.ScheduledCommand;
 import com.google.gwt.event.dom.client.ClickEvent;
@@ -40,8 +40,8 @@
 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 java.util.HashSet;
@@ -226,35 +226,38 @@
       fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().iconHeader());
       fmt.addStyleName(0, 2, Gerrit.RESOURCES.css().dataHeader());
       fmt.addStyleName(0, 3, Gerrit.RESOURCES.css().dataHeader());
-      if (Gerrit.getConfig().getGitwebLink() != null) {
+      if (Gerrit.getGitwebLink() != null) {
         fmt.addStyleName(0, 4, Gerrit.RESOURCES.css().dataHeader());
       }
     }
 
     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,
@@ -290,7 +293,7 @@
     }
 
     void populate(final int row, final Branch k) {
-      final GitwebLink c = Gerrit.getConfig().getGitwebLink();
+      final GitwebLink c = Gerrit.getGitwebLink();
 
       if (k.getCanDelete()) {
         table.setWidget(row, 1, new CheckBox());
@@ -308,16 +311,24 @@
       }
 
       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())));
       }
 
       final FlexCellFormatter fmt = table.getFlexCellFormatter();
-      fmt.addStyleName(row, 1, Gerrit.RESOURCES.css().iconCell());
-      fmt.addStyleName(row, 2, Gerrit.RESOURCES.css().dataCell());
-      fmt.addStyleName(row, 3, Gerrit.RESOURCES.css().dataCell());
+      String iconCellStyle = Gerrit.RESOURCES.css().iconCell();
+      String dataCellStyle = Gerrit.RESOURCES.css().dataCell();
+      if ("refs/meta/config".equals(k.getShortName())
+          || "HEAD".equals(k.getShortName())) {
+        iconCellStyle = Gerrit.RESOURCES.css().specialBranchIconCell();
+        dataCellStyle = Gerrit.RESOURCES.css().specialBranchDataCell();
+        fmt.setStyleName(row, 0, iconCellStyle);
+      }
+      fmt.addStyleName(row, 1, iconCellStyle);
+      fmt.addStyleName(row, 2, dataCellStyle);
+      fmt.addStyleName(row, 3, dataCellStyle);
       if (c != null) {
-        fmt.addStyleName(row, 4, Gerrit.RESOURCES.css().dataCell());
+        fmt.addStyleName(row, 4, dataCellStyle);
       }
 
       setRowItem(row, k);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectInfoScreen.java
index fb35855..944a169 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
@@ -20,8 +20,8 @@
 import com.google.gerrit.client.ui.OnEditEnabler;
 import com.google.gerrit.client.ui.SmallHeading;
 import com.google.gerrit.common.data.ProjectDetail;
-import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.Project.SubmitType;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.Project.SubmitType;
 import com.google.gwt.event.dom.client.ChangeEvent;
 import com.google.gwt.event.dom.client.ChangeHandler;
 import com.google.gwt.event.dom.client.ClickEvent;
@@ -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..3599c3c 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.reviewdb.Project;
+import com.google.gerrit.common.data.ProjectList;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gwt.user.client.History;
 import com.google.gwt.user.client.ui.VerticalPanel;
 
-import 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/ProjectScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectScreen.java
index 392f295..ccfe2e6 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectScreen.java
@@ -18,7 +18,7 @@
 
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.ui.MenuScreen;
-import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.reviewdb.client.Project;
 
 public abstract class ProjectScreen extends MenuScreen {
   public static final String INFO = "info";
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/RangeBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/RangeBox.java
new file mode 100644
index 0000000..6ff9bff
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/RangeBox.java
@@ -0,0 +1,90 @@
+// 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.gwt.editor.client.IsEditor;
+import com.google.gwt.editor.client.adapters.TakesValueEditor;
+import com.google.gwt.text.shared.Renderer;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.IntegerBox;
+import com.google.gwt.user.client.ui.ValueBoxBase.TextAlignment;
+import com.google.gwt.user.client.ui.ValueListBox;
+
+import java.io.IOException;
+
+abstract class RangeBox extends Composite implements
+    IsEditor<TakesValueEditor<Integer>> {
+  static final RangeRenderer rangeRenderer = new RangeRenderer();
+
+  private static class RangeRenderer implements Renderer<Integer> {
+    @Override
+    public String render(Integer object) {
+      if (0 <= object) {
+        return "+" + object;
+      } else {
+        return String.valueOf(object);
+      }
+    }
+
+    @Override
+    public void render(Integer object, Appendable appendable)
+        throws IOException {
+      appendable.append(render(object));
+    }
+  }
+
+  static class List extends RangeBox {
+    final ValueListBox<Integer> list;
+
+    List() {
+      list = new ValueListBox<Integer>(rangeRenderer);
+      initWidget(list);
+    }
+
+    @Override
+    void setEnabled(boolean on) {
+      DOM.setElementPropertyBoolean(list.getElement(), "disabled", !on);
+    }
+
+    @Override
+    public TakesValueEditor<Integer> asEditor() {
+      return list.asEditor();
+    }
+  }
+
+  static class Box extends RangeBox {
+    private final IntegerBox box;
+
+    Box() {
+      box = new IntegerBox();
+      box.setVisibleLength(10);
+      box.setAlignment(TextAlignment.RIGHT);
+      initWidget(box);
+    }
+
+    @Override
+    void setEnabled(boolean on) {
+      DOM.setElementPropertyBoolean(box.getElement(), "disabled", !on);
+    }
+
+    @Override
+    public TakesValueEditor<Integer> asEditor() {
+      return box.asEditor();
+    }
+  }
+
+  abstract void setEnabled(boolean on);
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/RefPatternBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/RefPatternBox.java
new file mode 100644
index 0000000..0cf5d48
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/RefPatternBox.java
@@ -0,0 +1,90 @@
+// 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.gwt.dom.client.Document;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.dom.client.KeyPressHandler;
+import com.google.gwt.text.shared.Parser;
+import com.google.gwt.text.shared.Renderer;
+import com.google.gwt.user.client.ui.ValueBox;
+import com.google.gwtexpui.globalkey.client.GlobalKey;
+
+import java.io.IOException;
+import java.text.ParseException;
+
+public class RefPatternBox extends ValueBox<String> {
+  private static final Renderer<String> RENDERER = new Renderer<String>() {
+    public String render(String ref) {
+      return ref;
+    }
+
+    public void render(String ref, Appendable dst) throws IOException {
+      dst.append(render(ref));
+    }
+  };
+
+  private static final Parser<String> PARSER = new Parser<String>() {
+    public String parse(CharSequence text) throws ParseException {
+      String ref = text.toString();
+
+      if (ref.isEmpty()) {
+        throw new ParseException(Util.C.refErrorEmpty(), 0);
+      }
+
+      if (ref.charAt(0) == '/') {
+        throw new ParseException(Util.C.refErrorBeginSlash(), 0);
+      }
+
+      if (ref.charAt(0) == '^') {
+        if (!ref.startsWith("^refs/")) {
+          ref = "^refs/heads/" + ref.substring(1);
+        }
+      } else if (!ref.startsWith("refs/")) {
+        ref = "refs/heads/" + ref;
+      }
+
+      for (int i = 0; i < ref.length(); i++) {
+        final char c = ref.charAt(i);
+
+        if (c == '/' && 0 < i && ref.charAt(i - 1) == '/') {
+          throw new ParseException(Util.C.refErrorDoubleSlash(), i);
+        }
+
+        if (c == ' ') {
+          throw new ParseException(Util.C.refErrorNoSpace(), i);
+        }
+
+        if (c < ' ') {
+          throw new ParseException(Util.C.refErrorPrintable(), i);
+        }
+      }
+      return ref;
+    }
+  };
+
+  public RefPatternBox() {
+    super(Document.get().createTextInputElement(), RENDERER, PARSER);
+    addKeyPressHandler(GlobalKey.STOP_PROPAGATION);
+    addKeyPressHandler(new KeyPressHandler() {
+      @Override
+      public void onKeyPress(KeyPressEvent event) {
+        if (event.getCharCode() == ' ') {
+          event.preventDefault();
+        }
+      }
+    });
+  }
+}
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 4167116..aa5b7b6 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
@@ -16,7 +16,7 @@
 
 import com.google.gerrit.common.data.GroupAdminService;
 import com.google.gerrit.common.data.ProjectAdminService;
-import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gwt.core.client.GWT;
 import com.google.gwtjsonrpc.client.JsonUtil;
 
@@ -32,6 +32,8 @@
 
     PROJECT_SVC = GWT.create(ProjectAdminService.class);
     JsonUtil.bind(PROJECT_SVC, "rpc/ProjectAdminService");
+
+    AdminResources.I.css().ensureInjected();
   }
 
   public static String toLongString(final Project.SubmitType type) {
@@ -51,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/admin/ValueEditor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ValueEditor.java
new file mode 100644
index 0000000..a7463b4
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ValueEditor.java
@@ -0,0 +1,213 @@
+// 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.gwt.core.client.GWT;
+import com.google.gwt.dom.client.DivElement;
+import com.google.gwt.dom.client.NativeEvent;
+import com.google.gwt.dom.client.Style.Display;
+import com.google.gwt.editor.client.EditorError;
+import com.google.gwt.editor.client.HasEditorErrors;
+import com.google.gwt.editor.client.IsEditor;
+import com.google.gwt.editor.client.LeafValueEditor;
+import com.google.gwt.editor.ui.client.adapters.ValueBoxEditor;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.DoubleClickEvent;
+import com.google.gwt.event.dom.client.DoubleClickHandler;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiChild;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.user.client.ui.Composite;
+import com.google.gwt.user.client.ui.Focusable;
+import com.google.gwt.user.client.ui.Image;
+import com.google.gwt.user.client.ui.Label;
+import com.google.gwt.user.client.ui.SimplePanel;
+import com.google.gwt.user.client.ui.ValueBoxBase;
+import com.google.gwt.user.client.ui.Widget;
+
+import java.text.ParseException;
+import java.util.List;
+
+public class ValueEditor<T> extends Composite implements HasEditorErrors<T>,
+    IsEditor<ValueBoxEditor<T>>, LeafValueEditor<T>, Focusable {
+  interface Binder extends UiBinder<Widget, ValueEditor<?>> {
+  }
+
+  static final Binder uiBinder = GWT.create(Binder.class);
+
+  @UiField
+  SimplePanel textPanel;
+  private Label textLabel;
+  private StartEditHandlers startHandlers;
+
+  @UiField
+  Image editIcon;
+
+  @UiField
+  SimplePanel editPanel;
+
+  @UiField
+  DivElement errorLabel;
+
+  private ValueBoxBase<T> editChild;
+  private ValueBoxEditor<T> editProxy;
+  private boolean ignoreEditorValue;
+  private T value;
+
+  public ValueEditor() {
+    startHandlers = new StartEditHandlers();
+    initWidget(uiBinder.createAndBindUi(this));
+    editPanel.setVisible(false);
+    editIcon.addClickHandler(startHandlers);
+  }
+
+  public void edit() {
+    textPanel.removeFromParent();
+    textPanel = null;
+    textLabel = null;
+
+    editIcon.removeFromParent();
+    editIcon = null;
+    startHandlers = null;
+
+    editPanel.setVisible(true);
+  }
+
+  public ValueBoxEditor<T> asEditor() {
+    if (editProxy == null) {
+      editProxy = new EditorProxy();
+    }
+    return editProxy;
+  }
+
+  @Override
+  public T getValue() {
+    return ignoreEditorValue ? value : asEditor().getValue();
+  }
+
+  @Override
+  public void setValue(T value) {
+    this.value = value;
+    asEditor().setValue(value);
+  }
+
+  void setIgnoreEditorValue(boolean off) {
+    ignoreEditorValue = off;
+  }
+
+  public void setEditTitle(String title) {
+    editIcon.setTitle(title);
+  }
+
+  @UiChild(limit = 1, tagname = "display")
+  public void setDisplay(Label widget) {
+    textLabel = widget;
+    textPanel.add(textLabel);
+
+    textLabel.addClickHandler(startHandlers);
+    textLabel.addDoubleClickHandler(startHandlers);
+  }
+
+  @UiChild(limit = 1, tagname = "editor")
+  public void setEditor(ValueBoxBase<T> widget) {
+    editChild = widget;
+    editPanel.add(editChild);
+    editProxy = null;
+  }
+
+  public void setEnabled(boolean enabled) {
+    editIcon.setVisible(enabled);
+    startHandlers.enabled = enabled;
+  }
+
+  public void showErrors(List<EditorError> errors) {
+    StringBuilder buf = new StringBuilder();
+    for (EditorError error : errors) {
+      if (error.getEditor().equals(editProxy)) {
+        buf.append("\n");
+        if (error.getUserData() instanceof ParseException) {
+          buf.append(((ParseException) error.getUserData()).getMessage());
+        } else {
+          buf.append(error.getMessage());
+        }
+      }
+    }
+
+    if (0 < buf.length()) {
+      errorLabel.setInnerText(buf.substring(1));
+      errorLabel.getStyle().setDisplay(Display.BLOCK);
+    } else {
+      errorLabel.setInnerText("");
+      errorLabel.getStyle().setDisplay(Display.NONE);
+    }
+  }
+
+  @Override
+  public void setAccessKey(char key) {
+    editChild.setAccessKey(key);
+  }
+
+  @Override
+  public void setFocus(boolean focused) {
+    editChild.setFocus(focused);
+    if (focused) {
+      editChild.setCursorPos(editChild.getText().length());
+    }
+  }
+
+  @Override
+  public int getTabIndex() {
+    return editChild.getTabIndex();
+  }
+
+  @Override
+  public void setTabIndex(int index) {
+    editChild.setTabIndex(index);
+  }
+
+  private class StartEditHandlers implements ClickHandler, DoubleClickHandler {
+    boolean enabled;
+
+    @Override
+    public void onClick(ClickEvent event) {
+      if (enabled && event.getNativeButton() == NativeEvent.BUTTON_LEFT) {
+        edit();
+      }
+    }
+
+    @Override
+    public void onDoubleClick(DoubleClickEvent event) {
+      if (enabled && event.getNativeButton() == NativeEvent.BUTTON_LEFT) {
+        edit();
+      }
+    }
+  }
+
+  private class EditorProxy extends ValueBoxEditor<T> {
+    EditorProxy() {
+      super(editChild);
+    }
+
+    @Override
+    public void setValue(T value) {
+      super.setValue(value);
+      if (textLabel == null) {
+        setDisplay(new Label());
+      }
+      textLabel.setText(editChild.getText());
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ValueEditor.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ValueEditor.ui.xml
new file mode 100644
index 0000000..9862848
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ValueEditor.ui.xml
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+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.
+-->
+<ui:UiBinder
+  xmlns:ui='urn:ui:com.google.gwt.uibinder'
+  xmlns:g='urn:import:com.google.gwt.user.client.ui'
+  >
+<ui:with field='res' type='com.google.gerrit.client.admin.AdminResources'/>
+<ui:style>
+  .panel {
+    position: relative;
+    white-space: nowrap;
+  }
+
+  .textPanel {
+    width: 100%;
+    padding-right: 21px;
+  }
+
+  .editIcon {
+    position: absolute;
+    top: 0;
+    right: 5px;
+  }
+
+  .editPanel {
+    width: 100%;
+  }
+
+  .errorLabel {
+    display: none;
+    color: red;
+    white-space: pre;
+  }
+</ui:style>
+<g:HTMLPanel stylePrimaryName='{style.panel}'>
+  <g:Image
+      ui:field='editIcon'
+      resource='{res.editText}'
+      stylePrimaryName='{style.editIcon}'
+      title='Edit'>
+    <ui:attribute name='title'/>
+  </g:Image>
+  <g:SimplePanel ui:field='textPanel' stylePrimaryName='{style.textPanel}'/>
+
+  <g:SimplePanel ui:field='editPanel' stylePrimaryName='{style.editPanel}'/>
+  <div
+      ui:field='errorLabel'
+      class='{style.errorLabel}'/>
+</g:HTMLPanel>
+</ui:UiBinder>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/admin.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/admin.css
new file mode 100644
index 0000000..eca4823
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/admin.css
@@ -0,0 +1,53 @@
+/* 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.
+ */
+
+@eval selectionColor com.google.gerrit.client.Gerrit.getTheme().selectionColor;
+@eval textColor com.google.gerrit.client.Gerrit.getTheme().textColor;
+@def deletedBackground #a9a9a9;
+
+@sprite .deleteIcon {
+  gwt-image: 'deleteNormal';
+  border: none;
+}
+
+@sprite .deleteIcon:hover {
+  gwt-image: 'deleteHover';
+  border: none;
+}
+
+@sprite .undoIcon {
+  gwt-image: 'undoNormal';
+  border: none;
+}
+
+.deleted {
+  background-color: deletedBackground;
+  color: #ffffff;
+  white-space: nowrap;
+  padding-left: 50px;
+}
+
+.deleted:hover {
+  background-color: selectionColor;
+  color: textColor;
+ }
+
+.deletedBorder {
+  background: 1px solid deletedBackground;
+}
+
+.deleteSectionHover {
+  background-color: selectionColor !important;
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/deleteHover.png b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/deleteHover.png
new file mode 100644
index 0000000..839e8ef
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/deleteHover.png
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/deleteNormal.png b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/deleteNormal.png
new file mode 100644
index 0000000..ffddb6f
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/deleteNormal.png
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/editText.png b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/editText.png
new file mode 100644
index 0000000..188e1c1
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/editText.png
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/undoNormal.png b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/undoNormal.png
new file mode 100644
index 0000000..8b0fef9
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/undoNormal.png
Binary files differ
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..ccdd1ec 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.client.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 dfcddf5..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
@@ -201,18 +201,24 @@
       public void onSuccess(final LoginResult result) {
         if (result.success) {
           String to = token;
-          if (result.isNew && !to.startsWith(PageLinks.REGISTER + ",")) {
-            to = PageLinks.REGISTER + "," + to;
+          if (!to.startsWith("/")) {
+            to = "/" + to;
           }
-
-          // Unfortunately we no longer support updating the web UI when the
-          // user signs in. Instead we must force a reload of the page, but
-          // that isn't easy because we might need to change the anchor. So
-          // we bounce through a little redirection servlet on the server.
-          //
-          Location.replace(Location.getPath() + "login/" + to);
+          if (result.isNew && !token.startsWith(PageLinks.REGISTER + "/")) {
+            to = PageLinks.REGISTER + to;
+          }
+          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/AccountDashboardScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java
index f5f3245..1d881b6 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/AccountDashboardScreen.java
@@ -22,7 +22,7 @@
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.data.AccountDashboardInfo;
 import com.google.gerrit.common.data.AccountInfo;
-import com.google.gerrit.reviewdb.Account;
+import com.google.gerrit.reviewdb.client.Account;
 
 
 public class AccountDashboardScreen extends Screen implements ChangeListScreen {
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 a20b02e..09716cc 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
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.client.changes;
 
+import com.google.gerrit.client.ConfirmationCallback;
+import com.google.gerrit.client.ConfirmationDialog;
 import com.google.gerrit.client.ErrorDialog;
 import com.google.gerrit.client.FormatUtil;
 import com.google.gerrit.client.Gerrit;
@@ -21,16 +23,17 @@
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.ui.AccountDashboardLink;
 import com.google.gerrit.client.ui.AddMemberBox;
+import com.google.gerrit.client.ui.ReviewerSuggestOracle;
 import com.google.gerrit.common.data.AccountInfoCache;
 import com.google.gerrit.common.data.ApprovalDetail;
 import com.google.gerrit.common.data.ApprovalType;
+import com.google.gerrit.common.data.ApprovalTypes;
 import com.google.gerrit.common.data.ChangeDetail;
 import com.google.gerrit.common.data.ReviewerResult;
-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.PatchSetApproval;
+import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.user.client.DOM;
@@ -38,33 +41,35 @@
 import com.google.gwt.user.client.ui.Composite;
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.Grid;
+import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
 import com.google.gwt.user.client.ui.Image;
 import com.google.gwt.user.client.ui.Panel;
 import com.google.gwt.user.client.ui.PushButton;
 import com.google.gwt.user.client.ui.Widget;
-import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
 import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
 
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.HashSet;
 import java.util.List;
-import java.util.Map;
-import java.util.Set;
 
 /** Displays a table of {@link ApprovalDetail} objects for a change record. */
 public class ApprovalTable extends Composite {
-  private final List<ApprovalType> types;
+  private final ApprovalTypes types;
   private final Grid table;
   private final Widget missing;
   private final Panel addReviewer;
+  private final ReviewerSuggestOracle reviewerSuggestOracle;
   private final AddMemberBox addMemberBox;
   private Change.Id changeId;
   private AccountInfoCache accountCache = AccountInfoCache.empty();
 
   public ApprovalTable() {
-    types = Gerrit.getConfig().getApprovalTypes().getApprovalTypes();
-    table = new Grid(1, 3 + types.size());
+    types = Gerrit.getConfig().getApprovalTypes();
+    table = new Grid(1, 3);
     table.addStyleName(Gerrit.RESOURCES.css().infoTable());
-    displayHeader();
 
     missing = new Widget() {
       {
@@ -75,8 +80,10 @@
 
     addReviewer = new FlowPanel();
     addReviewer.setStyleName(Gerrit.RESOURCES.css().addReviewer());
-    addMemberBox = new AddMemberBox();
-    addMemberBox.setAddButtonText(Util.C.approvalTableAddReviewer());
+    reviewerSuggestOracle = new ReviewerSuggestOracle();
+    addMemberBox =
+        new AddMemberBox(Util.C.approvalTableAddReviewer(),
+            Util.C.approvalTableAddReviewerHint(), reviewerSuggestOracle);
     addMemberBox.addClickHandler(new ClickHandler() {
       @Override
       public void onClick(final ClickEvent event) {
@@ -95,7 +102,9 @@
     setStyleName(Gerrit.RESOURCES.css().approvalTable());
   }
 
-  private void displayHeader() {
+  private void displayHeader(List<String> labels) {
+    table.resizeColumns(2 + labels.size());
+
     final CellFormatter fmt = table.getCellFormatter();
     int col = 0;
 
@@ -107,16 +116,12 @@
     fmt.setStyleName(0, col, Gerrit.RESOURCES.css().header());
     col++;
 
-    for (final ApprovalType t : types) {
-      table.setText(0, col, t.getCategory().getName());
+    for (String name : labels) {
+      table.setText(0, col, name);
       fmt.setStyleName(0, col, Gerrit.RESOURCES.css().header());
       col++;
     }
-
-    table.clearCell(0, col);
-    fmt.setStyleName(0, col, Gerrit.RESOURCES.css().header());
-    fmt.addStyleName(0, col, Gerrit.RESOURCES.css().rightmost());
-    col++;
+    fmt.addStyleName(0, col - 1, Gerrit.RESOURCES.css().rightmost());
   }
 
   public void setAccountInfoCache(final AccountInfoCache aic) {
@@ -128,64 +133,160 @@
     return AccountDashboardLink.link(accountCache, id);
   }
 
-  public void display(final Change change, final Set<ApprovalCategory.Id> need,
-      final List<ApprovalDetail> rows) {
-    changeId = change.getId();
+  void display(ChangeDetail detail) {
+    changeId = detail.getChange().getId();
+    reviewerSuggestOracle.setChange(changeId);
 
-    if (rows.isEmpty()) {
-      table.setVisible(false);
-    } else {
-      table.resizeRows(1 + rows.size());
-      for (int i = 0; i < rows.size(); i++) {
-        displayRow(i + 1, rows.get(i), change);
-      }
-      table.setVisible(true);
-    }
+    List<String> columns = new ArrayList<String>();
+    List<ApprovalDetail> rows = detail.getApprovals();
 
     final Element missingList = missing.getElement();
     while (DOM.getChildCount(missingList) > 0) {
       DOM.removeChild(missingList, DOM.getChild(missingList, 0));
     }
-
     missing.setVisible(false);
-    if (need != null) {
-      for (final ApprovalType at : types) {
-        if (need.contains(at.getCategory().getId())) {
-          final Element li = DOM.createElement("li");
-          li.setClassName(Gerrit.RESOURCES.css().missingApproval());
-          DOM.setInnerText(li, Util.M.needApproval(at.getCategory().getName(),
-              at.getMax().formatValue(), at.getMax().getName()));
-          DOM.appendChild(missingList, li);
-          missing.setVisible(true);
+
+    if (detail.getSubmitRecords() != null) {
+      HashSet<String> reportedMissing = new HashSet<String>();
+
+      HashMap<Account.Id, ApprovalDetail> byUser =
+          new HashMap<Account.Id, ApprovalDetail>();
+      for (ApprovalDetail ad : detail.getApprovals()) {
+        byUser.put(ad.getAccount(), ad);
+      }
+
+      for (SubmitRecord rec : detail.getSubmitRecords()) {
+        if (rec.labels == null) {
+          continue;
         }
+
+        for (SubmitRecord.Label lbl : rec.labels) {
+          if (!columns.contains(lbl.label)) {
+            columns.add(lbl.label);
+          }
+
+          switch (lbl.status) {
+            case OK: {
+              ApprovalDetail ad = byUser.get(lbl.appliedBy);
+              if (ad != null) {
+                ad.approved(lbl.label);
+              }
+              break;
+            }
+
+            case REJECT: {
+              ApprovalDetail ad = byUser.get(lbl.appliedBy);
+              if (ad != null) {
+                ad.rejected(lbl.label);
+              }
+              break;
+            }
+
+            case NEED:
+            case IMPOSSIBLE:
+              if (reportedMissing.add(lbl.label)) {
+                Element li = DOM.createElement("li");
+                li.setClassName(Gerrit.RESOURCES.css().missingApproval());
+                DOM.setInnerText(li, Util.M.needApproval(lbl.label));
+                DOM.appendChild(missingList, li);
+              }
+              break;
+          }
+        }
+      }
+      missing.setVisible(!reportedMissing.isEmpty());
+
+    } else {
+      for (ApprovalDetail ad : rows) {
+        for (PatchSetApproval psa : ad.getPatchSetApprovals()) {
+          ApprovalType legacyType = types.byId(psa.getCategoryId());
+          if (legacyType == null) {
+            continue;
+          }
+          String labelName = legacyType.getCategory().getLabelName();
+          if (psa.getValue() == legacyType.getMax().getValue()) {
+            ad.approved(labelName);
+          } else if (psa.getValue() == legacyType.getMin().getValue()) {
+            ad.rejected(labelName);
+          }
+          if (!columns.contains(labelName)) {
+            columns.add(labelName);
+          }
+        }
+        Collections.sort(columns, new Comparator<String>() {
+          @Override
+          public int compare(String o1, String o2) {
+            ApprovalType a = types.byLabel(o1);
+            ApprovalType b = types.byLabel(o2);
+            int cmp = 0;
+            if (a != null && b != null) {
+              cmp = a.getCategory().getPosition() - b.getCategory().getPosition();
+            }
+            if (cmp == 0) {
+              cmp = o1.compareTo(o2);
+            }
+            return cmp;
+          }
+        });
       }
     }
 
-    addReviewer.setVisible(Gerrit.isSignedIn() && change.getStatus().isOpen());
+    if (rows.isEmpty()) {
+      table.setVisible(false);
+    } else {
+      displayHeader(columns);
+      table.resizeRows(1 + rows.size());
+      for (int i = 0; i < rows.size(); i++) {
+        displayRow(i + 1, rows.get(i), detail.getChange(), columns);
+      }
+      table.setVisible(true);
+    }
+
+    addReviewer.setVisible(Gerrit.isSignedIn());
+
+    if (Gerrit.getConfig().testChangeMerge()
+        && !detail.getChange().isMergeable()) {
+      Element li = DOM.createElement("li");
+      li.setClassName(Gerrit.RESOURCES.css().missingApproval());
+      DOM.setInnerText(li, Util.C.messageNeedsRebaseOrHasDependency());
+      DOM.appendChild(missingList, li);
+      missing.setVisible(true);
+    }
   }
 
   private void doAddReviewer() {
-    final String nameEmail = addMemberBox.getText();
-    if (nameEmail.length() == 0) {
+    final String reviewer = addMemberBox.getText();
+    if (reviewer.length() == 0) {
       return;
     }
 
     addMemberBox.setEnabled(false);
     final List<String> reviewers = new ArrayList<String>();
-    reviewers.add(nameEmail);
+    reviewers.add(reviewer);
 
-    PatchUtil.DETAIL_SVC.addReviewers(changeId, reviewers,
+    addReviewers(reviewers, false);
+  }
+
+  private void addReviewers(final List<String> reviewers,
+      final boolean confirmed) {
+    PatchUtil.DETAIL_SVC.addReviewers(changeId, reviewers, confirmed,
         new GerritCallback<ReviewerResult>() {
           public void onSuccess(final ReviewerResult result) {
             addMemberBox.setEnabled(true);
             addMemberBox.setText("");
 
+            final ChangeDetail changeDetail = result.getChange();
+            if (changeDetail != null) {
+              setAccountInfoCache(changeDetail.getAccounts());
+              display(changeDetail);
+            }
+
             if (!result.getErrors().isEmpty()) {
               final SafeHtmlBuilder r = new SafeHtmlBuilder();
               for (final ReviewerResult.Error e : result.getErrors()) {
                 switch (e.getType()) {
-                  case ACCOUNT_NOT_FOUND:
-                    r.append(Util.M.accountNotFound(e.getName()));
+                  case REVIEWER_NOT_FOUND:
+                    r.append(Util.M.reviewerNotFound(e.getName()));
                     break;
 
                   case ACCOUNT_INACTIVE:
@@ -196,6 +297,23 @@
                     r.append(Util.M.changeNotVisibleTo(e.getName()));
                     break;
 
+                  case GROUP_EMPTY:
+                    r.append(Util.M.groupIsEmpty(e.getName()));
+                    break;
+
+                  case GROUP_HAS_TOO_MANY_MEMBERS:
+                    if (result.askForConfirmation() && !confirmed) {
+                      askForConfirmation(e.getName(), result.getMemberCount());
+                      return;
+                    } else {
+                      r.append(Util.M.groupHasTooManyMembers(e.getName()));
+                    }
+                    break;
+
+                  case GROUP_NOT_ALLOWED:
+                    r.append(Util.M.groupIsNotAllowed(e.getName()));
+                    break;
+
                   default:
                     r.append(e.getName());
                     r.append(" - ");
@@ -206,12 +324,25 @@
               }
               new ErrorDialog(r).center();
             }
+          }
 
-            final ChangeDetail r = result.getChange();
-            if (r != null) {
-              setAccountInfoCache(r.getAccounts());
-              display(r.getChange(), r.getMissingApprovals(), r.getApprovals());
-            }
+          private void askForConfirmation(final String groupName,
+              final int memberCount) {
+            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(),
+                    b.toSafeHtml(), new ConfirmationCallback() {
+                      @Override
+                      public void onOk() {
+                        addReviewers(reviewers, true);
+                      }
+                    });
+            confirmationDialog.center();
           }
 
           @Override
@@ -223,10 +354,8 @@
   }
 
   private void displayRow(final int row, final ApprovalDetail ad,
-      final Change change) {
+      final Change change, List<String> columns) {
     final CellFormatter fmt = table.getCellFormatter();
-    final Map<ApprovalCategory.Id, PatchSetApproval> am = ad.getApprovalMap();
-    final StringBuilder hint = new StringBuilder();
     int col = 0;
 
     table.setWidget(row, col++, link(ad.getAccount()));
@@ -250,31 +379,30 @@
     }
     fmt.setStyleName(row, col++, Gerrit.RESOURCES.css().removeReviewerCell());
 
-    for (final ApprovalType type : types) {
+    for (String labelName : columns) {
       fmt.setStyleName(row, col, Gerrit.RESOURCES.css().approvalscore());
 
-      final PatchSetApproval ca = am.get(type.getCategory().getId());
-      if (ca == null || ca.getValue() == 0) {
-        table.clearCell(row, col);
-        col++;
-        continue;
-      }
-
-      final ApprovalCategoryValue acv = type.getValue(ca);
-      if (acv != null) {
-        if (hint.length() > 0) {
-          hint.append("; ");
-        }
-        hint.append(acv.getName());
-      }
-
-      if (type.isMaxNegative(ca)) {
+      if (ad.isRejected(labelName)) {
         table.setWidget(row, col, new Image(Gerrit.RESOURCES.redNot()));
 
-      } else if (type.isMaxPositive(ca)) {
+      } else if (ad.isApproved(labelName)) {
         table.setWidget(row, col, new Image(Gerrit.RESOURCES.greenCheck()));
 
       } else {
+        ApprovalType legacyType = types.byLabel(labelName);
+        if (legacyType == null) {
+          table.clearCell(row, col);
+          col++;
+          continue;
+        }
+
+        PatchSetApproval ca = ad.getPatchSetApproval(legacyType.getCategory().getId());
+        if (ca == null || ca.getValue() == 0) {
+          table.clearCell(row, col);
+          col++;
+          continue;
+        }
+
         String vstr = String.valueOf(ca.getValue());
         if (ca.getValue() > 0) {
           vstr = "+" + vstr;
@@ -288,10 +416,7 @@
       col++;
     }
 
-    table.setText(row, col, hint.toString());
-    fmt.setStyleName(row, col, Gerrit.RESOURCES.css().rightmost());
-    fmt.addStyleName(row, col, Gerrit.RESOURCES.css().approvalhint());
-    col++;
+    fmt.addStyleName(row, col - 1, Gerrit.RESOURCES.css().rightmost());
   }
 
   private void doRemove(final ApprovalDetail ad, final PushButton remove) {
@@ -302,9 +427,20 @@
           public void onSuccess(ReviewerResult result) {
             if (result.getErrors().isEmpty()) {
               final ChangeDetail r = result.getChange();
-              display(r.getChange(), r.getMissingApprovals(), r.getApprovals());
+              display(r);
             } else {
-              new ErrorDialog(result.getErrors().get(0).toString()).center();
+              final ReviewerResult.Error resultError =
+                  result.getErrors().get(0);
+              String message;
+              switch (resultError.getType()) {
+                case REMOVE_NOT_PERMITTED:
+                  message = Util.C.approvalTableRemoveNotPermitted();
+                  break;
+                case COULD_NOT_REMOVE:
+                default:
+                  message = Util.C.approvalTableCouldNotRemove();
+              }
+              new ErrorDialog(message + " " + resultError.getName()).center();
             }
           }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeCache.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeCache.java
new file mode 100644
index 0000000..27f76f6
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeCache.java
@@ -0,0 +1,71 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.changes;
+
+import com.google.gerrit.client.ui.ListenableValue;
+import com.google.gerrit.common.data.ChangeInfo;
+import com.google.gerrit.reviewdb.client.Change;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/** A Cache to store common client side data by change */
+public class ChangeCache {
+  private static Map<Change.Id, ChangeCache> caches =
+    new HashMap<Change.Id, ChangeCache>();
+
+  public static ChangeCache get(Change.Id chg) {
+    ChangeCache cache = caches.get(chg);
+    if (cache == null) {
+      cache = new ChangeCache(chg);
+      caches.put(chg, cache);
+    }
+    return cache;
+  }
+
+  private Change.Id changeId;
+  private ChangeDetailCache detail;
+  private ListenableValue<ChangeInfo> info;
+  private StarCache starred;
+
+  protected ChangeCache(Change.Id chg) {
+    changeId = chg;
+  }
+
+  public Change.Id getChangeId() {
+    return changeId;
+  }
+
+  public ChangeDetailCache getChangeDetailCache() {
+    if (detail == null) {
+      detail = new ChangeDetailCache(changeId);
+    }
+    return detail;
+  }
+
+  public ListenableValue<ChangeInfo> getChangeInfoCache() {
+    if (info == null) {
+      info = new ListenableValue<ChangeInfo>();
+    }
+    return info;
+  }
+
+  public StarCache getStarCache() {
+    if (starred == null) {
+      starred = new StarCache(changeId);
+    }
+    return starred;
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeConstants.java
index e4fe6d2..3372096 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();
 
@@ -78,6 +79,10 @@
 
   String approvalTableReviewer();
   String approvalTableAddReviewer();
+  String approvalTableRemoveNotPermitted();
+  String approvalTableCouldNotRemove();
+  String approvalTableAddReviewerHint();
+  String approvalTableAddManyReviewersConfirmationDialogTitle();
 
   String changeInfoBlockOwner();
   String changeInfoBlockProject();
@@ -87,6 +92,9 @@
   String changeInfoBlockUpdated();
   String changeInfoBlockStatus();
   String changePermalink();
+  String changeInfoBlockCanMerge();
+  String changeInfoBlockCanMergeYes();
+  String changeInfoBlockCanMergeNo();
 
   String includedInTableBranch();
   String includedInTableTag();
@@ -95,6 +103,7 @@
   String messageExpandRecent();
   String messageExpandAll();
   String messageCollapseAll();
+  String messageNeedsRebaseOrHasDependency();
 
   String patchSetInfoAuthor();
   String patchSetInfoCommitter();
@@ -102,19 +111,20 @@
   String patchSetInfoParents();
   String initialCommit();
 
+  String buttonRebaseChange();
+
   String buttonRevertChangeBegin();
   String buttonRevertChangeSend();
-  String buttonRevertChangeCancel();
   String headingRevertMessage();
   String revertChangeTitle();
 
   String buttonAbandonChangeBegin();
   String buttonAbandonChangeSend();
-  String buttonAbandonChangeCancel();
   String headingAbandonMessage();
   String abandonChangeTitle();
   String oldVersionHistory();
   String baseDiffItem();
+  String autoMerge();
 
   String buttonReview();
   String buttonPublishCommentsSend();
@@ -125,13 +135,19 @@
 
   String buttonRestoreChangeBegin();
   String restoreChangeTitle();
-  String buttonRestoreChangeCancel();
   String headingRestoreMessage();
   String buttonRestoreChangeSend();
 
+  String buttonPublishPatchSet();
+
+  String buttonDeleteDraftChange();
+  String buttonDeleteDraftPatchSet();
+
   String pagedChangeListPrev();
   String pagedChangeListNext();
 
+  String draftPatchSetLabel();
+
   String reviewed();
   String submitFailed();
   String buttonClose();
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 7032dda..ad70674 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
@@ -55,6 +56,10 @@
 
 approvalTableReviewer = Reviewer
 approvalTableAddReviewer = Add Reviewer
+approvalTableRemoveNotPermitted = Not allowed to remove reviewer
+approvalTableCouldNotRemove = Could not remove reviewer
+approvalTableAddReviewerHint = Name or Email or Group
+approvalTableAddManyReviewersConfirmationDialogTitle = Adding Group Members as Reviewers
 
 changeInfoBlockOwner = Owner
 changeInfoBlockProject = Project
@@ -64,6 +69,9 @@
 changeInfoBlockUpdated = Updated
 changeInfoBlockStatus = Status
 changePermalink = Permalink
+changeInfoBlockCanMerge = Can Merge
+changeInfoBlockCanMergeYes = Yes
+changeInfoBlockCanMergeNo = No
 
 includedInTableBranch = Branch Name
 includedInTableTag = Tag Name
@@ -72,6 +80,7 @@
 messageExpandRecent = Expand Recent
 messageExpandAll = Expand All
 messageCollapseAll = Collapse All
+messageNeedsRebaseOrHasDependency = Need Rebase or Has Dependency
 
 patchSetInfoAuthor = Author
 patchSetInfoCommitter = Committer
@@ -81,21 +90,21 @@
 
 buttonAbandonChangeBegin = Abandon Change
 buttonAbandonChangeSend = Abandon Change
-buttonAbandonChangeCancel = Cancel
 headingAbandonMessage = Abandon Message:
 abandonChangeTitle = Code Review - Abandon Change
 oldVersionHistory = Old Version History:
 baseDiffItem = Base
+autoMerge = Auto Merge
+
+buttonRebaseChange = Rebase Change
 
 buttonRevertChangeBegin = Revert Change
 buttonRevertChangeSend = Revert Change
-buttonRevertChangeCancel = Cancel
 headingRevertMessage = Revert Commit Message:
 revertChangeTitle = Code Review - Revert Merged Change
 
 buttonRestoreChangeBegin = Restore Change
 restoreChangeTitle = Code Review - Restore Change
-buttonRestoreChangeCancel = Cancel
 headingRestoreMessage = Restore Message:
 buttonRestoreChangeSend = Restore Change
 
@@ -106,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;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDescriptionBlock.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDescriptionBlock.java
index 0532326..9282709 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDescriptionBlock.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDescriptionBlock.java
@@ -15,8 +15,8 @@
 package com.google.gerrit.client.changes;
 
 import com.google.gerrit.common.data.AccountInfoCache;
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.PatchSetInfo;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSetInfo;
 import com.google.gwt.user.client.ui.Composite;
 import com.google.gwt.user.client.ui.HorizontalPanel;
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDetailCache.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDetailCache.java
new file mode 100644
index 0000000..bb28e11
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDetailCache.java
@@ -0,0 +1,79 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.changes;
+
+import com.google.gerrit.client.ui.ListenableValue;
+import com.google.gerrit.common.data.ChangeDetail;
+import com.google.gerrit.reviewdb.client.Change;
+
+import com.google.gwtjsonrpc.common.AsyncCallback;
+import com.google.gwt.user.client.ui.FocusWidget;
+
+public class ChangeDetailCache extends ListenableValue<ChangeDetail> {
+  public static class GerritCallback extends
+      com.google.gerrit.client.rpc.GerritCallback<ChangeDetail> {
+    @Override
+    public void onSuccess(ChangeDetail detail) {
+      setChangeDetail(detail);
+    }
+  }
+
+  /*
+   * GerritCallback which will re-enable a FocusWidget
+   * {@link com.google.gwt.user.client.ui.FocusWidget} if we are returning
+   * with a failed result.
+   *
+   * It is up to the caller to handle the original disabling of the Widget.
+   */
+  public static class GerritWidgetCallback extends GerritCallback {
+    private FocusWidget widget;
+
+    public GerritWidgetCallback(FocusWidget widget) {
+      this.widget = widget;
+    }
+
+    @Override
+    public void onFailure(Throwable caught) {
+      widget.setEnabled(true);
+      super.onFailure(caught);
+    }
+  }
+
+  public static class IgnoreErrorCallback implements AsyncCallback<ChangeDetail> {
+    @Override
+    public void onSuccess(ChangeDetail detail) {
+      setChangeDetail(detail);
+    }
+
+    @Override
+    public void onFailure(Throwable caught) {
+    }
+  }
+
+  public static void setChangeDetail(ChangeDetail detail) {
+    Change.Id chgId = detail.getChange().getId();
+    ChangeCache.get(chgId).getChangeDetailCache().set(detail);
+  }
+
+  private final Change.Id changeId;
+
+  public ChangeDetailCache(final Change.Id chg) {
+    changeId = chg;
+  }
+
+  public void refresh() {
+    Util.DETAIL_SVC.changeDetail(changeId, new GerritCallback());
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfoBlock.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfoBlock.java
index 0c7106e..f8373cc 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfoBlock.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfoBlock.java
@@ -22,8 +22,8 @@
 import com.google.gerrit.client.ui.ChangeLink;
 import com.google.gerrit.client.ui.ProjectLink;
 import com.google.gerrit.common.data.AccountInfoCache;
-import com.google.gerrit.reviewdb.Branch;
-import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
 import com.google.gwt.user.client.ui.Composite;
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.Grid;
@@ -39,13 +39,20 @@
   private static final int R_UPLOADED = 5;
   private static final int R_UPDATED = 6;
   private static final int R_STATUS = 7;
-  private static final int R_PERMALINK = 8;
-  private static final int R_CNT = 9;
+  private static final int R_MERGE_TEST = 8;
+  private final int R_PERMALINK;
+  private static final int R_CNT = 10;
 
   private final Grid table;
 
   public ChangeInfoBlock() {
-    table = new Grid(R_CNT, 2);
+    if (Gerrit.getConfig().testChangeMerge()) {
+      table = new Grid(R_CNT, 2);
+      R_PERMALINK = 9;
+    } else {
+      table = new Grid(R_CNT - 1, 2);
+      R_PERMALINK = 8;
+    }
     table.setStyleName(Gerrit.RESOURCES.css().infoBlock());
     table.addStyleName(Gerrit.RESOURCES.css().changeInfoBlock());
 
@@ -57,6 +64,9 @@
     initRow(R_UPLOADED, Util.C.changeInfoBlockUploaded());
     initRow(R_UPDATED, Util.C.changeInfoBlockUpdated());
     initRow(R_STATUS, Util.C.changeInfoBlockStatus());
+    if (Gerrit.getConfig().testChangeMerge()) {
+      initRow(R_MERGE_TEST, Util.C.changeInfoBlockCanMerge());
+    }
 
     final CellFormatter fmt = table.getCellFormatter();
     fmt.addStyleName(0, 0, Gerrit.RESOURCES.css().topmost());
@@ -91,6 +101,10 @@
     table.setText(R_UPLOADED, 1, mediumFormat(chg.getCreatedOn()));
     table.setText(R_UPDATED, 1, mediumFormat(chg.getLastUpdatedOn()));
     table.setText(R_STATUS, 1, Util.toLongString(chg.getStatus()));
+    if (Gerrit.getConfig().testChangeMerge()) {
+      table.setText(R_MERGE_TEST, 1, chg.isMergeable() ? Util.C
+          .changeInfoBlockCanMergeYes() : Util.C.changeInfoBlockCanMergeNo());
+    }
 
     if (chg.getStatus().isClosed()) {
       table.getCellFormatter().addStyleName(R_STATUS, 1, Gerrit.RESOURCES.css().closedstate());
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java
index cbf4a6b..41dc6c1 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.java
@@ -27,6 +27,7 @@
   String revertChangeDefaultMessage(String commitMsg, String commitId);
 
   String changeScreenTitleId(String changeId);
+  String outdatedHeader(int outdated);
   String patchSetHeader(int id);
   String loadingPatchSet(int id);
   String submitPatchSet(int id);
@@ -43,16 +44,20 @@
   String copiedFrom(String sourcePath);
   String otherFrom(String sourcePath);
 
-  String needApproval(String categoryName, String value, String valueName);
+  String needApproval(String labelName);
   String publishComments(String changeId, int ps);
   String lineHeader(int line);
 
   String changeQueryWindowTitle(String query);
   String changeQueryPageTitle(String query);
 
-  String accountNotFound(String who);
+  String reviewerNotFound(String who);
   String accountInactive(String who);
   String changeNotVisibleTo(String who);
+  String groupIsEmpty(String group);
+  String groupIsNotAllowed(String group);
+  String groupHasTooManyMembers(String group);
+  String groupManyMembersConfirmation(String group, int memberCount);
 
   String anonymousDownload(String protocol);
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
index 5982ad5..40088a1 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
@@ -8,6 +8,7 @@
 revertChangeDefaultMessage = Revert \"{0}\"\n\nThis reverts commit {1}
 
 changeScreenTitleId = Change {0}
+outdatedHeader = Change depends on {0} outdated change(s) and should be rebased on the latest patch sets.
 patchSetHeader = Patch Set {0}
 loadingPatchSet = Loading Patch Set {0} ...
 submitPatchSet = Submit Patch Set {0}
@@ -24,15 +25,19 @@
 copiedFrom = copied from {0}
 otherFrom = from {0}
 
-needApproval = Need {0} {1} ({2})
+needApproval = Need {0}
 publishComments = Change {0} - Patch Set {1}: Publish Comments
 lineHeader = Line {0}:
 
 changeQueryWindowTitle = {0}
 changeQueryPageTitle = Search for {0}
 
-accountNotFound = {0} is not a registered user.
+reviewerNotFound = {0} is neither a registered user nor a group.
 accountInactive = {0} is not an active user.
 changeNotVisibleTo = {0} cannot access the change.
+groupIsEmpty = The group {0} does not have any members to add as reviewers.
+groupIsNotAllowed =  The group {0} cannot be added as reviewer.
+groupHasTooManyMembers = The group {0} has too many members to add them all as reviewers.
+groupManyMembersConfirmation = The group {0} has {1} members. Do you want to add them all as reviewers?
 
 anonymousDownload = Anonymous {0}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java
index edb25e4..8d5e105 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
@@ -14,10 +14,10 @@
 
 package com.google.gerrit.client.changes;
 
+import com.google.gerrit.client.Dispatcher;
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.rpc.ScreenLoadCallback;
 import com.google.gerrit.client.ui.CommentPanel;
+import com.google.gerrit.client.ui.ComplexDisclosurePanel;
 import com.google.gerrit.client.ui.ExpandAllCommand;
 import com.google.gerrit.client.ui.LinkMenuBar;
 import com.google.gerrit.client.ui.NeedsSignInKeyCommand;
@@ -26,17 +26,16 @@
 import com.google.gerrit.common.data.AccountInfoCache;
 import com.google.gerrit.common.data.ChangeDetail;
 import com.google.gerrit.common.data.ChangeInfo;
-import com.google.gerrit.common.data.ToggleStarRequest;
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.ChangeMessage;
-import com.google.gerrit.reviewdb.PatchSet;
-import com.google.gerrit.reviewdb.Change.Status;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Change.Status;
 import com.google.gwt.event.dom.client.ChangeEvent;
 import com.google.gwt.event.dom.client.ChangeHandler;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.logical.shared.ValueChangeEvent;
+import com.google.gwt.event.logical.shared.ValueChangeHandler;
 import com.google.gwt.event.shared.HandlerRegistration;
 import com.google.gwt.i18n.client.LocaleInfo;
 import com.google.gwt.user.client.ui.DisclosurePanel;
@@ -44,30 +43,32 @@
 import com.google.gwt.user.client.ui.Grid;
 import com.google.gwt.user.client.ui.HorizontalPanel;
 import com.google.gwt.user.client.ui.Image;
+import com.google.gwt.user.client.ui.InlineLabel;
 import com.google.gwt.user.client.ui.Label;
 import com.google.gwt.user.client.ui.ListBox;
 import com.google.gwt.user.client.ui.Panel;
 import com.google.gwtexpui.globalkey.client.GlobalKey;
 import com.google.gwtexpui.globalkey.client.KeyCommand;
 import com.google.gwtexpui.globalkey.client.KeyCommandSet;
-import com.google.gwtjsonrpc.client.VoidResult;
 
 import java.sql.Timestamp;
 import java.util.List;
 
 
-public class ChangeScreen extends Screen {
+public class ChangeScreen extends Screen
+    implements ValueChangeHandler<ChangeDetail> {
   private final Change.Id changeId;
   private final PatchSet.Id openPatchSetId;
+  private ChangeDetailCache detailCache;
+  private StarCache starred;
 
   private Image starChange;
-  private boolean starred;
   private ChangeDescriptionBlock descriptionBlock;
   private ApprovalTable approvals;
 
   private IncludedInTable includedInTable;
   private DisclosurePanel includedInPanel;
-  private DisclosurePanel dependenciesPanel;
+  private ComplexDisclosurePanel dependenciesPanel;
   private ChangeTable dependencies;
   private ChangeTable.Section dependsOn;
   private ChangeTable.Section neededBy;
@@ -127,7 +128,7 @@
   @Override
   protected void onLoad() {
     super.onLoad();
-    refresh();
+    detailCache.refresh();
   }
 
   @Override
@@ -153,33 +154,17 @@
     }
   }
 
-  public void refresh() {
-    Util.DETAIL_SVC.changeDetail(changeId,
-        new ScreenLoadCallback<ChangeDetail>(this) {
-          @Override
-          protected void preDisplay(final ChangeDetail r) {
-            display(r);
-          }
-
-          @Override
-          protected void postDisplay() {
-            patchSetsBlock.setRegisterKeys(true);
-          }
-        });
-  }
-
-  private void setStarred(final boolean s) {
-    if (s) {
-      starChange.setResource(Gerrit.RESOURCES.starFilled());
-    } else {
-      starChange.setResource(Gerrit.RESOURCES.starOpen());
-    }
-    starred = s;
-  }
-
   @Override
   protected void onInitUI() {
     super.onInitUI();
+
+    ChangeCache cache = ChangeCache.get(changeId);
+
+    detailCache = cache.getChangeDetailCache();
+    detailCache.addValueChangeHandler(this);
+
+    starred = cache.getStarCache();
+
     addStyleName(Gerrit.RESOURCES.css().changeScreen());
 
     keysNavigation = new KeyCommandSet(Gerrit.C.sectionNavigation());
@@ -188,20 +173,13 @@
     keysNavigation.add(new ExpandCollapseDependencySectionKeyCommand(0, 'd', Util.C.expandCollapseDependencies()));
 
     if (Gerrit.isSignedIn()) {
-      keysAction.add(new StarKeyCommand(0, 's', Util.C.changeTableStar()));
+      keysAction.add(starred.new KeyCommand(0, 's', Util.C.changeTableStar()));
       keysAction.add(new PublishCommentsKeyCommand(0, 'r', Util.C
           .keyPublishComments()));
 
-      starChange = new Image(Gerrit.RESOURCES.starOpen());
+      starChange = starred.createStar();
       starChange.setStyleName(Gerrit.RESOURCES.css().changeScreenStarIcon());
-      starChange.setVisible(Gerrit.isSignedIn());
-      starChange.addClickHandler(new ClickHandler() {
-        @Override
-        public void onClick(final ClickEvent event) {
-          toggleStar();
-        }
-      });
-      insertTitleWidget(starChange);
+      setTitleWest(starChange);
     }
 
     descriptionBlock = new ChangeDescriptionBlock();
@@ -226,7 +204,8 @@
     dependencies.addSection(dependsOn);
     dependencies.addSection(neededBy);
 
-    dependenciesPanel = new DisclosurePanel(Util.C.changeScreenDependencies());
+    dependenciesPanel = new ComplexDisclosurePanel(
+        Util.C.changeScreenDependencies(), false);
     dependenciesPanel.setContent(dependencies);
     add(dependenciesPanel);
 
@@ -246,7 +225,6 @@
         }
       }
     });
-    patchesList.addItem(Util.C.baseDiffItem());
 
     patchesGrid = new Grid(1, 2);
     patchesGrid.setStyleName(Gerrit.RESOURCES.css().selectPatchSetOldVersion());
@@ -254,7 +232,7 @@
     patchesGrid.setWidget(0, 1, patchesList);
     add(patchesGrid);
 
-    patchSetsBlock = new PatchSetsBlock(this);
+    patchSetsBlock = new PatchSetsBlock();
     add(patchSetsBlock);
 
     comments = new FlowPanel();
@@ -280,18 +258,16 @@
     setPageTitle(titleBuf.toString());
   }
 
-  void update(final ChangeDetail detail) {
-    display(detail);
-    patchSetsBlock.setRegisterKeys(true);
+  @Override
+  public void onValueChange(ValueChangeEvent<ChangeDetail> event) {
+    if (isAttached()) {
+      display(event.getValue());
+    }
   }
 
   private void display(final ChangeDetail detail) {
     displayTitle(detail.getChange().getKey(), detail.getChange().getSubject());
 
-    if (starChange != null) {
-      setStarred(detail.isStarred());
-    }
-
     if (Status.MERGED == detail.getChange().getStatus()) {
       includedInPanel.setVisible(true);
       includedInPanel.addOpenHandler(includedInTable);
@@ -306,9 +282,13 @@
         .getCurrentPatchSetDetail().getInfo(), detail.getAccounts());
     dependsOn.display(detail.getDependsOn());
     neededBy.display(detail.getNeededBy());
-    approvals.display(detail.getChange(), detail.getMissingApprovals(), detail
-        .getApprovals());
+    approvals.display(detail);
 
+    if (detail.getCurrentPatchSetDetail().getInfo().getParents().size() > 1) {
+      patchesList.addItem(Util.C.autoMerge());
+    } else {
+      patchesList.addItem(Util.C.baseDiffItem());
+    }
     for (PatchSet pId : detail.getPatchSets()) {
       if (patchesList != null) {
         patchesList.addItem(Util.M.patchSetHeader(pId.getPatchSetId()), pId
@@ -323,20 +303,35 @@
     patchSetsBlock.display(detail, diffBaseId);
     addComments(detail);
 
-    // If any dependency change is still open, show our dependency list.
+    // If any dependency change is still open, or is outdated,
+    // show our dependency list.
     //
     boolean depsOpen = false;
+    int outdated = 0;
     if (!detail.getChange().getStatus().isClosed()
         && detail.getDependsOn() != null) {
       for (final ChangeInfo ci : detail.getDependsOn()) {
-        if (ci.getStatus() != Change.Status.MERGED) {
+        if (! ci.isLatest()) {
           depsOpen = true;
-          break;
+          outdated++;
+        } else if (ci.getStatus() != Change.Status.MERGED) {
+          depsOpen = true;
         }
       }
     }
 
     dependenciesPanel.setOpen(depsOpen);
+
+    dependenciesPanel.getHeader().clear();
+    if (outdated > 0) {
+      dependenciesPanel.getHeader().add(new InlineLabel(
+        Util.M.outdatedHeader(outdated)));
+    }
+
+    if (!isCurrentView()) {
+      display();
+    }
+    patchSetsBlock.setRegisterKeys(true);
   }
 
   private void addComments(final ChangeDetail detail) {
@@ -406,24 +401,6 @@
     return menuBar;
   }
 
-  private void toggleStar() {
-    final boolean prior = starred;
-    setStarred(!prior);
-
-    final ToggleStarRequest req = new ToggleStarRequest();
-    req.toggle(changeId, starred);
-    Util.LIST_SVC.toggleStars(req, new GerritCallback<VoidResult>() {
-      public void onSuccess(final VoidResult result) {
-      }
-
-      @Override
-      public void onFailure(final Throwable caught) {
-        super.onFailure(caught);
-        setStarred(prior);
-      }
-    });
-  }
-
   public class UpToListKeyCommand extends KeyCommand {
     public UpToListKeyCommand(int mask, char key, String help) {
       super(mask, key, help);
@@ -446,17 +423,6 @@
     }
   }
 
-  public class StarKeyCommand extends NeedsSignInKeyCommand {
-    public StarKeyCommand(int mask, char key, String help) {
-      super(mask, key, help);
-    }
-
-    @Override
-    public void onKeyPress(final KeyPressEvent event) {
-      toggleStar();
-    }
-  }
-
   public class PublishCommentsKeyCommand extends NeedsSignInKeyCommand {
     public PublishCommentsKeyCommand(int mask, char key, String help) {
       super(mask, key, help);
@@ -465,8 +431,7 @@
     @Override
     public void onKeyPress(final KeyPressEvent event) {
       PatchSet.Id currentPatchSetId = patchSetsBlock.getCurrentPatchSet().getId();
-      Gerrit.display("change,publish," + currentPatchSetId.toString(),
-          new PublishCommentScreen(currentPatchSetId));
+      Gerrit.display(Dispatcher.toPublish(currentPatchSetId));
     }
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java
index eb7014e..5a568f1 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java
@@ -33,29 +33,25 @@
 import com.google.gerrit.common.data.ApprovalSummarySet;
 import com.google.gerrit.common.data.ApprovalType;
 import com.google.gerrit.common.data.ChangeInfo;
-import com.google.gerrit.common.data.ToggleStarRequest;
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.AccountGeneralPreferences;
-import com.google.gerrit.reviewdb.ApprovalCategory;
-import com.google.gerrit.reviewdb.ApprovalCategoryValue;
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
+import com.google.gerrit.reviewdb.client.ApprovalCategory;
+import com.google.gerrit.reviewdb.client.ApprovalCategoryValue;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gwt.dom.client.Element;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.event.dom.client.KeyCodes;
 import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.resources.client.ImageResource;
 import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
 import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.HTMLTable.Cell;
+import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
 import com.google.gwt.user.client.ui.Image;
 import com.google.gwt.user.client.ui.InlineLabel;
 import com.google.gwt.user.client.ui.UIObject;
-import com.google.gwt.user.client.ui.Widget;
-import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
-import com.google.gwt.user.client.ui.HTMLTable.Cell;
-import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
-import com.google.gwtjsonrpc.client.VoidResult;
 
 import java.util.ArrayList;
 import java.util.HashSet;
@@ -136,7 +132,7 @@
           return;
         }
         if (cell.getCellIndex() == C_STAR) {
-          onStarClick(cell.getRowIndex());
+          // Don't do anything (handled by star itself).
         } else if (cell.getCellIndex() == C_OWNER) {
           // Don't do anything.
         } else if (getRowItem(cell.getRowIndex()) != null) {
@@ -149,23 +145,7 @@
   protected void onStarClick(final int row) {
     final ChangeInfo c = getRowItem(row);
     if (c != null && Gerrit.isSignedIn()) {
-      final boolean prior = c.isStarred();
-      c.setStarred(!prior);
-      setStar(row, c);
-
-      final ToggleStarRequest req = new ToggleStarRequest();
-      req.toggle(c.getId(), c.isStarred());
-      Util.LIST_SVC.toggleStars(req, new GerritCallback<VoidResult>() {
-        public void onSuccess(final VoidResult result) {
-        }
-
-        @Override
-        public void onFailure(final Throwable caught) {
-          super.onFailure(caught);
-          c.setStarred(prior);
-          setStar(row, c);
-        }
-      });
+       ChangeCache.get(c.getId()).getStarCache().toggleStar();
     }
   }
 
@@ -212,10 +192,13 @@
   }
 
   private void populateChangeRow(final int row, final ChangeInfo c) {
+    ChangeCache cache = ChangeCache.get(c.getId());
+    cache.getChangeInfoCache().set(c);
+
     final String idstr = c.getKey().abbreviate();
     table.setWidget(row, C_ARROW, null);
     if (Gerrit.isSignedIn()) {
-      setStar(row, c);
+      table.setWidget(row, C_STAR, cache.getStarCache().createStar());
     }
     table.setWidget(row, C_ID, new TableChangeLink(idstr, c));
 
@@ -226,6 +209,13 @@
     if (c.getStatus() != null && c.getStatus() != Change.Status.NEW) {
       s += " (" + c.getStatus().name() + ")";
     }
+    if (! c.isLatest()) {
+      s += " [OUTDATED]";
+      table.getRowFormatter().addStyleName(row, Gerrit.RESOURCES.css().outdated());
+    } else {
+      table.getRowFormatter().removeStyleName(row, Gerrit.RESOURCES.css().outdated());
+    }
+
     table.setWidget(row, C_SUBJECT, new TableChangeLink(s, c));
     table.setWidget(row, C_OWNER, link(c.getOwner()));
     table.setWidget(row, C_PROJECT, new ProjectLink(c.getProject().getKey(), c
@@ -240,22 +230,6 @@
     return AccountDashboardLink.link(accountCache, id);
   }
 
-  private void setStar(final int row, final ChangeInfo c) {
-    final ImageResource star;
-    if (c.isStarred()) {
-      star = Gerrit.RESOURCES.starFilled();
-    } else {
-      star = Gerrit.RESOURCES.starOpen();
-    }
-
-    final Widget i = table.getWidget(row, C_STAR);
-    if (i instanceof Image) {
-      ((Image) i).setResource(star);
-    } else {
-      table.setWidget(row, C_STAR, new Image(star));
-    }
-  }
-
   public void addSection(final Section s) {
     assert s.parent == null;
 
@@ -426,7 +400,7 @@
 
     @Override
     public void go() {
-      movePointerTo(id);
+      movePointerTo(cid);
       super.go();
     }
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentedChangeActionDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentedChangeActionDialog.java
deleted file mode 100644
index 01d6299..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommentedChangeActionDialog.java
+++ /dev/null
@@ -1,158 +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.client.changes;
-
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.ui.SmallHeading;
-import com.google.gerrit.common.data.ChangeDetail;
-import com.google.gerrit.reviewdb.PatchSet;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.logical.shared.CloseEvent;
-import com.google.gwt.event.logical.shared.CloseHandler;
-import com.google.gwt.user.client.DOM;
-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.PopupPanel;
-import com.google.gwtexpui.globalkey.client.GlobalKey;
-import com.google.gwtexpui.globalkey.client.NpTextArea;
-import com.google.gwtexpui.user.client.AutoCenterDialogBox;
-
-public abstract class CommentedChangeActionDialog extends AutoCenterDialogBox implements CloseHandler<PopupPanel>{
-  private final FlowPanel panel;
-  private final NpTextArea message;
-  private final Button sendButton;
-  private final Button cancelButton;
-  private final PatchSet.Id psid;
-  private final AsyncCallback<ChangeDetail> callback;
-
-  private boolean buttonClicked = false;
-
-  public CommentedChangeActionDialog(final PatchSet.Id psi,
-      final AsyncCallback<ChangeDetail> callback, final String dialogTitle,
-      final String dialogHeading, final String buttonSend,
-      final String buttonCancel, final String dialogStyle,
-      final String messageStyle) {
-     this(psi, callback, dialogTitle, dialogHeading, buttonSend, buttonCancel, dialogStyle, messageStyle, null);
-  }
-
-  public CommentedChangeActionDialog(final PatchSet.Id psi,
-      final AsyncCallback<ChangeDetail> callback, final String dialogTitle,
-      final String dialogHeading, final String buttonSend,
-      final String buttonCancel, final String dialogStyle,
-      final String messageStyle, final String defaultMessage) {
-    super(/* auto hide */false, /* modal */true);
-    setGlassEnabled(true);
-
-    psid = psi;
-    this.callback = callback;
-    addStyleName(dialogStyle);
-    setText(dialogTitle);
-
-    panel = new FlowPanel();
-    add(panel);
-
-    panel.add(new SmallHeading(dialogHeading));
-
-    final FlowPanel mwrap = new FlowPanel();
-    mwrap.setStyleName(messageStyle);
-    panel.add(mwrap);
-
-    message = new NpTextArea();
-    message.setCharacterWidth(60);
-    message.setVisibleLines(10);
-    message.setText(defaultMessage);
-    DOM.setElementPropertyBoolean(message.getElement(), "spellcheck", true);
-    mwrap.add(message);
-
-    final FlowPanel buttonPanel = new FlowPanel();
-    panel.add(buttonPanel);
-    sendButton = new Button(buttonSend);
-    sendButton.addClickHandler(new ClickHandler() {
-      @Override
-      public void onClick(final ClickEvent event) {
-        sendButton.setEnabled(false);
-        cancelButton.setEnabled(false);
-        onSend();
-      }
-    });
-    buttonPanel.add(sendButton);
-
-    cancelButton = new Button(buttonCancel);
-    DOM.setStyleAttribute(cancelButton.getElement(), "marginLeft", "300px");
-    cancelButton.addClickHandler(new ClickHandler() {
-      @Override
-      public void onClick(final ClickEvent event) {
-        buttonClicked = true;
-        if (callback != null) {
-          callback.onFailure(null);
-        }
-        hide();
-      }
-    });
-    buttonPanel.add(cancelButton);
-
-    addCloseHandler(this);
-  }
-
-  @Override
-  public void center() {
-    super.center();
-    GlobalKey.dialog(this);
-    message.setFocus(true);
-  }
-
-  @Override
-  public void onClose(CloseEvent<PopupPanel> event) {
-    if (!buttonClicked) {
-      // the dialog was closed without one of the buttons being pressed
-      // e.g. the user pressed ESC to close the dialog
-      if (callback != null) {
-        callback.onFailure(null);
-      }
-    }
-  }
-
-  public abstract void onSend();
-
-  public PatchSet.Id getPatchSetId() {
-    return psid;
-  }
-
-  public String getMessageText() {
-    return message.getText().trim();
-  }
-
-  public GerritCallback<ChangeDetail> createCallback() {
-    return new GerritCallback<ChangeDetail>(){
-      @Override
-      public void onSuccess(ChangeDetail result) {
-        buttonClicked = true;
-        if (callback != null) {
-          callback.onSuccess(result);
-        }
-        hide();
-      }
-
-      @Override
-      public void onFailure(Throwable caught) {
-        sendButton.setEnabled(true);
-        cancelButton.setEnabled(true);
-        super.onFailure(caught);
-      }
-    };
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommitMessageBlock.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommitMessageBlock.java
index 48e257d..1a6ea3e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommitMessageBlock.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CommitMessageBlock.java
@@ -18,7 +18,6 @@
 import com.google.gerrit.client.ui.CommentLinkProcessor;
 import com.google.gwt.user.client.ui.Composite;
 import com.google.gwt.user.client.ui.HTML;
-import com.google.gwt.user.client.ui.ScrollPanel;
 import com.google.gwtexpui.safehtml.client.SafeHtml;
 import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
 
@@ -26,20 +25,9 @@
   private final HTML description;
 
   public CommitMessageBlock() {
-    this(null);
-  }
-
-  public CommitMessageBlock(String height) {
     description = new HTML();
     description.setStyleName(Gerrit.RESOURCES.css().changeScreenDescription());
-    if (height != null) {
-      ScrollPanel scrollPanel = new ScrollPanel();
-      scrollPanel.setHeight(height);
-      scrollPanel.add(description);
-      initWidget(scrollPanel);
-    } else {
-      initWidget(description);
-    }
+    initWidget(description);
   }
 
   public void display(final String commitMessage) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DownloadCommandLink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DownloadCommandLink.java
index 53771a97..c095d4d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DownloadCommandLink.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DownloadCommandLink.java
@@ -15,14 +15,14 @@
 package com.google.gerrit.client.changes;
 
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.reviewdb.AccountGeneralPreferences;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.user.client.rpc.AsyncCallback;
+import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.gwt.user.client.ui.Accessibility;
 import com.google.gwt.user.client.ui.Anchor;
 import com.google.gwt.user.client.ui.Widget;
-import com.google.gwtjsonrpc.client.VoidResult;
+import com.google.gwtjsonrpc.common.VoidResult;
 
 abstract class DownloadCommandLink extends Anchor implements ClickHandler {
   final AccountGeneralPreferences.DownloadCommand cmdType;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DownloadCommandPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DownloadCommandPanel.java
index 0affcfb..1516da1 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DownloadCommandPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DownloadCommandPanel.java
@@ -15,8 +15,8 @@
 package com.google.gerrit.client.changes;
 
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.reviewdb.AccountGeneralPreferences;
-import com.google.gerrit.reviewdb.AccountGeneralPreferences.DownloadCommand;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadCommand;
 import com.google.gwt.user.client.ui.Accessibility;
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.Widget;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DownloadUrlLink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DownloadUrlLink.java
index 6e72e20..2afafaa 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DownloadUrlLink.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DownloadUrlLink.java
@@ -15,14 +15,14 @@
 package com.google.gerrit.client.changes;
 
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.reviewdb.AccountGeneralPreferences;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.user.client.rpc.AsyncCallback;
+import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.gwt.user.client.ui.Accessibility;
 import com.google.gwt.user.client.ui.Anchor;
 import com.google.gwt.user.client.ui.Widget;
-import com.google.gwtjsonrpc.client.VoidResult;
+import com.google.gwtjsonrpc.common.VoidResult;
 
 class DownloadUrlLink extends Anchor implements ClickHandler {
   final AccountGeneralPreferences.DownloadScheme urlType;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DownloadUrlPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DownloadUrlPanel.java
index f4260e2..b3c9ebc 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DownloadUrlPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DownloadUrlPanel.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.client.changes;
 
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.reviewdb.AccountGeneralPreferences;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
 import com.google.gwt.user.client.ui.Accessibility;
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.Widget;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/IncludedInTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/IncludedInTable.java
index 533999d..3a1ccde 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/IncludedInTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/IncludedInTable.java
@@ -17,7 +17,7 @@
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.common.data.IncludedInDetail;
-import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.reviewdb.client.Change;
 import com.google.gwt.event.logical.shared.OpenEvent;
 import com.google.gwt.event.logical.shared.OpenHandler;
 import com.google.gwt.user.client.ui.Composite;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PagedSingleListScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PagedSingleListScreen.java
index 6ca102f..72300b2 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PagedSingleListScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PagedSingleListScreen.java
@@ -21,10 +21,10 @@
 import com.google.gerrit.client.ui.Screen;
 import com.google.gerrit.common.data.ChangeInfo;
 import com.google.gerrit.common.data.SingleListChangeInfo;
-import com.google.gerrit.reviewdb.AccountGeneralPreferences;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
 import com.google.gwt.event.dom.client.KeyPressEvent;
 import com.google.gwt.user.client.History;
-import com.google.gwt.user.client.rpc.AsyncCallback;
+import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.gwt.user.client.ui.HorizontalPanel;
 import com.google.gwtexpui.globalkey.client.KeyCommand;
 
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 740e08c..81b4f14 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,56 +17,59 @@
 import com.google.gerrit.client.Dispatcher;
 import com.google.gerrit.client.FormatUtil;
 import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.GitwebLink;
+import com.google.gerrit.client.patches.PatchUtil;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.ui.AccountDashboardLink;
+import com.google.gerrit.client.ui.CommentedActionDialog;
 import com.google.gerrit.client.ui.ComplexDisclosurePanel;
 import com.google.gerrit.client.ui.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;
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.AccountDiffPreference;
-import com.google.gerrit.reviewdb.AccountGeneralPreferences;
-import com.google.gerrit.reviewdb.ApprovalCategory;
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.ChangeMessage;
-import com.google.gerrit.reviewdb.Patch;
-import com.google.gerrit.reviewdb.PatchSet;
-import com.google.gerrit.reviewdb.PatchSetInfo;
-import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.UserIdentity;
-import com.google.gerrit.reviewdb.AccountGeneralPreferences.DownloadCommand;
-import com.google.gerrit.reviewdb.AccountGeneralPreferences.DownloadScheme;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountDiffPreference;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetInfo;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.UserIdentity;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadCommand;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme;
 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.event.logical.shared.OpenEvent;
 import com.google.gwt.event.logical.shared.OpenHandler;
 import com.google.gwt.user.client.Window;
-import com.google.gwt.user.client.rpc.AsyncCallback;
 import com.google.gwt.user.client.ui.Anchor;
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.DisclosurePanel;
 import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.FocusWidget;
 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.common.VoidResult;
 
-import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 
-class PatchSetComplexDisclosurePanel extends ComplexDisclosurePanel implements OpenHandler<DisclosurePanel> {
+class PatchSetComplexDisclosurePanel extends ComplexDisclosurePanel
+    implements OpenHandler<DisclosurePanel> {
   private static final int R_AUTHOR = 0;
   private static final int R_COMMITTER = 1;
   private static final int R_PARENTS = 2;
   private static final int R_DOWNLOAD = 3;
   private static final int R_CNT = 4;
 
-  private final ChangeScreen changeScreen;
+  private final ChangeDetailCache detailCache;
   private final ChangeDetail changeDetail;
   private final PatchSet patchSet;
   private final FlowPanel body;
@@ -82,42 +85,38 @@
    * Creates a closed complex disclosure panel for a patch set.
    * The patch set details are loaded when the complex disclosure panel is opened.
    */
-  PatchSetComplexDisclosurePanel(final ChangeScreen parent, final ChangeDetail detail,
-      final PatchSet ps) {
-    this(parent, detail, ps, false);
-    addOpenHandler(this);
-  }
-
-  /**
-   * Creates an open complex disclosure panel for a patch set.
-   */
-  PatchSetComplexDisclosurePanel(final ChangeScreen parent, final ChangeDetail detail,
-      final PatchSetDetail psd) {
-    this(parent, detail, psd.getPatchSet(), true);
-    ensureLoaded(psd);
-  }
-
-  private PatchSetComplexDisclosurePanel(final ChangeScreen parent, final ChangeDetail detail,
-      final PatchSet ps, boolean isOpen) {
+  public PatchSetComplexDisclosurePanel(final PatchSet ps, boolean isOpen) {
     super(Util.M.patchSetHeader(ps.getPatchSetId()), isOpen);
-    changeScreen = parent;
-    changeDetail = detail;
+    detailCache = ChangeCache.get(ps.getId().getParentKey()).getChangeDetailCache();
+    changeDetail = detailCache.get();
     patchSet = ps;
+
     body = new FlowPanel();
     setContent(body);
 
-    final GitwebLink gw = Gerrit.getConfig().getGitwebLink();
-
+    final GitwebLink gw = Gerrit.getGitwebLink();
     final InlineLabel revtxt = new InlineLabel(ps.getRevision().get() + " ");
     revtxt.addStyleName(Gerrit.RESOURCES.css().patchSetRevision());
     getHeader().add(revtxt);
     if (gw != null) {
       final Anchor revlink =
-          new Anchor("(gitweb)", false, gw.toRevision(detail.getChange()
+          new Anchor(gw.getLinkName(), false, gw.toRevision(changeDetail.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);
+    }
+
+    if (isOpen) {
+      ensureLoaded(changeDetail.getCurrentPatchSetDetail());
+    } else {
+      addOpenHandler(this);
+    }
   }
 
   public void setDiffBaseId(PatchSet.Id diffBaseId) {
@@ -158,16 +157,26 @@
     if (!patchSet.getId().equals(diffBaseId)) {
       patchTable = new PatchTable();
       patchTable.setSavePointerId("PatchTable " + patchSet.getId());
-      patchTable.setPatchSetIdToCompareWith(diffBaseId);
-      patchTable.display(detail);
+      patchTable.display(diffBaseId, detail);
 
       actionsPanel = new FlowPanel();
       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);
@@ -202,12 +211,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());
@@ -243,24 +260,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());
@@ -402,15 +423,15 @@
 
   private void populateActions(final PatchSetDetail detail) {
     final boolean isOpen = changeDetail.getChange().getStatus().isOpen();
-    Set<ApprovalCategory.Id> allowed = changeDetail.getCurrentActions();
-    if (allowed == null) {
-      allowed = Collections.emptySet();
-    }
 
-    if (isOpen && allowed.contains(ApprovalCategory.SUBMIT)) {
+    if (isOpen && changeDetail.canSubmit()) {
       final Button b =
           new Button(Util.M
               .submitPatchSet(detail.getPatchSet().getPatchSetId()));
+      if (Gerrit.getConfig().testChangeMerge()) {
+        b.setEnabled(changeDetail.getChange().isMergeable());
+      }
+
       b.addClickHandler(new ClickHandler() {
         @Override
         public void onClick(final ClickEvent event) {
@@ -438,15 +459,22 @@
         @Override
         public void onClick(final ClickEvent event) {
           b.setEnabled(false);
-          new CommentedChangeActionDialog(patchSet.getId(), createCommentedCallback(b),
-              Util.C.revertChangeTitle(), Util.C.headingRevertMessage(),
-              Util.C.buttonRevertChangeSend(), Util.C.buttonRevertChangeCancel(),
-              Gerrit.RESOURCES.css().revertChangeDialog(), Gerrit.RESOURCES.css().revertMessage(),
-              Util.M.revertChangeDefaultMessage(detail.getInfo().getSubject(), detail.getPatchSet().getRevision().get())) {
-                public void onSend() {
-                  Util.MANAGE_SVC.revertChange(getPatchSetId() , getMessageText(), createCallback());
-                }
-              }.center();
+          new ActionDialog(b, true, Util.C.revertChangeTitle(),
+              Util.C.headingRevertMessage()) {
+            {
+              sendButton.setText(Util.C.buttonRevertChangeSend());
+              message.setText(Util.M.revertChangeDefaultMessage(
+                  detail.getInfo().getSubject(),
+                  detail.getPatchSet().getRevision().get())
+              );
+            }
+
+            @Override
+            public void onSend() {
+              Util.MANAGE_SVC.revertChange(patchSet.getId(), getMessageText(),
+                 createCallback());
+            }
+          }.center();
         }
       });
       actionsPanel.add(b);
@@ -458,14 +486,42 @@
         @Override
         public void onClick(final ClickEvent event) {
           b.setEnabled(false);
-          new CommentedChangeActionDialog(patchSet.getId(), createCommentedCallback(b),
-              Util.C.abandonChangeTitle(), Util.C.headingAbandonMessage(),
-              Util.C.buttonAbandonChangeSend(), Util.C.buttonAbandonChangeCancel(),
-              Gerrit.RESOURCES.css().abandonChangeDialog(), Gerrit.RESOURCES.css().abandonMessage()) {
-                public void onSend() {
-                  Util.MANAGE_SVC.abandonChange(getPatchSetId() , getMessageText(), createCallback());
+          new ActionDialog(b, false, Util.C.abandonChangeTitle(),
+              Util.C.headingAbandonMessage()) {
+            {
+              sendButton.setText(Util.C.buttonAbandonChangeSend());
+            }
+
+            @Override
+            public void onSend() {
+              Util.MANAGE_SVC.abandonChange(patchSet.getId(), getMessageText(),
+                  createCallback());
+            }
+          }.center();
+        }
+      });
+      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);
                 }
-              }.center();
+
+                @Override
+                public void onFailure(Throwable caught) {
+                  b.setEnabled(true);
+                  super.onFailure(caught);
+                }
+              });
         }
       });
       actionsPanel.add(b);
@@ -477,14 +533,31 @@
         @Override
         public void onClick(final ClickEvent event) {
           b.setEnabled(false);
-          new CommentedChangeActionDialog(patchSet.getId(), createCommentedCallback(b),
-              Util.C.restoreChangeTitle(), Util.C.headingRestoreMessage(),
-              Util.C.buttonRestoreChangeSend(), Util.C.buttonRestoreChangeCancel(),
-              Gerrit.RESOURCES.css().abandonChangeDialog(), Gerrit.RESOURCES.css().abandonMessage()) {
-                public void onSend() {
-                  Util.MANAGE_SVC.restoreChange(getPatchSetId(), getMessageText(), createCallback());
-                }
-              }.center();
+          new ActionDialog(b, false, Util.C.restoreChangeTitle(),
+              Util.C.headingRestoreMessage()) {
+            {
+              sendButton.setText(Util.C.buttonRestoreChangeSend());
+            }
+
+            @Override
+            public void onSend() {
+              Util.MANAGE_SVC.restoreChange(patchSet.getId(), getMessageText(),
+                  createCallback());
+            }
+          }.center();
+        }
+      });
+      actionsPanel.add(b);
+    }
+
+    if (changeDetail.canRebase()) {
+      final Button b = new Button(Util.C.buttonRebaseChange());
+      b.addClickHandler(new ClickHandler() {
+        @Override
+        public void onClick(final ClickEvent event) {
+          b.setEnabled(false);
+          Util.MANAGE_SVC.rebaseChange(patchSet.getId(),
+              new ChangeDetailCache.GerritWidgetCallback(b));
         }
       });
       actionsPanel.add(b);
@@ -494,12 +567,10 @@
   private void populateDiffAllActions(final PatchSetDetail detail) {
     final Button diffAllSideBySide = new Button(Util.C.buttonDiffAllSideBySide());
     diffAllSideBySide.addClickHandler(new ClickHandler() {
-
       @Override
       public void onClick(ClickEvent event) {
         for (Patch p : detail.getPatches()) {
-          Window.open(Window.Location.getPath() + "#"
-              + Dispatcher.toPatchSideBySide(p.getKey()), "_blank", null);
+          openWindow(Dispatcher.toPatchSideBySide(diffBaseId, p.getKey()));
         }
       }
     });
@@ -507,25 +578,77 @@
 
     final Button diffAllUnified = new Button(Util.C.buttonDiffAllUnified());
     diffAllUnified.addClickHandler(new ClickHandler() {
-
       @Override
       public void onClick(ClickEvent event) {
         for (Patch p : detail.getPatches()) {
-          Window.open(Window.Location.getPath() + "#"
-              + Dispatcher.toPatchUnified(p.getKey()), "_blank", null);
+          openWindow(Dispatcher.toPatchUnified(diffBaseId, p.getKey()));
         }
       }
     });
     actionsPanel.add(diffAllUnified);
   }
 
+  private void openWindow(String token) {
+    String url = Window.Location.getPath() + "#" + token;
+    Window.open(url, "_blank", null);
+  }
+
   private void populateReviewAction() {
     final Button b = new Button(Util.C.buttonReview());
     b.addClickHandler(new ClickHandler() {
       @Override
       public void onClick(final ClickEvent event) {
-        Gerrit.display("change,publish," + patchSet.getId().toString(),
-            new PublishCommentScreen(patchSet.getId()));
+        Gerrit.display(Dispatcher.toPublish(patchSet.getId()));
+      }
+    });
+    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) {
+                detailCache.set(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) {
+                  detailCache.set(result);
+                } else {
+                  Gerrit.display(PageLinks.MINE);
+                }
+              }
+
+              @Override
+              public void onFailure(Throwable caught) {
+                b.setEnabled(true);
+                super.onFailure(caught);
+              }
+            });
       }
     });
     actionsPanel.add(b);
@@ -543,18 +666,15 @@
         new GerritCallback<PatchSetDetail>() {
           @Override
           public void onSuccess(PatchSetDetail result) {
-
             if (patchSet.getId().equals(diffBaseId)) {
               patchTable.setVisible(false);
               actionsPanel.setVisible(false);
             } else {
-
               if (patchTable != null) {
                 patchTable.removeFromParent();
               }
               patchTable = new PatchTable();
-              patchTable.setPatchSetIdToCompareWith(diffBaseId);
-              patchTable.display(result);
+              patchTable.display(diffBaseId, result);
               body.add(patchTable);
 
               for (ClickHandler clickHandler : registeredClickHandler) {
@@ -610,7 +730,7 @@
         new SubmitFailureDialog(result, msg).center();
       }
     }
-    changeScreen.update(result);
+    detailCache.set(result);
   }
 
   public PatchSet getPatchSet() {
@@ -636,15 +756,24 @@
     }
   }
 
-  private AsyncCallback<ChangeDetail> createCommentedCallback(final Button b) {
-    return new AsyncCallback<ChangeDetail>() {
-      public void onSuccess(ChangeDetail result) {
-        changeScreen.update(result);
-      }
+  private abstract class ActionDialog extends CommentedActionDialog<ChangeDetail> {
+    public ActionDialog(final FocusWidget enableOnFailure, final boolean redirect,
+        String dialogTitle, String dialogHeading) {
+      super(dialogTitle, dialogHeading, new ChangeDetailCache.IgnoreErrorCallback() {
+          @Override
+          public void onSuccess(ChangeDetail result) {
+            if (redirect) {
+              Gerrit.display(PageLinks.toChange(result.getChange().getId()));
+            } else {
+              super.onSuccess(result);
+            }
+          }
 
-      public void onFailure(Throwable caught) {
-        b.setEnabled(true);
-      }
-    };
+          @Override
+          public void onFailure(Throwable caught) {
+            enableOnFailure.setEnabled(true);
+          }
+        });
+    }
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetsBlock.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetsBlock.java
index e854cab..7e659a1 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetsBlock.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchSetsBlock.java
@@ -17,8 +17,8 @@
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.data.ChangeDetail;
-import com.google.gerrit.reviewdb.AccountGeneralPreferences;
-import com.google.gerrit.reviewdb.PatchSet;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
+import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.event.dom.client.KeyPressEvent;
@@ -43,11 +43,9 @@
  * that keyboard navigation to each changed file in all patch sets is possible.
  */
 public class PatchSetsBlock extends Composite {
-
   private final Map<PatchSet.Id, PatchSetComplexDisclosurePanel> patchSetPanels =
       new HashMap<PatchSet.Id, PatchSetComplexDisclosurePanel>();
 
-  private final ChangeScreen parent;
   private final FlowPanel body;
   private HandlerRegistration regNavigation;
 
@@ -65,8 +63,7 @@
   /** Patch sets on this change, in order. */
   private List<PatchSet> patchSets;
 
-  PatchSetsBlock(final ChangeScreen parent) {
-    this.parent = parent;
+  PatchSetsBlock() {
     body = new FlowPanel();
     initWidget(body);
   }
@@ -79,12 +76,6 @@
     currentPatchSetId = currps.getId();
     patchSets = detail.getPatchSets();
 
-    final List<PatchSet.Id> changePatchSets = new ArrayList<PatchSet.Id>();
-
-    for (final PatchSet ps : patchSets) {
-      changePatchSets.add(ps.getId());
-    }
-
     if (Gerrit.isSignedIn()) {
       final AccountGeneralPreferences p =
           Gerrit.getUserAccount().getGeneralPreferences();
@@ -96,19 +87,13 @@
     patchSetPanelsList = new ArrayList<PatchSetComplexDisclosurePanel>();
 
     for (final PatchSet ps : patchSets) {
-      final PatchSetComplexDisclosurePanel p;
-      if (ps == currps) {
-        p = new PatchSetComplexDisclosurePanel(parent, detail, detail
-            .getCurrentPatchSetDetail());
-        if (diffBaseId != null) {
-          p.setDiffBaseId(diffBaseId);
+      final PatchSetComplexDisclosurePanel p =
+          new PatchSetComplexDisclosurePanel(ps, ps == currps);
+      if (diffBaseId != null) {
+        p.setDiffBaseId(diffBaseId);
+        if (ps == currps) {
           p.refresh();
         }
-      } else {
-        p = new PatchSetComplexDisclosurePanel(parent, detail, ps);
-        if (diffBaseId != null) {
-          p.setDiffBaseId(diffBaseId);
-        }
       }
       add(p);
       patchSetPanelsList.add(p);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchTable.java
index a028619..7177525 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PatchTable.java
@@ -21,11 +21,11 @@
 import com.google.gerrit.client.ui.NavigationTable;
 import com.google.gerrit.client.ui.PatchLink;
 import com.google.gerrit.common.data.PatchSetDetail;
-import com.google.gerrit.reviewdb.Patch;
-import com.google.gerrit.reviewdb.PatchSet;
-import com.google.gerrit.reviewdb.Patch.ChangeType;
-import com.google.gerrit.reviewdb.Patch.Key;
-import com.google.gerrit.reviewdb.Patch.PatchType;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Patch.ChangeType;
+import com.google.gerrit.reviewdb.client.Patch.Key;
+import com.google.gerrit.reviewdb.client.Patch.PatchType;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.core.client.Scheduler;
 import com.google.gwt.core.client.Scheduler.RepeatingCommand;
@@ -55,6 +55,7 @@
   private Command onLoadCommand;
   private MyTable myTable;
   private String savePointerId;
+  private PatchSet.Id base;
   private List<Patch> patchList;
   private ListenableAccountDiffPreference listenablePrefs;
 
@@ -62,8 +63,6 @@
   private boolean active;
   private boolean registerKeys;
 
-  private PatchSet.Id patchSetIdToCompareWith;
-
   public PatchTable(ListenableAccountDiffPreference prefs) {
     listenablePrefs = prefs;
     myBody = new FlowPanel();
@@ -83,12 +82,13 @@
     return -1;
   }
 
-  public void display(PatchSetDetail detail) {
+  public void display(PatchSet.Id base, PatchSetDetail detail) {
+    this.base = base;
     this.detail = detail;
     this.patchList = detail.getPatches();
     myTable = null;
 
-    final DisplayCommand cmd = new DisplayCommand(patchList, patchSetIdToCompareWith);
+    final DisplayCommand cmd = new DisplayCommand(patchList, base);
     if (cmd.execute()) {
       cmd.initMeter();
       Scheduler.get().scheduleIncremental(cmd);
@@ -223,9 +223,9 @@
     PatchLink link;
     if (patchType == PatchScreen.Type.SIDE_BY_SIDE
         && patch.getPatchType() == Patch.PatchType.UNIFIED) {
-      link = new PatchLink.SideBySide("", thisKey, index, detail, this);
+      link = new PatchLink.SideBySide("", base, thisKey, index, detail, this);
     } else {
-      link = new PatchLink.Unified("", thisKey, index, detail, this);
+      link = new PatchLink.Unified("", base, thisKey, index, detail, this);
     }
     SafeHtmlBuilder text = new SafeHtmlBuilder();
     text.append(before);
@@ -275,14 +275,6 @@
     listenablePrefs = prefs;
   }
 
-  public PatchSet.Id getPatchSetIdToCompareWith() {
-    return patchSetIdToCompareWith;
-  }
-
-  public void setPatchSetIdToCompareWith(final PatchSet.Id psId) {
-    patchSetIdToCompareWith = psId;
-  }
-
   private class MyTable extends NavigationTable<Patch> {
     private static final int C_PATH = 2;
     private static final int C_DRAFT = 3;
@@ -381,13 +373,11 @@
 
       Widget nameCol;
       if (patch.getPatchType() == Patch.PatchType.UNIFIED) {
-        nameCol =
-            new PatchLink.SideBySide(getDisplayFileName(patch), patch.getKey(),
-                row - 1, detail, PatchTable.this);
+        nameCol = new PatchLink.SideBySide(getDisplayFileName(patch), base,
+            patch.getKey(), row - 1, detail, PatchTable.this);
       } else {
-        nameCol =
-            new PatchLink.Unified(getDisplayFileName(patch), patch.getKey(),
-                row - 1, detail, PatchTable.this);
+        nameCol = new PatchLink.Unified(getDisplayFileName(patch), base,
+            patch.getKey(), row - 1, detail, PatchTable.this);
       }
       if (patch.getSourceFileName() != null) {
         final String text;
@@ -409,16 +399,15 @@
 
       int C_UNIFIED = C_SIDEBYSIDE + 1;
       if (patch.getPatchType() == Patch.PatchType.UNIFIED) {
-        table.setWidget(row, C_SIDEBYSIDE, new PatchLink.SideBySide(Util.C
-            .patchTableDiffSideBySide(), patch.getKey(), row - 1, detail,
-            PatchTable.this));
-
+        table.setWidget(row, C_SIDEBYSIDE, new PatchLink.SideBySide(
+            Util.C.patchTableDiffSideBySide(), base, patch.getKey(), row - 1,
+            detail, PatchTable.this));
       } else if (patch.getPatchType() == Patch.PatchType.BINARY) {
         C_UNIFIED = C_SIDEBYSIDE + 2;
       }
-      table.setWidget(row, C_UNIFIED, new PatchLink.Unified(Util.C
-          .patchTableDiffUnified(), patch.getKey(), row - 1, detail,
-          PatchTable.this));
+      table.setWidget(row, C_UNIFIED, new PatchLink.Unified(
+          Util.C.patchTableDiffUnified(), base, patch.getKey(), row - 1,
+          detail, PatchTable.this));
     }
 
     void appendHeader(final SafeHtmlBuilder m) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PublishCommentScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PublishCommentScreen.java
index 0ed58b4..0c08491 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PublishCommentScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PublishCommentScreen.java
@@ -25,27 +25,29 @@
 import com.google.gerrit.client.ui.SmallHeading;
 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.ChangeDetail;
 import com.google.gerrit.common.data.PatchSetPublishDetail;
-import com.google.gerrit.reviewdb.ApprovalCategory;
-import com.google.gerrit.reviewdb.ApprovalCategoryValue;
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.Patch;
-import com.google.gerrit.reviewdb.PatchLineComment;
-import com.google.gerrit.reviewdb.PatchSet;
-import com.google.gerrit.reviewdb.PatchSetApproval;
+import com.google.gerrit.common.data.PermissionRange;
+import com.google.gerrit.reviewdb.client.ApprovalCategory;
+import com.google.gerrit.reviewdb.client.ApprovalCategoryValue;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.FormPanel;
+import com.google.gwt.user.client.ui.FormPanel.SubmitEvent;
 import com.google.gwt.user.client.ui.Panel;
 import com.google.gwt.user.client.ui.RadioButton;
 import com.google.gwt.user.client.ui.VerticalPanel;
 import com.google.gwt.user.client.ui.Widget;
-import com.google.gwt.user.client.ui.FormPanel.SubmitEvent;
 import com.google.gwtexpui.globalkey.client.NpTextArea;
-import com.google.gwtjsonrpc.client.VoidResult;
+import com.google.gwtjsonrpc.common.VoidResult;
 
 import java.util.ArrayList;
 import java.util.Collection;
@@ -54,7 +56,6 @@
 import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
 
 public class PublishCommentScreen extends AccountScreen implements
     ClickHandler, CommentEditorContainer {
@@ -218,16 +219,20 @@
   }
 
   private void initApprovals(final PatchSetPublishDetail r, final Panel body) {
-    for (final ApprovalType ct : Gerrit.getConfig().getApprovalTypes()
-        .getApprovalTypes()) {
-      if (r.isAllowed(ct.getCategory().getId())) {
-        initApprovalType(r, body, ct);
+    ApprovalTypes types = Gerrit.getConfig().getApprovalTypes();
+    for (PermissionRange range : r.getLabels()) {
+      ApprovalType type = types.byLabel(range.getLabel());
+      if (type != null) {
+        // Legacy type, use radio buttons.
+        initApprovalType(r, body, type, range);
+      } else {
+        // TODO Newer style label.
       }
     }
   }
 
   private void initApprovalType(final PatchSetPublishDetail r,
-      final Panel body, final ApprovalType ct) {
+      final Panel body, final ApprovalType ct, final PermissionRange range) {
     body.add(new SmallHeading(ct.getCategory().getName() + ":"));
 
     final VerticalPanel vp = new VerticalPanel();
@@ -236,11 +241,10 @@
         new ArrayList<ApprovalCategoryValue>(ct.getValues());
     Collections.reverse(lst);
     final ApprovalCategory.Id catId = ct.getCategory().getId();
-    final Set<ApprovalCategoryValue.Id> allowed = r.getAllowed(catId);
     final PatchSetApproval prior = r.getChangeApproval(catId);
 
     for (final ApprovalCategoryValue buttonValue : lst) {
-      if (!allowed.contains(buttonValue.getId())) {
+      if (!range.contains(buttonValue.getValue())) {
         continue;
       }
 
@@ -293,8 +297,8 @@
           draftsPanel.add(panel);
           // Parent table can be null here since we are not showing any
           // next/previous links
-          panel.add(new PatchLink.SideBySide(PatchTable
-              .getDisplayFileName(patchKey), patchKey, 0, null, null));
+          panel.add(new PatchLink.SideBySide(
+              PatchTable.getDisplayFileName(patchKey), null, patchKey, 0, null, null));
           priorFile = fn;
         }
 
@@ -306,7 +310,10 @@
       }
     }
 
-    submit.setVisible(r.isSubmitAllowed());
+    submit.setVisible(r.canSubmit());
+    if (Gerrit.getConfig().testChangeMerge()) {
+      submit.setEnabled(r.getChange().isMergeable());
+    }
   }
 
   private void onSend(final boolean submit) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/QueryScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/QueryScreen.java
index 1192dd0..cf9c526 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/QueryScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/QueryScreen.java
@@ -19,8 +19,8 @@
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.data.ChangeInfo;
 import com.google.gerrit.common.data.SingleListChangeInfo;
-import com.google.gerrit.reviewdb.RevId;
-import com.google.gwt.user.client.rpc.AsyncCallback;
+import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.gwtorm.client.KeyUtil;
 
 
@@ -37,7 +37,7 @@
   private final String query;
 
   public QueryScreen(final String encQuery, final String positionToken) {
-    super("q," + encQuery, positionToken);
+    super("/q/" + encQuery, positionToken);
     query = KeyUtil.decode(encQuery);
   }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarCache.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarCache.java
new file mode 100644
index 0000000..d7624a6
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarCache.java
@@ -0,0 +1,139 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.changes;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.ui.NeedsSignInKeyCommand;
+import com.google.gerrit.common.data.ChangeDetail;
+import com.google.gerrit.common.data.ChangeInfo;
+import com.google.gerrit.common.data.ToggleStarRequest;
+import com.google.gerrit.reviewdb.client.Change;
+
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.logical.shared.HasValueChangeHandlers;
+import com.google.gwt.event.logical.shared.ValueChangeEvent;
+import com.google.gwt.event.logical.shared.ValueChangeHandler;
+import com.google.gwt.event.shared.GwtEvent;
+import com.google.gwt.event.shared.HandlerManager;
+import com.google.gwt.event.shared.HandlerRegistration;
+import com.google.gwt.resources.client.ImageResource;
+import com.google.gwt.user.client.ui.Image;
+import com.google.gwtjsonrpc.common.VoidResult;
+
+public class StarCache implements HasValueChangeHandlers<Boolean> {
+  public class KeyCommand extends NeedsSignInKeyCommand {
+    public KeyCommand(int mask, char key, String help) {
+      super(mask, key, help);
+    }
+
+    @Override
+    public void onKeyPress(final KeyPressEvent event) {
+      StarCache.this.toggleStar();
+    }
+  }
+
+  ChangeCache cache;
+
+  private HandlerManager manager = new HandlerManager(this);
+
+  public StarCache(final Change.Id chg) {
+    cache = ChangeCache.get(chg);
+  }
+
+  public boolean get() {
+    ChangeDetail detail = cache.getChangeDetailCache().get();
+    if (detail != null) {
+      return detail.isStarred();
+    }
+    ChangeInfo info = cache.getChangeInfoCache().get();
+    if (info != null) {
+      return info.isStarred();
+    }
+    return false;
+  }
+
+  public void set(final boolean s) {
+    if (Gerrit.isSignedIn() && s != get()) {
+      final ToggleStarRequest req = new ToggleStarRequest();
+      req.toggle(cache.getChangeId(), s);
+
+      Util.LIST_SVC.toggleStars(req, new GerritCallback<VoidResult>() {
+        public void onSuccess(final VoidResult result) {
+          setStarred(s);
+          fireEvent(new ValueChangeEvent<Boolean>(s){});
+        }
+      });
+    }
+  }
+
+  private void setStarred(final boolean s) {
+    ChangeDetail detail = cache.getChangeDetailCache().get();
+    if (detail != null) {
+      detail.setStarred(s);
+    }
+    ChangeInfo info = cache.getChangeInfoCache().get();
+    if (info != null) {
+      info.setStarred(s);
+    }
+  }
+
+  public void toggleStar() {
+    set(!get());
+  }
+
+  @SuppressWarnings("unchecked")
+  public Image createStar() {
+    final Image star = new Image(getResource());
+    star.setVisible(Gerrit.isSignedIn());
+
+    star.addClickHandler(new ClickHandler() {
+      @Override
+      public void onClick(final ClickEvent event) {
+        StarCache.this.toggleStar();
+      }
+    });
+
+    @SuppressWarnings("rawtypes")
+    ValueChangeHandler starUpdater = new ValueChangeHandler() {
+        @Override
+        public void onValueChange(ValueChangeEvent event) {
+          star.setResource(StarCache.this.getResource());
+        }
+      };
+
+    cache.getChangeDetailCache().addValueChangeHandler(starUpdater);
+    cache.getChangeInfoCache().addValueChangeHandler(starUpdater);
+
+    this.addValueChangeHandler(starUpdater);
+
+    return star;
+  }
+
+  private ImageResource getResource() {
+    return get() ? Gerrit.RESOURCES.starFilled() : Gerrit.RESOURCES.starOpen();
+  }
+
+  public void fireEvent(GwtEvent<?> event) {
+    manager.fireEvent(event);
+  }
+
+  public HandlerRegistration addValueChangeHandler(
+      ValueChangeHandler<Boolean> handler) {
+    return manager.addHandler(ValueChangeEvent.getType(), handler);
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/SubmitFailureDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/SubmitFailureDialog.java
index f9f6f99..bedfb74 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/SubmitFailureDialog.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/SubmitFailureDialog.java
@@ -16,7 +16,7 @@
 
 import com.google.gerrit.client.ErrorDialog;
 import com.google.gerrit.common.data.ChangeDetail;
-import com.google.gerrit.reviewdb.ChangeMessage;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
 
 class SubmitFailureDialog extends ErrorDialog {
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..e84cac8 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
@@ -17,7 +17,7 @@
 import com.google.gerrit.common.data.ChangeDetailService;
 import com.google.gerrit.common.data.ChangeListService;
 import com.google.gerrit.common.data.ChangeManageService;
-import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.reviewdb.client.Change;
 import com.google.gwt.core.client.GWT;
 import com.google.gwtjsonrpc.client.JsonUtil;
 
@@ -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 a281393..1081e47 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
@@ -396,6 +396,10 @@
   border-spacing: 0;
 }
 
+.changeTable .outdated {
+  background: red;
+}
+
 .changeTable .iconCell {
   width: 1px;
   padding: 0px;
@@ -450,6 +454,10 @@
   border-left: 1px solid trimColor;
 }
 
+.changeTable .topMostCell {
+  border-top: 1px solid trimColor;
+}
+
 .changeTable .dataCell {
   padding-left: 5px;
   padding-right: 5px;
@@ -1036,6 +1044,10 @@
   white-space: nowrap;
 }
 
+.patchHistoryTablePatchSetHeader {
+  text-align: right;
+}
+
 
 /** AccountSettings  **/
 .usernameField {
@@ -1098,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;
@@ -1187,7 +1209,6 @@
 
 
 /** PublishCommentsScreen **/
-
 .publishCommentsScreen .smallHeading {
   font-size: small;
   font-weight: bold;
@@ -1237,57 +1258,29 @@
 }
 
 
-/** AbandonChangeDialog **/
-
-.abandonChangeDialog .gwt-DisclosurePanel .header td {
+/** CommentedActionDialog **/
+.commentedActionDialog .gwt-DisclosurePanel .header td {
   font-weight: bold;
   white-space: nowrap;
 }
-
-.abandonChangeDialog .smallHeading {
+.commentedActionDialog .smallHeading {
   font-size: small;
   font-weight: bold;
   white-space: nowrap;
 }
-.abandonChangeDialog .abandonMessage {
+.commentedActionDialog .commentedActionMessage {
   margin-left: 10px;
   background: trimColor;
   padding: 5px 5px 5px 5px;
 }
-.abandonChangeDialog .abandonMessage textarea {
+.commentedActionDialog .commentedActionMessage textarea {
   font-size: small;
 }
-.abandonChangeDialog .gwt-Hyperlink {
+.commentedActionDialog .gwt-Hyperlink {
   white-space: nowrap;
   font-size: small;
 }
 
-/** RevertChangeDialog **/
-
-.revertChangeDialog .gwt-DisclosurePanel .header td {
-  font-weight: bold;
-  white-space: nowrap;
-}
-
-.revertChangeDialog .smallHeading {
-  font-size: small;
-  font-weight: bold;
-  white-space: nowrap;
-}
-.revertChangeDialog .revertMessage {
-  margin-left: 10px;
-  background: trimColor;
-  padding: 5px 5px 5px 5px;
-}
-.revertChangeDialog .revertMessage textarea {
-  font-size: small;
-}
-.revertChangeDialog .gwt-Hyperlink {
-  white-space: nowrap;
-  font-size: small;
-}
-
-
 /** PatchBrowserPopup **/
 .patchBrowserPopup {
   opacity: 0.90;
@@ -1297,3 +1290,67 @@
   margin: 4px;
   opacity: 0.90;
 }
+
+
+/** AccountGroupInfoScreen **/
+.groupUUIDPanel {
+  margin-bottom: 10px;
+}
+.groupDescriptionPanel {
+  margin-bottom: 3px;
+}
+.groupExternalNameFilterTextBox {
+  margin-right: 2px;
+  margin-bottom: 2px;
+}
+.groupNamePanel {
+  margin-bottom: 3px;
+}
+.groupNameTextBox {
+  margin-bottom: 2px;
+}
+.groupOptionsPanel {
+  margin-bottom: 5px;
+}
+.groupOwnerPanel {
+  margin-bottom: 3px;
+}
+.groupOwnerTextBox {
+  margin-bottom: 2px;
+}
+.groupTypePanel {
+  margin-bottom: 3px;
+}
+.groupTypeSelectListBox {
+  margin-bottom: 2px;
+}
+
+
+/** AccountGroupMembersScreen **/
+.groupMembersTable {
+  margin-bottom: 2px;
+}
+.groupIncludesTable {
+  margin-bottom: 2px;
+}
+
+
+/** AddMemberBox **/
+.addMemberTextBox {
+  margin-right: 2px;
+  margin-bottom: 2px;
+}
+
+
+/** ProjectBranchesScreen **/
+.specialBranchIconCell {
+  background: #ECECEC;
+  border-bottom: 1px solid #FFFFFF;
+  border-top: 1px solid #FFFFFF;
+}
+.specialBranchDataCell {
+  background: #ECECEC;
+  border: 1px solid white;
+  font-style: italic;
+  padding: 2px 6px 1px;
+}
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 6452e08..8282caa 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
@@ -14,9 +14,9 @@
 
 package com.google.gerrit.client.patches;
 
+import com.google.gerrit.client.Dispatcher;
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.changes.PatchTable;
-import com.google.gerrit.client.changes.PublishCommentScreen;
 import com.google.gerrit.client.changes.Util;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.ui.CommentPanel;
@@ -26,10 +26,14 @@
 import com.google.gerrit.common.data.AccountInfoCache;
 import com.google.gerrit.common.data.CommentDetail;
 import com.google.gerrit.common.data.PatchScript;
+import com.google.gerrit.prettify.client.ClientSideFormatter;
+import com.google.gerrit.prettify.common.PrettyFormatter;
 import com.google.gerrit.prettify.common.SparseFileContent;
-import com.google.gerrit.reviewdb.Patch;
-import com.google.gerrit.reviewdb.PatchLineComment;
-import com.google.gerrit.reviewdb.PatchSet;
+import com.google.gerrit.prettify.common.SparseHtmlFile;
+import com.google.gerrit.reviewdb.client.AccountDiffPreference;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gwt.event.dom.client.BlurEvent;
 import com.google.gwt.event.dom.client.BlurHandler;
 import com.google.gwt.event.dom.client.ClickEvent;
@@ -45,9 +49,9 @@
 import com.google.gwt.user.client.History;
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.Focusable;
+import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
 import com.google.gwt.user.client.ui.UIObject;
 import com.google.gwt.user.client.ui.Widget;
-import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
 import com.google.gwtexpui.globalkey.client.GlobalKey;
 import com.google.gwtexpui.globalkey.client.KeyCommand;
 import com.google.gwtexpui.globalkey.client.KeyCommandSet;
@@ -171,11 +175,41 @@
     render(s);
   }
 
+  protected SparseHtmlFile getSparseHtmlFileA(PatchScript s) {
+    AccountDiffPreference dp = new AccountDiffPreference(s.getDiffPrefs());
+    dp.setShowWhitespaceErrors(false);
+
+    PrettyFormatter f = ClientSideFormatter.FACTORY.get();
+    f.setDiffPrefs(dp);
+    f.setFileName(s.getA().getPath());
+    f.setEditFilter(PrettyFormatter.A);
+    f.setEditList(s.getEdits());
+    f.format(s.getA());
+    return f;
+  }
+
+  protected SparseHtmlFile getSparseHtmlFileB(PatchScript s) {
+    AccountDiffPreference dp = new AccountDiffPreference(s.getDiffPrefs());
+
+    PrettyFormatter f = ClientSideFormatter.FACTORY.get();
+    f.setDiffPrefs(dp);
+    f.setFileName(s.getB().getPath());
+    f.setEditFilter(PrettyFormatter.B);
+    f.setEditList(s.getEdits());
+
+    if (dp.isSyntaxHighlighting() && s.getA().isWholeFile() && !s.getB().isWholeFile()) {
+      f.format(s.getB().apply(s.getA(), s.getEdits()));
+    } else {
+      f.format(s.getB());
+    }
+    return f;
+  }
+
   protected abstract void render(PatchScript script);
 
   protected abstract void onInsertComment(PatchLine pl);
 
-  public abstract void display(CommentDetail comments);
+  public abstract void display(CommentDetail comments, boolean expandComments);
 
   @Override
   protected MyFlexTable createFlexTable() {
@@ -331,9 +365,12 @@
     return getRowItem(row) instanceof CommentList;
   }
 
-  /** Invoked when the user clicks on a table cell. */
+  /** Invoked when the user double clicks on a table cell. */
   protected abstract void onCellDoubleClick(int row, int column);
 
+  /** Invoked when the user clicks on a table cell. */
+  protected abstract void onCellSingleClick(int row, int column);
+
   /**
    * Invokes createCommentEditor() with an empty string as value for the comment
    * parent UUID. This method is invoked by callers that want to create an
@@ -479,6 +516,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);
@@ -529,7 +582,7 @@
   }
 
   protected void bindComment(final int row, final int col,
-      final PatchLineComment line, final boolean isLast) {
+      final PatchLineComment line, final boolean isLast, boolean expandComment) {
     if (line.getStatus() == PatchLineComment.Status.DRAFT) {
       final CommentEditorPanel plc = new CommentEditorPanel(line);
       plc.addFocusHandler(this);
@@ -541,6 +594,7 @@
       final AccountInfo author = accountCache.get(line.getAuthor());
       final PublishedCommentPanel panel =
           new PublishedCommentPanel(author, line);
+      panel.setOpen(expandComment);
       panel.addFocusHandler(this);
       panel.addBlurHandler(this);
       table.setWidget(row, col, panel);
@@ -619,6 +673,7 @@
           final int row = rowOf(td);
           if (getRowItem(row) != null) {
             movePointerTo(row);
+            onCellSingleClick(rowOf(td), columnOf(td));
             return;
           }
           break;
@@ -677,8 +732,7 @@
     @Override
     public void onKeyPress(final KeyPressEvent event) {
       final PatchSet.Id id = patchKey.getParentKey();
-      Gerrit.display("change,publish," + id.toString(),
-          new PublishCommentScreen(id));
+      Gerrit.display(Dispatcher.toPublish(id));
     }
   }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommentEditorPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommentEditorPanel.java
index 5df1e14..b0d825b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommentEditorPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/CommentEditorPanel.java
@@ -17,7 +17,7 @@
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.ui.CommentPanel;
-import com.google.gerrit.reviewdb.PatchLineComment;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.event.dom.client.DoubleClickEvent;
@@ -26,11 +26,11 @@
 import com.google.gwt.event.dom.client.KeyDownEvent;
 import com.google.gwt.event.dom.client.KeyDownHandler;
 import com.google.gwt.user.client.Timer;
-import com.google.gwt.user.client.rpc.AsyncCallback;
+import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.Widget;
 import com.google.gwtexpui.globalkey.client.NpTextArea;
-import com.google.gwtjsonrpc.client.VoidResult;
+import com.google.gwtjsonrpc.common.VoidResult;
 
 import java.sql.Timestamp;
 
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 cb85c24..cb54411 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
@@ -14,16 +14,17 @@
 
 package com.google.gerrit.client.patches;
 
+import com.google.gerrit.client.Dispatcher;
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.changes.Util;
 import com.google.gerrit.client.ui.FancyFlexTable;
-import com.google.gerrit.reviewdb.Patch;
-import com.google.gerrit.reviewdb.PatchSet;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gwt.user.client.DOM;
 import com.google.gwt.user.client.Event;
+import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
 import com.google.gwt.user.client.ui.HasHorizontalAlignment;
 import com.google.gwt.user.client.ui.RadioButton;
-import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -43,19 +44,28 @@
   }
 
   void onClick(final HistoryRadio b) {
+    PatchSet.Id sideA = screen.idSideA;
+    PatchSet.Id sideB = screen.idSideB;
     switch (b.file) {
       case 0:
-        screen.setSideA(b.patchSetId);
+        sideA = b.patchSetId;
         break;
       case 1:
-        screen.setSideB(b.patchSetId);
+        sideB = b.patchSetId;
         break;
       default:
         return;
     }
-
     enableAll(false);
-    screen.refresh(false);
+    Patch.Key k = new Patch.Key(sideB, screen.getPatchKey().get());
+    switch (screen.getPatchScreenType()) {
+      case SIDE_BY_SIDE:
+        Gerrit.display(Dispatcher.toPatchSideBySide(sideA, k));
+        break;
+      case UNIFIED:
+        Gerrit.display(Dispatcher.toPatchUnified(sideA, k));
+        break;
+    }
   }
 
   void enableAll(final boolean on) {
@@ -66,40 +76,57 @@
 
   void display(final List<Patch> result) {
     all.clear();
+    final FlexCellFormatter fmt = table.getFlexCellFormatter();
+    table.setText(0, 0, PatchUtil.C.patchHeaderPatchSet());
+    fmt.setStyleName(0, 0, Gerrit.RESOURCES.css().dataHeader());
+    table.setText(1, 0, PatchUtil.C.patchHeaderOld());
+    fmt.setStyleName(1, 0, Gerrit.RESOURCES.css().dataHeader());
+    table.setText(2, 0, PatchUtil.C.patchHeaderNew());
+    fmt.setStyleName(2, 0, Gerrit.RESOURCES.css().dataHeader());
+    table.setText(3, 0, Util.C.patchTableColumnComments());
+    fmt.setStyleName(3, 0, Gerrit.RESOURCES.css().dataHeader());
 
-    final SafeHtmlBuilder nc = new SafeHtmlBuilder();
-    appendHeader(nc);
-    appendRow(nc, null);
-    for (final Patch k : result) {
-      appendRow(nc, k);
+    if (screen.getPatchSetDetail().getInfo().getParents().size() > 1) {
+      table.setText(0, 1, PatchUtil.C.patchBaseAutoMerge());
+    } else {
+      table.setText(0, 1, PatchUtil.C.patchBase());
     }
-    resetHtml(nc);
+    fmt.setStyleName(0, 1, Gerrit.RESOURCES.css().dataCell());
+    fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().topMostCell());
+    fmt.setStyleName(1, 1, Gerrit.RESOURCES.css().dataCell());
+    fmt.setStyleName(2, 1, Gerrit.RESOURCES.css().dataCell());
+    fmt.setStyleName(3, 1, Gerrit.RESOURCES.css().dataCell());
 
-    int row = 1;
-    {
-      final Patch k = new Patch(new Patch.Key(null, ""));
-      setRowItem(row, k);
-      installRadio(row, k, 0, screen.idSideA);
-      row++;
-    }
+    installRadio(1, 1, null, screen.idSideA, 0);
+
+    int col=2;
     for (final Patch k : result) {
-      setRowItem(row, k);
-      installRadio(row, k, 0, screen.idSideA);
-      installRadio(row, k, 1, screen.idSideB);
-      row++;
+      final PatchSet.Id psId = k.getKey().getParentKey();
+      table.setText(0, col, String.valueOf(psId.get()));
+      fmt.setStyleName(0, col, Gerrit.RESOURCES.css().patchHistoryTablePatchSetHeader());
+      fmt.addStyleName(0, col, Gerrit.RESOURCES.css().dataCell());
+      fmt.addStyleName(0, col, Gerrit.RESOURCES.css().topMostCell());
+
+      installRadio(1, col, psId, screen.idSideA, 0);
+      installRadio(2, col, psId, screen.idSideB, 1);
+
+      fmt.setStyleName(3, col, Gerrit.RESOURCES.css().dataCell());
+      if (k.getCommentCount() > 0) {
+        table.setText(3, col, Integer.toString(k.getCommentCount()));
+      }
+      col++;
     }
   }
 
-  private void installRadio(final int row, final Patch k, final int file,
-      final PatchSet.Id cur) {
-    final PatchSet.Id psid = k.getKey().getParentKey();
-    final HistoryRadio b = new HistoryRadio(psid, file);
-    b.setValue(eq(cur, psid));
+  private void installRadio(final int row, final int col, final PatchSet.Id psId,
+      final PatchSet.Id cur, final int file) {
+    final HistoryRadio b = new HistoryRadio(psId, file);
+    b.setValue(eq(cur, psId));
 
-    final int cell = radioCell(file);
-    table.setWidget(row, cell, b);
-    table.getCellFormatter().setHorizontalAlignment(row, cell,
-        HasHorizontalAlignment.ALIGN_CENTER);
+    table.setWidget(row, col, b);
+    final FlexCellFormatter fmt = table.getFlexCellFormatter();
+    fmt.setHorizontalAlignment(row, col, HasHorizontalAlignment.ALIGN_CENTER);
+    fmt.setStyleName(row, col, Gerrit.RESOURCES.css().dataCell());
     all.add(b);
   }
 
@@ -110,84 +137,6 @@
     return psid != null && psid.equals(cur);
   }
 
-  private int radioCell(final int file) {
-    return 2 + file;
-  }
-
-  private void appendHeader(final SafeHtmlBuilder m) {
-    m.openTr();
-
-    m.openTd();
-    m.addStyleName(Gerrit.RESOURCES.css().iconHeader());
-    m.addStyleName(Gerrit.RESOURCES.css().leftMostCell());
-    m.nbsp();
-    m.closeTd();
-
-    m.openTd();
-    m.setStyleName(Gerrit.RESOURCES.css().dataHeader());
-    m.nbsp();
-    m.closeTd();
-
-    m.openTd();
-    m.setStyleName(Gerrit.RESOURCES.css().dataHeader());
-    m.append(PatchUtil.C.patchHeaderOld());
-    m.closeTd();
-
-    m.openTd();
-    m.setStyleName(Gerrit.RESOURCES.css().dataHeader());
-    m.append(PatchUtil.C.patchHeaderNew());
-    m.closeTd();
-
-    m.openTd();
-    m.setStyleName(Gerrit.RESOURCES.css().dataHeader());
-    m.append(Util.C.patchTableColumnComments());
-    m.closeTd();
-
-    m.closeTr();
-  }
-
-  private void appendRow(final SafeHtmlBuilder m, final Patch k) {
-    m.openTr();
-
-    m.openTd();
-    m.addStyleName(Gerrit.RESOURCES.css().iconCell());
-    m.addStyleName(Gerrit.RESOURCES.css().leftMostCell());
-    m.nbsp();
-    m.closeTd();
-
-    m.openTd();
-    m.setStyleName(Gerrit.RESOURCES.css().dataCell());
-    m.setAttribute("align", "right");
-    if (k != null) {
-      final PatchSet.Id psId = k.getKey().getParentKey();
-      m.append(Util.M.patchSetHeader(psId.get()));
-    } else {
-      m.append("Base");
-    }
-    m.closeTd();
-
-    m.openTd();
-    m.setStyleName(Gerrit.RESOURCES.css().dataCell());
-    m.nbsp();
-    m.closeTd();
-
-    m.openTd();
-    m.setStyleName(Gerrit.RESOURCES.css().dataCell());
-    m.nbsp();
-    m.closeTd();
-
-    m.openTd();
-    m.setStyleName(Gerrit.RESOURCES.css().dataCell());
-    if (k != null && k.getCommentCount() > 0) {
-      m.append(Util.M.patchTableComments(k.getCommentCount()));
-    } else {
-      m.nbsp();
-    }
-    m.closeTd();
-
-    m.closeTr();
-  }
-
   private class HistoryRadio extends RadioButton {
     final PatchSet.Id patchSetId;
     final int file;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/NavLinks.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/NavLinks.java
index 6d42cc9..9f36342 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/NavLinks.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/NavLinks.java
@@ -19,12 +19,12 @@
 import com.google.gerrit.client.changes.Util;
 import com.google.gerrit.client.ui.ChangeLink;
 import com.google.gerrit.client.ui.InlineHyperlink;
-import com.google.gerrit.reviewdb.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gwt.event.dom.client.KeyPressEvent;
 import com.google.gwt.user.client.ui.Composite;
 import com.google.gwt.user.client.ui.Grid;
-import com.google.gwt.user.client.ui.HasHorizontalAlignment;
 import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
+import com.google.gwt.user.client.ui.HasHorizontalAlignment;
 import com.google.gwtexpui.globalkey.client.KeyCommand;
 import com.google.gwtexpui.globalkey.client.KeyCommandSet;
 import com.google.gwtexpui.safehtml.client.SafeHtml;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchBrowserPopup.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchBrowserPopup.java
index e3fb1e9..9af1aa3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchBrowserPopup.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchBrowserPopup.java
@@ -17,15 +17,15 @@
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.changes.PatchTable;
 import com.google.gerrit.client.changes.Util;
-import com.google.gerrit.reviewdb.Patch;
+import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gwt.event.logical.shared.ResizeEvent;
 import com.google.gwt.event.logical.shared.ResizeHandler;
 import com.google.gwt.event.shared.HandlerRegistration;
 import com.google.gwt.user.client.Command;
 import com.google.gwt.user.client.Window;
 import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.ScrollPanel;
 import com.google.gwt.user.client.ui.PopupPanel.PositionCallback;
+import com.google.gwt.user.client.ui.ScrollPanel;
 import com.google.gwtexpui.globalkey.client.GlobalKey;
 import com.google.gwtexpui.globalkey.client.HidePopupPanelCommand;
 import com.google.gwtexpui.user.client.PluginSafeDialogBox;
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 9b85131..fd34729 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
@@ -26,6 +26,9 @@
   String buttonDiscard();
 
   String noDifference();
+  String patchBase();
+  String patchBaseAutoMerge();
+  String patchHeaderPatchSet();
   String patchHeaderOld();
   String patchHeaderNew();
 
@@ -65,4 +68,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 a22fc1e..6a1dbc1 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
@@ -9,6 +9,9 @@
 buttonDiscard = Discard
 
 noDifference = No Differences
+patchBase = Base
+patchBaseAutoMerge = Auto Merge
+patchHeaderPatchSet = Patch Set
 patchHeaderOld = Old Version
 patchHeaderNew = New Version
 patchHistoryTitle = Patch History
@@ -44,3 +47,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 d48c2f8..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,8 +19,7 @@
 import java.util.Date;
 
 public interface PatchMessages extends Messages {
-  String patchWindowTitle(String changeId, String file);
-  String patchPageTitle(String changeId, String path);
-  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 961b0e5e..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,6 +1,4 @@
-patchWindowTitle = Change {0}: {1}
-patchPageTitle = Change {0}: {1}
-
-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 736d1f0..ffc8960 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
@@ -29,30 +29,23 @@
 import com.google.gerrit.common.data.PatchSetDetail;
 import com.google.gerrit.prettify.client.ClientSideFormatter;
 import com.google.gerrit.prettify.common.PrettyFactory;
-import com.google.gerrit.reviewdb.AccountDiffPreference;
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.Patch;
-import com.google.gerrit.reviewdb.PatchSet;
+import com.google.gerrit.reviewdb.client.AccountDiffPreference;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gwt.core.client.Scheduler;
 import com.google.gwt.core.client.Scheduler.ScheduledCommand;
 import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.event.logical.shared.CloseEvent;
-import com.google.gwt.event.logical.shared.CloseHandler;
-import com.google.gwt.event.logical.shared.OpenEvent;
-import com.google.gwt.event.logical.shared.OpenHandler;
 import com.google.gwt.event.logical.shared.ValueChangeEvent;
 import com.google.gwt.event.logical.shared.ValueChangeHandler;
 import com.google.gwt.event.shared.HandlerRegistration;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import com.google.gwt.user.client.ui.DisclosurePanel;
+import com.google.gwtjsonrpc.common.AsyncCallback;
+import com.google.gwt.user.client.ui.CheckBox;
 import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.HorizontalPanel;
 import com.google.gwt.user.client.ui.Label;
-import com.google.gwt.user.client.ui.VerticalPanel;
 import com.google.gwtexpui.globalkey.client.GlobalKey;
 import com.google.gwtexpui.globalkey.client.KeyCommand;
 import com.google.gwtexpui.globalkey.client.KeyCommandSet;
-import com.google.gwtjsonrpc.client.VoidResult;
+import com.google.gwtjsonrpc.common.VoidResult;
 
 public abstract class PatchScreen extends Screen implements
     CommentEditorContainer {
@@ -60,8 +53,9 @@
 
   public static class SideBySide extends PatchScreen {
     public SideBySide(final Patch.Key id, final int patchIndex,
-        final PatchSetDetail patchSetDetail, final PatchTable patchTable) {
-      super(id, patchIndex, patchSetDetail, patchTable);
+        final PatchSetDetail patchSetDetail, final PatchTable patchTable,
+        final TopView topView, final PatchSet.Id baseId) {
+       super(id, patchIndex, patchSetDetail, patchTable, topView, baseId);
     }
 
     @Override
@@ -70,15 +64,16 @@
     }
 
     @Override
-    protected PatchScreen.Type getPatchScreenType() {
+    public PatchScreen.Type getPatchScreenType() {
       return PatchScreen.Type.SIDE_BY_SIDE;
     }
   }
 
   public static class Unified extends PatchScreen {
     public Unified(final Patch.Key id, final int patchIndex,
-        final PatchSetDetail patchSetDetail, final PatchTable patchTable) {
-      super(id, patchIndex, patchSetDetail, patchTable);
+        final PatchSetDetail patchSetDetail, final PatchTable patchTable,
+        final TopView topView, final PatchSet.Id baseId) {
+      super(id, patchIndex, patchSetDetail, patchTable, topView, baseId);
     }
 
     @Override
@@ -87,30 +82,17 @@
     }
 
     @Override
-    protected PatchScreen.Type getPatchScreenType() {
+    public PatchScreen.Type getPatchScreenType() {
       return PatchScreen.Type.UNIFIED;
     }
   }
 
-  // Which patch set id's are being diff'ed
-  private static PatchSet.Id diffSideA = null;
-  private static PatchSet.Id diffSideB = null;
-
-  private static Boolean historyOpen = null;
-  private static final OpenHandler<DisclosurePanel> cacheOpenState =
-      new OpenHandler<DisclosurePanel>() {
-        @Override
-        public void onOpen(OpenEvent<DisclosurePanel> event) {
-          historyOpen = true;
-        }
-      };
-  private static final CloseHandler<DisclosurePanel> cacheCloseState =
-      new CloseHandler<DisclosurePanel>() {
-        @Override
-        public void onClose(CloseEvent<DisclosurePanel> event) {
-          historyOpen = false;
-        }
-      };
+  /**
+   * What should be displayed in the top of the screen
+   */
+  public static enum TopView {
+    MAIN, COMMIT, PREFERENCES, PATCH_SETS, FILES
+  }
 
   protected final Patch.Key patchKey;
   protected PatchSetDetail patchSetDetail;
@@ -118,9 +100,11 @@
   protected PatchSet.Id idSideA;
   protected PatchSet.Id idSideB;
   protected PatchScriptSettingsPanel settingsPanel;
+  protected TopView topView;
 
-  private DisclosurePanel historyPanel;
+  private CheckBox reviewed;
   private HistoryTable historyTable;
+  private FlowPanel topPanel;
   private FlowPanel contentPanel;
   private Label noDifference;
   private AbstractPatchContentTable contentTable;
@@ -148,26 +132,31 @@
   }
 
   protected PatchScreen(final Patch.Key id, final int patchIndex,
-      final PatchSetDetail detail, final PatchTable patchTable) {
+      final PatchSetDetail detail, final PatchTable patchTable,
+      final TopView top, final PatchSet.Id baseId) {
     patchKey = id;
     patchSetDetail = detail;
     fileList = patchTable;
+    topView = top;
 
-    if (patchTable != null) {
-      diffSideA = patchTable.getPatchSetIdToCompareWith();
-    } else {
-      diffSideA = null;
-    }
-    if (diffSideA == null) {
-      historyOpen = null;
-    }
-
-    idSideA = diffSideA; // null here means we're diff'ing from the Base
-    idSideB = diffSideB != null ? diffSideB : id.getParentKey();
+    idSideA = baseId; // null here means we're diff'ing from the Base
+    idSideB = id.getParentKey();
     this.patchIndex = patchIndex;
 
+    reviewed = new CheckBox(Util.C.reviewed());
+    reviewed.addValueChangeHandler(
+        new ValueChangeHandler<Boolean>() {
+          @Override
+          public void onValueChange(ValueChangeEvent<Boolean> event) {
+            setReviewedByCurrentUser(event.getValue());
+          }
+        });
+
     prefs = fileList != null ? fileList.getPreferences() :
                                new ListenableAccountDiffPreference();
+    if (Gerrit.isSignedIn()) {
+      prefs.reset();
+    }
     prefs.addValueChangeHandler(
         new ValueChangeHandler<AccountDiffPreference>() {
           @Override
@@ -177,13 +166,6 @@
         });
 
     settingsPanel = new PatchScriptSettingsPanel(prefs);
-    settingsPanel.getReviewedCheckBox().addValueChangeHandler(
-        new ValueChangeHandler<Boolean>() {
-          @Override
-          public void onValueChange(ValueChangeEvent<Boolean> event) {
-            setReviewedByCurrentUser(event.getValue());
-          }
-        });
   }
 
   @Override
@@ -197,6 +179,13 @@
   }
 
   private void update(AccountDiffPreference dp) {
+    // Did the user just turn on auto-review?
+    if (!reviewed.getValue() && prefs.getOld().isManualReview()
+        && !dp.isManualReview()) {
+      reviewed.setValue(true);
+      setReviewedByCurrentUser(true);
+    }
+
     if (lastScript != null && canReuse(dp, lastScript)) {
       lastScript.setDiffPrefs(dp);
       RpcStatus.INSTANCE.onRpcStart(null);
@@ -246,31 +235,20 @@
   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()));
 
     historyTable = new HistoryTable(this);
-    historyPanel = new DisclosurePanel(PatchUtil.C.patchHistoryTitle());
-    historyPanel.setContent(historyTable);
-    historyPanel.setVisible(false);
-    // If the user selected a different patch set than the default for either
-    // side, expand the history panel
-    historyPanel.setOpen(diffSideA != null || diffSideB != null
-        || (historyOpen != null && historyOpen));
-    historyPanel.addOpenHandler(cacheOpenState);
-    historyPanel.addCloseHandler(cacheCloseState);
 
+    commitMessageBlock = new CommitMessageBlock();
 
-    VerticalPanel vp = new VerticalPanel();
-    vp.add(historyPanel);
-    vp.add(settingsPanel);
-    commitMessageBlock = new CommitMessageBlock("6em");
-    HorizontalPanel hp = new HorizontalPanel();
-    hp.setWidth("100%");
-    hp.add(vp);
-    hp.add(commitMessageBlock);
-    add(hp);
+    topPanel = new FlowPanel();
+    add(topPanel);
 
     noDifference = new Label(PatchUtil.C.noDifference());
     noDifference.setStyleName(Gerrit.RESOURCES.css().patchNoDifference());
@@ -327,7 +305,7 @@
               patchSetDetail = result;
               if (fileList == null) {
                 fileList = new PatchTable(prefs);
-                fileList.display(result);
+                fileList.display(idSideA, result);
                 patchIndex = fileList.indexOf(patchKey);
               }
               refresh(true);
@@ -360,7 +338,31 @@
 
   protected abstract AbstractPatchContentTable createContentTable();
 
-  protected abstract PatchScreen.Type getPatchScreenType();
+  public abstract PatchScreen.Type getPatchScreenType();
+
+  public PatchSet.Id getSideA() {
+    return idSideA;
+  }
+
+  public Patch.Key getPatchKey() {
+    return patchKey;
+  }
+
+  public int getPatchIndex() {
+    return patchIndex;
+  }
+
+  public PatchSetDetail getPatchSetDetail() {
+    return patchSetDetail;
+  }
+
+  public PatchTable getFileList() {
+    return fileList;
+  }
+
+  public TopView getTopView() {
+    return topView;
+  }
 
   protected void refresh(final boolean isFirst) {
     final int rpcseq = ++rpcSequence;
@@ -386,8 +388,6 @@
   }
 
   private void onResult(final PatchScript script, final boolean isFirst) {
-
-    final Change.Key cid = script.getChangeId();
     final String path = PatchTable.getDisplayFileName(patchKey);
     String fileName = path;
     final int last = fileName.lastIndexOf('/');
@@ -395,8 +395,8 @@
       fileName = fileName.substring(last + 1);
     }
 
-    setWindowTitle(PatchUtil.M.patchWindowTitle(cid.abbreviate(), fileName));
-    setPageTitle(PatchUtil.M.patchPageTitle(cid.abbreviate(), path));
+    setWindowTitle(fileName);
+    setPageTitle(path);
 
     if (idSideB.equals(patchSetDetail.getPatchSet().getId())) {
       commitMessageBlock.setVisible(true);
@@ -414,7 +414,6 @@
     }
 
     historyTable.display(script.getHistory());
-    historyPanel.setVisible(true);
 
     // True if there are differences between the two patch sets
     boolean hasEdits = !script.getEdits().isEmpty();
@@ -433,12 +432,12 @@
       contentTable = new UnifiedDiffTable();
       contentTable.fileList = fileList;
       contentPanel.add(contentTable);
-      setToken(Dispatcher.toPatchUnified(patchKey));
+      setToken(Dispatcher.toPatchUnified(idSideA, patchKey));
     }
 
     if (hasDifferences) {
       contentTable.display(patchKey, idSideA, idSideB, script);
-      contentTable.display(script.getCommentDetail());
+      contentTable.display(script.getCommentDetail(), script.isExpandAllComments());
       contentTable.finishDisplay();
     }
     showPatch(hasDifferences);
@@ -452,10 +451,20 @@
       bottomNav.display(patchIndex, getPatchScreenType(), fileList);
     }
 
-    // Mark this file reviewed as soon we display the diff screen
-    if (Gerrit.isSignedIn() && isFirst) {
-      settingsPanel.getReviewedCheckBox().setValue(true);
-      setReviewedByCurrentUser(true /* reviewed */);
+    if (Gerrit.isSignedIn()) {
+      boolean isReviewed = false;
+      if (isFirst && !prefs.get().isManualReview()) {
+        isReviewed = true;
+        setReviewedByCurrentUser(isReviewed);
+      } else {
+        for (Patch p : patchSetDetail.getPatches()) {
+          if (p.getKey().equals(patchKey)) {
+            isReviewed = p.isReviewedByCurrentUser();
+            break;
+          }
+        }
+      }
+      reviewed.setValue(isReviewed);
     }
 
     intralineFailure = isFirst && script.hasIntralineFailure();
@@ -468,6 +477,9 @@
       intralineFailure = false;
       new ErrorDialog(PatchUtil.C.intralineFailure()).show();
     }
+    if (topView != null && prefs.get().isRetainHeader()) {
+      setTopView(topView);
+    }
   }
 
   private void showPatch(final boolean showPatch) {
@@ -476,19 +488,21 @@
     contentTable.setRegisterKeys(isCurrentView() && showPatch);
   }
 
-  public void setSideA(PatchSet.Id patchSetId) {
-    idSideA = patchSetId;
-    diffSideA = patchSetId;
-    if (fileList != null) {
-      fileList.setPatchSetIdToCompareWith(patchSetId);
+  public void setTopView(TopView tv) {
+    topView = tv;
+    topPanel.clear();
+    switch(tv) {
+      case COMMIT:      topPanel.add(commitMessageBlock);
+        break;
+      case PREFERENCES: topPanel.add(settingsPanel);
+        break;
+      case PATCH_SETS:  topPanel.add(historyTable);
+        break;
+      case FILES:       topPanel.add(fileList);
+        break;
     }
   }
 
-  public void setSideB(PatchSet.Id patchSetId) {
-    idSideB = patchSetId;
-    diffSideB = patchSetId;
-  }
-
   public class FileListCmd extends KeyCommand {
     public FileListCmd(int mask, int key, String help) {
       super(mask, key, help);
@@ -503,7 +517,7 @@
         Util.DETAIL_SVC.patchSetDetail(psid,
             new GerritCallback<PatchSetDetail>() {
               public void onSuccess(final PatchSetDetail result) {
-                fileList.display(result);
+                fileList.display(idSideA, result);
               }
             });
       }
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 404eeeb..871eb2b 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
@@ -20,8 +20,8 @@
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.ui.ListenableAccountDiffPreference;
 import com.google.gerrit.client.ui.NpIntTextBox;
-import com.google.gerrit.reviewdb.AccountDiffPreference;
-import com.google.gerrit.reviewdb.AccountDiffPreference.Whitespace;
+import com.google.gerrit.reviewdb.client.AccountDiffPreference;
+import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.KeyCodes;
@@ -41,7 +41,7 @@
 import com.google.gwt.user.client.ui.HasWidgets;
 import com.google.gwt.user.client.ui.ListBox;
 import com.google.gwt.user.client.ui.Widget;
-import com.google.gwtjsonrpc.client.VoidResult;
+import com.google.gwtjsonrpc.common.VoidResult;
 
 public class PatchScriptSettingsPanel extends Composite implements
     HasValueChangeHandlers<AccountDiffPreference> {
@@ -79,7 +79,7 @@
   CheckBox showTabs;
 
   @UiField
-  CheckBox reviewed;
+  CheckBox manualReview;
 
   @UiField
   CheckBox skipDeleted;
@@ -87,10 +87,18 @@
   @UiField
   CheckBox skipUncommented;
 
+  @UiField
+  CheckBox expandAllComments;
+
+  @UiField
+  CheckBox retainHeader;
 
   @UiField
   Button update;
 
+  @UiField
+  Button save;
+
   /**
    * Counts +1 for every setEnabled(true) and -1 for every setEnabled(false)
    *
@@ -111,14 +119,14 @@
     initIgnoreWhitespace(ignoreWhitespace);
     initContext(context);
     if (!Gerrit.isSignedIn()) {
-      reviewed.setVisible(false);
+      save.setVisible(false);
     }
 
     KeyPressHandler onEnter = new KeyPressHandler() {
       @Override
       public void onKeyPress(KeyPressEvent event) {
         if (event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER) {
-          update();
+          save();
         }
       }
     };
@@ -157,7 +165,7 @@
     } else {
       syntaxHighlighting.setValue(false);
     }
-    toggleEnabledStatus(update.isEnabled());
+    toggleEnabledStatus(save.isEnabled());
   }
 
   public void setEnableIntralineDifference(final boolean on) {
@@ -167,7 +175,7 @@
     } else {
       intralineDifference.setValue(false);
     }
-    toggleEnabledStatus(update.isEnabled());
+    toggleEnabledStatus(save.isEnabled());
   }
 
   private void toggleEnabledStatus(final boolean on) {
@@ -179,10 +187,6 @@
     syntaxHighlighting.setTitle(title);
   }
 
-  public CheckBox getReviewedCheckBox() {
-    return reviewed;
-  }
-
   public AccountDiffPreference getValue() {
     return listenablePrefs.get();
   }
@@ -209,6 +213,9 @@
     showTabs.setValue(dp.isShowTabs());
     skipDeleted.setValue(dp.isSkipDeleted());
     skipUncommented.setValue(dp.isSkipUncommented());
+    expandAllComments.setValue(dp.isExpandAllComments());
+    retainHeader.setValue(dp.isRetainHeader());
+    manualReview.setValue(dp.isManualReview());
   }
 
   @UiHandler("update")
@@ -216,6 +223,11 @@
     update();
   }
 
+  @UiHandler("save")
+  void onSave(ClickEvent event) {
+    save();
+  }
+
   private void update() {
     if (colWidth.getIntValue() <= 0) {
       new ErrorDialog(PatchUtil.C.illegalNumberOfColumns()).center();
@@ -233,9 +245,15 @@
     dp.setShowTabs(showTabs.getValue());
     dp.setSkipDeleted(skipDeleted.getValue());
     dp.setSkipUncommented(skipUncommented.getValue());
+    dp.setExpandAllComments(expandAllComments.getValue());
+    dp.setRetainHeader(retainHeader.getValue());
+    dp.setManualReview(manualReview.getValue());
 
     listenablePrefs.set(dp);
+  }
 
+  private void save() {
+    update();
     if (Gerrit.isSignedIn()) {
       persistDiffPreferences();
     }
@@ -243,11 +261,9 @@
 
   private void persistDiffPreferences() {
     setEnabled(false);
-    Util.ACCOUNT_SVC.changeDiffPreferences(getValue(),
-        new GerritCallback<VoidResult>() {
+    listenablePrefs.save(new GerritCallback<VoidResult>() {
       @Override
       public void onSuccess(VoidResult result) {
-        Gerrit.setAccountDiffPreference(getValue());
         setEnabled(true);
       }
 
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 f0f1b4d..2c7afff 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
@@ -117,37 +117,61 @@
 
     <td rowspan='2'>
       <g:CheckBox
+          ui:field='expandAllComments'
+          text='Expand All Comments'
+          tabIndex='9'>
+        <ui:attribute name='text'/>
+      </g:CheckBox>
+      <br/>
+      <g:CheckBox
+          ui:field='retainHeader'
+          text='Retain Header On File Switch'
+          tabIndex='10'>
+        <ui:attribute name='text'/>
+      </g:CheckBox>
+    </td>
+
+    <td rowspan='2'>
+      <g:CheckBox
           ui:field='skipUncommented'
           text='Skip Uncommented Files'
-          tabIndex='9'>
+          tabIndex='11'>
         <ui:attribute name='text'/>
       </g:CheckBox>
       <br/>
       <g:CheckBox
           ui:field='skipDeleted'
           text='Skip Deleted Files'
-          tabIndex='10'>
+          tabIndex='12'>
         <ui:attribute name='text'/>
       </g:CheckBox>
     </td>
 
     <td valign='bottom' rowspan='2'>
+      <g:CheckBox
+          ui:field='manualReview'
+          text='Manual Review'
+          tabIndex='13'>
+        <ui:attribute name='text'/>
+      </g:CheckBox>
+    </td>
+
+    <td rowspan='2'>
+      <br/>
       <g:Button
           ui:field='update'
           text='Update'
           styleName='{style.updateButton}'
-          tabIndex='11'>
+          tabIndex='14'>
         <ui:attribute name='text'/>
       </g:Button>
-    </td>
-
-    <td>
-      <g:CheckBox
-          ui:field='reviewed'
-          text='Reviewed'
-          tabIndex='12'>
+      <g:Button
+          ui:field='save'
+          text='Save'
+          styleName='{style.updateButton}'
+          tabIndex='15'>
         <ui:attribute name='text'/>
-      </g:CheckBox>
+      </g:Button>
     </td>
   </tr>
 
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 2939e71..6379e23 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/SideBySideTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/SideBySideTable.java
@@ -25,11 +25,17 @@
 import com.google.gerrit.common.data.PatchScript.FileMode;
 import com.google.gerrit.prettify.common.EditList;
 import com.google.gerrit.prettify.common.SparseHtmlFile;
-import com.google.gerrit.reviewdb.Patch;
-import com.google.gerrit.reviewdb.PatchLineComment;
-import com.google.gerrit.reviewdb.Patch.ChangeType;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.Patch.ChangeType;
 import com.google.gwt.core.client.GWT;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.user.client.ui.Anchor;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.HTMLTable.Cell;
 import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
+import com.google.gwt.user.client.ui.Label;
 import com.google.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) {
@@ -64,6 +75,13 @@
   }
 
   @Override
+  protected void onCellSingleClick(int row, int column) {
+    if (column == 1 || column == 3) {
+      onCellDoubleClick(row, column);
+    }
+  }
+
+  @Override
   protected void onInsertComment(final PatchLine line) {
     final int row = getCurrentRow();
     createCommentEditor(row + 1, 4, line.getLineB(), (short) 1);
@@ -71,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 = getSparseHtmlFileA(script);
+    b = getSparseHtmlFileB(script);
+    final ArrayList<Object> lines = new ArrayList<Object>();
     final SafeHtmlBuilder nc = new SafeHtmlBuilder();
     final boolean intraline =
         script.getDiffPrefs().isIntralineDifference()
@@ -90,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()) {
@@ -149,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));
+      }
     }
   }
 
@@ -186,7 +210,7 @@
   }
 
   @Override
-  public void display(final CommentDetail cd) {
+  public void display(final CommentDetail cd, boolean expandComments) {
     if (cd.isEmpty()) {
       return;
     }
@@ -205,13 +229,13 @@
           final PatchLineComment ac = ai.next();
           final PatchLineComment bc = bi.next();
           insertRow(row);
-          bindComment(row, COL_A, ac, !ai.hasNext());
-          bindComment(row, COL_B, bc, !bi.hasNext());
+          bindComment(row, COL_A, ac, !ai.hasNext(), expandComments);
+          bindComment(row, COL_B, bc, !bi.hasNext(), expandComments);
           row++;
         }
 
-        row = finish(ai, row, COL_A);
-        row = finish(bi, row, COL_B);
+        row = finish(ai, row, COL_A, expandComments);
+        row = finish(bi, row, COL_B, expandComments);
       } else {
         row++;
       }
@@ -228,11 +252,11 @@
     fmt.addStyleName(row, COL_B, Gerrit.RESOURCES.css().diffText());
   }
 
-  private int finish(final Iterator<PatchLineComment> i, int row, final int col) {
+  private int finish(final Iterator<PatchLineComment> i, int row, final int col, boolean expandComment) {
     while (i.hasNext()) {
       final PatchLineComment c = i.next();
       insertRow(row);
-      bindComment(row, col, c, !i.hasNext());
+      bindComment(row, col, c, !i.hasNext(), expandComment);
       row++;
     }
     return row;
@@ -308,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");
@@ -335,7 +460,7 @@
       final boolean fullBlock) {
     m.openTd();
     m.setStyleName(Gerrit.RESOURCES.css().lineNumber());
-    m.append(lineNumberMinusOne + 1);
+    m.append(SafeHtml.asis("<a href=\"javascript:void(0)\">"+ (lineNumberMinusOne + 1) + "</a>"));
     m.closeTd();
 
     m.openTd();
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/patches/UnifiedDiffTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UnifiedDiffTable.java
index 508e508..f0f619e 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UnifiedDiffTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UnifiedDiffTable.java
@@ -23,10 +23,10 @@
 import com.google.gerrit.common.data.PatchScript;
 import com.google.gerrit.common.data.PatchScript.DisplayMethod;
 import com.google.gerrit.prettify.common.EditList;
-import com.google.gerrit.prettify.common.SparseHtmlFile;
 import com.google.gerrit.prettify.common.EditList.Hunk;
-import com.google.gerrit.reviewdb.Patch;
-import com.google.gerrit.reviewdb.PatchLineComment;
+import com.google.gerrit.prettify.common.SparseHtmlFile;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
 import com.google.gwtexpui.safehtml.client.SafeHtml;
@@ -65,6 +65,15 @@
   }
 
   @Override
+  protected void onCellSingleClick(int row, int column) {
+    if (column == 1 || column == 2) {
+      if (!"".equals(table.getText(row, column))) {
+        onCellDoubleClick(row, column);
+      }
+    }
+  }
+
+  @Override
   protected void onInsertComment(final PatchLine pl) {
     final int row = getCurrentRow();
     switch (pl.getType()) {
@@ -86,8 +95,8 @@
 
   @Override
   protected void render(final PatchScript script) {
-    final SparseHtmlFile a = script.getSparseHtmlFileA();
-    final SparseHtmlFile b = script.getSparseHtmlFileB();
+    final SparseHtmlFile a = getSparseHtmlFileA(script);
+    final SparseHtmlFile b = getSparseHtmlFileB(script);
     final SafeHtmlBuilder nc = new SafeHtmlBuilder();
 
     // Display the patch header
@@ -205,7 +214,7 @@
   }
 
   @Override
-  public void display(final CommentDetail cd) {
+  public void display(final CommentDetail cd, boolean expandComments) {
     if (cd.isEmpty()) {
       return;
     }
@@ -224,13 +233,13 @@
           all.addAll(fora);
           all.addAll(forb);
           Collections.sort(all, BY_DATE);
-          row = insert(all, row);
+          row = insert(all, row, expandComments);
 
         } else if (!fora.isEmpty()) {
-          row = insert(fora, row);
+          row = insert(fora, row, expandComments);
 
         } else if (!forb.isEmpty()) {
-          row = insert(forb, row);
+          row = insert(forb, row, expandComments);
         }
       } else {
         row++;
@@ -248,11 +257,11 @@
     fmt.addStyleName(row, PC, Gerrit.RESOURCES.css().diffText());
   }
 
-  private int insert(final List<PatchLineComment> in, int row) {
+  private int insert(final List<PatchLineComment> in, int row, boolean expandComment) {
     for (Iterator<PatchLineComment> ci = in.iterator(); ci.hasNext();) {
       final PatchLineComment c = ci.next();
       insertRow(row);
-      bindComment(row, PC, c, !ci.hasNext());
+      bindComment(row, PC, c, !ci.hasNext(), expandComment);
       row++;
     }
     return row;
@@ -374,7 +383,7 @@
   private void appendLineNumber(final SafeHtmlBuilder m, final int idx) {
     m.openTd();
     m.setStyleName(Gerrit.RESOURCES.css().lineNumber());
-    m.append(idx + 1);
+    m.append(SafeHtml.asis("<a href=\"javascript:void(0)\">"+ (idx + 1) + "</a>"));
     m.closeTd();
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UpToChangeCommand.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UpToChangeCommand.java
index 3faa998..72318dd 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UpToChangeCommand.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/UpToChangeCommand.java
@@ -17,7 +17,7 @@
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.changes.ChangeScreen;
 import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.reviewdb.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gwt.event.dom.client.KeyPressEvent;
 import com.google.gwtexpui.globalkey.client.KeyCommand;
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/GerritCallback.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/GerritCallback.java
index 1871bb7..98ae46f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/GerritCallback.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/GerritCallback.java
@@ -24,11 +24,11 @@
 import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.common.errors.NotSignedInException;
 import com.google.gwt.core.client.GWT;
-import com.google.gwt.user.client.rpc.AsyncCallback;
+import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.gwt.user.client.rpc.InvocationException;
-import com.google.gwtjsonrpc.client.JsonUtil;
 import com.google.gwtjsonrpc.client.RemoteJsonException;
 import com.google.gwtjsonrpc.client.ServerUnavailableException;
+import com.google.gwtjsonrpc.common.JsonConstants;
 
 /** Abstract callback handling generic error conditions automatically */
 public abstract class GerritCallback<T> implements AsyncCallback<T> {
@@ -70,7 +70,7 @@
 
   private static boolean isInvalidXSRF(final Throwable caught) {
     return caught instanceof InvocationException
-        && caught.getMessage().equals(JsonUtil.ERROR_INVALID_XSRF);
+        && caught.getMessage().equals(JsonConstants.ERROR_INVALID_XSRF);
   }
 
   private static boolean isNotSignedIn(final Throwable caught) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountDashboardLink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountDashboardLink.java
index bd8a1ce..5233a6b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountDashboardLink.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountDashboardLink.java
@@ -20,7 +20,7 @@
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.data.AccountInfo;
 import com.google.gerrit.common.data.AccountInfoCache;
-import com.google.gerrit.reviewdb.Account;
+import com.google.gerrit.reviewdb.client.Account;
 
 /** Link to any user's account dashboard. */
 public class AccountDashboardLink extends InlineHyperlink {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountGroupSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountGroupSuggestOracle.java
index 0a9cedf..885f53b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountGroupSuggestOracle.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountGroupSuggestOracle.java
@@ -16,26 +16,34 @@
 
 import com.google.gerrit.client.RpcStatus;
 import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.reviewdb.AccountGroupName;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gwt.user.client.ui.SuggestOracle;
 import com.google.gwtexpui.safehtml.client.HighlightSuggestOracle;
 
 import java.util.ArrayList;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 /** Suggestion Oracle for AccountGroup entities. */
 public class AccountGroupSuggestOracle extends HighlightSuggestOracle {
+  private Map<String, AccountGroup.UUID> priorResults =
+      new HashMap<String, AccountGroup.UUID>();
+
   @Override
   public void onRequestSuggestions(final Request req, final Callback callback) {
     RpcStatus.hide(new Runnable() {
       public void run() {
         SuggestUtil.SVC.suggestAccountGroup(req.getQuery(), req.getLimit(),
-            new GerritCallback<List<AccountGroupName>>() {
-              public void onSuccess(final List<AccountGroupName> result) {
+            new GerritCallback<List<GroupReference>>() {
+              public void onSuccess(final List<GroupReference> result) {
+                priorResults.clear();
                 final ArrayList<AccountGroupSuggestion> r =
                     new ArrayList<AccountGroupSuggestion>(result.size());
-                for (final AccountGroupName p : result) {
+                for (final GroupReference p : result) {
                   r.add(new AccountGroupSuggestion(p));
+                  priorResults.put(p.getName(), p.getUUID());
                 }
                 callback.onSuggestionsReady(req, new Response(r));
               }
@@ -46,9 +54,9 @@
 
   private static class AccountGroupSuggestion implements
       SuggestOracle.Suggestion {
-    private final AccountGroupName info;
+    private final GroupReference info;
 
-    AccountGroupSuggestion(final AccountGroupName k) {
+    AccountGroupSuggestion(final GroupReference k) {
       info = k;
     }
 
@@ -60,4 +68,9 @@
       return info.getName();
     }
   }
+
+  /** @return the group UUID, or null if it cannot be found. */
+  public AccountGroup.UUID getUUID(String name) {
+    return priorResults.get(name);
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AddIncludedGroupBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AddIncludedGroupBox.java
deleted file mode 100644
index 60b8ccb1..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AddIncludedGroupBox.java
+++ /dev/null
@@ -1,106 +0,0 @@
-// Copyright (C) 2011 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.ui;
-
-import com.google.gerrit.client.admin.Util;
-import com.google.gerrit.client.ui.HintTextBox;
-import com.google.gerrit.client.ui.RPCSuggestOracle;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.dom.client.KeyCodes;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.event.dom.client.KeyPressHandler;
-import com.google.gwt.event.logical.shared.SelectionEvent;
-import com.google.gwt.event.logical.shared.SelectionHandler;
-import com.google.gwt.user.client.ui.Button;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.SuggestBox;
-import com.google.gwt.user.client.ui.SuggestBox.DefaultSuggestionDisplay;
-import com.google.gwt.user.client.ui.SuggestOracle.Suggestion;
-
-public class AddIncludedGroupBox extends Composite {
-  private final FlowPanel addPanel;
-  private final Button addMember;
-  private final HintTextBox nameTxtBox;
-  private final SuggestBox nameTxt;
-  private boolean submitOnSelection;
-
-  public AddIncludedGroupBox() {
-    addPanel = new FlowPanel();
-    addMember = new Button(Util.C.buttonAddIncludedGroup());
-    nameTxtBox = new HintTextBox();
-    nameTxt = new SuggestBox(new RPCSuggestOracle(
-        new AccountGroupSuggestOracle()), nameTxtBox);
-
-    nameTxtBox.setVisibleLength(50);
-    nameTxtBox.setHintText(Util.C.defaultAccountGroupName());
-    nameTxtBox.addKeyPressHandler(new KeyPressHandler() {
-      @Override
-      public void onKeyPress(KeyPressEvent event) {
-        submitOnSelection = false;
-
-        if (event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER) {
-          if (((DefaultSuggestionDisplay) nameTxt.getSuggestionDisplay())
-              .isSuggestionListShowing()) {
-            submitOnSelection = true;
-          } else {
-            doAdd();
-          }
-        }
-      }
-    });
-    nameTxt.addSelectionHandler(new SelectionHandler<Suggestion>() {
-      @Override
-      public void onSelection(SelectionEvent<Suggestion> event) {
-        if (submitOnSelection) {
-          submitOnSelection = false;
-          doAdd();
-        }
-      }
-    });
-
-    addPanel.add(nameTxt);
-    addPanel.add(addMember);
-
-    initWidget(addPanel);
-  }
-
-  public void setAddButtonText(final String text) {
-    addMember.setText(text);
-  }
-
-  public void addClickHandler(final ClickHandler handler) {
-    addMember.addClickHandler(handler);
-  }
-
-  public String getText() {
-    String s = nameTxtBox.getText();
-    return s == null ? "" : s;
-  }
-
-  public void setEnabled(boolean enabled) {
-    addMember.setEnabled(enabled);
-    nameTxtBox.setEnabled(enabled);
-  }
-
-  public void setText(String text) {
-    nameTxtBox.setText(text);
-  }
-
-  private void doAdd() {
-    addMember.fireEvent(new ClickEvent() {});
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AddMemberBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AddMemberBox.java
index c63ae84..72a011f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AddMemberBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AddMemberBox.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.client.ui;
 
+import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.admin.Util;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
@@ -27,6 +28,7 @@
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.SuggestBox;
 import com.google.gwt.user.client.ui.SuggestBox.DefaultSuggestionDisplay;
+import com.google.gwt.user.client.ui.SuggestOracle;
 import com.google.gwt.user.client.ui.SuggestOracle.Suggestion;
 
 public class AddMemberBox extends Composite {
@@ -37,14 +39,21 @@
   private boolean submitOnSelection;
 
   public AddMemberBox() {
+    this(Util.C.buttonAddGroupMember(), Util.C.defaultAccountName(),
+        new AccountSuggestOracle());
+  }
+
+  public AddMemberBox(final String buttonLabel, final String hint,
+      final SuggestOracle suggestOracle) {
     addPanel = new FlowPanel();
-    addMember = new Button(Util.C.buttonAddGroupMember());
+    addMember = new Button(buttonLabel);
     nameTxtBox = new HintTextBox();
     nameTxt = new SuggestBox(new RPCSuggestOracle(
-        new AccountSuggestOracle()), nameTxtBox);
+        suggestOracle), nameTxtBox);
+    nameTxt.setStyleName(Gerrit.RESOURCES.css().addMemberTextBox());
 
     nameTxtBox.setVisibleLength(50);
-    nameTxtBox.setHintText(Util.C.defaultAccountName());
+    nameTxtBox.setHintText(hint);
     nameTxtBox.addKeyPressHandler(new KeyPressHandler() {
       @Override
       public void onKeyPress(KeyPressEvent event) {
@@ -76,10 +85,6 @@
     initWidget(addPanel);
   }
 
-  public void setAddButtonText(final String text) {
-    addMember.setText(text);
-  }
-
   public void addClickHandler(final ClickHandler handler) {
     addMember.addClickHandler(handler);
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/BranchLink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/BranchLink.java
index 01d7d8b..fddae84 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/BranchLink.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/BranchLink.java
@@ -17,9 +17,9 @@
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.changes.QueryScreen;
 import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.reviewdb.Branch;
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
 
 /** Link to the open changes of a project. */
 public class BranchLink extends InlineHyperlink {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ChangeLink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ChangeLink.java
index 0d0dfdb..d708286 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ChangeLink.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ChangeLink.java
@@ -18,8 +18,8 @@
 import com.google.gerrit.client.changes.ChangeScreen;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.data.ChangeInfo;
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.PatchSet;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.user.client.DOM;
 
@@ -28,26 +28,31 @@
     return GWT.getHostPageBaseURL() + c.get();
   }
 
-  protected Change.Id id;
-  protected PatchSet.Id ps;
-  private ChangeInfo info;
+  protected Change.Id cid;
+  protected PatchSet.Id psid;
 
   public ChangeLink(final String text, final Change.Id c) {
     super(text, PageLinks.toChange(c));
     DOM.setElementProperty(getElement(), "href", permalink(c));
-    id = c;
-    ps = null;
+    cid = c;
+    psid = null;
   }
 
   public ChangeLink(final String text, final PatchSet.Id ps) {
     super(text, PageLinks.toChange(ps));
-    id = ps.getParentKey();
-    this.ps = ps;
+    cid = ps.getParentKey();
+    psid = ps;
   }
 
-  public ChangeLink(final String text, final ChangeInfo c) {
-    this(text, c.getId());
-    info = c;
+  public ChangeLink(final String text, final ChangeInfo info) {
+    super(text, getTarget(info));
+    cid = info.getId();
+    psid = info.getPatchSetId();
+  }
+
+  public static String getTarget(final ChangeInfo info) {
+    PatchSet.Id ps = info.getPatchSetId();
+    return (ps == null) ? PageLinks.toChange(info) : PageLinks.toChange(ps);
   }
 
   @Override
@@ -56,12 +61,10 @@
   }
 
   private Screen createScreen() {
-    if (info != null) {
-      return new ChangeScreen(info);
-    } else if (ps != null) {
-      return new ChangeScreen(ps);
+    if (psid != null) {
+      return new ChangeScreen(psid);
     } else {
-      return new ChangeScreen(id);
+      return new ChangeScreen(cid);
     }
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentLinkProcessor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentLinkProcessor.java
index f5ef2f4..a3c7a3c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentLinkProcessor.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentLinkProcessor.java
@@ -15,10 +15,10 @@
 package com.google.gerrit.client.ui;
 
 import com.google.gerrit.client.Gerrit;
-import com.google.gwt.user.client.rpc.AsyncCallback;
+import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.gwtexpui.safehtml.client.RegexFindReplace;
 import com.google.gwtexpui.safehtml.client.SafeHtml;
-import com.google.gwtjsonrpc.client.VoidResult;
+import com.google.gwtjsonrpc.common.VoidResult;
 
 import java.util.ArrayList;
 import java.util.Collections;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentPanel.java
index 249c70c..0cea2c7 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentPanel.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentPanel.java
@@ -35,11 +35,11 @@
 import com.google.gwt.user.client.ui.FlexTable;
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.HTML;
+import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
 import com.google.gwt.user.client.ui.HasHorizontalAlignment;
 import com.google.gwt.user.client.ui.InlineLabel;
 import com.google.gwt.user.client.ui.Panel;
 import com.google.gwt.user.client.ui.Widget;
-import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
 import com.google.gwtexpui.safehtml.client.SafeHtml;
 import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentedActionDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentedActionDialog.java
new file mode 100644
index 0000000..26b61f5
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommentedActionDialog.java
@@ -0,0 +1,142 @@
+// 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.client.ui;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.ui.SmallHeading;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.logical.shared.CloseEvent;
+import com.google.gwt.event.logical.shared.CloseHandler;
+import com.google.gwt.user.client.DOM;
+import com.google.gwtjsonrpc.common.AsyncCallback;
+import com.google.gwt.user.client.ui.Button;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.PopupPanel;
+import com.google.gwtexpui.globalkey.client.GlobalKey;
+import com.google.gwtexpui.globalkey.client.NpTextArea;
+import com.google.gwtexpui.user.client.AutoCenterDialogBox;
+
+public abstract class CommentedActionDialog<T> extends AutoCenterDialogBox
+    implements CloseHandler<PopupPanel> {
+  protected final FlowPanel panel;
+  protected final NpTextArea message;
+  protected final Button sendButton;
+  protected final Button cancelButton;
+  protected final FlowPanel buttonPanel;
+  protected AsyncCallback<T> callback;
+
+  protected boolean sent = false;
+
+  public CommentedActionDialog(final String title, final String heading,
+      AsyncCallback<T> callback) {
+    super(/* auto hide */false, /* modal */true);
+    this.callback = callback;
+
+    setGlassEnabled(true);
+    setText(title);
+
+    addStyleName(Gerrit.RESOURCES.css().commentedActionDialog());
+
+    message = new NpTextArea();
+    message.setCharacterWidth(60);
+    message.setVisibleLines(10);
+    DOM.setElementPropertyBoolean(message.getElement(), "spellcheck", true);
+
+    sendButton = new Button(Util.C.commentedActionButtonSend());
+    sendButton.addClickHandler(new ClickHandler() {
+      @Override
+      public void onClick(final ClickEvent event) {
+        enableButtons(false);
+        onSend();
+      }
+    });
+
+    cancelButton = new Button(Util.C.commentedActionButtonCancel());
+    DOM.setStyleAttribute(cancelButton.getElement(), "marginLeft", "300px");
+    cancelButton.addClickHandler(new ClickHandler() {
+      @Override
+      public void onClick(final ClickEvent event) {
+        hide();
+      }
+    });
+
+    final FlowPanel mwrap = new FlowPanel();
+    mwrap.setStyleName(Gerrit.RESOURCES.css().commentedActionMessage());
+    mwrap.add(message);
+
+    buttonPanel = new FlowPanel();
+    buttonPanel.add(sendButton);
+    buttonPanel.add(cancelButton);
+
+    panel = new FlowPanel();
+    panel.add(new SmallHeading(heading));
+    panel.add(mwrap);
+    panel.add(buttonPanel);
+    add(panel);
+
+    addCloseHandler(this);
+  }
+
+  public void enableButtons(boolean enable) {
+    sendButton.setEnabled(enable);
+    cancelButton.setEnabled(enable);
+  }
+
+  @Override
+  public void center() {
+    super.center();
+    GlobalKey.dialog(this);
+    message.setFocus(true);
+  }
+
+  @Override
+  public void onClose(CloseEvent<PopupPanel> event) {
+    if (!sent) {
+      // the dialog was closed without the send button being pressed
+      // e.g. the user pressed Cancel or ESC to close the dialog
+      if (callback != null) {
+        callback.onFailure(null);
+      }
+    }
+    sent = false;
+  }
+
+  public abstract void onSend();
+
+  public String getMessageText() {
+    return message.getText().trim();
+  }
+
+  public AsyncCallback<T> createCallback() {
+    return new GerritCallback<T>(){
+      @Override
+      public void onSuccess(T result) {
+        sent = true;
+        if (callback != null) {
+          callback.onSuccess(result);
+        }
+        hide();
+      }
+
+      @Override
+      public void onFailure(Throwable caught) {
+        enableButtons(true);
+        super.onFailure(caught);
+      }
+    };
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FancyFlexTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FancyFlexTable.java
index 3fca1b1..1edb8fd 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FancyFlexTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FancyFlexTable.java
@@ -21,8 +21,8 @@
 import com.google.gwt.user.client.Element;
 import com.google.gwt.user.client.ui.Composite;
 import com.google.gwt.user.client.ui.FlexTable;
-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.safehtml.client.SafeHtml;
 
 import java.util.Iterator;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/HintTextBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/HintTextBox.java
index 814fb1f..09550a6 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/HintTextBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/HintTextBox.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.client.ui;
 
 import com.google.gerrit.client.Gerrit;
-
 import com.google.gwt.event.dom.client.BlurEvent;
 import com.google.gwt.event.dom.client.BlurHandler;
 import com.google.gwt.event.dom.client.FocusEvent;
@@ -24,10 +23,10 @@
 import com.google.gwt.event.dom.client.KeyDownEvent;
 import com.google.gwt.event.dom.client.KeyDownHandler;
 import com.google.gwt.event.shared.HandlerRegistration;
-import com.google.gwtexpui.globalkey.client.NpTextBox;
 import com.google.gwt.user.client.ui.SuggestBox;
-import com.google.gwt.user.client.ui.Widget;
 import com.google.gwt.user.client.ui.SuggestBox.DefaultSuggestionDisplay;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwtexpui.globalkey.client.NpTextBox;
 
 
 public class HintTextBox extends NpTextBox {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Hyperlink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Hyperlink.java
index 6c18db1..1261b42 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Hyperlink.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Hyperlink.java
@@ -25,6 +25,10 @@
 public class Hyperlink extends com.google.gwt.user.client.ui.Hyperlink {
   static final HyperlinkImpl impl = GWT.create(HyperlinkImpl.class);
 
+  /** Initialize a default hyperlink with no target and no text. */
+  public Hyperlink() {
+  }
+
   /**
    * Creates a hyperlink with its text and target history token specified.
    *
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ListenableAccountDiffPreference.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ListenableAccountDiffPreference.java
index 195adf5..27bc107 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ListenableAccountDiffPreference.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ListenableAccountDiffPreference.java
@@ -15,12 +15,37 @@
 package com.google.gerrit.client.ui;
 
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.reviewdb.AccountDiffPreference;
+import com.google.gerrit.client.account.Util;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.reviewdb.client.AccountDiffPreference;
+import com.google.gwtjsonrpc.common.VoidResult;
 
 public class ListenableAccountDiffPreference
-    extends ListenableValue<AccountDiffPreference> {
+    extends ListenableOldValue<AccountDiffPreference> {
 
   public ListenableAccountDiffPreference() {
+    reset();
+  }
+
+  public void save(final GerritCallback<VoidResult> cb) {
+    if (Gerrit.isSignedIn()) {
+      Util.ACCOUNT_SVC.changeDiffPreferences(get(),
+          new GerritCallback<VoidResult>() {
+        @Override
+        public void onSuccess(VoidResult result) {
+          Gerrit.setAccountDiffPreference(get());
+          cb.onSuccess(result);
+        }
+
+        @Override
+        public void onFailure(Throwable caught) {
+          cb.onFailure(caught);
+        }
+      });
+    }
+  }
+
+  public void reset() {
     if (Gerrit.isSignedIn() && Gerrit.getAccountDiffPreference() != null) {
       set(Gerrit.getAccountDiffPreference());
     } else {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ListenableOldValue.java
similarity index 63%
copy from gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectConstants.java
copy to gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ListenableOldValue.java
index 8b13392..5c3e127 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ListenableOldValue.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2010 The Android Open Source Project
+// Copyright (C) 2012 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -14,12 +14,17 @@
 
 package com.google.gerrit.client.ui;
 
-import com.google.gwt.i18n.client.Constants;
+public class ListenableOldValue<T> extends ListenableValue<T> {
 
-public interface ProjectConstants extends Constants {
-  String projectName();
-  String projectDescription();
-  String projectListOpen();
-  String projectListPrev();
-  String projectListNext();
+  private T oldValue;
+
+  public T getOld() {
+    return oldValue;
+  }
+
+  public void set(final T value) {
+    oldValue = get();
+    super.set(value);
+    oldValue = null; // allow it to be gced before the next event
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/MenuScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/MenuScreen.java
index 704185c..cf49ce7 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/MenuScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/MenuScreen.java
@@ -53,9 +53,20 @@
     body.add(w);
   }
 
-  protected void link(String text, String target) {
+  protected void link(final String text, final String target) {
+    link(text, target, true);
+  }
+
+  protected void link(final String text, final String target,
+      final boolean visible) {
     final LinkMenuItem item = new LinkMenuItem(text, target);
     item.setStyleName(Gerrit.RESOURCES.css().menuItem());
+    item.setVisible(visible);
     menu.add(item);
   }
+
+  protected void setLinkVisible(final String token, final boolean visible) {
+    final LinkMenuItem item = menu.find(token);
+    item.setVisible(visible);
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/MorphingTabPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/MorphingTabPanel.java
new file mode 100644
index 0000000..8c6fe07
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/MorphingTabPanel.java
@@ -0,0 +1,101 @@
+// Copyright (C) 2011 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.ui;
+
+import com.google.gwt.event.logical.shared.SelectionEvent;
+import com.google.gwt.event.logical.shared.SelectionHandler;
+import com.google.gwt.user.client.ui.TabPanel;
+import com.google.gwt.user.client.ui.Widget;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** A TabPanel which allows entries to be hidden.  This class is not yet
+ *  designed to handle removes or any other add methods than the one
+ *  overridden here.  It is also not designed to handle anything other
+ *  than text for the tab.
+ */
+public class MorphingTabPanel extends TabPanel {
+  // Keep track of the order the widgets/texts should be in when not hidden.
+  private List<Widget> widgets = new ArrayList<Widget>();
+  private List<String> texts = new ArrayList<String>();
+
+  // currently visible widgets
+  private List<Widget> visibles = new ArrayList<Widget>();
+
+  private int selection;
+
+  public MorphingTabPanel() {
+    addSelectionHandler(new SelectionHandler<Integer>() {
+        @Override
+        public void onSelection(SelectionEvent<Integer> ev) {
+          selection = ev.getSelectedItem();
+        }
+      });
+  }
+
+  public int getSelectedIndex() {
+    return selection;
+  }
+
+  public Widget getSelectedWidget() {
+    return getWidget(getSelectedIndex());
+  }
+
+  @Override
+  public void clear() {
+    super.clear();
+    widgets.clear();
+    texts.clear();
+    visibles.clear();
+  }
+
+  @Override
+  public void add(Widget w, String tabText) {
+    addInvisible(w, tabText);
+    visibles.add(w);
+    super.add(w, tabText);
+  }
+
+  public void addInvisible(Widget w, String tabText) {
+    widgets.add(w);
+    texts.add(tabText);
+  }
+
+  public void setVisible(Widget w, boolean visible) {
+    if (visible) {
+      if (visibles.indexOf(w) == -1) {
+        int origPos = widgets.indexOf(w);
+
+        /* Re-insert the widget right after the first visible widget found
+           when scanning backwards from the current widget */
+        for (int pos = origPos -1; pos >=0 ; pos--) {
+          int visiblePos = visibles.indexOf(widgets.get(pos));
+          if (visiblePos != -1) {
+            visibles.add(visiblePos + 1, w);
+            insert(w, texts.get(origPos), visiblePos + 1);
+            break;
+          }
+        }
+      }
+    } else {
+      int i = visibles.indexOf(w);
+      if (i != -1) {
+        visibles.remove(i);
+        remove(i);
+      }
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/NavigationTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/NavigationTable.java
index 0d45529..8d74bc3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/NavigationTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/NavigationTable.java
@@ -20,11 +20,11 @@
 import com.google.gwt.event.shared.HandlerRegistration;
 import com.google.gwt.user.client.DOM;
 import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
 import com.google.gwt.user.client.ui.Image;
 import com.google.gwt.user.client.ui.ScrollPanel;
 import com.google.gwt.user.client.ui.UIObject;
 import com.google.gwt.user.client.ui.Widget;
-import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
 import com.google.gwtexpui.globalkey.client.GlobalKey;
 import com.google.gwtexpui.globalkey.client.KeyCommand;
 import com.google.gwtexpui.globalkey.client.KeyCommandSet;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/NpIntTextBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/NpIntTextBox.java
index e548e75..820a631 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/NpIntTextBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/NpIntTextBox.java
@@ -39,13 +39,15 @@
       public void onKeyPress(KeyPressEvent event) {
         char c = event.getCharCode();
         if (c < '0' || '9' < c) {
-          switch (c) {
+          final int nativeCode = event.getNativeEvent().getKeyCode();
+          switch (nativeCode) {
             case KeyCodes.KEY_BACKSPACE:
             case KeyCodes.KEY_LEFT:
             case KeyCodes.KEY_RIGHT:
             case KeyCodes.KEY_HOME:
             case KeyCodes.KEY_END:
             case KeyCodes.KEY_TAB:
+            case KeyCodes.KEY_DELETE:
               break;
 
             default:
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/PatchLink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/PatchLink.java
index 9979edf..f79dc51 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/PatchLink.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/PatchLink.java
@@ -16,60 +16,89 @@
 
 import com.google.gerrit.client.Dispatcher;
 import com.google.gerrit.client.changes.PatchTable;
+import com.google.gerrit.client.patches.PatchScreen;
 import com.google.gerrit.common.data.PatchSetDetail;
-import com.google.gerrit.reviewdb.Patch;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.PatchSet;
 
-public abstract class PatchLink extends InlineHyperlink {
+public class PatchLink extends InlineHyperlink {
+  protected PatchSet.Id base;
   protected Patch.Key patchKey;
   protected int patchIndex;
   protected PatchSetDetail patchSetDetail;
   protected PatchTable parentPatchTable;
+  protected PatchScreen.TopView topView;
 
   /**
    * @param text The text of this link
+   * @param base optional base to compare against.
    * @param patchKey The key for this patch
    * @param patchIndex The index of the current patch in the patch set
    * @param historyToken The history token
-   * @parma patchSetDetail Detailed information about the patch set.
+   * @param patchSetDetail Detailed information about the patch set.
    * @param parentPatchTable The table used to display this link
    */
-  public PatchLink(final String text, final Patch.Key patchKey,
-      final int patchIndex, final String historyToken,
-      final PatchSetDetail patchSetDetail,
-      final PatchTable parentPatchTable) {
+  protected PatchLink(String text, PatchSet.Id base, Patch.Key patchKey,
+      int patchIndex, String historyToken,
+      PatchSetDetail patchSetDetail, PatchTable parentPatchTable,
+      PatchScreen.TopView topView) {
     super(text, historyToken);
+    this.base = base;
     this.patchKey = patchKey;
     this.patchIndex = patchIndex;
     this.patchSetDetail = patchSetDetail;
     this.parentPatchTable = parentPatchTable;
+    this.parentPatchTable = parentPatchTable;
+    this.topView = topView;
+  }
+
+  /**
+   * @param text The text of this link
+   * @param type The type of the link to create (unified/side-by-side)
+   * @param patchScreen The patchScreen to grab contents to link to from
+   */
+  public PatchLink(String text, PatchScreen.Type type, PatchScreen patchScreen) {
+    this(text, //
+        patchScreen.getSideA(), //
+        patchScreen.getPatchKey(), //
+        patchScreen.getPatchIndex(), //
+        Dispatcher.toPatch(type, patchScreen.getPatchKey()), //
+        patchScreen.getPatchSetDetail(), //
+        patchScreen.getFileList(), //
+        patchScreen.getTopView() //
+        );
   }
 
   @Override
   public void go() {
     Dispatcher.patch( //
         getTargetHistoryToken(), //
+        base, //
         patchKey, //
         patchIndex, //
         patchSetDetail, //
-        parentPatchTable //
+        parentPatchTable,
+        topView //
         );
   }
 
   public static class SideBySide extends PatchLink {
-    public SideBySide(final String text, final Patch.Key patchKey,
-        final int patchIndex, PatchSetDetail patchSetDetail,
+    public SideBySide(String text, PatchSet.Id base, Patch.Key patchKey,
+        int patchIndex, PatchSetDetail patchSetDetail,
         PatchTable parentPatchTable) {
-      super(text, patchKey, patchIndex, Dispatcher.toPatchSideBySide(patchKey),
-          patchSetDetail, parentPatchTable);
+      super(text, base, patchKey, patchIndex,
+          Dispatcher.toPatchSideBySide(base, patchKey),
+          patchSetDetail, parentPatchTable, null);
     }
   }
 
   public static class Unified extends PatchLink {
-    public Unified(final String text, final Patch.Key patchKey,
-        final int patchIndex, PatchSetDetail patchSetDetail,
+    public Unified(String text, PatchSet.Id base, final Patch.Key patchKey,
+        int patchIndex, PatchSetDetail patchSetDetail,
         PatchTable parentPatchTable) {
-      super(text, patchKey, patchIndex, Dispatcher.toPatchUnified(patchKey),
-          patchSetDetail, parentPatchTable);
+      super(text, base, patchKey, patchIndex,
+          Dispatcher.toPatchUnified(base, patchKey),
+          patchSetDetail, parentPatchTable, null);
     }
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectLink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectLink.java
index b2c4c3e..05def9c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectLink.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectLink.java
@@ -17,9 +17,9 @@
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.changes.QueryScreen;
 import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.Change.Status;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.Change.Status;
 
 /** Link to the open changes of a project. */
 public class ProjectLink extends InlineHyperlink {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectNameSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectNameSuggestOracle.java
index 49fe165..be82eff 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectNameSuggestOracle.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectNameSuggestOracle.java
@@ -16,7 +16,7 @@
 
 import com.google.gerrit.client.RpcStatus;
 import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gwt.user.client.ui.SuggestOracle;
 import com.google.gwtexpui.safehtml.client.HighlightSuggestOracle;
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectsTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectsTable.java
index 96089b9..b768643 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectsTable.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectsTable.java
@@ -15,8 +15,7 @@
 package com.google.gerrit.client.ui;
 
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.ui.NavigationTable;
-import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gwt.event.dom.client.KeyCodes;
 import com.google.gwt.user.client.DOM;
 import com.google.gwt.user.client.Element;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ReviewerSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ReviewerSuggestOracle.java
new file mode 100644
index 0000000..747ef40
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ReviewerSuggestOracle.java
@@ -0,0 +1,83 @@
+// Copyright (C) 2011 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.ui;
+
+import com.google.gerrit.client.FormatUtil;
+import com.google.gerrit.client.RpcStatus;
+import com.google.gerrit.client.admin.Util;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.common.data.AccountInfo;
+import com.google.gerrit.common.data.ReviewerInfo;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gwt.user.client.ui.SuggestOracle;
+import com.google.gwtexpui.safehtml.client.HighlightSuggestOracle;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/** Suggestion Oracle for reviewers. */
+public class ReviewerSuggestOracle extends HighlightSuggestOracle {
+
+  private Change.Id changeId;
+
+  @Override
+  protected void onRequestSuggestions(final Request req, final Callback callback) {
+    RpcStatus.hide(new Runnable() {
+      public void run() {
+        SuggestUtil.SVC.suggestChangeReviewer(changeId, req.getQuery(),
+            req.getLimit(), new GerritCallback<List<ReviewerInfo>>() {
+              public void onSuccess(final List<ReviewerInfo> result) {
+                final List<ReviewerSuggestion> r =
+                    new ArrayList<ReviewerSuggestion>(result
+                        .size());
+                for (final ReviewerInfo reviewer : result) {
+                  r.add(new ReviewerSuggestion(reviewer));
+                }
+                callback.onSuggestionsReady(req, new Response(r));
+              }
+            });
+      }
+    });
+  }
+
+  public void setChange(Change.Id changeId) {
+    this.changeId = changeId;
+  }
+
+  private static class ReviewerSuggestion implements SuggestOracle.Suggestion {
+    private final ReviewerInfo reviewerInfo;
+
+    ReviewerSuggestion(final ReviewerInfo reviewerInfo) {
+      this.reviewerInfo = reviewerInfo;
+    }
+
+    public String getDisplayString() {
+      final AccountInfo accountInfo = reviewerInfo.getAccountInfo();
+      if (accountInfo != null) {
+        return FormatUtil.nameEmail(accountInfo);
+      }
+      return reviewerInfo.getGroup().getName() + " ("
+          + Util.C.suggestedGroupLabel() + ")";
+    }
+
+    public String getReplacementString() {
+      final AccountInfo accountInfo = reviewerInfo.getAccountInfo();
+      if (accountInfo != null) {
+        return FormatUtil.nameEmail(accountInfo);
+      }
+      return reviewerInfo.getGroup().getName();
+    }
+  }
+}
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 a854abc..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) {
@@ -127,6 +159,7 @@
     if (windowTitle != null) {
       Gerrit.setWindowTitle(this, windowTitle);
     }
+    Gerrit.updateMenus(this);
     registerKeys();
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/UIConstants.java
similarity index 86%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectConstants.java
rename to gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/UIConstants.java
index 8b13392..ebbb049 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/UIConstants.java
@@ -16,7 +16,10 @@
 
 import com.google.gwt.i18n.client.Constants;
 
-public interface ProjectConstants extends Constants {
+public interface UIConstants extends Constants {
+  String commentedActionButtonSend();
+  String commentedActionButtonCancel();
+
   String projectName();
   String projectDescription();
   String projectListOpen();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/UIConstants.properties
similarity index 69%
rename from gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectConstants.properties
rename to gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/UIConstants.properties
index 15de117..aa00cee 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/UIConstants.properties
@@ -1,3 +1,6 @@
+commentedActionButtonSend = Submit
+commentedActionButtonCancel = Cancel
+
 projectName = Project Name
 projectDescription = Project Description
 projectListOpen = Select project
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Util.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Util.java
index ebed764..98f13ef 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Util.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Util.java
@@ -17,5 +17,5 @@
 import com.google.gwt.core.client.GWT;
 
 public class Util {
-  public static final ProjectConstants C = GWT.create(ProjectConstants.class);
+  public static final UIConstants C = GWT.create(UIConstants.class);
 }
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/.settings/org.eclipse.core.resources.prefs b/gerrit-httpd/.settings/org.eclipse.core.resources.prefs
index 82eb859..9df523e 100644
--- a/gerrit-httpd/.settings/org.eclipse.core.resources.prefs
+++ b/gerrit-httpd/.settings/org.eclipse.core.resources.prefs
@@ -1,3 +1,6 @@
-#Tue Sep 02 16:59:24 PDT 2008
+#Thu Jul 28 11:02:36 PDT 2011
 eclipse.preferences.version=1
+encoding//src/main/java=UTF-8
+encoding//src/main/resources=UTF-8
+encoding//src/test/java=UTF-8
 encoding/<project>=UTF-8
diff --git a/gerrit-httpd/.settings/org.eclipse.jdt.core.prefs b/gerrit-httpd/.settings/org.eclipse.jdt.core.prefs
index 04afc7f..470942d 100644
--- a/gerrit-httpd/.settings/org.eclipse.jdt.core.prefs
+++ b/gerrit-httpd/.settings/org.eclipse.jdt.core.prefs
@@ -1,4 +1,4 @@
-#Tue May 12 17:44:13 PDT 2009
+#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
@@ -9,6 +9,7 @@
 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
diff --git a/gerrit-httpd/pom.xml b/gerrit-httpd/pom.xml
index 2a9fa7d..a6374da 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.1-SNAPSHOT</version>
+    <version>2.4-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-httpd</artifactId>
@@ -55,22 +55,11 @@
     </dependency>
 
     <dependency>
-      <groupId>com.google.gwt</groupId>
-      <artifactId>gwt-servlet</artifactId>
-    </dependency>
-
-    <dependency>
       <groupId>eu.medsea.mimeutil</groupId>
       <artifactId>mime-util</artifactId>
     </dependency>
 
     <dependency>
-      <groupId>org.openid4java</groupId>
-      <artifactId>openid4java-consumer</artifactId>
-      <type>pom</type>
-    </dependency>
-
-    <dependency>
       <groupId>gwtjsonrpc</groupId>
       <artifactId>gwtjsonrpc</artifactId>
     </dependency>
@@ -93,4 +82,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/AdvertisedObjectsCacheKey.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/AdvertisedObjectsCacheKey.java
index 95a4f14..d594d77 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/AdvertisedObjectsCacheKey.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/AdvertisedObjectsCacheKey.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.httpd;
 
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Project;
 
 class AdvertisedObjectsCacheKey {
   private final Account.Id account;
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..fb30a4d
--- /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.client.Account;
+import com.google.gerrit.reviewdb.client.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/ChangeQueryServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ChangeQueryServlet.java
index 468c6d8..9c4c598 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ChangeQueryServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ChangeQueryServlet.java
@@ -69,6 +69,7 @@
         return;
     }
 
+    p.setIncludeComments(get(req, "comments", false));
     p.setIncludeCurrentPatchSet(get(req, "current-patch-set", false));
     p.setIncludePatchSets(get(req, "patch-sets", false));
     p.setIncludeApprovals(get(req, "all-approvals", false));
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
new file mode 100644
index 0000000..c0e3f42
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ContainerAuthFilter.java
@@ -0,0 +1,114 @@
+// 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 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.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;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpServletResponseWrapper;
+
+/**
+ * Trust the authentication which is done by the container.
+ * <p>
+ * Check whether the container has already authenticated the user. If yes, then
+ * lookup the account and set the account ID in our current session.
+ * <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 GitOverHttpServlet} and its handled URLs, which provide remote
+ * repository access over HTTP.
+ */
+@Singleton
+class ContainerAuthFilter implements Filter {
+  public static final String REALM_NAME = "Gerrit Code Review";
+
+  private final Provider<WebSession> session;
+  private final AccountCache accountCache;
+  private final Config config;
+
+  @Inject
+  ContainerAuthFilter(Provider<WebSession> session, AccountCache accountCache,
+      @GerritServerConfig Config config) throws XsrfException {
+    this.session = session;
+    this.accountCache = accountCache;
+    this.config = config;
+  }
+
+  @Override
+  public void init(FilterConfig config) {
+  }
+
+  @Override
+  public void destroy() {
+  }
+
+  @Override
+  public void doFilter(ServletRequest request, ServletResponse response,
+      FilterChain chain) throws IOException, ServletException {
+    HttpServletRequest req = (HttpServletRequest) request;
+    if (!GitSmartHttpTools.isGitClient(req)) {
+      chain.doFilter(request, response);
+      return;
+    }
+
+    HttpServletResponseWrapper rsp =
+        new HttpServletResponseWrapper((HttpServletResponse) response);
+
+    if (verify(req, rsp)) {
+      chain.doFilter(req, response);
+    }
+  }
+
+  private boolean verify(HttpServletRequest req, HttpServletResponseWrapper rsp)
+      throws IOException {
+    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;
+    }
+    session.get().setUserAccountId(who.getAccount().getId());
+    return true;
+  }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/DirectChangeByCommit.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/DirectChangeByCommit.java
new file mode 100644
index 0000000..407784e
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/DirectChangeByCommit.java
@@ -0,0 +1,89 @@
+// Copyright 2011 Google Inc. All Rights Reserved.
+
+package com.google.gerrit.httpd;
+
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.QueryParseException;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeDataSource;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder;
+import com.google.gerrit.server.query.change.ChangeQueryRewriter;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.HashSet;
+
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+@Singleton
+class DirectChangeByCommit extends HttpServlet {
+  private static final long serialVersionUID = 1L;
+  private static final Logger log =
+      LoggerFactory.getLogger(DirectChangeByCommit.class);
+
+  private final ChangeQueryBuilder.Factory queryBuilder;
+  private final Provider<ChangeQueryRewriter> queryRewriter;
+  private final Provider<CurrentUser> currentUser;
+
+  @Inject
+  DirectChangeByCommit(ChangeQueryBuilder.Factory queryBuilder,
+      Provider<ChangeQueryRewriter> queryRewriter,
+      Provider<CurrentUser> currentUser) {
+    this.queryBuilder = queryBuilder;
+    this.queryRewriter = queryRewriter;
+    this.currentUser = currentUser;
+  }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  protected void doGet(final HttpServletRequest req,
+      final HttpServletResponse rsp) throws IOException {
+    String query = req.getPathInfo();
+    HashSet<Change.Id> ids = new HashSet<Change.Id>();
+    try {
+      ChangeQueryBuilder builder = queryBuilder.create(currentUser.get());
+      Predicate<ChangeData> visibleToMe = builder.is_visible();
+      Predicate<ChangeData> q = builder.parse(query);
+      q = Predicate.and(q, builder.sortkey_before("z"), builder.limit(2), visibleToMe);
+
+      ChangeQueryRewriter rewriter = queryRewriter.get();
+      Predicate<ChangeData> s = rewriter.rewrite(q);
+      if (!(s instanceof ChangeDataSource)) {
+        s = rewriter.rewrite(Predicate.and(builder.status_open(), q));
+      }
+
+      if (s instanceof ChangeDataSource) {
+        for (ChangeData d : ((ChangeDataSource) s).read()) {
+          ids.add(d.getId());
+        }
+      }
+    } catch (QueryParseException e) {
+      log.warn("Received invalid query by URL: /r/" + query, e);
+    } catch (OrmException e) {
+      log.warn("Cannot process query by URL: /r/" + query, e);
+    }
+
+    String token;
+    if (ids.size() == 1) {
+      // If exactly one change matches, link to that change.
+      // TODO Link to a specific patch set, if one matched.
+      token = PageLinks.toChange(ids.iterator().next());
+
+    } else {
+      // Otherwise, link to the query page.
+      token = PageLinks.toChangeQuery(query);
+    }
+    UrlModule.toGerrit(token, req, rsp);
+  }
+}
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 dd2ef69..1953480 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
@@ -16,14 +16,14 @@
 
 import com.google.gerrit.common.data.ApprovalTypes;
 import com.google.gerrit.common.data.GerritConfig;
-import com.google.gerrit.common.data.GitwebLink;
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.common.data.GitwebConfig;
+import com.google.gerrit.reviewdb.client.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;
-import com.google.gerrit.server.config.WildProjectName;
 import com.google.gerrit.server.contact.ContactStore;
 import com.google.gerrit.server.mail.EmailSender;
 import com.google.gerrit.server.ssh.SshInfo;
@@ -50,20 +50,21 @@
   private final AuthConfig authConfig;
   private final DownloadSchemeConfig schemeConfig;
   private final GitWebConfig gitWebConfig;
-  private final Project.NameKey wildProject;
+  private final AllProjectsName wildProject;
   private final SshInfo sshInfo;
   private final ApprovalTypes approvalTypes;
 
   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,
-      @WildProjectName final Project.NameKey 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;
@@ -74,6 +75,7 @@
     approvalTypes = at;
     contactStore = cs;
     servletContext = sc;
+    anonymousCowardName = acn;
   }
 
   @Inject(optional = true)
@@ -92,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());
@@ -103,6 +111,9 @@
     config.setApprovalTypes(approvalTypes);
     config.setDocumentationAvailable(servletContext
         .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()) {
@@ -116,7 +127,7 @@
     config.setEditableAccountFields(fields);
 
     if (gitWebConfig.getUrl() != null) {
-      config.setGitwebLink(new GitwebLink(gitWebConfig.getUrl(), gitWebConfig
+      config.setGitwebLink(new GitwebConfig(gitWebConfig.getUrl(), gitWebConfig
           .getGitWebType()));
     }
 
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 62%
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 a071c77..c36df04a 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,15 +14,15 @@
 
 package com.google.gerrit.httpd;
 
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.common.data.Capable;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.AccessPath;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.cache.Cache;
 import com.google.gerrit.server.cache.CacheModule;
-import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.git.AsyncReceiveCommits;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.ReceiveCommits;
 import com.google.gerrit.server.git.TagCache;
@@ -39,6 +39,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;
@@ -50,8 +52,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;
@@ -59,35 +59,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
@@ -102,61 +109,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;
@@ -172,26 +138,23 @@
     public Repository open(HttpServletRequest req, String projectName)
         throws RepositoryNotFoundException, ServiceNotAuthorizedException,
         ServiceNotEnabledException {
+      while (projectName.endsWith("/")) {
+        projectName = projectName.substring(0, projectName.length() - 1);
+      }
+
       if (projectName.endsWith(".git")) {
         // Be nice and drop the trailing ".git" suffix, which we never keep
         // in our database, but clients might mistakenly provide anyway.
         //
         projectName = projectName.substring(0, projectName.length() - 4);
-      }
-
-      if (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);
+        while (projectName.endsWith("/")) {
+          projectName = projectName.substring(0, projectName.length() - 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);
       }
@@ -208,75 +171,93 @@
     }
   }
 
-  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.setAdvertiseRefsHook(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 AsyncReceiveCommits.Factory factory;
+    private final Provider<WebSession> session;
+
+    @Inject
+    ReceiveFactory(AsyncReceiveCommits.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 ReceiveCommits.Capable s = rc.canUpload();
-        if (s != ReceiveCommits.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).getReceiveCommits();
+      rc.getReceivePack().setRefLogIdent(user.newRefLogIdent());
+      req.setAttribute(ATT_RC, rc);
+      session.get().setAccessPath(AccessPath.GIT);
+      return rc.getReceivePack();
     }
   }
 
@@ -297,15 +278,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) {
@@ -344,7 +333,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..7de4bc3 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,33 @@
     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"));
+    type.setFileHistory(cfg.getString("gitweb", null, "filehistory"));
+    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.");
@@ -54,10 +78,13 @@
     } else if (type.getRevision() == null) {
       log.warn("No Pattern specified for gitweb.revision, disabling.");
       type = null;
+    } else if (type.getFileHistory() == null) {
+      log.warn("No Pattern specified for gitweb.filehistory, disabling.");
+      type = null;
     }
 
     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 +199,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..6c420a5 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 Provider<CurrentUser> currUserProvider;
 
   @Inject
-  HttpIdentifiedUserProvider(final CurrentUser u) {
-    user = u;
+  HttpIdentifiedUserProvider(Provider<CurrentUser> currUserProvider) {
+    this.currUserProvider = currUserProvider;
   }
 
   @Override
   public IdentifiedUser get() {
+    CurrentUser user = currUserProvider.get();
     if (user instanceof IdentifiedUser) {
       return (IdentifiedUser) user;
     }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpLogoutServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpLogoutServlet.java
index 7b2c3f0..13a6f43 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpLogoutServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpLogoutServlet.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.httpd;
 
+import com.google.common.base.Strings;
 import com.google.gerrit.server.account.AccountManager;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.CanonicalWebUrl;
@@ -53,7 +54,17 @@
     if (logoutUrl != null) {
       rsp.sendRedirect(logoutUrl);
     } else {
-      rsp.sendRedirect(urlProvider.get());
+      String url = urlProvider.get();
+      if (Strings.isNullOrEmpty(url)) {
+        url = req.getContextPath();
+      }
+      if (Strings.isNullOrEmpty(url)) {
+        url = "/";
+      }
+      if (!url.endsWith("/")) {
+        url += "/";
+      }
+      rsp.sendRedirect(url);
     }
   }
 }
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 a55c5a7..f90c20d 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
@@ -23,6 +23,8 @@
 import com.google.gerrit.httpd.raw.SshInfoServlet;
 import com.google.gerrit.httpd.raw.StaticServlet;
 import com.google.gerrit.httpd.raw.ToolServlet;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gwtexpui.server.CacheControlFilter;
 import com.google.inject.Key;
 import com.google.inject.Provider;
@@ -52,10 +54,6 @@
     serve("/static/*").with(StaticServlet.class);
     serve("/tools/*").with(ToolServlet.class);
 
-    filter("/p/*").through(ProjectAccessPathFilter.class);
-    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());
@@ -63,14 +61,14 @@
     serve("/all").with(query("status:merged"));
     serve("/mine").with(screen(PageLinks.MINE));
     serve("/open").with(query("status:open"));
-    serve("/settings").with(screen(PageLinks.SETTINGS));
     serve("/watched").with(query("is:watched status:open"));
     serve("/starred").with(query("is:starred"));
 
-    serveRegex( //
-        "^/([1-9][0-9]*)/?$", //
-        "^/r/(.+)/?$" //
-    ).with(changeQuery());
+    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);
   }
 
   private Key<HttpServlet> notFound() {
@@ -110,14 +108,43 @@
     });
   }
 
-  private Key<HttpServlet> changeQuery() {
+  private Key<HttpServlet> directChangeById() {
     return key(new HttpServlet() {
       private static final long serialVersionUID = 1L;
 
       @Override
       protected void doGet(final HttpServletRequest req,
           final HttpServletResponse rsp) throws IOException {
-        toGerrit(PageLinks.toChangeQuery(req.getPathInfo()), req, rsp);
+        try {
+          Change.Id id = Change.Id.parse(req.getPathInfo());
+          toGerrit(PageLinks.toChange(id), req, rsp);
+        } catch (IllegalArgumentException err) {
+          rsp.sendError(HttpServletResponse.SC_NOT_FOUND);
+        }
+      }
+    });
+  }
+
+  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);
       }
     });
   }
@@ -146,7 +173,7 @@
     return srv;
   }
 
-  private void toGerrit(final String target, final HttpServletRequest req,
+  static void toGerrit(final String target, final HttpServletRequest req,
       final HttpServletResponse rsp) throws IOException {
     final StringBuilder url = new StringBuilder();
     url.append(req.getContextPath());
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 cc2e144..8ee2c41 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,11 @@
 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.gerrit.server.util.GuiceRequestScopePropagator;
+import com.google.gerrit.server.util.RequestScopePropagator;
 import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
-import com.google.inject.Provider;
 import com.google.inject.ProvisionException;
 import com.google.inject.servlet.RequestScoped;
 import com.google.inject.servlet.ServletModule;
@@ -53,21 +50,15 @@
 import javax.annotation.Nullable;
 
 public class WebModule extends FactoryModule {
-  private final Provider<SshInfo> sshInfoProvider;
-  private final Provider<SshKeyCache> sshKeyCacheProvider;
-  private final AuthType authType;
+  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.authType = authConfig.getAuthType();
+    this.authConfig = authConfig;
     this.wantSSL = canonicalUrl != null && canonicalUrl.startsWith("https:");
 
     this.gitWebConfig =
@@ -87,16 +78,13 @@
         filter("/*").through(RequestCleanupFilter.class);
       }
     });
+    bind(RequestScopePropagator.class).to(GuiceRequestScopePropagator.class);
 
     if (wantSSL) {
       install(new RequireSslFilter.Module());
     }
 
-    switch (authType) {
-      case OPENID:
-        install(new OpenIdModule());
-        break;
-
+    switch (authConfig.getAuthType()) {
       case HTTP:
       case HTTP_LDAP:
         install(new HttpAuthModule());
@@ -120,17 +108,18 @@
         });
         break;
 
+      case OPENID:
+        // OpenID support is bound in WebAppInitializer and Daemon.
+      case CUSTOM_EXTENSION:
+        break;
       default:
-        throw new ProvisionException("Unsupported loginType: " + authType);
+        throw new ProvisionException("Unsupported loginType: " + authConfig.getAuthType());
     }
 
     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 +140,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 c20f8a1..2925896 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,192 +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.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.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.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 AnonymousUser anonymous;
-  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 AnonymousUser anonymous,
-      final IdentifiedUser.RequestFactory identified) {
-    this.request = request;
-    this.response = response;
-    this.manager = manager;
-    this.authConfig = authConfig;
-    this.anonymous = anonymous;
-    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 anonymous;
-  }
-
-  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..55d0ca5 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSessionManager.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSessionManager.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.httpd;
 
+import static com.google.gerrit.httpd.CacheBasedWebSession.MAX_AGE_MINUTES;
 import static com.google.gerrit.server.ioutil.BasicSerialization.readFixInt64;
 import static com.google.gerrit.server.ioutil.BasicSerialization.readString;
 import static com.google.gerrit.server.ioutil.BasicSerialization.readVarInt32;
@@ -23,15 +24,19 @@
 import static com.google.gerrit.server.ioutil.BasicSerialization.writeVarInt32;
 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.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gerrit.server.cache.Cache;
+import com.google.gerrit.server.config.ConfigUtil;
+import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.inject.Inject;
 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/become/BecomeAnyAccountLoginServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
index c3f7de1..4710c39 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountLoginServlet.java
@@ -14,21 +14,21 @@
 
 package com.google.gerrit.httpd.auth.become;
 
-import static com.google.gerrit.reviewdb.AccountExternalId.SCHEME_USERNAME;
+import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_USERNAME;
 
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.httpd.HtmlDomUtil;
 import com.google.gerrit.httpd.WebSession;
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.AccountExternalId;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountExternalId;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.account.AccountException;
 import com.google.gerrit.server.account.AccountManager;
 import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.account.AuthResult;
-import com.google.gerrit.server.config.CanonicalWebUrl;
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.client.SchemaFactory;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
+import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -43,8 +43,8 @@
 import java.util.List;
 import java.util.UUID;
 
-import javax.annotation.Nullable;
 import javax.servlet.ServletContext;
+import javax.servlet.ServletException;
 import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
@@ -56,44 +56,26 @@
 
   private final SchemaFactory<ReviewDb> schema;
   private final Provider<WebSession> webSession;
-  private final Provider<String> urlProvider;
   private final AccountManager accountManager;
-  private final byte[] raw;
 
   @Inject
   BecomeAnyAccountLoginServlet(final Provider<WebSession> ws,
       final SchemaFactory<ReviewDb> sf,
-      final @CanonicalWebUrl @Nullable Provider<String> up,
-      final AccountManager am, final ServletContext servletContext)
-      throws IOException {
+      final AccountManager am, final ServletContext servletContext) {
     webSession = ws;
     schema = sf;
-    urlProvider = up;
     accountManager = am;
-
-    final String pageName = "BecomeAnyAccount.html";
-    final Document doc = HtmlDomUtil.parseFile(getClass(), pageName);
-    if (doc == null) {
-      throw new FileNotFoundException("No " + pageName + " in webapp");
-    }
-    if (!IS_DEV) {
-      final Element devmode = HtmlDomUtil.find(doc, "gerrit_gwtdevmode");
-      if (devmode != null) {
-        devmode.getParentNode().removeChild(devmode);
-      }
-    }
-    raw = HtmlDomUtil.toUTF8(doc);
   }
 
   @Override
   protected void doGet(final HttpServletRequest req,
-      final HttpServletResponse rsp) throws IOException {
+      final HttpServletResponse rsp) throws IOException, ServletException {
     doPost(req, rsp);
   }
 
   @Override
   protected void doPost(final HttpServletRequest req,
-      final HttpServletResponse rsp) throws IOException {
+      final HttpServletResponse rsp) throws IOException, ServletException {
     rsp.setHeader("Expires", "Fri, 01 Jan 1980 00:00:00 GMT");
     rsp.setHeader("Pragma", "no-cache");
     rsp.setHeader("Cache-Control", "no-cache, must-revalidate");
@@ -112,6 +94,12 @@
       res = byAccountId(rsp, req.getParameter("account_id"));
 
     } else {
+      byte[] raw;
+      try {
+        raw = prepareHtmlOutput();
+      } catch (OrmException e) {
+        throw new ServletException(e);
+      }
       rsp.setContentType("text/html");
       rsp.setCharacterEncoding(HtmlDomUtil.ENC);
       rsp.setContentLength(raw.length);
@@ -127,7 +115,7 @@
     if (res != null) {
       webSession.get().login(res, false);
       final StringBuilder rdr = new StringBuilder();
-      rdr.append(urlProvider.get());
+      rdr.append(req.getContextPath());
       if (IS_DEV && req.getParameter("gwt.codesvr") != null) {
         if (rdr.indexOf("?") < 0) {
           rdr.append("?");
@@ -139,7 +127,6 @@
       rdr.append('#');
       if (res.isNew()) {
         rdr.append(PageLinks.REGISTER);
-        rdr.append(',');
       }
       rdr.append(PageLinks.MINE);
       rsp.sendRedirect(rdr.toString());
@@ -157,6 +144,48 @@
     }
   }
 
+  private byte[] prepareHtmlOutput() throws IOException, OrmException {
+    final String pageName = "BecomeAnyAccount.html";
+    final Document doc = HtmlDomUtil.parseFile(getClass(), pageName);
+    if (doc == null) {
+      throw new FileNotFoundException("No " + pageName + " in webapp");
+    }
+    if (!IS_DEV) {
+      final Element devmode = HtmlDomUtil.find(doc, "gerrit_gwtdevmode");
+      if (devmode != null) {
+        devmode.getParentNode().removeChild(devmode);
+      }
+    }
+
+    Element userlistElement = HtmlDomUtil.find(doc, "userlist");
+    ReviewDb db = schema.open();
+    try {
+      ResultSet<Account> accounts = db.accounts().firstNById(5);
+      for (Account a : accounts) {
+        String displayName;
+        if (a.getUserName() != null) {
+          displayName = a.getUserName();
+        } else if (a.getFullName() != null) {
+          displayName = a.getFullName();
+        } else if (a.getPreferredEmail() != null) {
+          displayName = a.getPreferredEmail();
+        } else {
+          displayName = a.getId().toString();
+        }
+
+        Element linkElement = doc.createElement("a");
+        linkElement.setAttribute("href", "?account_id=" + a.getId().toString());
+        linkElement.setTextContent(displayName);
+        userlistElement.appendChild(linkElement);
+        userlistElement.appendChild(doc.createElement("br"));
+      }
+    } finally {
+      db.close();
+    }
+
+    return HtmlDomUtil.toUTF8(doc);
+  }
+
   private AuthResult auth(final Account account) {
     if (account != null) {
       return new AuthResult(account.getId(), null, false);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpAuthFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpAuthFilter.java
index 1afd72d..eb8a76b 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpAuthFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpAuthFilter.java
@@ -17,7 +17,7 @@
 import com.google.gerrit.httpd.HtmlDomUtil;
 import com.google.gerrit.httpd.WebSession;
 import com.google.gerrit.httpd.raw.HostPageServlet;
-import com.google.gwt.user.server.rpc.RPCServletUtils;
+import com.google.gwtjsonrpc.server.RPCServletUtils;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java
index 58f589a..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;
     }
@@ -130,9 +130,8 @@
     final StringBuilder rdr = new StringBuilder();
     rdr.append(urlProvider.get());
     rdr.append('#');
-    if (arsp.isNew() && !token.startsWith(PageLinks.REGISTER + ",")) {
+    if (arsp.isNew() && !token.startsWith(PageLinks.REGISTER + "/")) {
       rdr.append(PageLinks.REGISTER);
-      rdr.append(',');
     }
     rdr.append(token);
 
@@ -165,11 +164,10 @@
 
   private String getToken(final HttpServletRequest req) {
     String token = req.getPathInfo();
-    if (token != null && token.startsWith("/")) {
-      token = token.substring(1);
-    }
     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/container/HttpsClientSslCertLoginServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertLoginServlet.java
index 20b6352..c5fa1ba 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertLoginServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertLoginServlet.java
@@ -63,9 +63,6 @@
 
   private String getToken(final HttpServletRequest req) {
     String token = req.getPathInfo();
-    if (token != null && token.startsWith("/")) {
-      token = token.substring(1);
-    }
     if (token == null || token.isEmpty()) {
       token = PageLinks.MINE;
     }
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 7e04358..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
@@ -59,7 +59,7 @@
       token = getToken(req);
     } else {
       final String msg = "Session cookie not available.";
-      token = "SignInFailure," + SignInMode.SIGN_IN + "," + msg;
+      token = "/SignInFailure," + SignInMode.SIGN_IN + "," + msg;
     }
 
     final StringBuilder rdr = new StringBuilder();
@@ -75,11 +75,10 @@
 
   private String getToken(final HttpServletRequest req) {
     String token = req.getPathInfo();
-    if (token != null && token.startsWith("/")) {
-      token = token.substring(1);
-    }
     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..9d14872 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,33 +17,38 @@
 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.client.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.gwt.user.client.rpc.AsyncCallback;
+import com.google.gerrit.server.auth.AuthenticationUnavailableException;
+import com.google.gerrit.server.config.AuthConfig;
+import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
 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/gitweb/GitWebCssServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitWebCssServlet.java
index a4df147..e397961 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitWebCssServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitWebCssServlet.java
@@ -17,7 +17,7 @@
 import com.google.gerrit.httpd.GitWebConfig;
 import com.google.gerrit.httpd.HtmlDomUtil;
 import com.google.gerrit.server.config.SitePaths;
-import com.google.gwt.user.server.rpc.RPCServletUtils;
+import com.google.gwtjsonrpc.server.RPCServletUtils;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitWebServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitWebServlet.java
index 947fbb4..fb04226 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitWebServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitWebServlet.java
@@ -32,13 +32,15 @@
 import com.google.gerrit.common.data.GerritConfig;
 import com.google.gerrit.httpd.GitWebConfig;
 import com.google.gerrit.launcher.GerritLauncher;
-import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.git.LocalDiskRepositoryManager;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
@@ -83,15 +85,19 @@
   private final URI gitwebUrl;
   private final LocalDiskRepositoryManager repoManager;
   private final ProjectControl.Factory projectControl;
+  private final Provider<AnonymousUser> anonymousUserProvider;
   private final EnvList _env;
 
   @Inject
   GitWebServlet(final LocalDiskRepositoryManager repoManager,
-      final ProjectControl.Factory projectControl, final SitePaths site,
+      final ProjectControl.Factory projectControl,
+      final Provider<AnonymousUser> anonymousUserProvider,
+      final SitePaths site,
       final GerritConfig gerritConfig, final GitWebConfig gitWebConfig)
       throws IOException {
     this.repoManager = repoManager;
     this.projectControl = projectControl;
+    this.anonymousUserProvider = anonymousUserProvider;
     this.gitwebCgi = gitWebConfig.getGitwebCGI();
     this.deniedActions = new HashSet<String>();
 
@@ -259,9 +265,9 @@
       p.print("+branch:$1,n,z};\n"); // wrapped
       p.print("  } elsif ($h =~ /^refs\\/changes\\/\\d{2}\\/(\\d+)\\/\\d+$/) ");
       p.print("{\n"); // wrapped
-      p.print("    $q = qq{#change,$1};\n");
+      p.print("    $q = qq{#/c/$1};\n");
       p.print("  } else {\n");
-      p.print("    $q = qq{#q,$h,n,z};\n");
+      p.print("    $q = qq{#/q/$h,n,z};\n");
       p.print("  }\n");
       p.print("  my $r = qq{$ENV{'GERRIT_CONTEXT_PATH'}$q};\n");
       p.print("  push @{$feature{'actions'}{'default'}},\n");
@@ -507,7 +513,7 @@
     env.set("GERRIT_CONTEXT_PATH", req.getContextPath() + "/");
     env.set("GERRIT_PROJECT_NAME", project.getProject().getName());
 
-    if (project.forAnonymousUser().isVisible()) {
+    if (project.forUser(anonymousUserProvider.get()).isVisible()) {
       env.set("GERRIT_ANONYMOUS_READ", "1");
     }
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/CatServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/CatServlet.java
index b4c3519..adf4ad5 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/CatServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/CatServlet.java
@@ -14,16 +14,16 @@
 
 package com.google.gerrit.httpd.raw;
 
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.Patch;
-import com.google.gerrit.reviewdb.PatchSet;
-import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.FileTypeRegistry;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
-import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/HostPageServlet.java
index 1c2e5b8..6a43d95 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,10 +17,12 @@
 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.gwtjsonrpc.server.RPCServletUtils;
 import com.google.gwtexpui.linker.server.Permutation;
 import com.google.gwtexpui.linker.server.PermutationSelector;
 import com.google.gwtjsonrpc.server.JsonServlet;
@@ -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/raw/LegacyGerritServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/LegacyGerritServlet.java
index 5a72230..d387f6e 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/LegacyGerritServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/LegacyGerritServlet.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.httpd.raw;
 
 import com.google.gerrit.httpd.HtmlDomUtil;
-import com.google.gwt.user.server.rpc.RPCServletUtils;
+import com.google.gwtjsonrpc.server.RPCServletUtils;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/StaticServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/StaticServlet.java
index c33a2aa..988dadb 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/StaticServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/StaticServlet.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.httpd.raw;
 
 import com.google.gerrit.server.config.SitePaths;
-import com.google.gwt.user.server.rpc.RPCServletUtils;
+import com.google.gwtjsonrpc.server.RPCServletUtils;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ToolServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ToolServlet.java
index 4a688aa..810cc2a 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ToolServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ToolServlet.java
@@ -25,7 +25,7 @@
 import com.google.gerrit.common.Version;
 import com.google.gerrit.server.tools.ToolsCatalog;
 import com.google.gerrit.server.tools.ToolsCatalog.Entry;
-import com.google.gwt.user.server.rpc.RPCServletUtils;
+import com.google.gwtjsonrpc.server.RPCServletUtils;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/BaseServiceImplementation.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/BaseServiceImplementation.java
index ed21b39..26db6f9 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/BaseServiceImplementation.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/BaseServiceImplementation.java
@@ -18,14 +18,14 @@
 import com.google.gerrit.common.errors.InvalidQueryException;
 import com.google.gerrit.common.errors.NoSuchEntityException;
 import com.google.gerrit.common.errors.NoSuchGroupException;
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import com.google.gwtorm.client.OrmException;
+import com.google.gwtjsonrpc.common.AsyncCallback;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Provider;
 
 /** Support for services which require a {@link ReviewDb} instance. */
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 5c19e11..9a101d3 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
@@ -17,17 +17,19 @@
 import com.google.gerrit.common.data.AccountDashboardInfo;
 import com.google.gerrit.common.data.ChangeInfo;
 import com.google.gerrit.common.data.ChangeListService;
+import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.SingleListChangeInfo;
 import com.google.gerrit.common.data.ToggleStarRequest;
 import com.google.gerrit.common.errors.InvalidQueryException;
 import com.google.gerrit.common.errors.NoSuchEntityException;
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.ChangeAccess;
-import com.google.gerrit.reviewdb.PatchSetApproval;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gerrit.reviewdb.StarredChange;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.StarredChange;
+import com.google.gerrit.reviewdb.server.ChangeAccess;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.AccountControl;
 import com.google.gerrit.server.account.AccountInfoCacheFactory;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
@@ -37,11 +39,11 @@
 import com.google.gerrit.server.query.change.ChangeDataSource;
 import com.google.gerrit.server.query.change.ChangeQueryBuilder;
 import com.google.gerrit.server.query.change.ChangeQueryRewriter;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import com.google.gwtjsonrpc.client.VoidResult;
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.client.ResultSet;
-import com.google.gwtorm.client.impl.ListResultSet;
+import com.google.gwtjsonrpc.common.AsyncCallback;
+import com.google.gwtjsonrpc.common.VoidResult;
+import com.google.gwtorm.server.ListResultSet;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
@@ -79,12 +81,6 @@
         }
       };
 
-  private static final int MAX_PER_PAGE = 100;
-
-  private static int safePageSize(final int pageSize) {
-    return 0 < pageSize && pageSize <= MAX_PER_PAGE ? pageSize : MAX_PER_PAGE;
-  }
-
   private final Provider<CurrentUser> currentUser;
   private final ChangeControl.Factory changeControlFactory;
   private final AccountInfoCacheFactory.Factory accountInfoCacheFactory;
@@ -107,9 +103,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;
     }
@@ -118,25 +114,33 @@
   @Override
   public void allQueryPrev(final String query, final String pos,
       final int pageSize, final AsyncCallback<SingleListChangeInfo> callback) {
-    run(callback, new QueryPrev(pageSize, pos) {
-      @Override
-      ResultSet<Change> query(ReviewDb db, int lim, String key)
-          throws OrmException, InvalidQueryException {
-        return searchQuery(db, query, lim, key, QUERY_PREV);
-      }
-    });
+    try {
+      run(callback, new QueryPrev(pageSize, pos) {
+        @Override
+        ResultSet<Change> query(ReviewDb db, int lim, String key)
+            throws OrmException, InvalidQueryException {
+          return searchQuery(db, query, lim, key, QUERY_PREV);
+        }
+      });
+    } catch (InvalidQueryException e) {
+      callback.onFailure(e);
+    }
   }
 
   @Override
   public void allQueryNext(final String query, final String pos,
       final int pageSize, final AsyncCallback<SingleListChangeInfo> callback) {
-    run(callback, new QueryNext(pageSize, pos) {
-      @Override
-      ResultSet<Change> query(ReviewDb db, int lim, String key)
-          throws OrmException, InvalidQueryException {
-        return searchQuery(db, query, lim, key, QUERY_NEXT);
-      }
-    });
+    try {
+      run(callback, new QueryNext(pageSize, pos) {
+        @Override
+        ResultSet<Change> query(ReviewDb db, int lim, String key)
+            throws OrmException, InvalidQueryException {
+          return searchQuery(db, query, lim, key, QUERY_NEXT);
+        }
+      });
+    } catch (InvalidQueryException e) {
+      callback.onFailure(e);
+    }
   }
 
   @SuppressWarnings("unchecked")
@@ -184,7 +188,7 @@
         //
         if (!want.isEmpty()) {
           for (Change c : db.changes().get(want)) {
-            if (canRead(c)) {
+            if (canRead(c, db)) {
               r.add(c);
             }
           }
@@ -233,23 +237,32 @@
         }
 
         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);
         }
 
+        // User dashboards are visible to other users, if the current user
+        // can see any of the changes in the dashboard.
+        if (!target.equals(me)
+            && d.getByOwner().isEmpty()
+            && d.getClosed().isEmpty()
+            && d.getForReview().isEmpty()) {
+          throw new Failure(new NoSuchEntityException());
+        }
+
         d.setAccounts(ac.create());
         return d;
       }
@@ -290,11 +303,22 @@
     callback.onSuccess(currentUser.get().getStarredChanges());
   }
 
+  private int safePageSize(final int pageSize) throws InvalidQueryException {
+    int maxLimit = currentUser.get().getCapabilities()
+      .getRange(GlobalCapability.QUERY_LIMIT)
+      .getMax();
+    if (maxLimit <= 0) {
+      throw new InvalidQueryException("Search Disabled");
+    }
+    return 0 < pageSize && pageSize <= maxLimit ? pageSize : maxLimit;
+  }
+
   private List<ChangeInfo> filter(final ResultSet<Change> rs,
-      final Set<Change.Id> starred, final AccountInfoCacheFactory accts) {
+      final 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()));
@@ -309,7 +333,7 @@
     protected final int limit;
     protected final int slim;
 
-    QueryNext(final int pageSize, final String pos) {
+    QueryNext(final int pageSize, final String pos) throws InvalidQueryException {
       this.pos = pos;
       this.limit = safePageSize(pageSize);
       this.slim = limit + 1;
@@ -324,6 +348,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()));
@@ -353,7 +380,7 @@
   }
 
   private abstract class QueryPrev extends QueryNext {
-    QueryPrev(int pageSize, String pos) {
+    QueryPrev(int pageSize, String pos) throws InvalidQueryException {
       super(pageSize, pos);
     }
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/GerritJsonServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/GerritJsonServlet.java
index 8f7754a..3513f89 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/GerritJsonServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/GerritJsonServlet.java
@@ -18,10 +18,10 @@
 import com.google.gerrit.common.errors.NotSignedInException;
 import com.google.gerrit.httpd.WebSession;
 import com.google.gson.GsonBuilder;
-import com.google.gwtjsonrpc.client.RemoteJsonService;
+import com.google.gwtjsonrpc.common.RemoteJsonService;
 import com.google.gwtjsonrpc.server.ActiveCall;
 import com.google.gwtjsonrpc.server.JsonServlet;
-import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/GerritJsonServletProvider.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/GerritJsonServletProvider.java
index 823ee7a..9361130 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/GerritJsonServletProvider.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/GerritJsonServletProvider.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.httpd.rpc;
 
-import com.google.gwtjsonrpc.client.RemoteJsonService;
+import com.google.gwtjsonrpc.common.RemoteJsonService;
 import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/Handler.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/Handler.java
index 29aaf2c..911d1cc 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/Handler.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/Handler.java
@@ -19,9 +19,9 @@
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.NoSuchRefException;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import com.google.gwtjsonrpc.client.VoidResult;
-import com.google.gwtorm.client.OrmException;
+import com.google.gwtjsonrpc.common.AsyncCallback;
+import com.google.gwtjsonrpc.common.VoidResult;
+import com.google.gwtorm.server.OrmException;
 
 import java.util.concurrent.Callable;
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/RpcServletModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/RpcServletModule.java
index 29abee6..876fee3 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/RpcServletModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/RpcServletModule.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.httpd.rpc;
 
-import com.google.gwtjsonrpc.client.RemoteJsonService;
+import com.google.gwtjsonrpc.common.RemoteJsonService;
 import com.google.inject.Key;
 import com.google.inject.Scopes;
 import com.google.inject.internal.UniqueAnnotations;
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 379fee6..f3e8e65e 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
@@ -15,25 +15,34 @@
 package com.google.gerrit.httpd.rpc;
 
 import com.google.gerrit.common.data.AccountInfo;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.common.data.ReviewerInfo;
 import com.google.gerrit.common.data.SuggestService;
 import com.google.gerrit.common.errors.NoSuchGroupException;
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.AccountExternalId;
-import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.AccountGroup.Id;
-import com.google.gerrit.reviewdb.AccountGroupName;
-import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountExternalId;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupName;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.AccountControl;
+import com.google.gerrit.server.account.AccountVisibility;
+import com.google.gerrit.server.account.GroupCache;
 import com.google.gerrit.server.account.GroupControl;
-import com.google.gerrit.server.config.AuthConfig;
+import com.google.gerrit.server.account.GroupMembers;
 import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.patch.AddReviewer;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectState;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import com.google.gwtorm.client.OrmException;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gwtjsonrpc.common.AsyncCallback;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
@@ -41,7 +50,6 @@
 
 import java.util.ArrayList;
 import java.util.Collections;
-import java.util.HashSet;
 import java.util.LinkedHashMap;
 import java.util.List;
 import java.util.Map;
@@ -51,171 +59,260 @@
     SuggestService {
   private static final String MAX_SUFFIX = "\u9fa5";
 
-  private final AuthConfig authConfig;
+  private final Provider<ReviewDb> reviewDbProvider;
+  private final ProjectControl.Factory projectControlFactory;
   private final ProjectCache projectCache;
   private final AccountCache accountCache;
   private final GroupControl.Factory groupControlFactory;
-  private final IdentifiedUser.GenericFactory userFactory;
-  private final Provider<CurrentUser> currentUser;
-  private final SuggestAccountsEnum suggestAccounts;
+  private final GroupMembers.Factory groupMembersFactory;
+  private final IdentifiedUser.GenericFactory identifiedUserFactory;
+  private final AccountControl.Factory accountControlFactory;
+  private final ChangeControl.Factory changeControlFactory;
+  private final Config cfg;
+  private final GroupCache groupCache;
+  private final boolean suggestAccounts;
 
   @Inject
   SuggestServiceImpl(final Provider<ReviewDb> schema,
-      final AuthConfig authConfig,
+      final ProjectControl.Factory projectControlFactory,
       final ProjectCache projectCache, final AccountCache accountCache,
       final GroupControl.Factory groupControlFactory,
-      final IdentifiedUser.GenericFactory userFactory,
+      final GroupMembers.Factory groupMembersFactory,
       final Provider<CurrentUser> currentUser,
-      @GerritServerConfig final Config cfg) {
+      final IdentifiedUser.GenericFactory identifiedUserFactory,
+      final AccountControl.Factory accountControlFactory,
+      final ChangeControl.Factory changeControlFactory,
+      @GerritServerConfig final Config cfg, final GroupCache groupCache) {
     super(schema, currentUser);
-    this.authConfig = authConfig;
+    this.reviewDbProvider = schema;
+    this.projectControlFactory = projectControlFactory;
     this.projectCache = projectCache;
     this.accountCache = accountCache;
     this.groupControlFactory = groupControlFactory;
-    this.userFactory = userFactory;
-    this.currentUser = currentUser;
-    this.suggestAccounts =
-        cfg.getEnum("suggest", null, "accounts", SuggestAccountsEnum.ALL);
+    this.groupMembersFactory = groupMembersFactory;
+    this.identifiedUserFactory = identifiedUserFactory;
+    this.accountControlFactory = accountControlFactory;
+    this.changeControlFactory = changeControlFactory;
+    this.cfg = cfg;
+    this.groupCache = groupCache;
+
+    if ("OFF".equals(cfg.getString("suggest", null, "accounts"))) {
+      this.suggestAccounts = false;
+    } else {
+      boolean suggestAccounts;
+      try {
+        AccountVisibility av =
+            cfg.getEnum("suggest", null, "accounts", AccountVisibility.ALL);
+        suggestAccounts = (av != AccountVisibility.NONE);
+      } catch (IllegalArgumentException err) {
+        suggestAccounts = cfg.getBoolean("suggest", null, "accounts", true);
+      }
+      this.suggestAccounts = suggestAccounts;
+    }
   }
 
   public void suggestProjectNameKey(final String query, final int limit,
       final AsyncCallback<List<Project.NameKey>> callback) {
-    run(callback, new Action<List<Project.NameKey>>() {
-      public List<Project.NameKey> run(final ReviewDb db) throws OrmException {
-        final String a = query;
-        final String b = a + MAX_SUFFIX;
-        final int max = 10;
-        final int n = limit <= 0 ? max : Math.min(limit, max);
+    final int max = 10;
+    final int n = limit <= 0 ? max : Math.min(limit, max);
 
-        final CurrentUser user = currentUser.get();
-        final List<Project.NameKey> r = new ArrayList<Project.NameKey>();
-        for (final Project p : db.projects().suggestByName(a, b, n)) {
-          final ProjectState e = projectCache.get(p.getNameKey());
-          if (e != null && e.controlFor(user).isVisible()) {
-            r.add(p.getNameKey());
-          }
-        }
-        return r;
+    final List<Project.NameKey> r = new ArrayList<Project.NameKey>(n);
+    for (final Project.NameKey nameKey : projectCache.byName(query)) {
+      final ProjectControl ctl;
+      try {
+        ctl = projectControlFactory.validateFor(nameKey);
+      } catch (NoSuchProjectException e) {
+        continue;
       }
-    });
+
+      r.add(ctl.getProject().getNameKey());
+      if (r.size() == n) {
+        break;
+      }
+    }
+    callback.onSuccess(r);
+  }
+
+  private interface VisibilityControl {
+    boolean isVisible(Account account) throws OrmException;
   }
 
   public void suggestAccount(final String query, final Boolean active,
       final int limit, final AsyncCallback<List<AccountInfo>> callback) {
-    if (suggestAccounts == SuggestAccountsEnum.OFF) {
-      callback.onSuccess(Collections.<AccountInfo> emptyList());
-      return;
-    }
-
     run(callback, new Action<List<AccountInfo>>() {
       public List<AccountInfo> run(final ReviewDb db) throws OrmException {
-        final String a = query;
-        final String b = a + MAX_SUFFIX;
-        final int max = 10;
-        final int n = limit <= 0 ? max : Math.min(limit, max);
-
-        final LinkedHashMap<Account.Id, AccountInfo> r =
-            new LinkedHashMap<Account.Id, AccountInfo>();
-        for (final Account p : db.accounts().suggestByFullName(a, b, n)) {
-          addSuggestion(r, p, new AccountInfo(p), active);
-        }
-        if (r.size() < n) {
-          for (final Account p : db.accounts().suggestByPreferredEmail(a, b,
-              n - r.size())) {
-            addSuggestion(r, p, new AccountInfo(p), active);
+        return suggestAccount(db, query, active, limit, new VisibilityControl() {
+          @Override
+          public boolean isVisible(Account account) throws OrmException {
+            return accountControlFactory.get().canSee(account);
           }
-        }
-        if (r.size() < n) {
-          for (final AccountExternalId e : db.accountExternalIds()
-              .suggestByEmailAddress(a, b, n - r.size())) {
-            if (!r.containsKey(e.getAccountId())) {
-              final Account p = accountCache.get(e.getAccountId()).getAccount();
-              final AccountInfo info = new AccountInfo(p);
-              info.setPreferredEmail(e.getEmailAddress());
-              addSuggestion(r, p, info, active);
-            }
-          }
-        }
-        return new ArrayList<AccountInfo>(r.values());
+        });
       }
     });
   }
 
+  private List<AccountInfo> suggestAccount(final ReviewDb db,
+      final String query, final Boolean active, final int limit,
+      VisibilityControl visibilityControl)
+      throws OrmException {
+    if (!suggestAccounts) {
+      return Collections.<AccountInfo> emptyList();
+    }
+
+    final String a = query;
+    final String b = a + MAX_SUFFIX;
+    final int max = 10;
+    final int n = limit <= 0 ? max : Math.min(limit, max);
+
+    final LinkedHashMap<Account.Id, AccountInfo> r =
+        new LinkedHashMap<Account.Id, AccountInfo>();
+    for (final Account p : db.accounts().suggestByFullName(a, b, n)) {
+      addSuggestion(r, p, new AccountInfo(p), active, visibilityControl);
+    }
+    if (r.size() < n) {
+      for (final Account p : db.accounts().suggestByPreferredEmail(a, b,
+          n - r.size())) {
+        addSuggestion(r, p, new AccountInfo(p), active, visibilityControl);
+      }
+    }
+    if (r.size() < n) {
+      for (final AccountExternalId e : db.accountExternalIds()
+          .suggestByEmailAddress(a, b, n - r.size())) {
+        if (!r.containsKey(e.getAccountId())) {
+          final Account p = accountCache.get(e.getAccountId()).getAccount();
+          final AccountInfo info = new AccountInfo(p);
+          info.setPreferredEmail(e.getEmailAddress());
+          addSuggestion(r, p, info, active, visibilityControl);
+        }
+      }
+    }
+    return new ArrayList<AccountInfo>(r.values());
+  }
+
   private void addSuggestion(Map<Account.Id, AccountInfo> map, Account account,
-      AccountInfo info, Boolean active) {
+      AccountInfo info, Boolean active, VisibilityControl visibilityControl)
+      throws OrmException {
     if (map.containsKey(account.getId())) {
       return;
     }
     if (active != null && active != account.isActive()) {
       return;
     }
-    switch (suggestAccounts) {
-      case ALL:
-        map.put(account.getId(), info);
-        break;
-      case SAME_GROUP: {
-        Set<AccountGroup.Id> usersGroups = groupsOf(account);
-        usersGroups.removeAll(authConfig.getRegisteredGroups());
-        usersGroups.remove(authConfig.getBatchUsersGroup());
-        for (AccountGroup.Id myGroup : currentUser.get().getEffectiveGroups()) {
-          if (usersGroups.contains(myGroup)) {
-            map.put(account.getId(), info);
-            break;
-          }
-        }
-        break;
-      }
-      case VISIBLE_GROUP: {
-        Set<AccountGroup.Id> usersGroups = groupsOf(account);
-        usersGroups.removeAll(authConfig.getRegisteredGroups());
-        usersGroups.remove(authConfig.getBatchUsersGroup());
-        for (AccountGroup.Id usersGroup : usersGroups) {
-          try {
-            if (groupControlFactory.controlFor(usersGroup).isVisible()) {
-              map.put(account.getId(), info);
-              break;
-            }
-          } catch (NoSuchGroupException e) {
-            continue;
-          }
-        }
-        break;
-      }
-      case OFF:
-        break;
-      default:
-        throw new IllegalStateException("Bad SuggestAccounts " + suggestAccounts);
+    if (visibilityControl.isVisible(account)) {
+      map.put(account.getId(), info);
     }
   }
 
-  private Set<Id> groupsOf(Account account) {
-    IdentifiedUser user = userFactory.create(account.getId());
-    return new HashSet<AccountGroup.Id>(user.getEffectiveGroups());
-  }
-
   public void suggestAccountGroup(final String query, final int limit,
-      final AsyncCallback<List<AccountGroupName>> callback) {
-    run(callback, new Action<List<AccountGroupName>>() {
-      public List<AccountGroupName> run(final ReviewDb db) throws OrmException {
-        final String a = query;
-        final String b = a + MAX_SUFFIX;
-        final int max = 10;
-        final int n = limit <= 0 ? max : Math.min(limit, max);
-        Set<AccountGroup.Id> memberOf = currentUser.get().getEffectiveGroups();
-        List<AccountGroupName> names = new ArrayList<AccountGroupName>(n);
-        for (AccountGroupName group : db.accountGroupNames()
-              .suggestByName(a, b, n)) {
-          try {
-            if (memberOf.contains(group.getId())
-                || groupControlFactory.controlFor(group.getId()).isVisible()) {
-              names.add(group);
-            }
-          } catch (NoSuchGroupException e) {
-            continue;
-          }
-        }
-        return names;
+      final AsyncCallback<List<GroupReference>> callback) {
+    run(callback, new Action<List<GroupReference>>() {
+      public List<GroupReference> run(final ReviewDb db) throws OrmException {
+        return suggestAccountGroup(db, query, limit);
       }
     });
   }
+
+  private List<GroupReference> suggestAccountGroup(final ReviewDb db,
+      final String query, final int limit) throws OrmException {
+    final String a = query;
+    final String b = a + MAX_SUFFIX;
+    final int max = 10;
+    final int n = limit <= 0 ? max : Math.min(limit, max);
+    List<GroupReference> r = new ArrayList<GroupReference>(n);
+    for (AccountGroupName group : db.accountGroupNames().suggestByName(a, b, n)) {
+      try {
+        if (groupControlFactory.controlFor(group.getId()).isVisible()) {
+          AccountGroup g = groupCache.get(group.getId());
+          if (g != null && g.getGroupUUID() != null) {
+            r.add(GroupReference.forGroup(g));
+          }
+        }
+      } catch (NoSuchGroupException e) {
+        continue;
+      }
+    }
+    return r;
+  }
+
+  @Override
+  public void suggestReviewer(Project.NameKey project, String query, int limit,
+      AsyncCallback<List<ReviewerInfo>> callback) {
+    // The RPC is deprecated, but return an empty list for RPC API compatibility.
+    callback.onSuccess(Collections.<ReviewerInfo>emptyList());
+  }
+
+  @Override
+  public void suggestChangeReviewer(final Change.Id change,
+      final String query, final int limit,
+      final AsyncCallback<List<ReviewerInfo>> callback) {
+    run(callback, new Action<List<ReviewerInfo>>() {
+      public List<ReviewerInfo> run(final ReviewDb db) throws OrmException {
+        final ChangeControl changeControl;
+        try {
+          changeControl = changeControlFactory.controlFor(change);
+        } catch (NoSuchChangeException e) {
+          return Collections.emptyList();
+        }
+        VisibilityControl visibilityControl = new VisibilityControl() {
+          @Override
+          public boolean isVisible(Account account) throws OrmException {
+            IdentifiedUser who =
+                identifiedUserFactory.create(reviewDbProvider, account.getId());
+            return changeControl.forUser(who).isVisible(reviewDbProvider.get());
+          }
+        };
+
+        final List<AccountInfo> suggestedAccounts =
+            suggestAccount(db, query, Boolean.TRUE, limit, visibilityControl);
+        final List<ReviewerInfo> reviewer =
+            new ArrayList<ReviewerInfo>(suggestedAccounts.size());
+        for (final AccountInfo a : suggestedAccounts) {
+          reviewer.add(new ReviewerInfo(a));
+        }
+        final List<GroupReference> suggestedAccountGroups =
+            suggestAccountGroup(db, query, limit);
+        for (final GroupReference g : suggestedAccountGroups) {
+          if (suggestGroupAsReviewer(changeControl.getProject().getNameKey(), g)) {
+            reviewer.add(new ReviewerInfo(g));
+          }
+        }
+
+        Collections.sort(reviewer);
+        if (reviewer.size() <= limit) {
+          return reviewer;
+        } else {
+          return reviewer.subList(0, limit);
+        }
+      }
+    });
+  }
+
+  private boolean suggestGroupAsReviewer(final Project.NameKey project,
+      final GroupReference group) throws OrmException {
+    if (!AddReviewer.isLegalReviewerGroup(group.getUUID())) {
+      return false;
+    }
+
+    try {
+      final Set<Account> members =
+          groupMembersFactory.create().listAccounts(group.getUUID(), project);
+
+      if (members.isEmpty()) {
+        return false;
+      }
+
+      final int maxAllowed =
+          cfg.getInt("addreviewer", "maxAllowed",
+              AddReviewer.DEFAULT_MAX_REVIEWERS);
+      if (maxAllowed > 0 && members.size() > maxAllowed) {
+        return false;
+      }
+    } catch (NoSuchGroupException e) {
+      return false;
+    } catch (NoSuchProjectException e) {
+      return false;
+    }
+
+    return true;
+  }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SystemInfoServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SystemInfoServiceImpl.java
index b29f0a9..9de7d88 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SystemInfoServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SystemInfoServiceImpl.java
@@ -14,15 +14,16 @@
 
 package com.google.gerrit.httpd.rpc;
 
+import com.google.gerrit.common.data.GerritConfig;
 import com.google.gerrit.common.data.SshHostKey;
 import com.google.gerrit.common.data.SystemInfoService;
-import com.google.gerrit.reviewdb.ContributorAgreement;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.client.ContributorAgreement;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ssh.SshInfo;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import com.google.gwtjsonrpc.client.VoidResult;
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.client.SchemaFactory;
+import com.google.gwtjsonrpc.common.AsyncCallback;
+import com.google.gwtjsonrpc.common.VoidResult;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
@@ -46,13 +47,15 @@
   private final SchemaFactory<ReviewDb> schema;
   private final List<HostKey> hostKeys;
   private final Provider<HttpServletRequest> httpRequest;
+  private final Provider<GerritConfig> config;
 
   @Inject
   SystemInfoServiceImpl(final SchemaFactory<ReviewDb> sf, final SshInfo daemon,
-      final Provider<HttpServletRequest> hsr) {
+      final Provider<HttpServletRequest> hsr, Provider<GerritConfig> cfg) {
     schema = sf;
     hostKeys = daemon.getHostKeys();
     httpRequest = hsr;
+    config = cfg;
   }
 
   public void contributorAgreements(
@@ -91,4 +94,9 @@
     log.error("Client UI JavaScript error: User-Agent=" + ua + ": " + message);
     callback.onSuccess(VoidResult.INSTANCE);
   }
+
+  @Override
+  public void gerritConfig(final AsyncCallback<GerritConfig> callback) {
+    callback.onSuccess(config.get());
+  }
 }
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 13dd9b4..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() {
@@ -32,9 +33,11 @@
         factory(CreateGroup.Factory.class);
         factory(DeleteExternalIds.Factory.class);
         factory(ExternalIdDetailFactory.Factory.class);
-        factory(GroupDetailFactory.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 77662a1..aa94759f 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,21 +14,24 @@
 
 package com.google.gerrit.httpd.rpc.account;
 
+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;
 import com.google.gerrit.common.errors.InvalidSshKeyException;
 import com.google.gerrit.common.errors.NameAlreadyUsedException;
 import com.google.gerrit.common.errors.NoSuchEntityException;
+import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.httpd.rpc.BaseServiceImplementation;
 import com.google.gerrit.httpd.rpc.Handler;
-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.ContactInformation;
-import com.google.gerrit.reviewdb.ContributorAgreement;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountAgreement;
+import com.google.gerrit.reviewdb.client.AccountExternalId;
+import com.google.gerrit.reviewdb.client.AccountSshKey;
+import com.google.gerrit.reviewdb.client.AuthType;
+import com.google.gerrit.reviewdb.client.ContactInformation;
+import com.google.gerrit.reviewdb.client.ContributorAgreement;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountByEmailCache;
@@ -43,21 +46,18 @@
 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.gwtjsonrpc.common.AsyncCallback;
+import com.google.gwtjsonrpc.common.VoidResult;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-import org.eclipse.jgit.util.Base64;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.io.UnsupportedEncodingException;
 import java.util.Collections;
 import java.util.List;
 import java.util.Set;
@@ -69,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;
@@ -83,10 +84,13 @@
   private final ExternalIdDetailFactory.Factory externalIdDetailFactory;
   private final MyGroupsFactory.Factory myGroupsFactory;
 
+  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,
@@ -95,12 +99,14 @@
       final ChangeUserName.CurrentUser changeUserNameFactory,
       final DeleteExternalIds.Factory deleteExternalIdsFactory,
       final ExternalIdDetailFactory.Factory externalIdDetailFactory,
-      final MyGroupsFactory.Factory myGroupsFactory) {
+      final MyGroupsFactory.Factory myGroupsFactory,
+      final ChangeHooks hooks) {
     super(schema, currentUser);
     contactStore = cs;
     authConfig = ac;
     realm = r;
     user = u;
+    emailTokenVerifier = etv;
     registerNewEmailFactory = esf;
     sshKeyCache = skc;
     byEmailCache = abec;
@@ -115,6 +121,7 @@
     this.deleteExternalIdsFactory = deleteExternalIdsFactory;
     this.externalIdDetailFactory = externalIdDetailFactory;
     this.myGroupsFactory = myGroupsFactory;
+    this.hooks = hooks;
   }
 
   public void mySshKeys(final AsyncCallback<List<AccountSshKey>> callback) {
@@ -198,8 +205,13 @@
   }
 
   @Override
-  public void myGroups(final AsyncCallback<List<AccountGroup>> callback) {
-    myGroupsFactory.create().to(callback);
+  public void myGroups(final AsyncCallback<List<GroupDetail>> callback) {
+    run(callback, new Action<List<GroupDetail>>() {
+      public List<GroupDetail> run(final ReviewDb db) throws OrmException,
+          NoSuchGroupException, Failure {
+        return myGroupsFactory.create().call();
+      }
+    });
   }
 
   public void deleteExternalIds(final Set<AccountExternalId.Key> keys,
@@ -262,6 +274,8 @@
                 .getAccountId(), id));
         if (cla.isAutoVerify()) {
           a.review(AccountAgreement.Status.VERIFIED, null);
+
+          hooks.doClaSignupHook(user.get().getAccount(), cla);
         }
         db.accountAgreements().insert(Collections.singleton(a));
         return VoidResult.INSTANCE;
@@ -270,41 +284,42 @@
   }
 
   public void registerEmail(final String address,
-      final AsyncCallback<VoidResult> cb) {
-    try {
-      final RegisterNewEmailSender sender;
-      sender = registerNewEmailFactory.create(address);
-      sender.send();
-      cb.onSuccess(VoidResult.INSTANCE);
-    } catch (EmailException e) {
-      log.error("Cannot send email verification message to " + address, e);
-      cb.onFailure(e);
-    } catch (RuntimeException e) {
-      log.error("Cannot send email verification message to " + address, e);
-      cb.onFailure(e);
+      final AsyncCallback<Account> cb) {
+    if (authConfig.getAuthType() == AuthType.DEVELOPMENT_BECOME_ANY_ACCOUNT) {
+      try {
+        accountManager.link(user.get().getAccountId(),
+            AuthRequest.forEmail(address));
+        cb.onSuccess(user.get().getAccount());
+      } catch (AccountException e) {
+        cb.onFailure(e);
+      }
+    } else {
+      try {
+        final RegisterNewEmailSender sender;
+        sender = registerNewEmailFactory.create(address);
+        sender.send();
+      } catch (EmailException e) {
+        log.error("Cannot send email verification message to " + address, e);
+        cb.onFailure(e);
+      } catch (RuntimeException e) {
+        log.error("Cannot send email verification message to " + address, e);
+        cb.onFailure(e);
+      }
     }
   }
 
-  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/AccountServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountServiceImpl.java
index 3951554..2fe3124 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountServiceImpl.java
@@ -20,22 +20,22 @@
 import com.google.gerrit.common.errors.InvalidQueryException;
 import com.google.gerrit.common.errors.NoSuchEntityException;
 import com.google.gerrit.httpd.rpc.BaseServiceImplementation;
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.AccountDiffPreference;
-import com.google.gerrit.reviewdb.AccountGeneralPreferences;
-import com.google.gerrit.reviewdb.AccountProjectWatch;
-import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountDiffPreference;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.query.QueryParseException;
 import com.google.gerrit.server.query.change.ChangeQueryBuilder;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import com.google.gwtjsonrpc.client.VoidResult;
-import com.google.gwtorm.client.OrmDuplicateKeyException;
-import com.google.gwtorm.client.OrmException;
+import com.google.gwtjsonrpc.common.AsyncCallback;
+import com.google.gwtjsonrpc.common.VoidResult;
+import com.google.gwtorm.server.OrmDuplicateKeyException;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AgreementInfoFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AgreementInfoFactory.java
index f638d48..39712e4 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AgreementInfoFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AgreementInfoFactory.java
@@ -16,12 +16,13 @@
 
 import com.google.gerrit.common.data.AgreementInfo;
 import com.google.gerrit.httpd.rpc.Handler;
-import com.google.gerrit.reviewdb.AccountAgreement;
-import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.AccountGroupAgreement;
-import com.google.gerrit.reviewdb.ContributorAgreement;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.client.AccountAgreement;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupAgreement;
+import com.google.gerrit.reviewdb.client.ContributorAgreement;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.GroupCache;
 import com.google.inject.Inject;
 
 import java.util.ArrayList;
@@ -36,13 +37,16 @@
   }
 
   private final ReviewDb db;
+  private final GroupCache groupCache;
   private final IdentifiedUser user;
 
   private AgreementInfo info;
 
   @Inject
-  AgreementInfoFactory(final ReviewDb db, final IdentifiedUser user) {
+  AgreementInfoFactory(final ReviewDb db, final GroupCache groupCache,
+      final IdentifiedUser user) {
     this.db = db;
+    this.groupCache = groupCache;
     this.user = user;
   }
 
@@ -55,9 +59,14 @@
 
     final List<AccountGroupAgreement> groupAccepted =
         new ArrayList<AccountGroupAgreement>();
-    for (final AccountGroup.Id groupId : user.getEffectiveGroups()) {
+    for (final AccountGroup.UUID groupUUID : user.getEffectiveGroups().getKnownGroups()) {
+      AccountGroup group = groupCache.get(groupUUID);
+      if (group == null) {
+        continue;
+      }
+
       final List<AccountGroupAgreement> temp =
-          db.accountGroupAgreements().byGroup(groupId).toList();
+          db.accountGroupAgreements().byGroup(group.getId()).toList();
 
       Collections.reverse(temp);
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/CreateGroup.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/CreateGroup.java
index 874cc74..ebebd04 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/CreateGroup.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/CreateGroup.java
@@ -15,12 +15,13 @@
 package com.google.gerrit.httpd.rpc.account;
 
 import com.google.gerrit.common.errors.NameAlreadyUsedException;
+import com.google.gerrit.common.errors.PermissionDeniedException;
 import com.google.gerrit.httpd.rpc.Handler;
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.AccountGroup;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.PerformCreateGroup;
-import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -44,7 +45,8 @@
   }
 
   @Override
-  public AccountGroup.Id call() throws OrmException, NameAlreadyUsedException {
+  public AccountGroup.Id call() throws OrmException, NameAlreadyUsedException,
+      PermissionDeniedException {
     final PerformCreateGroup performCreateGroup = performCreateGroupFactory.create();
     final Account.Id me = user.getAccountId();
     return performCreateGroup.createGroup(groupName, null, false, null, Collections.singleton(me), null);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/DeleteExternalIds.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/DeleteExternalIds.java
index 4edd571..12bc761 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/DeleteExternalIds.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/DeleteExternalIds.java
@@ -15,12 +15,12 @@
 package com.google.gerrit.httpd.rpc.account;
 
 import com.google.gerrit.httpd.rpc.Handler;
-import com.google.gerrit.reviewdb.AccountExternalId;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.client.AccountExternalId;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountByEmailCache;
 import com.google.gerrit.server.account.AccountCache;
-import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/ExternalIdDetailFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/ExternalIdDetailFactory.java
index de0dec9..d876f2e 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/ExternalIdDetailFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/ExternalIdDetailFactory.java
@@ -14,15 +14,15 @@
 
 package com.google.gerrit.httpd.rpc.account;
 
-import static com.google.gerrit.reviewdb.AccountExternalId.SCHEME_USERNAME;
+import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_USERNAME;
 
 import com.google.gerrit.httpd.WebSession;
 import com.google.gerrit.httpd.rpc.Handler;
-import com.google.gerrit.reviewdb.AccountExternalId;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.client.AccountExternalId;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.config.AuthConfig;
-import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 
 import java.util.Collections;
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 a564907..e90c2bf 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
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.common.data.GroupAdminService;
 import com.google.gerrit.common.data.GroupDetail;
+import com.google.gerrit.common.data.GroupList;
 import com.google.gerrit.common.data.GroupOptions;
 import com.google.gerrit.common.errors.InactiveAccountException;
 import com.google.gerrit.common.errors.NameAlreadyUsedException;
@@ -23,13 +24,13 @@
 import com.google.gerrit.common.errors.NoSuchEntityException;
 import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.httpd.rpc.BaseServiceImplementation;
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.AccountGroupInclude;
-import com.google.gerrit.reviewdb.AccountGroupIncludeAudit;
-import com.google.gerrit.reviewdb.AccountGroupMember;
-import com.google.gerrit.reviewdb.AccountGroupMemberAudit;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupInclude;
+import com.google.gerrit.reviewdb.client.AccountGroupIncludeAudit;
+import com.google.gerrit.reviewdb.client.AccountGroupMember;
+import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountResolver;
@@ -37,21 +38,21 @@
 import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.account.GroupIncludeCache;
 import com.google.gerrit.server.account.Realm;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import com.google.gwtjsonrpc.client.VoidResult;
-import com.google.gwtorm.client.OrmException;
+import com.google.gwtjsonrpc.common.AsyncCallback;
+import com.google.gwtjsonrpc.common.VoidResult;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
+import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 
 class GroupAdminServiceImpl extends BaseServiceImplementation implements
     GroupAdminService {
-  private final Provider<IdentifiedUser> identifiedUser;
   private final AccountCache accountCache;
   private final AccountResolver accountResolver;
   private final Realm accountRealm;
@@ -61,7 +62,8 @@
 
   private final CreateGroup.Factory createGroupFactory;
   private final RenameGroup.Factory renameGroupFactory;
-  private final GroupDetailFactory.Factory groupDetailFactory;
+  private final GroupDetailHandler.Factory groupDetailFactory;
+  private final VisibleGroupsHandler.Factory visibleGroupsFactory;
 
   @Inject
   GroupAdminServiceImpl(final Provider<ReviewDb> schema,
@@ -73,9 +75,9 @@
       final GroupControl.Factory groupControlFactory,
       final CreateGroup.Factory createGroupFactory,
       final RenameGroup.Factory renameGroupFactory,
-      final GroupDetailFactory.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;
@@ -85,32 +87,11 @@
     this.createGroupFactory = createGroupFactory;
     this.renameGroupFactory = renameGroupFactory;
     this.groupDetailFactory = groupDetailFactory;
+    this.visibleGroupsFactory = visibleGroupsFactory;
   }
 
-  public void visibleGroups(final AsyncCallback<List<AccountGroup>> callback) {
-    run(callback, new Action<List<AccountGroup>>() {
-      public List<AccountGroup> run(ReviewDb db) throws OrmException {
-        final IdentifiedUser user = identifiedUser.get();
-        final List<AccountGroup> result;
-        if (user.isAdministrator()) {
-          result = db.accountGroups().all().toList();
-        } else {
-          result = new ArrayList<AccountGroup>();
-          for(final AccountGroup group : db.accountGroups().all().toList()) {
-            final GroupControl c = groupControlFactory.controlFor(group);
-            if (c.isVisible()) {
-              result.add(c.getAccountGroup());
-            }
-          }
-        }
-        Collections.sort(result, new Comparator<AccountGroup>() {
-          public int compare(final AccountGroup a, final AccountGroup b) {
-            return a.getName().compareTo(b.getName());
-          }
-        });
-        return result;
-      }
-    });
+  public void visibleGroups(final AsyncCallback<GroupList> callback) {
+    visibleGroupsFactory.create().to(callback);
   }
 
   public void createGroup(final String newName,
@@ -118,8 +99,14 @@
     createGroupFactory.create(newName).to(callback);
   }
 
-  public void groupDetail(final AccountGroup.Id groupId,
-      final AsyncCallback<GroupDetail> callback) {
+  public void groupDetail(AccountGroup.Id groupId, AccountGroup.UUID groupUUID,
+      AsyncCallback<GroupDetail> callback) {
+    if (groupId == null && groupUUID != null) {
+      AccountGroup g = groupCache.get(groupUUID);
+      if (g != null) {
+        groupId = g.getId();
+      }
+    }
     groupDetailFactory.create(groupId).to(callback);
   }
 
@@ -144,7 +131,6 @@
         final AccountGroup group = db.accountGroups().get(groupId);
         assertAmGroupOwner(db, group);
         group.setVisibleToAll(groupOptions.isVisibleToAll());
-        group.setEmailOnlyAuthors(groupOptions.isEmailOnlyAuthors());
         db.accountGroups().update(Collections.singleton(group));
         groupCache.evict(group);
         return VoidResult.INSTANCE;
@@ -281,7 +267,7 @@
               Collections.singleton(new AccountGroupIncludeAudit(m,
                   getAccountId())));
           db.accountGroupIncludes().insert(Collections.singleton(m));
-          groupIncludeCache.evictInclude(a.getId());
+          groupIncludeCache.evictInclude(a.getGroupUUID());
         }
 
         return groupDetailFactory.create(groupId).call();
@@ -361,6 +347,7 @@
         }
 
         final Account.Id me = getAccountId();
+        final Set<AccountGroup.Id> groupsToEvict = new HashSet<AccountGroup.Id>();
         for (final AccountGroupInclude.Key k : keys) {
           final AccountGroupInclude m =
               db.accountGroupIncludes().get(k);
@@ -385,9 +372,12 @@
                   Collections.singleton(audit));
             }
             db.accountGroupIncludes().delete(Collections.singleton(m));
-            groupIncludeCache.evictInclude(m.getIncludeId());
+            groupsToEvict.add(k.getIncludeId());
           }
         }
+        for (AccountGroup group : db.accountGroups().get(groupsToEvict)) {
+          groupIncludeCache.evictInclude(group.getGroupUUID());
+        }
         return VoidResult.INSTANCE;
       }
     });
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/GroupDetailHandler.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/GroupDetailHandler.java
new file mode 100644
index 0000000..ce74218
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/GroupDetailHandler.java
@@ -0,0 +1,46 @@
+// 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.GroupDetail;
+import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.httpd.rpc.Handler;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.account.GroupDetailFactory;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+public class GroupDetailHandler extends Handler<GroupDetail> {
+  public interface Factory {
+    GroupDetailHandler create(AccountGroup.Id groupId);
+  }
+
+  private final GroupDetailFactory.Factory groupDetailFactory;
+
+  private final AccountGroup.Id groupId;
+
+  @Inject
+  GroupDetailHandler(final GroupDetailFactory.Factory groupDetailFactory,
+      @Assisted final AccountGroup.Id groupId) {
+    this.groupDetailFactory = groupDetailFactory;
+    this.groupId = groupId;
+  }
+
+  @Override
+  public GroupDetail call() throws OrmException, NoSuchGroupException {
+    return groupDetailFactory.create(groupId).call();
+  }
+}
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 b3e993e..fd274fd 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.server.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() throws Exception {
-    final Set<AccountGroup.Id> effective = user.getEffectiveGroups();
-    final int cnt = effective.size();
-    final List<AccountGroup> groupList = new ArrayList<AccountGroup>(cnt);
-    for (final AccountGroup.Id 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 d62f0c0..09dc582 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
@@ -18,40 +18,26 @@
 import com.google.gerrit.common.errors.NameAlreadyUsedException;
 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.account.GroupCache;
-import com.google.gerrit.server.account.GroupControl;
-import com.google.gwtorm.client.OrmDuplicateKeyException;
-import com.google.gwtorm.client.OrmException;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.account.PerformRenameGroup;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
-import java.util.Collections;
-
 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 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,
+  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.performRenameGroupFactory = performRenameGroupFactory;
     this.groupId = groupId;
     this.newName = newName;
   }
@@ -59,42 +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);
-
-    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 d2f59f5..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/AbandonChange.java
+++ /dev/null
@@ -1,87 +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.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 abandonedSenderFactory;
-  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 abandonedSenderFactory,
-      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.abandonedSenderFactory = abandonedSenderFactory;
-    this.changeDetailFactory = changeDetailFactory;
-
-    this.patchSetId = patchSetId;
-    this.message = message;
-    this.hooks = hooks;
-  }
-
-  @Override
-  public ChangeDetail call() throws NoSuchChangeException, OrmException,
-      EmailException, NoSuchEntityException, 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,
-       abandonedSenderFactory, 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..3aecb0c
--- /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.client.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.server.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 4016641..47a9395 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
@@ -19,28 +19,35 @@
 import com.google.gerrit.common.data.ApprovalTypes;
 import com.google.gerrit.common.data.ChangeDetail;
 import com.google.gerrit.common.data.ChangeInfo;
+import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.common.errors.NoSuchEntityException;
 import com.google.gerrit.httpd.rpc.Handler;
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.ApprovalCategory;
-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.RevId;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetAncestor;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.AnonymousUser;
+import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountInfoCacheFactory;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.git.MergeOp;
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.workflow.CategoryFunction;
 import com.google.gerrit.server.workflow.FunctionState;
-import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
+import org.eclipse.jgit.lib.Config;
+
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.Comparator;
@@ -61,12 +68,17 @@
   private final FunctionState.Factory functionState;
   private final PatchSetDetailFactory.Factory patchSetDetail;
   private final AccountInfoCacheFactory aic;
+  private final AnonymousUser anonymousUser;
   private final ReviewDb db;
 
   private final Change.Id changeId;
 
   private ChangeDetail detail;
   private ChangeControl control;
+  private Map<PatchSet.Id, PatchSet> patchsetsById;
+
+  private final MergeOp.Factory opFactory;
+  private boolean testMerge;
 
   @Inject
   ChangeDetailFactory(final ApprovalTypes approvalTypes,
@@ -74,14 +86,21 @@
       final PatchSetDetailFactory.Factory patchSetDetail, final ReviewDb db,
       final ChangeControl.Factory changeControlFactory,
       final AccountInfoCacheFactory.Factory accountInfoCacheFactory,
+      final AnonymousUser anonymousUser,
+      final MergeOp.Factory opFactory,
+      @GerritServerConfig final Config cfg,
       @Assisted final Change.Id id) {
     this.approvalTypes = approvalTypes;
     this.functionState = functionState;
     this.patchSetDetail = patchSetDetail;
     this.db = db;
     this.changeControlFactory = changeControlFactory;
+    this.anonymousUser = anonymousUser;
     this.aic = accountInfoCacheFactory.create();
 
+    this.opFactory = opFactory;
+    this.testMerge = cfg.getBoolean("changeMerge", "test", false);
+
     this.changeId = id;
   }
 
@@ -99,15 +118,37 @@
 
     detail = new ChangeDetail();
     detail.setChange(change);
-    detail.setAllowsAnonymous(control.forAnonymousUser().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.setCanRebase(detail.getChange().getStatus().isOpen() && control.canRebase());
+
+    detail.setCanEdit(control.getRefControl().canWrite());
+
+    if (detail.getChange().getStatus().isOpen()) {
+      List<SubmitRecord> submitRecords = control.canSubmit(db, patch.getId());
+      for (SubmitRecord rec : submitRecords) {
+        if (rec.labels != null) {
+          for (SubmitRecord.Label lbl : rec.labels) {
+            aic.want(lbl.appliedBy);
+          }
+        }
+        if (rec.status == SubmitRecord.Status.OK && control.getRefControl().canSubmit()) {
+          detail.setCanSubmit(true);
+        }
+      }
+      detail.setSubmitRecords(submitRecords);
+    }
+
+    patchsetsById = new HashMap<PatchSet.Id, PatchSet>();
     loadPatchSets();
     loadMessages();
     if (change.currentPatchSetId() != null) {
@@ -119,45 +160,53 @@
   }
 
   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>();
+    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());
     }
   }
 
-  private void load() throws OrmException {
+  private void load() throws OrmException, NoSuchChangeException {
+    if (detail.getChange().getStatus().equals(Change.Status.NEW) && testMerge) {
+      ChangeUtil.testMerge(opFactory, detail.getChange());
+    }
+
     final PatchSet.Id psId = detail.getChange().currentPatchSetId();
     final List<PatchSetApproval> allApprovals =
         db.patchSetApprovals().byChange(changeId).toList();
 
     if (detail.getChange().getStatus().isOpen()) {
-      final FunctionState fs =
-          functionState.create(detail.getChange(), psId, allApprovals);
-
-      final Set<ApprovalCategory.Id> missingApprovals =
-          new HashSet<ApprovalCategory.Id>();
-
-      final Set<ApprovalCategory.Id> currentActions =
-          new HashSet<ApprovalCategory.Id>();
+      final FunctionState fs = functionState.create(control, psId, allApprovals);
 
       for (final ApprovalType at : approvalTypes.getApprovalTypes()) {
         CategoryFunction.forCategory(at.getCategory()).run(at, fs);
-        if (!fs.isValid(at)) {
-          missingApprovals.add(at.getCategory().getId());
-        }
       }
-      for (final ApprovalType at : approvalTypes.getActionTypes()) {
-        if (CategoryFunction.forCategory(at.getCategory()).isValid(
-            control.getCurrentUser(), at, fs)) {
-          currentActions.add(at.getCategory().getId());
-        }
-      }
-      detail.setMissingApprovals(missingApprovals);
-      detail.setCurrentActions(currentActions);
     }
 
     final boolean canRemoveReviewers = detail.getChange().getStatus().isOpen() //
@@ -193,18 +242,23 @@
   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 =
+        new HashMap<Change.Id,PatchSet.Id>();
     final List<Change.Id> ancestorOrder = new ArrayList<Change.Id>();
     for (PatchSetAncestor a : db.patchSetAncestors().ancestorsOf(psId)) {
       for (PatchSet p : db.patchSets().byRevision(a.getAncestorRevision())) {
         final Change.Id ck = p.getId().getParentKey();
         if (changesToGet.add(ck)) {
+          ancestorPatchIds.put(ck, p.getId());
           ancestorOrder.add(ck);
         }
       }
@@ -227,7 +281,7 @@
     for (final Change.Id a : ancestorOrder) {
       final Change ac = m.get(a);
       if (ac != null) {
-        dependsOn.add(newChangeInfo(ac));
+        dependsOn.add(newChangeInfo(ac, ancestorPatchIds));
       }
     }
 
@@ -235,7 +289,7 @@
     for (final Change.Id a : descendants) {
       final Change ac = m.get(a);
       if (ac != null) {
-        neededBy.add(newChangeInfo(ac));
+        neededBy.add(newChangeInfo(ac, null));
       }
     }
 
@@ -249,9 +303,31 @@
     detail.setNeededBy(neededBy);
   }
 
-  private ChangeInfo newChangeInfo(final Change ac) {
+  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());
-    ChangeInfo ci = new ChangeInfo(ac);
+    ChangeInfo ci;
+    if (ancestorPatchIds == null) {
+      ci = new ChangeInfo(ac);
+    } else {
+      ci = new ChangeInfo(ac, ancestorPatchIds.get(ac.getId()));
+    }
     ci.setStarred(isStarred(ac));
     return ci;
   }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailServiceImpl.java
index fa0579e..8090451 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailServiceImpl.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/ChangeDetailServiceImpl.java
@@ -19,10 +19,10 @@
 import com.google.gerrit.common.data.IncludedInDetail;
 import com.google.gerrit.common.data.PatchSetDetail;
 import com.google.gerrit.common.data.PatchSetPublishDetail;
-import com.google.gerrit.reviewdb.AccountDiffPreference;
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.PatchSet;
-import com.google.gwt.user.client.rpc.AsyncCallback;
+import com.google.gerrit.reviewdb.client.AccountDiffPreference;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.inject.Inject;
 
 class ChangeDetailServiceImpl implements ChangeDetailService {
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..f99921b 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
@@ -16,25 +16,35 @@
 
 import com.google.gerrit.common.data.ChangeDetail;
 import com.google.gerrit.common.data.ChangeManageService;
-import com.google.gerrit.reviewdb.PatchSet;
-import com.google.gwt.user.client.rpc.AsyncCallback;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gwtjsonrpc.common.AsyncCallback;
+import com.google.gwtjsonrpc.common.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 RebaseChange.Factory rebaseChangeFactory;
+  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 RebaseChange.Factory rebaseChangeFactory,
+      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.rebaseChangeFactory = rebaseChangeFactory;
+    this.restoreChangeHandlerFactory = restoreChangeHandlerFactory;
     this.revertChangeFactory = revertChangeFactory;
+    this.publishAction = publishAction;
+    this.deleteDraftChangeFactory = deleteDraftChangeFactory;
   }
 
   public void submit(final PatchSet.Id patchSetId,
@@ -44,7 +54,12 @@
 
   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 rebaseChange(final PatchSet.Id patchSetId,
+      final AsyncCallback<ChangeDetail> callback) {
+    rebaseChangeFactory.create(patchSetId).to(callback);
   }
 
   public void revertChange(final PatchSet.Id patchSetId, final String message,
@@ -54,6 +69,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..b672a439 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,17 @@
     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(RebaseChange.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..66c11c0
--- /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.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.ReplicationQueue;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gwtjsonrpc.common.VoidResult;
+import com.google.gwtorm.server.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/IncludedInDetailFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/IncludedInDetailFactory.java
index 398ca7e..c07ee51 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/IncludedInDetailFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/IncludedInDetailFactory.java
@@ -17,13 +17,13 @@
 import com.google.gerrit.common.data.IncludedInDetail;
 import com.google.gerrit.common.errors.InvalidRevisionException;
 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.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
-import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
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..de3ff2f 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
@@ -17,15 +17,15 @@
 import com.google.gerrit.common.data.PatchSetDetail;
 import com.google.gerrit.common.errors.NoSuchEntityException;
 import com.google.gerrit.httpd.rpc.Handler;
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.AccountDiffPreference;
-import com.google.gerrit.reviewdb.AccountDiffPreference.Whitespace;
-import com.google.gerrit.reviewdb.AccountPatchReview;
-import com.google.gerrit.reviewdb.Patch;
-import com.google.gerrit.reviewdb.PatchLineComment;
-import com.google.gerrit.reviewdb.PatchSet;
-import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountDiffPreference;
+import com.google.gerrit.reviewdb.client.AccountPatchReview;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.patch.PatchList;
@@ -35,7 +35,7 @@
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
-import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -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 779475e..638bfe3 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetPublishDetailFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PatchSetPublishDetailFactory.java
@@ -14,75 +14,63 @@
 
 package com.google.gerrit.httpd.rpc.changedetail;
 
-import com.google.gerrit.common.data.AccountInfoCache;
 import com.google.gerrit.common.data.ApprovalType;
 import com.google.gerrit.common.data.ApprovalTypes;
 import com.google.gerrit.common.data.PatchSetPublishDetail;
+import com.google.gerrit.common.data.PermissionRange;
+import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.httpd.rpc.Handler;
-import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.ApprovalCategory;
-import com.google.gerrit.reviewdb.ApprovalCategoryValue;
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.PatchLineComment;
-import com.google.gerrit.reviewdb.PatchSet;
-import com.google.gerrit.reviewdb.PatchSetApproval;
-import com.google.gerrit.reviewdb.PatchSetInfo;
-import com.google.gerrit.reviewdb.RefRight;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.PatchSetInfo;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountInfoCacheFactory;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
-import com.google.gerrit.server.project.CanSubmitResult;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectState;
-import com.google.gerrit.server.project.RefControl;
-import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
+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;
 
 final class PatchSetPublishDetailFactory extends Handler<PatchSetPublishDetail> {
   interface Factory {
     PatchSetPublishDetailFactory create(PatchSet.Id patchSetId);
   }
 
-  private final ProjectCache projectCache;
   private final PatchSetInfoFactory infoFactory;
-  private final ApprovalTypes approvalTypes;
   private final ReviewDb db;
   private final ChangeControl.Factory changeControlFactory;
+  private final ApprovalTypes approvalTypes;
   private final AccountInfoCacheFactory aic;
   private final IdentifiedUser user;
 
   private final PatchSet.Id patchSetId;
 
-  private AccountInfoCache accounts;
   private PatchSetInfo patchSetInfo;
   private Change change;
   private List<PatchLineComment> drafts;
-  private Map<ApprovalCategory.Id, Set<ApprovalCategoryValue.Id>> allowed;
-  private Map<ApprovalCategory.Id, PatchSetApproval> given;
 
   @Inject
   PatchSetPublishDetailFactory(final PatchSetInfoFactory infoFactory,
-      final ProjectCache projectCache, final ApprovalTypes approvalTypes,
       final ReviewDb db,
       final AccountInfoCacheFactory.Factory accountInfoCacheFactory,
       final ChangeControl.Factory changeControlFactory,
+      final ApprovalTypes approvalTypes,
       final IdentifiedUser user, @Assisted final PatchSet.Id patchSetId) {
-    this.projectCache = projectCache;
     this.infoFactory = infoFactory;
-    this.approvalTypes = approvalTypes;
     this.db = db;
     this.changeControlFactory = changeControlFactory;
+    this.approvalTypes = approvalTypes;
     this.aic = accountInfoCacheFactory.create();
     this.user = user;
 
@@ -95,68 +83,94 @@
     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();
-
-    allowed = new HashMap<ApprovalCategory.Id, Set<ApprovalCategoryValue.Id>>();
-    given = new HashMap<ApprovalCategory.Id, PatchSetApproval>();
-    if (change.getStatus().isOpen()
-        && patchSetId.equals(change.currentPatchSetId())) {
-      computeAllowed();
-      for (final PatchSetApproval a : db.patchSetApprovals().byPatchSetUser(
-          patchSetId, user.getAccountId())) {
-        given.put(a.getCategoryId(), a);
-      }
-    }
+    patchSetInfo = infoFactory.get(db, patchSetId);
+    drafts = db.patchComments().draftByPatchSetAuthor(patchSetId, user.getAccountId()).toList();
 
     aic.want(change.getOwner());
-    accounts = aic.create();
 
     PatchSetPublishDetail detail = new PatchSetPublishDetail();
-    detail.setAccounts(accounts);
     detail.setPatchSetInfo(patchSetInfo);
     detail.setChange(change);
     detail.setDrafts(drafts);
-    detail.setAllowed(allowed);
-    detail.setGiven(given);
 
-    final CanSubmitResult canSubmitResult = control.canSubmit(patchSetId);
-    detail.setSubmitAllowed(canSubmitResult == CanSubmitResult.OK);
+    List<PermissionRange> allowed = Collections.emptyList();
+    List<PatchSetApproval> given = Collections.emptyList();
 
-    return detail;
-  }
+    if (change.getStatus().isOpen()
+        && patchSetId.equals(change.currentPatchSetId())) {
+      // TODO Push this selection of labels down into the Prolog interpreter.
+      // Ideally we discover the labels the user can apply here based on doing
+      // a findall() over the space of labels they can apply combined against
+      // the submit rule, thereby skipping any mutually exclusive cases. However
+      // those are not common, so it might just be reasonable to take this
+      // simple approach.
 
-  private void computeAllowed() {
-    final Set<AccountGroup.Id> am = user.getEffectiveGroups();
-    final ProjectState pe = projectCache.get(change.getProject());
-    for (ApprovalCategory.Id category : approvalTypes.getApprovalCategories()) {
-      RefControl rc = pe.controlFor(user).controlForRef(change.getDest());
-      List<RefRight> categoryRights = rc.getApplicableRights(category);
-      computeAllowed(am, categoryRights, category);
-    }
-  }
-
-  private void computeAllowed(final Set<AccountGroup.Id> am,
-      final List<RefRight> list, ApprovalCategory.Id category) {
-
-    Set<ApprovalCategoryValue.Id> s = allowed.get(category);
-    if (s == null) {
-      s = new HashSet<ApprovalCategoryValue.Id>();
-      allowed.put(category, s);
-    }
-
-    for (final RefRight r : list) {
-      if (!am.contains(r.getAccountGroupId())) {
-        continue;
-      }
-      final ApprovalType at =
-          approvalTypes.getApprovalType(r.getApprovalCategoryId());
-      for (short m = r.getMinValue(); m <= r.getMaxValue(); m++) {
-        final ApprovalCategoryValue v = at.getValue(m);
-        if (v != null) {
-          s.add(v.getId());
+      Map<String, PermissionRange> rangeByName =
+          new HashMap<String, PermissionRange>();
+      for (PermissionRange r : control.getLabelRanges()) {
+        if (r.isLabel()) {
+          rangeByName.put(r.getLabel(), r);
         }
       }
+      allowed = new ArrayList<PermissionRange>();
+
+      given = db.patchSetApprovals() //
+          .byPatchSetUser(patchSetId, user.getAccountId()) //
+          .toList();
+
+      boolean couldSubmit = false;
+      List<SubmitRecord> submitRecords = control.canSubmit(db, patchSetId);
+      for (SubmitRecord rec : submitRecords) {
+        if (rec.status == SubmitRecord.Status.OK) {
+          couldSubmit = true;
+        }
+
+        if (rec.labels != null) {
+          int ok = 0;
+
+          for (SubmitRecord.Label lbl : rec.labels) {
+            boolean canMakeOk = false;
+            PermissionRange range = rangeByName.get(lbl.label);
+            if (range != null) {
+              if (!allowed.contains(range)) {
+                allowed.add(range);
+              }
+
+              ApprovalType at = approvalTypes.byLabel(lbl.label);
+              if (at == null || at.getMax().getValue() == range.getMax()) {
+                canMakeOk = true;
+              }
+            }
+
+            switch (lbl.status) {
+              case OK:
+                ok++;
+                break;
+
+              case NEED:
+                if (canMakeOk) {
+                  ok++;
+                }
+                break;
+            }
+          }
+
+          if (rec.status == SubmitRecord.Status.NOT_READY
+              && ok == rec.labels.size()) {
+            couldSubmit = true;
+          }
+        }
+      }
+
+      if (couldSubmit && control.getRefControl().canSubmit()) {
+        detail.setCanSubmit(true);
+      }
     }
+
+    detail.setLabels(allowed);
+    detail.setGiven(given);
+    detail.setAccounts(aic.create());
+
+    return detail;
   }
 }
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..4ea279f
--- /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.client.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.server.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/RebaseChange.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RebaseChange.java
new file mode 100644
index 0000000..3c29074
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RebaseChange.java
@@ -0,0 +1,110 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.ApprovalTypes;
+import com.google.gerrit.common.data.ChangeDetail;
+import com.google.gerrit.common.errors.NoSuchEntityException;
+import com.google.gerrit.httpd.rpc.Handler;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.ReplicationQueue;
+import com.google.gerrit.server.mail.EmailException;
+import com.google.gerrit.server.mail.RebasedPatchSetSender;
+import com.google.gerrit.server.patch.PatchSetInfoFactory;
+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.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.lib.PersonIdent;
+
+import java.io.IOException;
+
+class RebaseChange extends Handler<ChangeDetail> {
+  interface Factory {
+    RebaseChange create(PatchSet.Id patchSetId);
+  }
+
+  private final ChangeControl.Factory changeControlFactory;
+  private final ReviewDb db;
+  private final IdentifiedUser currentUser;
+  private final RebasedPatchSetSender.Factory rebasedPatchSetSenderFactory;
+
+  private final ChangeDetailFactory.Factory changeDetailFactory;
+  private final ReplicationQueue replication;
+
+  private final PatchSet.Id patchSetId;
+
+  private final ChangeHookRunner hooks;
+
+  private final GitRepositoryManager gitManager;
+  private final PatchSetInfoFactory patchSetInfoFactory;
+
+  private final PersonIdent myIdent;
+
+  private final ApprovalTypes approvalTypes;
+
+  @Inject
+  RebaseChange(final ChangeControl.Factory changeControlFactory,
+      final ReviewDb db, final IdentifiedUser currentUser,
+      final RebasedPatchSetSender.Factory rebasedPatchSetSenderFactory,
+      final ChangeDetailFactory.Factory changeDetailFactory,
+      @Assisted final PatchSet.Id patchSetId, final ChangeHookRunner hooks,
+      final GitRepositoryManager gitManager,
+      final PatchSetInfoFactory patchSetInfoFactory,
+      final ReplicationQueue replication,
+      @GerritPersonIdent final PersonIdent myIdent,
+      final ApprovalTypes approvalTypes) {
+    this.changeControlFactory = changeControlFactory;
+    this.db = db;
+    this.currentUser = currentUser;
+    this.rebasedPatchSetSenderFactory = rebasedPatchSetSenderFactory;
+    this.changeDetailFactory = changeDetailFactory;
+
+    this.patchSetId = patchSetId;
+    this.hooks = hooks;
+    this.gitManager = gitManager;
+
+    this.patchSetInfoFactory = patchSetInfoFactory;
+    this.replication = replication;
+    this.myIdent = myIdent;
+
+    this.approvalTypes = approvalTypes;
+  }
+
+  @Override
+  public ChangeDetail call() throws NoSuchChangeException, OrmException,
+      EmailException, NoSuchEntityException, PatchSetInfoNotAvailableException,
+      MissingObjectException, IncorrectObjectTypeException, IOException,
+      InvalidChangeOperationException {
+
+    ChangeUtil.rebaseChange(patchSetId, currentUser, db,
+        rebasedPatchSetSenderFactory, hooks, gitManager, patchSetInfoFactory,
+        replication, myIdent, changeControlFactory, approvalTypes);
+
+    return changeDetailFactory.create(patchSetId.getParentKey()).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 f951439..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RestoreChange.java
+++ /dev/null
@@ -1,85 +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.*;
-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.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 AbandonedSender.Factory abandonedSenderFactory;
-  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 AbandonedSender.Factory abandonedSenderFactory,
-      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.abandonedSenderFactory = abandonedSenderFactory;
-    this.changeDetailFactory = changeDetailFactory;
-
-    this.patchSetId = patchSetId;
-    this.message = message;
-    this.hooks = hooks;
-  }
-
-  @Override
-  public ChangeDetail call() throws NoSuchChangeException, OrmException,
-      EmailException, NoSuchEntityException, 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,
-       abandonedSenderFactory, 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..f018750
--- /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.client.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.server.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..9fb9ae1 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,13 +14,13 @@
 
 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;
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.PatchSet;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
@@ -32,7 +32,7 @@
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
-import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -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 721656c..80100ad 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
@@ -14,23 +14,16 @@
 
 package com.google.gerrit.httpd.rpc.changedetail;
 
-import com.google.gerrit.common.data.ApprovalTypes;
 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.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.reviewdb.client.PatchSet;
+import com.google.gerrit.server.changedetail.Submit;
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
-import com.google.gerrit.server.project.CanSubmitResult;
-import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.NoSuchChangeException;
-import com.google.gerrit.server.workflow.FunctionState;
-import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -39,52 +32,31 @@
     SubmitAction create(PatchSet.Id patchSetId);
   }
 
-  private final ReviewDb db;
-  private final MergeQueue merger;
-  private final ApprovalTypes approvalTypes;
-  private final FunctionState.Factory functionState;
-  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 ApprovalTypes at,
-      final FunctionState.Factory fs, 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.approvalTypes = at;
-    this.functionState = fs;
-    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);
-
-    CanSubmitResult err =
-        changeControl.canSubmit(patchSetId, db, approvalTypes, functionState);
-    if (err == CanSubmitResult.OK) {
-      ChangeUtil.submit(patchSetId, user, db, opFactory, merger);
-      return changeDetailFactory.create(changeId).call();
-    } else {
-      throw new IllegalStateException(err.getMessage());
+      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());
     }
+    return changeDetailFactory.create(result.getChangeId()).call();
   }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/AddReviewer.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/AddReviewer.java
deleted file mode 100644
index efe93f5..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/AddReviewer.java
+++ /dev/null
@@ -1,161 +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.patch;
-
-import com.google.gerrit.common.data.ReviewerResult;
-import com.google.gerrit.common.data.ApprovalType;
-import com.google.gerrit.common.data.ApprovalTypes;
-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.ApprovalCategory;
-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.IdentifiedUser;
-import com.google.gerrit.server.account.AccountResolver;
-import com.google.gerrit.server.mail.AddReviewerSender;
-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.util.ArrayList;
-import java.util.Collection;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
-class AddReviewer extends Handler<ReviewerResult> {
-  interface Factory {
-    AddReviewer create(Change.Id changeId, Collection<String> nameOrEmails);
-  }
-
-  private final AddReviewerSender.Factory addReviewerSenderFactory;
-  private final AccountResolver accountResolver;
-  private final ChangeControl.Factory changeControlFactory;
-  private final ChangeDetailFactory.Factory changeDetailFactory;
-  private final ReviewDb db;
-  private final IdentifiedUser currentUser;
-  private final IdentifiedUser.GenericFactory identifiedUserFactory;
-  private final ApprovalCategory.Id addReviewerCategoryId;
-
-  private final Change.Id changeId;
-  private final Collection<String> reviewers;
-
-  @Inject
-  AddReviewer(final AddReviewerSender.Factory addReviewerSenderFactory,
-      final AccountResolver accountResolver,
-      final ChangeControl.Factory changeControlFactory, final ReviewDb db,
-      final IdentifiedUser.GenericFactory identifiedUserFactory,
-      final IdentifiedUser currentUser, final ApprovalTypes approvalTypes,
-      final ChangeDetailFactory.Factory changeDetailFactory,
-      @Assisted final Change.Id changeId,
-      @Assisted final Collection<String> nameOrEmails) {
-    this.addReviewerSenderFactory = addReviewerSenderFactory;
-    this.accountResolver = accountResolver;
-    this.db = db;
-    this.changeControlFactory = changeControlFactory;
-    this.identifiedUserFactory = identifiedUserFactory;
-    this.currentUser = currentUser;
-    this.changeDetailFactory = changeDetailFactory;
-
-    final List<ApprovalType> allTypes = approvalTypes.getApprovalTypes();
-    addReviewerCategoryId =
-        allTypes.get(allTypes.size() - 1).getCategory().getId();
-
-    this.changeId = changeId;
-    this.reviewers = nameOrEmails;
-  }
-
-  @Override
-  public ReviewerResult call() throws Exception {
-    final Set<Account.Id> reviewerIds = new HashSet<Account.Id>();
-    final ChangeControl control = changeControlFactory.validateFor(changeId);
-
-    final ReviewerResult result = new ReviewerResult();
-    for (final String nameOrEmail : reviewers) {
-      final Account account = accountResolver.find(nameOrEmail);
-      if (account == null) {
-        result.addError(new ReviewerResult.Error(
-            ReviewerResult.Error.Type.ACCOUNT_NOT_FOUND, nameOrEmail));
-        continue;
-      }
-      if (!account.isActive()) {
-        result.addError(new ReviewerResult.Error(
-            ReviewerResult.Error.Type.ACCOUNT_INACTIVE, nameOrEmail));
-        continue;
-      }
-
-      final IdentifiedUser user = identifiedUserFactory.create(account.getId());
-      if (!control.forUser(user).isVisible()) {
-        result.addError(new ReviewerResult.Error(
-            ReviewerResult.Error.Type.CHANGE_NOT_VISIBLE, nameOrEmail));
-        continue;
-      }
-
-      reviewerIds.add(account.getId());
-    }
-
-    if (reviewerIds.isEmpty()) {
-      return result;
-    }
-
-    // Add the reviewers to the database
-    //
-    final Set<Account.Id> added = new HashSet<Account.Id>();
-    final List<PatchSetApproval> toInsert = new ArrayList<PatchSetApproval>();
-    final PatchSet.Id psid = control.getChange().currentPatchSetId();
-    for (final Account.Id reviewer : reviewerIds) {
-      if (!exists(psid, reviewer)) {
-        // This reviewer has not entered an approval for this change yet.
-        //
-        final PatchSetApproval myca = dummyApproval(psid, reviewer);
-        toInsert.add(myca);
-        added.add(reviewer);
-      }
-    }
-    db.patchSetApprovals().insert(toInsert);
-
-    // Email the reviewers
-    //
-    // The user knows they added themselves, don't bother emailing them.
-    added.remove(currentUser.getAccountId());
-    if (!added.isEmpty()) {
-      final AddReviewerSender cm;
-
-      cm = addReviewerSenderFactory.create(control.getChange());
-      cm.setFrom(currentUser.getAccountId());
-      cm.addReviewers(added);
-      cm.send();
-    }
-
-    result.setChange(changeDetailFactory.create(changeId).call());
-    return result;
-  }
-
-  private boolean exists(final PatchSet.Id patchSetId,
-      final Account.Id reviewerId) throws OrmException {
-    return db.patchSetApprovals().byPatchSetUser(patchSetId, reviewerId)
-        .iterator().hasNext();
-  }
-
-  private PatchSetApproval dummyApproval(final PatchSet.Id patchSetId,
-      final Account.Id reviewerId) {
-    return new PatchSetApproval(new PatchSetApproval.Key(patchSetId,
-        reviewerId, addReviewerCategoryId), (short) 0);
-  }
-}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/AddReviewerHandler.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/AddReviewerHandler.java
new file mode 100644
index 0000000..60d6e60
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/AddReviewerHandler.java
@@ -0,0 +1,61 @@
+// 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.patch;
+
+import com.google.gerrit.common.data.ReviewerResult;
+import com.google.gerrit.httpd.rpc.Handler;
+import com.google.gerrit.httpd.rpc.changedetail.ChangeDetailFactory;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.patch.AddReviewer;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import java.util.Collection;
+
+class AddReviewerHandler extends Handler<ReviewerResult> {
+  interface Factory {
+    AddReviewerHandler create(Change.Id changeId, Collection<String> reviewers,
+        boolean confirmed);
+  }
+
+  private final AddReviewer.Factory addReviewerFactory;
+  private final ChangeDetailFactory.Factory changeDetailFactory;
+
+  private final Change.Id changeId;
+  private final Collection<String> reviewers;
+  private final boolean confirmed;
+
+  @Inject
+  AddReviewerHandler(final AddReviewer.Factory addReviewerFactory,
+      final ChangeDetailFactory.Factory changeDetailFactory,
+      @Assisted final Change.Id changeId,
+      @Assisted final Collection<String> reviewers,
+      @Assisted final boolean confirmed) {
+
+    this.addReviewerFactory = addReviewerFactory;
+    this.changeDetailFactory = changeDetailFactory;
+
+    this.changeId = changeId;
+    this.reviewers = reviewers;
+    this.confirmed = confirmed;
+  }
+
+  @Override
+  public ReviewerResult call() throws Exception {
+    ReviewerResult result = addReviewerFactory.create(changeId, reviewers, confirmed).call();
+    result.setChange(changeDetailFactory.create(changeId).call());
+    return result;
+  }
+}
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 da52596..e90467e 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,36 +14,41 @@
 
 package com.google.gerrit.httpd.rpc.patch;
 
-import com.google.gerrit.common.data.ReviewerResult;
 import com.google.gerrit.common.data.ApprovalSummary;
 import com.google.gerrit.common.data.ApprovalSummarySet;
 import com.google.gerrit.common.data.ApprovalTypes;
+import com.google.gerrit.common.data.ChangeDetail;
 import com.google.gerrit.common.data.PatchDetailService;
 import com.google.gerrit.common.data.PatchScript;
+import com.google.gerrit.common.data.ReviewResult;
+import com.google.gerrit.common.data.ReviewerResult;
 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.reviewdb.Account;
-import com.google.gerrit.reviewdb.AccountDiffPreference;
-import com.google.gerrit.reviewdb.AccountPatchReview;
-import com.google.gerrit.reviewdb.ApprovalCategory;
-import com.google.gerrit.reviewdb.ApprovalCategoryValue;
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.Patch;
-import com.google.gerrit.reviewdb.PatchLineComment;
-import com.google.gerrit.reviewdb.PatchSet;
-import com.google.gerrit.reviewdb.PatchSetApproval;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gerrit.reviewdb.Patch.Key;
+import com.google.gerrit.httpd.rpc.changedetail.ChangeDetailFactory;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountDiffPreference;
+import com.google.gerrit.reviewdb.client.AccountPatchReview;
+import com.google.gerrit.reviewdb.client.ApprovalCategory;
+import com.google.gerrit.reviewdb.client.ApprovalCategoryValue;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.Patch.Key;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.AccountInfoCacheFactory;
+import com.google.gerrit.server.changedetail.DeleteDraftPatchSet;
+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;
 import com.google.gerrit.server.workflow.FunctionState;
-import com.google.gwt.user.client.rpc.AsyncCallback;
-import com.google.gwtjsonrpc.client.VoidResult;
-import com.google.gwtorm.client.OrmException;
+import com.google.gwtjsonrpc.common.AsyncCallback;
+import com.google.gwtjsonrpc.common.VoidResult;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
@@ -58,37 +63,43 @@
   private final ApprovalTypes approvalTypes;
 
   private final AccountInfoCacheFactory.Factory accountInfoCacheFactory;
-  private final AddReviewer.Factory addReviewerFactory;
+  private final AddReviewerHandler.Factory addReviewerHandlerFactory;
   private final ChangeControl.Factory changeControlFactory;
-  private final RemoveReviewer.Factory removeReviewerFactory;
+  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 ChangeDetailFactory.Factory changeDetailFactory;
 
   @Inject
   PatchDetailServiceImpl(final Provider<ReviewDb> schema,
       final Provider<CurrentUser> currentUser,
       final ApprovalTypes approvalTypes,
       final AccountInfoCacheFactory.Factory accountInfoCacheFactory,
-      final AddReviewer.Factory addReviewerFactory,
-      final RemoveReviewer.Factory removeReviewerFactory,
+      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 ChangeDetailFactory.Factory changeDetailFactory) {
     super(schema, currentUser);
     this.approvalTypes = approvalTypes;
 
     this.accountInfoCacheFactory = accountInfoCacheFactory;
-    this.addReviewerFactory = addReviewerFactory;
-    this.removeReviewerFactory = removeReviewerFactory;
+    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.changeDetailFactory = changeDetailFactory;
   }
 
   public void patchScript(final Patch.Key patchKey, final PatchSet.Id psa,
@@ -110,18 +121,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 +174,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,26 +187,32 @@
         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;
       }
     });
   }
 
   public void addReviewers(final Change.Id id, final List<String> reviewers,
-      final AsyncCallback<ReviewerResult> callback) {
-    addReviewerFactory.create(id, reviewers).to(callback);
+      final boolean confirmed, final AsyncCallback<ReviewerResult> callback) {
+    addReviewerHandlerFactory.create(id, reviewers, confirmed).to(callback);
   }
 
   public void removeReviewer(final Change.Id id, final Account.Id reviewerId,
       final AsyncCallback<ReviewerResult> callback) {
-    removeReviewerFactory.create(id, reviewerId).to(callback);
+    removeReviewerHandlerFactory.create(id, reviewerId).to(callback);
   }
 
   public void userApprovals(final Set<Change.Id> cids, final Account.Id aid,
@@ -182,16 +233,18 @@
             final Map<ApprovalCategory.Id, PatchSetApproval> psas =
                 new HashMap<ApprovalCategory.Id, PatchSetApproval>();
             final FunctionState fs =
-                functionStateFactory.create(change, ps_id, psas.values());
+                functionStateFactory.create(cc, ps_id, psas.values());
 
             for (final PatchSetApproval ca : db.patchSetApprovals()
                 .byPatchSetUser(ps_id, aid)) {
               final ApprovalCategory.Id category = ca.getCategoryId();
-              if (change.getStatus().isOpen()) {
-                fs.normalize(approvalTypes.getApprovalType(category), ca);
+              if (ApprovalCategory.SUBMIT.equals(category)) {
+                continue;
               }
-              if (ca.getValue() == 0
-                  || ApprovalCategory.SUBMIT.equals(category)) {
+              if (change.getStatus().isOpen()) {
+                fs.normalize(approvalTypes.byId(category), ca);
+              }
+              if (ca.getValue() == 0) {
                 continue;
               }
               psas.put(category, ca);
@@ -227,15 +280,17 @@
             final Map<ApprovalCategory.Id, PatchSetApproval> psas =
                 new HashMap<ApprovalCategory.Id, PatchSetApproval>();
             final FunctionState fs =
-                functionStateFactory.create(change, ps_id, psas.values());
+                functionStateFactory.create(cc, ps_id, psas.values());
 
             for (PatchSetApproval ca : db.patchSetApprovals().byPatchSet(ps_id)) {
               final ApprovalCategory.Id category = ca.getCategoryId();
-              if (change.getStatus().isOpen()) {
-                fs.normalize(approvalTypes.getApprovalType(category), ca);
+              if (ApprovalCategory.SUBMIT.equals(category)) {
+                continue;
               }
-              if (ca.getValue() == 0
-                  || ApprovalCategory.SUBMIT.equals(category)) {
+              if (change.getStatus().isOpen()) {
+                fs.normalize(approvalTypes.byId(category), ca);
+              }
+              if (ca.getValue() == 0) {
                 continue;
               }
               boolean keep = true;
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchModule.java
index 7d6d05e..3e94b4c 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchModule.java
@@ -28,8 +28,8 @@
     install(new FactoryModule() {
       @Override
       protected void configure() {
-        factory(AddReviewer.Factory.class);
-        factory(RemoveReviewer.Factory.class);
+        factory(AddReviewerHandler.Factory.class);
+        factory(RemoveReviewerHandler.Factory.class);
         factory(PatchScriptFactory.Factory.class);
         factory(SaveDraft.Factory.class);
       }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptBuilder.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptBuilder.java
index 302135e..ad7746f 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptBuilder.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/PatchScriptBuilder.java
@@ -19,12 +19,12 @@
 import com.google.gerrit.common.data.PatchScript.DisplayMethod;
 import com.google.gerrit.prettify.common.EditList;
 import com.google.gerrit.prettify.common.SparseFileContent;
-import com.google.gerrit.reviewdb.AccountDiffPreference;
-import com.google.gerrit.reviewdb.AccountDiffPreference.Whitespace;
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.Patch;
-import com.google.gerrit.reviewdb.PatchLineComment;
-import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.reviewdb.client.AccountDiffPreference;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
 import com.google.gerrit.server.FileTypeRegistry;
 import com.google.gerrit.server.patch.IntraLineDiff;
 import com.google.gerrit.server.patch.IntraLineDiffKey;
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..d61e6e7 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
@@ -17,27 +17,28 @@
 import com.google.gerrit.common.data.CommentDetail;
 import com.google.gerrit.common.data.PatchScript;
 import com.google.gerrit.httpd.rpc.Handler;
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.AccountDiffPreference;
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.Patch;
-import com.google.gerrit.reviewdb.PatchLineComment;
-import com.google.gerrit.reviewdb.PatchSet;
-import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gerrit.reviewdb.AccountDiffPreference.Whitespace;
-import com.google.gerrit.reviewdb.Patch.ChangeType;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountDiffPreference;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
+import com.google.gerrit.reviewdb.client.Patch.ChangeType;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountInfoCacheFactory;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.LargeObjectException;
 import com.google.gerrit.server.patch.PatchList;
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.patch.PatchListEntry;
 import com.google.gerrit.server.patch.PatchListKey;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
-import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
@@ -120,7 +121,8 @@
   }
 
   @Override
-  public PatchScript call() throws OrmException, NoSuchChangeException {
+  public PatchScript call() throws OrmException, NoSuchChangeException,
+      LargeObjectException {
     validatePatchSetId(psa);
     validatePatchSetId(psb);
 
@@ -156,6 +158,8 @@
       } catch (IOException e) {
         log.error("File content unavailable", e);
         throw new NoSuchChangeException(changeId, e);
+      } catch (org.eclipse.jgit.errors.LargeObjectException err) {
+        throw new LargeObjectException("File content is too large", err);
       }
     } finally {
       git.close();
@@ -287,7 +291,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 +307,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/RemoveReviewer.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/RemoveReviewer.java
deleted file mode 100644
index 342a674..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/RemoveReviewer.java
+++ /dev/null
@@ -1,95 +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.rpc.patch;
-
-import com.google.gerrit.common.data.ReviewerResult;
-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.Change;
-import com.google.gerrit.reviewdb.PatchSetApproval;
-import com.google.gerrit.reviewdb.ReviewDb;
-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.util.ArrayList;
-import java.util.List;
-
-/**
- * Implement the remote logic that removes a reviewer from a change.
- */
-class RemoveReviewer extends Handler<ReviewerResult> {
-  interface Factory {
-    RemoveReviewer create(Change.Id changeId, Account.Id reviewerId);
-  }
-
-  private final Account.Id reviewerId;
-  private final ChangeControl.Factory changeControlFactory;
-  private final ReviewDb db;
-  private final Change.Id changeId;
-  private final ChangeDetailFactory.Factory changeDetailFactory;
-
-  @Inject
-  RemoveReviewer(final ReviewDb db, final ChangeControl.Factory changeControlFactory,
-      final ChangeDetailFactory.Factory changeDetailFactory,
-      @Assisted Change.Id changeId, @Assisted Account.Id reviewerId) {
-    this.db = db;
-    this.changeControlFactory = changeControlFactory;
-    this.changeId = changeId;
-    this.reviewerId = reviewerId;
-    this.changeDetailFactory = changeDetailFactory;
-  }
-
-  @Override
-  public ReviewerResult call() throws Exception {
-    ReviewerResult result = new ReviewerResult();
-    ChangeControl ctl = changeControlFactory.validateFor(changeId);
-    boolean permitted = true;
-
-    List<PatchSetApproval> toDelete = new ArrayList<PatchSetApproval>();
-    for (PatchSetApproval psa : db.patchSetApprovals().byChange(changeId)) {
-      if (psa.getAccountId().equals(reviewerId)) {
-        if (ctl.canRemoveReviewer(psa)) {
-          toDelete.add(psa);
-        } else {
-          permitted = false;
-          break;
-        }
-      }
-    }
-
-    if (permitted) {
-      try {
-        db.patchSetApprovals().delete(toDelete);
-      } catch (OrmException ex) {
-        result.addError(new ReviewerResult.Error(
-            ReviewerResult.Error.Type.COULD_NOT_REMOVE,
-            "Could not remove reviewer " + reviewerId));
-      }
-    } else {
-      result.addError(new ReviewerResult.Error(
-          ReviewerResult.Error.Type.COULD_NOT_REMOVE,
-          "Not allowed to remove reviewer " + reviewerId));
-    }
-
-    // Note: call setChange() after the deletion has been made or it will still
-    // contain the reviewer we want to delete.
-    result.setChange(changeDetailFactory.create(changeId).call());
-    return result;
-  }
-
-}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/RemoveReviewerHandler.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/RemoveReviewerHandler.java
new file mode 100644
index 0000000..2b31c1c
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/RemoveReviewerHandler.java
@@ -0,0 +1,59 @@
+// 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.rpc.patch;
+
+import com.google.gerrit.common.data.ReviewerResult;
+import com.google.gerrit.httpd.rpc.Handler;
+import com.google.gerrit.httpd.rpc.changedetail.ChangeDetailFactory;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.patch.RemoveReviewer;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import java.util.Collections;
+
+/**
+ * Implement the remote logic that removes a reviewer from a change.
+ */
+class RemoveReviewerHandler extends Handler<ReviewerResult> {
+  interface Factory {
+    RemoveReviewerHandler create(Change.Id changeId, Account.Id reviewerId);
+  }
+
+  private final RemoveReviewer.Factory removeReviewerFactory;
+  private final Account.Id reviewerId;
+  private final Change.Id changeId;
+  private final ChangeDetailFactory.Factory changeDetailFactory;
+
+  @Inject
+  RemoveReviewerHandler(final RemoveReviewer.Factory removeReviewerFactory,
+      final ChangeDetailFactory.Factory changeDetailFactory,
+      @Assisted Change.Id changeId, @Assisted Account.Id reviewerId) {
+    this.removeReviewerFactory = removeReviewerFactory;
+    this.changeId = changeId;
+    this.reviewerId = reviewerId;
+    this.changeDetailFactory = changeDetailFactory;
+  }
+
+  @Override
+  public ReviewerResult call() throws Exception {
+    ReviewerResult result = removeReviewerFactory.create(
+        changeId, Collections.singleton(reviewerId)).call();
+    result.setChange(changeDetailFactory.create(changeId).call());
+    return result;
+  }
+
+}
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..06c21df 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
@@ -15,17 +15,17 @@
 package com.google.gerrit.httpd.rpc.patch;
 
 import com.google.gerrit.httpd.rpc.Handler;
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.Patch;
-import com.google.gerrit.reviewdb.PatchLineComment;
-import com.google.gerrit.reviewdb.PatchSet;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
-import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -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..44c3141 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,20 +14,20 @@
 
 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;
 import com.google.gerrit.httpd.rpc.Handler;
-import com.google.gerrit.reviewdb.Branch;
-import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.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/AddRefRight.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/AddRefRight.java
deleted file mode 100644
index 358b542..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/AddRefRight.java
+++ /dev/null
@@ -1,207 +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.rpc.project;
-
-import com.google.gerrit.common.data.ApprovalType;
-import com.google.gerrit.common.data.ApprovalTypes;
-import com.google.gerrit.common.data.ProjectDetail;
-import com.google.gerrit.common.errors.InvalidNameException;
-import com.google.gerrit.common.errors.NoSuchGroupException;
-import com.google.gerrit.httpd.rpc.Handler;
-import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.ApprovalCategory;
-import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.RefRight;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gerrit.server.account.GroupCache;
-import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.project.NoSuchRefException;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectControl;
-import com.google.gerrit.server.project.RefControl;
-import com.google.gwtorm.client.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
-
-import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.lib.Repository;
-
-import java.util.Collections;
-
-import javax.annotation.Nullable;
-
-class AddRefRight extends Handler<ProjectDetail> {
-  interface Factory {
-    AddRefRight create(@Assisted Project.NameKey projectName,
-        @Assisted ApprovalCategory.Id categoryId,
-        @Assisted("groupName") String groupName,
-        @Nullable @Assisted("refPattern") String refPattern,
-        @Assisted("min") short min, @Assisted("max") short max);
-  }
-
-  private final ProjectDetailFactory.Factory projectDetailFactory;
-  private final ProjectControl.Factory projectControlFactory;
-  private final ProjectCache projectCache;
-  private final GroupCache groupCache;
-  private final ReviewDb db;
-  private final ApprovalTypes approvalTypes;
-
-  private final Project.NameKey projectName;
-  private final ApprovalCategory.Id categoryId;
-  private final AccountGroup.NameKey groupName;
-  private final String refPattern;
-  private final short min;
-  private final short max;
-
-  @Inject
-  AddRefRight(final ProjectDetailFactory.Factory projectDetailFactory,
-      final ProjectControl.Factory projectControlFactory,
-      final ProjectCache projectCache, final GroupCache groupCache,
-      final ReviewDb db, final ApprovalTypes approvalTypes,
-
-      @Assisted final Project.NameKey projectName,
-      @Assisted final ApprovalCategory.Id categoryId,
-      @Assisted("groupName") final String groupName,
-      @Nullable @Assisted("refPattern") final String refPattern,
-      @Assisted("min") final short min, @Assisted("max") final short max) {
-    this.projectDetailFactory = projectDetailFactory;
-    this.projectControlFactory = projectControlFactory;
-    this.projectCache = projectCache;
-    this.groupCache = groupCache;
-    this.approvalTypes = approvalTypes;
-    this.db = db;
-
-    this.projectName = projectName;
-    this.categoryId = categoryId;
-    this.groupName = new AccountGroup.NameKey(groupName);
-    this.refPattern = refPattern != null ? refPattern.trim() : null;
-
-    if (min <= max) {
-      this.min = min;
-      this.max = max;
-    } else {
-      this.min = max;
-      this.max = min;
-    }
-  }
-
-  @Override
-  public ProjectDetail call() throws NoSuchProjectException, OrmException,
-      NoSuchGroupException, InvalidNameException, NoSuchRefException {
-    final ProjectControl projectControl =
-        projectControlFactory.controlFor(projectName);
-
-    final ApprovalType at = approvalTypes.getApprovalType(categoryId);
-    if (at == null || at.getValue(min) == null || at.getValue(max) == null) {
-      throw new IllegalArgumentException("Invalid category " + categoryId
-          + " or range " + min + ".." + max);
-    }
-
-    String refPattern = this.refPattern;
-    if (refPattern == null || refPattern.isEmpty()) {
-      if (categoryId.equals(ApprovalCategory.SUBMIT)
-          || categoryId.equals(ApprovalCategory.PUSH_HEAD)) {
-        // Explicitly related to a branch head.
-        refPattern = Constants.R_HEADS + "*";
-
-      } else if (!at.getCategory().isAction()) {
-        // Non actions are approval votes on a change, assume these apply
-        // to branch heads only.
-        refPattern = Constants.R_HEADS + "*";
-
-      } else if (categoryId.equals(ApprovalCategory.PUSH_TAG)) {
-        // Explicitly related to the tag namespace.
-        refPattern = Constants.R_TAGS + "*";
-
-      } else if (categoryId.equals(ApprovalCategory.READ)
-          || categoryId.equals(ApprovalCategory.OWN)) {
-        // Currently these are project-wide rights, so apply that way.
-        refPattern = RefRight.ALL;
-
-      } else {
-        // Assume project wide for the default.
-        refPattern = RefRight.ALL;
-      }
-    }
-
-    boolean exclusive = refPattern.startsWith("-");
-    if (exclusive) {
-      refPattern = refPattern.substring(1);
-    }
-
-    while (refPattern.startsWith("/")) {
-      refPattern = refPattern.substring(1);
-    }
-
-    if (refPattern.startsWith(RefRight.REGEX_PREFIX)) {
-      String example = RefControl.shortestExample(refPattern);
-
-      if (!example.startsWith(Constants.R_REFS)) {
-        refPattern = RefRight.REGEX_PREFIX + Constants.R_HEADS
-                + refPattern.substring(RefRight.REGEX_PREFIX.length());
-        example = RefControl.shortestExample(refPattern);
-      }
-
-      if (!Repository.isValidRefName(example)) {
-        throw new InvalidNameException();
-      }
-
-    } else {
-      if (!refPattern.startsWith(Constants.R_REFS)) {
-        refPattern = Constants.R_HEADS + refPattern;
-      }
-
-      if (refPattern.endsWith("/*")) {
-        final String prefix = refPattern.substring(0, refPattern.length() - 2);
-        if (!"refs".equals(prefix) && !Repository.isValidRefName(prefix)) {
-          throw new InvalidNameException();
-        }
-      } else {
-        if (!Repository.isValidRefName(refPattern)) {
-          throw new InvalidNameException();
-        }
-      }
-    }
-
-    if (!projectControl.controlForRef(refPattern).isOwner()) {
-      throw new NoSuchRefException(refPattern);
-    }
-
-    if (exclusive) {
-      refPattern = "-" + refPattern;
-    }
-
-    final AccountGroup group = groupCache.get(groupName);
-    if (group == null) {
-      throw new NoSuchGroupException(groupName);
-    }
-    final RefRight.Key key =
-        new RefRight.Key(projectName, new RefRight.RefPattern(refPattern),
-            categoryId, group.getId());
-    RefRight rr = db.refRights().get(key);
-    if (rr == null) {
-      rr = new RefRight(key);
-      rr.setMinValue(min);
-      rr.setMaxValue(max);
-      db.refRights().insert(Collections.singleton(rr));
-    } else {
-      rr.setMinValue(min);
-      rr.setMaxValue(max);
-      db.refRights().update(Collections.singleton(rr));
-    }
-    projectCache.evictAll();
-    return projectDetailFactory.create(projectName).call();
-  }
-}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectAccess.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectAccess.java
new file mode 100644
index 0000000..777329b
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ChangeProjectAccess.java
@@ -0,0 +1,209 @@
+// 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.rpc.project;
+
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.common.data.ProjectAccess;
+import com.google.gerrit.common.errors.InvalidNameException;
+import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.httpd.rpc.Handler;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.project.RefControl;
+import com.google.gwtorm.server.OrmConcurrencyException;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.ObjectId;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+class ChangeProjectAccess extends Handler<ProjectAccess> {
+  interface Factory {
+    ChangeProjectAccess create(@Assisted Project.NameKey projectName,
+        @Nullable @Assisted ObjectId base,
+        @Assisted List<AccessSection> sectionList,
+        @Nullable @Assisted String message);
+  }
+
+  private final ProjectAccessFactory.Factory projectAccessFactory;
+  private final ProjectControl.Factory projectControlFactory;
+  private final ProjectCache projectCache;
+  private final GroupCache groupCache;
+  private final MetaDataUpdate.User metaDataUpdateFactory;
+
+  private final Project.NameKey projectName;
+  private final ObjectId base;
+  private List<AccessSection> sectionList;
+  private String message;
+
+  @Inject
+  ChangeProjectAccess(final ProjectAccessFactory.Factory projectAccessFactory,
+      final ProjectControl.Factory projectControlFactory,
+      final ProjectCache projectCache, final GroupCache groupCache,
+      final MetaDataUpdate.User metaDataUpdateFactory,
+
+      @Assisted final Project.NameKey projectName,
+      @Nullable @Assisted final ObjectId base,
+      @Assisted List<AccessSection> sectionList,
+      @Nullable @Assisted String message) {
+    this.projectAccessFactory = projectAccessFactory;
+    this.projectControlFactory = projectControlFactory;
+    this.projectCache = projectCache;
+    this.groupCache = groupCache;
+    this.metaDataUpdateFactory = metaDataUpdateFactory;
+
+    this.projectName = projectName;
+    this.base = base;
+    this.sectionList = sectionList;
+    this.message = message;
+  }
+
+  @Override
+  public ProjectAccess call() throws NoSuchProjectException, IOException,
+      ConfigInvalidException, InvalidNameException, NoSuchGroupException,
+      OrmConcurrencyException {
+    final ProjectControl projectControl =
+        projectControlFactory.controlFor(projectName);
+
+    final MetaDataUpdate md;
+    try {
+      md = metaDataUpdateFactory.create(projectName);
+    } catch (RepositoryNotFoundException notFound) {
+      throw new NoSuchProjectException(projectName);
+    }
+    try {
+      ProjectConfig config = ProjectConfig.read(md, base);
+      Set<String> toDelete = scanSectionNames(config);
+
+      for (AccessSection section : mergeSections(sectionList)) {
+        String name = section.getName();
+
+        if (AccessSection.GLOBAL_CAPABILITIES.equals(name)) {
+          if (!projectControl.isOwner()) {
+            continue;
+          }
+          replace(config, toDelete, section);
+
+        } else if (AccessSection.isValid(name)) {
+          if (!projectControl.controlForRef(name).isOwner()) {
+            continue;
+          }
+
+          RefControl.validateRefPattern(name);
+
+          replace(config, toDelete, section);
+        }
+      }
+
+      for (String name : toDelete) {
+        if (AccessSection.GLOBAL_CAPABILITIES.equals(name)) {
+          if (projectControl.isOwner()) {
+            config.remove(config.getAccessSection(name));
+          }
+
+        } else if (projectControl.controlForRef(name).isOwner()) {
+          config.remove(config.getAccessSection(name));
+        }
+      }
+
+      if (message != null && !message.isEmpty()) {
+        if (!message.endsWith("\n")) {
+          message += "\n";
+        }
+        md.setMessage(message);
+      } else {
+        md.setMessage("Modify access rules\n");
+      }
+
+      if (config.commit(md)) {
+        projectCache.evict(config.getProject());
+        return projectAccessFactory.create(projectName).call();
+
+      } else {
+        throw new OrmConcurrencyException("Cannot update " + projectName);
+      }
+    } finally {
+      md.close();
+    }
+  }
+
+  private void replace(ProjectConfig config, Set<String> toDelete,
+      AccessSection section) throws NoSuchGroupException {
+    for (Permission permission : section.getPermissions()) {
+      for (PermissionRule rule : permission.getRules()) {
+        lookupGroup(rule);
+      }
+    }
+    config.replace(section);
+    toDelete.remove(section.getName());
+  }
+
+  private static List<AccessSection> mergeSections(List<AccessSection> src) {
+    Map<String, AccessSection> map = new LinkedHashMap<String, AccessSection>();
+    for (AccessSection section : src) {
+      if (section.getPermissions().isEmpty()) {
+        continue;
+      }
+
+      AccessSection prior = map.get(section.getName());
+      if (prior != null) {
+        prior.mergeFrom(section);
+      } else {
+        map.put(section.getName(), section);
+      }
+    }
+    return new ArrayList<AccessSection>(map.values());
+  }
+
+  private static Set<String> scanSectionNames(ProjectConfig config) {
+    Set<String> names = new HashSet<String>();
+    for (AccessSection section : config.getAccessSections()) {
+      names.add(section.getName());
+    }
+    return names;
+  }
+
+  private void lookupGroup(PermissionRule rule) throws NoSuchGroupException {
+    GroupReference ref = rule.getGroup();
+    if (ref.getUUID() == null) {
+      AccountGroup.NameKey name = new AccountGroup.NameKey(ref.getName());
+      AccountGroup group = groupCache.get(name);
+      if (group == null) {
+        throw new NoSuchGroupException(name);
+      }
+      ref.setUUID(group.getGroupUUID());
+    }
+  }
+}
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 88c85b8f..bd6e460 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
@@ -16,17 +16,23 @@
 
 import com.google.gerrit.common.data.ProjectDetail;
 import com.google.gerrit.httpd.rpc.Handler;
-import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.client.Project;
 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;
-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.OrmException;
+import com.google.gwtorm.server.OrmConcurrencyException;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 
-import java.util.Collections;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+
+import java.io.IOException;
 
 class ChangeProjectSettings extends Handler<ProjectDetail> {
   interface Factory {
@@ -35,9 +41,9 @@
 
   private final ProjectDetailFactory.Factory projectDetailFactory;
   private final ProjectControl.Factory projectControlFactory;
-  private final ProjectCache projectCache;
-  private final ReviewDb db;
-  private final GitRepositoryManager repoManager;
+  private final GitRepositoryManager mgr;
+  private final MetaDataUpdate.User metaDataUpdateFactory;
+  private final Provider<PerRequestProjectControlCache> userCache;
 
   private final Project update;
 
@@ -45,14 +51,15 @@
   ChangeProjectSettings(
       final ProjectDetailFactory.Factory projectDetailFactory,
       final ProjectControl.Factory projectControlFactory,
-      final ProjectCache projectCache, final ReviewDb db,
-      final GitRepositoryManager grm,
+      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.db = db;
-    this.repoManager = grm;
+    this.mgr = mgr;
+    this.userCache = uc;
+    this.metaDataUpdateFactory = metaDataUpdateFactory;
 
     this.update = update;
   }
@@ -60,20 +67,34 @@
   @Override
   public ProjectDetail call() throws NoSuchProjectException, OrmException {
     final Project.NameKey projectName = update.getNameKey();
-    final ProjectControl projectControl =
-        projectControlFactory.ownerFor(projectName);
+    projectControlFactory.ownerFor(projectName);
 
-    final Project proj = db.projects().get(projectName);
-    if (proj == null) {
+    final MetaDataUpdate md;
+    try {
+      md = metaDataUpdateFactory.create(projectName);
+    } catch (RepositoryNotFoundException notFound) {
       throw new NoSuchProjectException(projectName);
     }
+    try {
+      // TODO We really should take advantage of the Git commit DAG and
+      // ensure the current version matches the old version the caller read.
+      //
+      ProjectConfig config = ProjectConfig.read(md);
+      config.getProject().copySettingsFrom(update);
 
-    proj.copySettingsFrom(update);
-    db.projects().update(Collections.singleton(proj));
-    projectCache.evict(proj);
-
-    if (!projectControl.getProjectState().isSpecialWildProject()) {
-      repoManager.setProjectDescription(projectName, update.getDescription());
+      md.setMessage("Modified project settings\n");
+      if (config.commit(md)) {
+        mgr.setProjectDescription(projectName, update.getDescription());
+        userCache.get().evict(config.getProject());
+      } else {
+        throw new OrmConcurrencyException("Cannot update " + projectName);
+      }
+    } catch (ConfigInvalidException err) {
+      throw new OrmException("Cannot read project " + projectName, err);
+    } catch (IOException err) {
+      throw new OrmException("Cannot update project " + projectName, err);
+    } finally {
+      md.close();
     }
 
     return projectDetailFactory.create(projectName).call();
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..039a301
--- /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.client.Project;
+import com.google.gerrit.reviewdb.client.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.common.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..073e2d7 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,10 +14,10 @@
 
 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;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.ReplicationQueue;
@@ -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/DeleteRefRights.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/DeleteRefRights.java
deleted file mode 100644
index ae8b98b..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/DeleteRefRights.java
+++ /dev/null
@@ -1,91 +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.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.reviewdb.RefRight;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gerrit.server.project.NoSuchProjectException;
-import com.google.gerrit.server.project.NoSuchRefException;
-import com.google.gerrit.server.project.ProjectCache;
-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.Set;
-
-class DeleteRefRights extends Handler<ProjectDetail> {
-  interface Factory {
-    DeleteRefRights create(@Assisted Project.NameKey projectName,
-        @Assisted Set<RefRight.Key> toRemove);
-  }
-
-  private final ProjectDetailFactory.Factory projectDetailFactory;
-  private final ProjectControl.Factory projectControlFactory;
-  private final ProjectCache projectCache;
-  private final ReviewDb db;
-
-  private final Project.NameKey projectName;
-  private final Set<RefRight.Key> toRemove;
-
-  @Inject
-  DeleteRefRights(final ProjectDetailFactory.Factory projectDetailFactory,
-      final ProjectControl.Factory projectControlFactory,
-      final ProjectCache projectCache, final ReviewDb db,
-
-      @Assisted final Project.NameKey projectName,
-      @Assisted final Set<RefRight.Key> toRemove) {
-    this.projectDetailFactory = projectDetailFactory;
-    this.projectControlFactory = projectControlFactory;
-    this.projectCache = projectCache;
-    this.db = db;
-
-    this.projectName = projectName;
-    this.toRemove = toRemove;
-  }
-
-  @Override
-  public ProjectDetail call() throws NoSuchProjectException, OrmException,
-      NoSuchRefException {
-    final ProjectControl projectControl =
-        projectControlFactory.controlFor(projectName);
-
-    for (final RefRight.Key k : toRemove) {
-      if (!projectName.equals(k.getProjectNameKey())) {
-        throw new IllegalArgumentException("All keys must be from same project");
-      }
-      String refPattern = k.getRefPattern();
-      if (refPattern.startsWith("-")) {
-        refPattern = refPattern.substring(1);
-      }
-      if (!projectControl.controlForRef(refPattern).isOwner()) {
-        throw new NoSuchRefException(refPattern);
-      }
-    }
-
-    for (final RefRight.Key k : toRemove) {
-      final RefRight m = db.refRights().get(k);
-      if (m != null) {
-        db.refRights().delete(Collections.singleton(m));
-      }
-    }
-    projectCache.evictAll();
-    return projectDetailFactory.create(projectName).call();
-  }
-}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ListBranches.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ListBranches.java
index 2762acb..68fde45 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ListBranches.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ListBranches.java
@@ -16,9 +16,9 @@
 
 import com.google.gerrit.common.data.ListBranchesResult;
 import com.google.gerrit.httpd.rpc.Handler;
-import com.google.gerrit.reviewdb.Branch;
-import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.RevId;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RevId;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectControl;
@@ -69,6 +69,7 @@
 
     final List<Branch> branches = new ArrayList<Branch>();
     Branch headBranch = null;
+    Branch configBranch = null;
     final Set<String> targets = new HashSet<String>();
 
     final Repository db;
@@ -128,18 +129,13 @@
           continue;
         }
 
-        RefControl refControl = pctl.controlForRef(ref.getName());
-
-        if (ref.getName().startsWith(Constants.R_HEADS)
-            && refControl.isVisible()) {
-          final Branch b = createBranch(ref.getName());
-          if (ref.getObjectId() != null) {
-            b.setRevision(new RevId(ref.getObjectId().name()));
+        final RefControl refControl = pctl.controlForRef(ref.getName());
+        if (refControl.isVisible()) {
+          if (ref.getName().startsWith(Constants.R_HEADS)) {
+            branches.add(createBranch(ref, refControl, targets));
+          } else if (GitRepositoryManager.REF_CONFIG.equals(ref.getName())) {
+            configBranch = createBranch(ref, refControl, targets);
           }
-
-          b.setCanDelete(!targets.contains(ref.getName()) && refControl.canDelete());
-
-          branches.add(b);
         }
       }
     } finally {
@@ -151,12 +147,25 @@
         return a.getName().compareTo(b.getName());
       }
     });
+    if (configBranch != null) {
+      branches.add(0, configBranch);
+    }
     if (headBranch != null) {
       branches.add(0, headBranch);
     }
     return new ListBranchesResult(branches, pctl.canAddRefs(), false);
   }
 
+  private Branch createBranch(final Ref ref, final RefControl refControl,
+      final Set<String> targets) {
+    final Branch b = createBranch(ref.getName());
+    if (ref.getObjectId() != null) {
+      b.setRevision(new RevId(ref.getObjectId().name()));
+    }
+    b.setCanDelete(!targets.contains(ref.getName()) && refControl.canDelete());
+    return b;
+  }
+
   private Branch createBranch(final String name) {
     return new Branch(new Branch.NameKey(projectName, name));
   }
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
new file mode 100644
index 0000000..7ac4ec3
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java
@@ -0,0 +1,208 @@
+// 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.AccessSection;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.common.data.ProjectAccess;
+import com.google.gerrit.common.data.RefConfigSection;
+import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.httpd.rpc.Handler;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.account.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;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.project.RefControl;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+class ProjectAccessFactory extends Handler<ProjectAccess> {
+  interface Factory {
+    ProjectAccessFactory create(@Assisted Project.NameKey name);
+  }
+
+  private final GroupCache groupCache;
+  private final ProjectCache projectCache;
+  private final ProjectControl.Factory projectControlFactory;
+  private final GroupControl.Factory groupControlFactory;
+  private final MetaDataUpdate.Server metaDataUpdateFactory;
+  private final AllProjectsName allProjectsName;
+
+  private final Project.NameKey projectName;
+  private ProjectControl pc;
+
+  @Inject
+  ProjectAccessFactory(final GroupCache groupCache,
+      final ProjectCache projectCache,
+      final ProjectControl.Factory projectControlFactory,
+      final GroupControl.Factory groupControlFactory,
+      final MetaDataUpdate.Server metaDataUpdateFactory,
+      final AllProjectsName allProjectsName,
+
+      @Assisted final Project.NameKey name) {
+    this.groupCache = groupCache;
+    this.projectCache = projectCache;
+    this.projectControlFactory = projectControlFactory;
+    this.groupControlFactory = groupControlFactory;
+    this.metaDataUpdateFactory = metaDataUpdateFactory;
+    this.allProjectsName = allProjectsName;
+
+    this.projectName = name;
+  }
+
+  @Override
+  public ProjectAccess call() throws NoSuchProjectException, IOException,
+      ConfigInvalidException {
+    pc = open();
+
+    // Load the current configuration from the repository, ensuring its the most
+    // recent version available. If it differs from what was in the project
+    // state, force a cache flush now.
+    //
+    ProjectConfig config;
+    MetaDataUpdate md = metaDataUpdateFactory.create(projectName);
+    try {
+      config = ProjectConfig.read(md);
+
+      if (config.updateGroupNames(groupCache)) {
+        md.setMessage("Update group names\n");
+        if (config.commit(md)) {
+          projectCache.evict(config.getProject());
+          pc = open();
+        }
+      } else if (config.getRevision() != null
+          && !config.getRevision().equals(
+              pc.getProjectState().getConfig().getRevision())) {
+        projectCache.evict(config.getProject());
+        pc = open();
+      }
+    } finally {
+      md.close();
+    }
+
+    List<AccessSection> local = new ArrayList<AccessSection>();
+    Set<String> ownerOf = new HashSet<String>();
+    Map<AccountGroup.UUID, Boolean> visibleGroups =
+        new HashMap<AccountGroup.UUID, Boolean>();
+
+    for (AccessSection section : config.getAccessSections()) {
+      String name = section.getName();
+      if (AccessSection.GLOBAL_CAPABILITIES.equals(name)) {
+        if (pc.isOwner()) {
+          local.add(section);
+          ownerOf.add(name);
+        }
+
+      } else if (RefConfigSection.isValid(name)) {
+        RefControl rc = pc.controlForRef(name);
+        if (rc.isOwner()) {
+          local.add(section);
+          ownerOf.add(name);
+
+        } else if (rc.isVisible()) {
+          // Filter the section to only add rules describing groups that
+          // are visible to the current-user. This includes any group the
+          // user is a member of, as well as groups they own or that
+          // are visible to all users.
+
+          AccessSection dst = null;
+          for (Permission srcPerm : section.getPermissions()) {
+            Permission dstPerm = null;
+
+            for (PermissionRule srcRule : srcPerm.getRules()) {
+              AccountGroup.UUID group = srcRule.getGroup().getUUID();
+              if (group == null) {
+                continue;
+              }
+
+              Boolean canSeeGroup = visibleGroups.get(group);
+              if (canSeeGroup == null) {
+                try {
+                  canSeeGroup = groupControlFactory.controlFor(group).isVisible();
+                } catch (NoSuchGroupException e) {
+                  canSeeGroup = Boolean.FALSE;
+                }
+                visibleGroups.put(group, canSeeGroup);
+              }
+
+              if (canSeeGroup) {
+                if (dstPerm == null) {
+                  if (dst == null) {
+                    dst = new AccessSection(name);
+                    local.add(dst);
+                  }
+                  dstPerm = dst.getPermission(srcPerm.getName(), true);
+                }
+                dstPerm.add(srcRule);
+              }
+            }
+          }
+        }
+      }
+    }
+
+    if (ownerOf.isEmpty() && pc.isOwnerAnyRef()) {
+      // Special case: If the section list is empty, this project has no current
+      // access control information. Rely on what ProjectControl determines
+      // is ownership, which probably means falling back to site administrators.
+      ownerOf.add(AccessSection.ALL);
+    }
+
+    final ProjectAccess detail = new ProjectAccess();
+    detail.setProjectName(projectName);
+
+    if (config.getRevision() != null) {
+      detail.setRevision(config.getRevision().name());
+    }
+
+    detail.setInheritsFrom(config.getProject().getParent(allProjectsName));
+
+    if (projectName.equals(allProjectsName)) {
+      if (pc.isOwner()) {
+        ownerOf.add(AccessSection.GLOBAL_CAPABILITIES);
+      }
+    }
+
+    detail.setLocal(local);
+    detail.setOwnerOf(ownerOf);
+    detail.setConfigVisible(pc.isOwner()
+        || pc.controlForRef(GitRepositoryManager.REF_CONFIG).isVisible());
+    return detail;
+  }
+
+  private ProjectControl open() throws NoSuchProjectException {
+    return projectControlFactory.validateFor( //
+        projectName, //
+        ProjectControl.OWNER | ProjectControl.VISIBLE);
+  }
+}
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 9b06dd9..a6bb74f 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
@@ -14,78 +14,105 @@
 
 package com.google.gerrit.httpd.rpc.project;
 
+import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.ListBranchesResult;
+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.reviewdb.ApprovalCategory;
-import com.google.gerrit.reviewdb.Branch;
-import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.RefRight;
-import com.google.gwt.user.client.rpc.AsyncCallback;
+import com.google.gerrit.common.data.ProjectList;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gwtjsonrpc.common.AsyncCallback;
+import com.google.gwtjsonrpc.common.VoidResult;
 import com.google.inject.Inject;
 
+import org.eclipse.jgit.lib.ObjectId;
+
 import java.util.List;
 import java.util.Set;
 
 class ProjectAdminServiceImpl implements ProjectAdminService {
   private final AddBranch.Factory addBranchFactory;
+  private final ChangeProjectAccess.Factory changeProjectAccessFactory;
   private final ChangeProjectSettings.Factory changeProjectSettingsFactory;
   private final DeleteBranches.Factory deleteBranchesFactory;
   private final ListBranches.Factory listBranchesFactory;
   private final VisibleProjects.Factory visibleProjectsFactory;
+  private final VisibleProjectDetails.Factory visibleProjectDetailsFactory;
+  private final ProjectAccessFactory.Factory projectAccessFactory;
+  private final CreateProjectHandler.Factory createProjectHandlerFactory;
   private final ProjectDetailFactory.Factory projectDetailFactory;
-  private final AddRefRight.Factory addRefRightFactory;
-  private final DeleteRefRights.Factory deleteRefRightsFactory;
+  private final SuggestParentCandidatesHandler.Factory suggestParentCandidatesHandlerFactory;
 
   @Inject
   ProjectAdminServiceImpl(final AddBranch.Factory addBranchFactory,
+      final ChangeProjectAccess.Factory changeProjectAccessFactory,
       final ChangeProjectSettings.Factory changeProjectSettingsFactory,
       final DeleteBranches.Factory deleteBranchesFactory,
       final ListBranches.Factory listBranchesFactory,
       final VisibleProjects.Factory visibleProjectsFactory,
+      final VisibleProjectDetails.Factory visibleProjectDetailsFactory,
+      final ProjectAccessFactory.Factory projectAccessFactory,
       final ProjectDetailFactory.Factory projectDetailFactory,
-      final AddRefRight.Factory addRefRightFactory,
-      final DeleteRefRights.Factory deleteRefRightsFactory) {
+      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.addRefRightFactory = addRefRightFactory;
-    this.deleteRefRightsFactory = deleteRefRightsFactory;
+    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);
   }
 
   @Override
+  public void projectAccess(final Project.NameKey projectName,
+      final AsyncCallback<ProjectAccess> callback) {
+    projectAccessFactory.create(projectName).to(callback);
+  }
+
+  @Override
   public void changeProjectSettings(final Project update,
       final AsyncCallback<ProjectDetail> callback) {
     changeProjectSettingsFactory.create(update).to(callback);
   }
 
   @Override
-  public void deleteRight(final Project.NameKey projectName,
-      final Set<RefRight.Key> toRemove, final AsyncCallback<ProjectDetail> callback) {
-    deleteRefRightsFactory.create(projectName, toRemove).to(callback);
-  }
-
-  @Override
-  public void addRight(final Project.NameKey projectName,
-      final ApprovalCategory.Id categoryId, final String groupName,
-      final String refPattern, final short min, final short max,
-      final AsyncCallback<ProjectDetail> callback) {
-    addRefRightFactory.create(projectName, categoryId, groupName, refPattern,
-        min, max).to(callback);
+  public void changeProjectAccess(Project.NameKey projectName,
+      String baseRevision, String msg, List<AccessSection> sections,
+      AsyncCallback<ProjectAccess> cb) {
+    ObjectId base;
+    if (baseRevision != null && !baseRevision.isEmpty()) {
+      base = ObjectId.fromString(baseRevision);
+    } else {
+      base = null;
+    }
+    changeProjectAccessFactory.create(projectName, base, sections, msg).to(cb);
   }
 
   @Override
@@ -108,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 ef632c4..d782da4 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
@@ -14,52 +14,39 @@
 
 package com.google.gerrit.httpd.rpc.project;
 
-import com.google.gerrit.common.data.ApprovalType;
-import com.google.gerrit.common.data.ApprovalTypes;
-import com.google.gerrit.common.data.InheritedRefRight;
 import com.google.gerrit.common.data.ProjectDetail;
 import com.google.gerrit.httpd.rpc.Handler;
-import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.RefRight;
-import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.reviewdb.client.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.gerrit.server.project.RefControl;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
+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 ApprovalTypes approvalTypes;
-  private final GroupCache groupCache;
   private final ProjectControl.Factory projectControlFactory;
+  private final GitRepositoryManager gitRepositoryManager;
 
   private final Project.NameKey projectName;
-  private Map<AccountGroup.Id, AccountGroup> groups;
 
   @Inject
-  ProjectDetailFactory(final ApprovalTypes approvalTypes,
-      final GroupCache groupCache,
-      final ProjectControl.Factory projectControlFactory,
-
+  ProjectDetailFactory(final ProjectControl.Factory projectControlFactory,
+      final GitRepositoryManager gitRepositoryManager,
       @Assisted final Project.NameKey name) {
-    this.approvalTypes = approvalTypes;
-    this.groupCache = groupCache;
     this.projectControlFactory = projectControlFactory;
-
+    this.gitRepositoryManager = gitRepositoryManager;
     this.projectName = name;
   }
 
@@ -72,88 +59,34 @@
     final ProjectDetail detail = new ProjectDetail();
     detail.setProject(projectState.getProject());
 
-    groups = new HashMap<AccountGroup.Id, AccountGroup>();
-    final List<InheritedRefRight> refRights = new ArrayList<InheritedRefRight>();
-
-    for (final RefRight r : projectState.getInheritedRights()) {
-      RefControl rc = pc.controlForRef(r.getRefPattern());
-      boolean isOwner = rc.isOwner();
-
-      if (!isOwner && !rc.isVisible()) {
-        continue;
-      }
-
-      InheritedRefRight refRight = new InheritedRefRight(r, true, isOwner);
-      if (!refRights.contains(refRight)) {
-        refRights.add(refRight);
-        wantGroup(r.getAccountGroupId());
-      }
-    }
-
-    for (final RefRight r : projectState.getLocalRights()) {
-      RefControl rc = pc.controlForRef(r.getRefPattern());
-      boolean isOwner = rc.isOwner();
-
-      if (!isOwner && !rc.isVisible()) {
-        continue;
-      }
-
-      refRights.add(new InheritedRefRight(r, false, isOwner));
-      wantGroup(r.getAccountGroupId());
-    }
-
-    loadGroups();
-
-    Collections.sort(refRights, new Comparator<InheritedRefRight>() {
-      @Override
-      public int compare(final InheritedRefRight a, final InheritedRefRight b) {
-        final RefRight right1 = a.getRight();
-        final RefRight right2 = b.getRight();
-        int rc = categoryOf(right1).compareTo(categoryOf(right2));
-        if (rc == 0) {
-          rc = right1.getRefPattern().compareTo(right2.getRefPattern());
-        }
-        if (rc == 0) {
-          rc = groupOf(right1).compareTo(groupOf(right2));
-        }
-        return rc;
-      }
-
-      private String categoryOf(final RefRight r) {
-        final ApprovalType type =
-            approvalTypes.getApprovalType(r.getApprovalCategoryId());
-        if (type == null) {
-          return r.getApprovalCategoryId().get();
-        }
-        return type.getCategory().getName();
-      }
-
-      private String groupOf(final RefRight r) {
-        return groups.get(r.getAccountGroupId()).getName();
-      }
-    });
-
     final boolean userIsOwner = pc.isOwner();
     final boolean userIsOwnerAnyRef = pc.isOwnerAnyRef();
 
-    detail.setRights(refRights);
-    detail.setGroups(groups);
     detail.setCanModifyAccess(userIsOwnerAnyRef);
     detail.setCanModifyAgreements(userIsOwner);
     detail.setCanModifyDescription(userIsOwner);
     detail.setCanModifyMergeType(userIsOwner);
-    return detail;
-  }
+    detail.setCanModifyState(userIsOwner);
 
-  private void wantGroup(final AccountGroup.Id id) {
-    groups.put(id, null);
-  }
-
-  private void loadGroups() {
-    final Set<AccountGroup.Id> toGet = groups.keySet();
-    groups = new HashMap<AccountGroup.Id, AccountGroup>();
-    for (AccountGroup.Id groupId : toGet) {
-      groups.put(groupId, groupCache.get(groupId));
+    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 7932c79..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
@@ -29,13 +29,16 @@
       @Override
       protected void configure() {
         factory(AddBranch.Factory.class);
-        factory(AddRefRight.Factory.class);
+        factory(ChangeProjectAccess.Factory.class);
+        factory(CreateProjectHandler.Factory.class);
         factory(ChangeProjectSettings.Factory.class);
         factory(DeleteBranches.Factory.class);
-        factory(DeleteRefRights.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..ba0e4cd
--- /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.client.Project;
+import com.google.gerrit.server.project.SuggestParentCandidates;
+import com.google.inject.Inject;
+
+import java.util.List;
+
+public class SuggestParentCandidatesHandler extends Handler<List<Project>> {
+  interface Factory {
+    SuggestParentCandidatesHandler create();
+  }
+
+  private final SuggestParentCandidates suggestParentCandidates;
+
+  @Inject
+  SuggestParentCandidatesHandler(final SuggestParentCandidates suggestParentCandidates) {
+    this.suggestParentCandidates = suggestParentCandidates;
+  }
+
+  @Override
+  public List<Project> call() throws Exception {
+    return suggestParentCandidates.getProjects();
+  }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/VisibleProjectDetails.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/VisibleProjectDetails.java
new file mode 100644
index 0000000..e12cfb1
--- /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.client.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 2588350..ba65617 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,14 +14,13 @@
 
 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.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectControl;
-import com.google.gwtorm.client.OrmException;
 import com.google.inject.Inject;
 
 import java.util.ArrayList;
@@ -29,39 +28,41 @@
 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;
-  private final ReviewDb db;
 
   @Inject
   VisibleProjects(final ProjectControl.Factory projectControlFactory,
-      final CurrentUser user, final ReviewDb db) {
+      final ProjectCache projectCache, final CurrentUser user) {
     this.projectControlFactory = projectControlFactory;
+    this.projectCache = projectCache;
     this.user = user;
-    this.db = db;
   }
 
   @Override
-  public List<Project> call() throws OrmException {
-    final List<Project> result;
-    if (user.isAdministrator()) {
-      result = db.projects().all().toList();
-    } else {
-      result = new ArrayList<Project>();
-      for (Project p : db.projects().all().toList()) {
-        try {
-          ProjectControl c = projectControlFactory.controlFor(p.getNameKey());
-          if (c.isVisible() || c.isOwner()) {
-            result.add(p);
-          }
-        } catch (NoSuchProjectException e) {
-          continue;
+  public ProjectList call() {
+    ProjectList result = new ProjectList();
+    result.setProjects(getProjects());
+    result.setCanCreateProject(user.getCapabilities().canCreateProject());
+    return result;
+  }
+
+  private List<Project> getProjects() {
+    List<Project> result = new ArrayList<Project>();
+    for (Project.NameKey p : projectCache.all()) {
+      try {
+        ProjectControl c = projectControlFactory.controlFor(p);
+        if (c.isVisible() || c.isOwner()) {
+          result.add(c.getProject());
         }
+      } catch (NoSuchProjectException e) {
+        continue;
       }
     }
     Collections.sort(result, new Comparator<Project>() {
diff --git a/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/auth/become/BecomeAnyAccount.html b/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/auth/become/BecomeAnyAccount.html
index 4795e91..c3c7503 100644
--- a/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/auth/become/BecomeAnyAccount.html
+++ b/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/auth/become/BecomeAnyAccount.html
@@ -62,6 +62,11 @@
           </form>
         </td>
       </tr>
+
+      <tr>
+        <th>Choose:</th>
+        <td id="userlist"/>
+      </tr>
     </table>
 
     <hr />
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-httpd/src/test/java/com/google/gerrit/httpd/rpc/project/ListBranchesTest.java b/gerrit-httpd/src/test/java/com/google/gerrit/httpd/rpc/project/ListBranchesTest.java
index 51fcf2e..5cfde6a 100644
--- a/gerrit-httpd/src/test/java/com/google/gerrit/httpd/rpc/project/ListBranchesTest.java
+++ b/gerrit-httpd/src/test/java/com/google/gerrit/httpd/rpc/project/ListBranchesTest.java
@@ -31,8 +31,8 @@
 import static org.junit.Assert.fail;
 
 import com.google.gerrit.common.data.ListBranchesResult;
-import com.google.gerrit.reviewdb.Branch;
-import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectControl;
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/.settings/org.eclipse.core.resources.prefs b/gerrit-launcher/.settings/org.eclipse.core.resources.prefs
index 82eb859..c780f44 100644
--- a/gerrit-launcher/.settings/org.eclipse.core.resources.prefs
+++ b/gerrit-launcher/.settings/org.eclipse.core.resources.prefs
@@ -1,3 +1,4 @@
-#Tue Sep 02 16:59:24 PDT 2008
+#Thu Jul 28 11:02:36 PDT 2011
 eclipse.preferences.version=1
+encoding//src/main/java=UTF-8
 encoding/<project>=UTF-8
diff --git a/gerrit-launcher/.settings/org.eclipse.jdt.core.prefs b/gerrit-launcher/.settings/org.eclipse.jdt.core.prefs
index 04afc7f..470942d 100644
--- a/gerrit-launcher/.settings/org.eclipse.jdt.core.prefs
+++ b/gerrit-launcher/.settings/org.eclipse.jdt.core.prefs
@@ -1,4 +1,4 @@
-#Tue May 12 17:44:13 PDT 2009
+#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
@@ -9,6 +9,7 @@
 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
diff --git a/gerrit-launcher/pom.xml b/gerrit-launcher/pom.xml
index 3c9c979..7d07652 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.1-SNAPSHOT</version>
+    <version>2.4-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 a097d75..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;
@@ -120,7 +122,17 @@
       try {
         String cn = name;
         if (cn.equals(cn.toLowerCase())) {
-          cn = cn.substring(0, 1).toUpperCase() + cn.substring(1);
+          StringBuilder buf = new StringBuilder();
+          buf.append(Character.toUpperCase(cn.charAt(0)));
+          for (int i = 1; i < cn.length(); i++) {
+            if (cn.charAt(i) == '-' && i + 1 < cn.length()) {
+              i++;
+              buf.append(Character.toUpperCase(cn.charAt(i)));
+            } else {
+              buf.append(cn.charAt(i));
+            }
+          }
+          cn = buf.toString();
         }
         clazz = Class.forName(pkg + "." + cn, true, loader);
       } catch (ClassNotFoundException cnfe) {
@@ -225,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/.settings/org.eclipse.core.resources.prefs b/gerrit-main/.settings/org.eclipse.core.resources.prefs
index 82eb859..c780f44 100644
--- a/gerrit-main/.settings/org.eclipse.core.resources.prefs
+++ b/gerrit-main/.settings/org.eclipse.core.resources.prefs
@@ -1,3 +1,4 @@
-#Tue Sep 02 16:59:24 PDT 2008
+#Thu Jul 28 11:02:36 PDT 2011
 eclipse.preferences.version=1
+encoding//src/main/java=UTF-8
 encoding/<project>=UTF-8
diff --git a/gerrit-main/.settings/org.eclipse.jdt.core.prefs b/gerrit-main/.settings/org.eclipse.jdt.core.prefs
index 4b4b392..a7bc058 100644
--- a/gerrit-main/.settings/org.eclipse.jdt.core.prefs
+++ b/gerrit-main/.settings/org.eclipse.jdt.core.prefs
@@ -1,4 +1,4 @@
-#Mon Jan 04 14:26:32 PST 2010
+#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.2
@@ -9,6 +9,7 @@
 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.2
 org.eclipse.jdt.core.formatter.align_type_members_on_columns=false
 org.eclipse.jdt.core.formatter.alignment_for_arguments_in_allocation_expression=16
diff --git a/gerrit-main/pom.xml b/gerrit-main/pom.xml
index a46aa4f..9d8320c 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.1-SNAPSHOT</version>
+    <version>2.4-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..ed2625e
--- /dev/null
+++ b/gerrit-openid/pom.xml
@@ -0,0 +1,86 @@
+<?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.4-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>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 98%
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
index 3dce967..0593bce 100644
--- 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
@@ -21,7 +21,7 @@
 import com.google.gerrit.common.auth.openid.OpenIdService;
 import com.google.gerrit.common.auth.openid.OpenIdUrls;
 import com.google.gerrit.httpd.WebSession;
-import com.google.gerrit.reviewdb.Account;
+import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.UrlEncoded;
 import com.google.gerrit.server.account.AccountException;
@@ -30,7 +30,7 @@
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gwt.user.client.rpc.AsyncCallback;
+import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.gwtorm.client.KeyUtil;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -462,16 +462,15 @@
   private void callback(final boolean isNew, final HttpServletRequest req,
       final HttpServletResponse rsp) throws IOException {
     String token = req.getParameter(P_TOKEN);
-    if (token == null || token.isEmpty() || token.startsWith("SignInFailure,")) {
+    if (token == null || token.isEmpty() || token.startsWith("/SignInFailure,")) {
       token = PageLinks.MINE;
     }
 
     final StringBuilder rdr = new StringBuilder();
     rdr.append(urlProvider.get());
     rdr.append('#');
-    if (isNew && !token.startsWith(PageLinks.REGISTER + ",")) {
+    if (isNew && !token.startsWith(PageLinks.REGISTER + "/")) {
       rdr.append(PageLinks.REGISTER);
-      rdr.append(',');
     }
     rdr.append(token);
     rsp.sendRedirect(rdr.toString());
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/.settings/org.eclipse.core.resources.prefs b/gerrit-patch-commonsnet/.settings/org.eclipse.core.resources.prefs
index 82eb859..589908f 100644
--- a/gerrit-patch-commonsnet/.settings/org.eclipse.core.resources.prefs
+++ b/gerrit-patch-commonsnet/.settings/org.eclipse.core.resources.prefs
@@ -1,3 +1,4 @@
-#Tue Sep 02 16:59:24 PDT 2008
+#Thu Jul 28 11:02:35 PDT 2011
 eclipse.preferences.version=1
+encoding//src/main/java=UTF-8
 encoding/<project>=UTF-8
diff --git a/gerrit-patch-commonsnet/.settings/org.eclipse.jdt.core.prefs b/gerrit-patch-commonsnet/.settings/org.eclipse.jdt.core.prefs
index 04afc7f..5f73a7f 100644
--- a/gerrit-patch-commonsnet/.settings/org.eclipse.jdt.core.prefs
+++ b/gerrit-patch-commonsnet/.settings/org.eclipse.jdt.core.prefs
@@ -1,4 +1,4 @@
-#Tue May 12 17:44:13 PDT 2009
+#Thu Jul 28 11:02:35 PDT 2011
 eclipse.preferences.version=1
 org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
 org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
@@ -9,6 +9,7 @@
 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
diff --git a/gerrit-patch-commonsnet/pom.xml b/gerrit-patch-commonsnet/pom.xml
index 5bbae03..75ee12e 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.1-SNAPSHOT</version>
+    <version>2.4-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/.settings/org.eclipse.core.resources.prefs b/gerrit-patch-jgit/.settings/org.eclipse.core.resources.prefs
index 82eb859..589908f 100644
--- a/gerrit-patch-jgit/.settings/org.eclipse.core.resources.prefs
+++ b/gerrit-patch-jgit/.settings/org.eclipse.core.resources.prefs
@@ -1,3 +1,4 @@
-#Tue Sep 02 16:59:24 PDT 2008
+#Thu Jul 28 11:02:35 PDT 2011
 eclipse.preferences.version=1
+encoding//src/main/java=UTF-8
 encoding/<project>=UTF-8
diff --git a/gerrit-patch-jgit/.settings/org.eclipse.jdt.core.prefs b/gerrit-patch-jgit/.settings/org.eclipse.jdt.core.prefs
index 04afc7f..5f73a7f 100644
--- a/gerrit-patch-jgit/.settings/org.eclipse.jdt.core.prefs
+++ b/gerrit-patch-jgit/.settings/org.eclipse.jdt.core.prefs
@@ -1,4 +1,4 @@
-#Tue May 12 17:44:13 PDT 2009
+#Thu Jul 28 11:02:35 PDT 2011
 eclipse.preferences.version=1
 org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
 org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
@@ -9,6 +9,7 @@
 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
diff --git a/gerrit-patch-jgit/pom.xml b/gerrit-patch-jgit/pom.xml
index f30eace..f8190f5 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.1-SNAPSHOT</version>
+    <version>2.4-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-patch-jgit</artifactId>
@@ -49,4 +49,20 @@
       <scope>provided</scope>
     </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-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/.settings/org.eclipse.core.resources.prefs b/gerrit-pgm/.settings/org.eclipse.core.resources.prefs
index 82eb859..9df523e 100644
--- a/gerrit-pgm/.settings/org.eclipse.core.resources.prefs
+++ b/gerrit-pgm/.settings/org.eclipse.core.resources.prefs
@@ -1,3 +1,6 @@
-#Tue Sep 02 16:59:24 PDT 2008
+#Thu Jul 28 11:02:36 PDT 2011
 eclipse.preferences.version=1
+encoding//src/main/java=UTF-8
+encoding//src/main/resources=UTF-8
+encoding//src/test/java=UTF-8
 encoding/<project>=UTF-8
diff --git a/gerrit-pgm/.settings/org.eclipse.jdt.core.prefs b/gerrit-pgm/.settings/org.eclipse.jdt.core.prefs
index 04afc7f..470942d 100644
--- a/gerrit-pgm/.settings/org.eclipse.jdt.core.prefs
+++ b/gerrit-pgm/.settings/org.eclipse.jdt.core.prefs
@@ -1,4 +1,4 @@
-#Tue May 12 17:44:13 PDT 2009
+#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
@@ -9,6 +9,7 @@
 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
diff --git a/gerrit-pgm/pom.xml b/gerrit-pgm/pom.xml
index f658f52..1463d15 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.1-SNAPSHOT</version>
+    <version>2.4-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 8c6879d..25b7699 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,9 +16,16 @@
 
 import static com.google.gerrit.server.schema.DataSourceProvider.Context.MULTI_USER;
 
+import com.google.gerrit.common.ChangeHookRunner;
+import com.google.gerrit.ehcache.EhcachePoolImpl;
+import com.google.gerrit.httpd.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.GetUserFilter;
 import com.google.gerrit.pgm.http.jetty.JettyEnv;
 import com.google.gerrit.pgm.http.jetty.JettyModule;
 import com.google.gerrit.pgm.http.jetty.ProjectQoSFilter;
@@ -26,11 +33,21 @@
 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.client.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.ReceiveCommitsExecutorModule;
+import com.google.gerrit.server.git.WorkQueue;
+import com.google.gerrit.server.mail.SignedTokenEmailTokenVerifier;
+import com.google.gerrit.server.mail.SmtpEmailSender;
+import com.google.gerrit.server.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;
@@ -113,10 +130,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 {
@@ -185,8 +198,16 @@
 
   private Injector createSysInjector() {
     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(new ReceiveCommitsExecutorModule());
     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
@@ -215,11 +236,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);
   }
@@ -237,8 +262,23 @@
 
   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());
+    }
+    modules.add(sysInjector.getInstance(GetUserFilter.Module.class));
+
     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 71512f2..5f0bc80 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,20 +17,18 @@
 import static com.google.gerrit.server.schema.DataSourceProvider.Context.MULTI_USER;
 
 import com.google.gerrit.common.data.ApprovalTypes;
+import com.google.gerrit.ehcache.EhcachePoolImpl;
 import com.google.gerrit.lifecycle.LifecycleManager;
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.pgm.util.SiteProgram;
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.PatchSet;
-import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.GerritPersonIdentProvider;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.account.AccountCacheImpl;
 import com.google.gerrit.server.account.GroupCacheImpl;
 import com.google.gerrit.server.cache.CachePool;
 import com.google.gerrit.server.config.ApprovalTypesProvider;
-import com.google.gerrit.server.config.AuthConfigModule;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.CanonicalWebUrlProvider;
 import com.google.gerrit.server.config.FactoryModule;
@@ -38,8 +36,9 @@
 import com.google.gerrit.server.git.CreateCodeReviewNotes;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.LocalDiskRepositoryManager;
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.client.SchemaFactory;
+import com.google.gerrit.server.schema.SchemaVersionCheck;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
@@ -47,7 +46,6 @@
 
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.lib.TextProgressMonitor;
 import org.eclipse.jgit.lib.ThreadSafeProgressMonitor;
@@ -94,18 +92,15 @@
     gitInjector = dbInjector.createChildInjector(new AbstractModule() {
       @Override
       protected void configure() {
-        bind(GitRepositoryManager.class).to(LocalDiskRepositoryManager.class);
+        install(SchemaVersionCheck.module());
         bind(ApprovalTypes.class).toProvider(ApprovalTypesProvider.class).in(
             Scopes.SINGLETON);
         bind(String.class).annotatedWith(CanonicalWebUrl.class)
             .toProvider(CanonicalWebUrlProvider.class).in(Scopes.SINGLETON);
-        bind(PersonIdent.class).annotatedWith(GerritPersonIdent.class)
-            .toProvider(GerritPersonIdentProvider.class).in(Scopes.SINGLETON);
-        bind(CachePool.class);
 
         install(AccountCacheImpl.module());
         install(GroupCacheImpl.module());
-        install(new AuthConfigModule());
+        install(new EhcachePoolImpl.Module());
         install(new FactoryModule() {
           @Override
           protected void configure() {
@@ -115,7 +110,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/Init.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Init.java
index 7127027..f06946f 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Init.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Init.java
@@ -28,20 +28,18 @@
 import com.google.gerrit.pgm.util.ErrorLogFile;
 import com.google.gerrit.pgm.util.IoUtil;
 import com.google.gerrit.pgm.util.SiteProgram;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.config.SitePath;
 import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.git.GitProjectImporter;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.LocalDiskRepositoryManager;
 import com.google.gerrit.server.schema.SchemaUpdater;
 import com.google.gerrit.server.schema.UpdateUI;
 import com.google.gerrit.server.util.HostPlatform;
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.client.SchemaFactory;
-import com.google.gwtorm.client.StatementExecutor;
 import com.google.gwtorm.jdbc.JdbcExecutor;
 import com.google.gwtorm.jdbc.JdbcSchema;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.gwtorm.server.StatementExecutor;
 import com.google.inject.AbstractModule;
 import com.google.inject.CreationException;
 import com.google.inject.Guice;
@@ -62,9 +60,6 @@
   @Option(name = "--batch", usage = "Batch mode; skip interactive prompting")
   private boolean batchMode;
 
-  @Option(name = "--import-projects", usage = "Import git repositories as projects")
-  private boolean importProjects;
-
   @Option(name = "--no-auto-start", usage = "Don't automatically start daemon after init")
   private boolean noAutoStart;
 
@@ -73,7 +68,6 @@
     ErrorLogFile.errorOnlyConsole();
 
     final SiteInit init = createSiteInit();
-    init.flags.importProjects = importProjects;
     init.flags.autoStart = !noAutoStart && init.site.isNew;
 
     final SiteRun run;
@@ -83,7 +77,6 @@
 
       run = createSiteRun(init);
       run.upgradeSchema();
-      run.importGit();
     } catch (Exception failure) {
       if (init.flags.deleteOnFailure) {
         recursiveDelete(getSitePath());
@@ -166,7 +159,6 @@
     final SchemaUpdater schemaUpdater;
     final SchemaFactory<ReviewDb> schema;
     final GitRepositoryManager repositoryManager;
-    final GitProjectImporter gitProjectImporter;
     final Browser browser;
 
     @Inject
@@ -174,14 +166,13 @@
         final SchemaUpdater schemaUpdater,
         final SchemaFactory<ReviewDb> schema,
         final GitRepositoryManager repositoryManager,
-        final GitProjectImporter gitProjectImporter, final Browser browser) {
+        final Browser browser) {
       this.ui = ui;
       this.site = site;
       this.flags = flags;
       this.schemaUpdater = schemaUpdater;
       this.schema = schema;
       this.repositoryManager = repositoryManager;
-      this.gitProjectImporter = gitProjectImporter;
       this.browser = browser;
     }
 
@@ -241,23 +232,6 @@
       }
     }
 
-    void importGit() throws OrmException, IOException {
-      if (flags.importProjects) {
-        gitProjectImporter.run(new GitProjectImporter.Messages() {
-          @Override
-          public void info(String msg) {
-            System.err.println(msg);
-            System.err.flush();
-          }
-
-          @Override
-          public void warning(String msg) {
-            info(msg);
-          }
-        });
-      }
-    }
-
     void start() throws Exception {
       if (flags.autoStart) {
         if (HostPlatform.isWin32()) {
@@ -317,9 +291,6 @@
       protected void configure() {
         bind(ConsoleUI.class).toInstance(init.ui);
         bind(InitFlags.class).toInstance(init.flags);
-
-        bind(GitRepositoryManager.class).to(LocalDiskRepositoryManager.class);
-        bind(GitProjectImporter.class);
       }
     });
     return createDbInjector(SINGLE_USER).createChildInjector(modules);
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..323d7f28
--- /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.client.AccountExternalId;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.schema.SchemaVersionCheck;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.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/PrologShell.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/PrologShell.java
new file mode 100644
index 0000000..803b702
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/PrologShell.java
@@ -0,0 +1,93 @@
+// 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 com.google.gerrit.pgm.util.AbstractProgram;
+
+import com.googlecode.prolog_cafe.lang.BufferingPrologControl;
+import com.googlecode.prolog_cafe.lang.HaltException;
+import com.googlecode.prolog_cafe.lang.Prolog;
+import com.googlecode.prolog_cafe.lang.PrologClassLoader;
+import com.googlecode.prolog_cafe.lang.PrologMain;
+import com.googlecode.prolog_cafe.lang.SymbolTerm;
+
+import org.kohsuke.args4j.Option;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.EnumSet;
+import java.util.List;
+
+public class PrologShell extends AbstractProgram {
+  @Option(name = "-s", multiValued = true, metaVar = "FILE.pl", usage = "file to load")
+  private List<String> fileName = new ArrayList<String>();
+
+  @Override
+  public int run() {
+    banner();
+
+    BufferingPrologControl pcl = new BufferingPrologControl();
+    pcl.setPrologClassLoader(new PrologClassLoader(getClass().getClassLoader()));
+    pcl.setEnabled(EnumSet.allOf(Prolog.Feature.class), false);
+    pcl.setEnabled(Prolog.Feature.IO, true);
+    pcl.setEnabled(Prolog.Feature.STATISTICS_RUNTIME, true);
+
+    pcl.initialize(Prolog.BUILTIN);
+    pcl.execute(Prolog.BUILTIN, "set_prolog_flag",
+        SymbolTerm.intern("print_stack_trace"),
+        SymbolTerm.intern("on"));
+
+    for (String file : fileName) {
+      String path;
+      try {
+        path = new File(file).getCanonicalPath();
+      } catch (IOException e) {
+        path = new File(file).getAbsolutePath();
+      }
+      pcl.execute(Prolog.BUILTIN, "consult", SymbolTerm.create(path));
+      System.err.println();
+      System.err.flush();
+    }
+
+    try {
+      pcl.execute(Prolog.BUILTIN, "cafeteria");
+      write("% halt\n");
+      return 0;
+    } catch (HaltException halt) {
+      write("% halt(" + halt.getStatus() + ")\n");
+      return halt.getStatus();
+    }
+  }
+
+  private void banner() {
+    System.err.format("Gerrit Code Review %s - Interactive Prolog Shell",
+        com.google.gerrit.common.Version.getVersion());
+    System.err.println();
+    System.err.println("based on " + PrologMain.VERSION);
+    System.err.println("         " + PrologMain.COPYRIGHT);
+    System.err.println("(type Ctrl-D or \"halt.\" to exit,"
+        + " \"['path/to/file.pl'].\" to load a file)");
+    System.err.println();
+    System.err.flush();
+  }
+
+  private void write(String msg) {
+    System.out.flush();
+    System.err.flush();
+    System.out.println(msg);
+    System.out.flush();
+  }
+}
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..17b7017
--- /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.server.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/Rulec.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Rulec.java
new file mode 100644
index 0000000..451ed30
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Rulec.java
@@ -0,0 +1,115 @@
+// 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.SINGLE_USER;
+
+import com.google.gerrit.lifecycle.LifecycleManager;
+import com.google.gerrit.pgm.util.SiteProgram;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.rules.PrologCompiler;
+import com.google.gerrit.server.config.FactoryModule;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+
+import com.googlecode.prolog_cafe.compiler.CompileException;
+
+import org.eclipse.jgit.lib.Repository;
+import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.Option;
+
+import java.util.ArrayList;
+import java.util.LinkedHashSet;
+import java.util.List;
+
+/**
+ * Gets rules.pl at refs/meta/config and compiles into jar file called
+ * rules-(sha1 of rules.pl).jar in (site-path)/cache/rules
+ */
+public class Rulec extends SiteProgram {
+  @Option(name = "--all", usage = "recompile all rules")
+  private boolean all;
+
+  @Option(name = "--quiet", usage = "supress some messsages")
+  private boolean quiet;
+
+  @Argument(index = 0, multiValued = true, metaVar = "PROJECT", usage = "project to compile rules for")
+  private List<String> projectNames = new ArrayList<String>();
+
+  private Injector dbInjector;
+
+  private final LifecycleManager manager = new LifecycleManager();
+
+  @Inject
+  private GitRepositoryManager gitManager;
+
+  @Inject
+  private PrologCompiler.Factory jarFactory;
+
+  @Override
+  public int run() throws Exception {
+    dbInjector = createDbInjector(SINGLE_USER);
+    manager.add(dbInjector);
+    manager.start();
+    dbInjector.createChildInjector(new FactoryModule() {
+      @Override
+      protected void configure() {
+        factory(PrologCompiler.Factory.class);
+      }
+    }).injectMembers(this);
+
+    LinkedHashSet<Project.NameKey> names = new LinkedHashSet<Project.NameKey>();
+    for (String name : projectNames) {
+      names.add(new Project.NameKey(name));
+    }
+    if (all) {
+      names.addAll(gitManager.list());
+    }
+
+    boolean error = false;
+    for (Project.NameKey project : names) {
+      Repository git = gitManager.openRepository(project);
+      try {
+        switch (jarFactory.create(git).call()) {
+          case NO_RULES:
+            if (!all || projectNames.contains(project.get())) {
+              System.err.println("error: No rules.pl in " + project.get());
+              error = true;
+            }
+            break;
+
+          case COMPILED:
+            if (!quiet) {
+              System.out.format("Compiled %-60s ... SUCCESS", project.get());
+              System.out.println();
+            }
+            break;
+        }
+      } catch (CompileException err) {
+        if (showStackTrace) {
+          err.printStackTrace();
+        } else {
+          System.err.println("fatal: " + err.getMessage());
+        }
+        error = true;
+      } finally {
+        git.close();
+      }
+    }
+
+    return !error ? 0 : 1;
+  }
+}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ScanTrackingIds.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ScanTrackingIds.java
index beeed24..c09329a 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ScanTrackingIds.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ScanTrackingIds.java
@@ -17,18 +17,17 @@
 import static com.google.gerrit.server.schema.DataSourceProvider.Context.MULTI_USER;
 
 import com.google.gerrit.lifecycle.LifecycleManager;
-import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.pgm.util.SiteProgram;
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.PatchSet;
-import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.config.TrackingFooters;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.LocalDiskRepositoryManager;
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.client.SchemaFactory;
+import com.google.gerrit.server.schema.SchemaVersionCheck;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
 
@@ -56,7 +55,6 @@
   private List<Change> todo;
 
   private Injector dbInjector;
-  private Injector gitInjector;
 
   @Inject
   private TrackingFooters footers;
@@ -74,17 +72,11 @@
     }
 
     dbInjector = createDbInjector(MULTI_USER);
-    gitInjector = dbInjector.createChildInjector(new LifecycleModule() {
-      @Override
-      protected void configure() {
-        bind(GitRepositoryManager.class).to(LocalDiskRepositoryManager.class);
-        listener().to(LocalDiskRepositoryManager.Lifecycle.class);
-      }
-    });
-
-    manager.add(dbInjector, gitInjector);
+    manager.add(
+        dbInjector,
+        dbInjector.createChildInjector(SchemaVersionCheck.module()));
     manager.start();
-    gitInjector.injectMembers(this);
+    dbInjector.injectMembers(this);
 
     final ReviewDb db = database.open();
     try {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/GetUserFilter.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/GetUserFilter.java
new file mode 100644
index 0000000..f54b3c5
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/GetUserFilter.java
@@ -0,0 +1,86 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.pgm.http.jetty;
+
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+import com.google.inject.servlet.ServletModule;
+
+import org.eclipse.jgit.lib.Config;
+
+import java.io.IOException;
+import java.net.URI;
+
+import javax.servlet.Filter;
+import javax.servlet.FilterChain;
+import javax.servlet.FilterConfig;
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+
+/**
+ * Stores as a request attribute, so the {@link HttpLog} can include the the
+ * user for the request outside of the request scope.
+ */
+@Singleton
+public class GetUserFilter implements Filter {
+
+  static final String REQ_ATTR_KEY = CurrentUser.class.toString();
+
+  public static class Module extends ServletModule {
+
+    private boolean loggingEnabled;
+
+    @Inject
+    Module(@GerritServerConfig final Config cfg) {
+      URI[] urls = JettyServer.listenURLs(cfg);
+      boolean reverseProxy = JettyServer.isReverseProxied(urls);
+      this.loggingEnabled = cfg.getBoolean("httpd", "requestlog", !reverseProxy);
+    }
+
+    @Override
+    protected void configureServlets() {
+      if (loggingEnabled) {
+        filter("/*").through(GetUserFilter.class);
+      }
+    }
+  }
+
+  private final Provider<CurrentUser> userProvider;
+
+  @Inject
+  GetUserFilter(final Provider<CurrentUser> userProvider) {
+    this.userProvider = userProvider;
+  }
+
+  @Override
+  public void doFilter(
+      ServletRequest req, ServletResponse resp, FilterChain chain)
+      throws IOException, ServletException {
+    req.setAttribute(REQ_ATTR_KEY, userProvider.get());
+    chain.doFilter(req, resp);
+  }
+
+  @Override
+  public void destroy() {
+  }
+
+  @Override
+  public void init(FilterConfig arg0) {
+  }
+}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/HttpLog.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/HttpLog.java
index a4e13d3..6f439d2 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/HttpLog.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/HttpLog.java
@@ -17,8 +17,6 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.config.SitePaths;
-import com.google.inject.Provider;
-import com.google.inject.servlet.GuiceHelper;
 
 import org.apache.log4j.Appender;
 import org.apache.log4j.AsyncAppender;
@@ -32,6 +30,7 @@
 import org.eclipse.jetty.server.RequestLog;
 import org.eclipse.jetty.server.Response;
 import org.eclipse.jetty.util.component.AbstractLifeCycle;
+import org.eclipse.jgit.lib.Config;
 
 import java.io.File;
 import java.io.IOException;
@@ -55,11 +54,8 @@
   private static final String P_USER_AGENT = "User-Agent";
 
   private final AsyncAppender async;
-  private final Provider<CurrentUser> userProvider;
 
-  HttpLog(final SitePaths site, final Provider<CurrentUser> userProvider) {
-    this.userProvider = userProvider;
-
+  HttpLog(final SitePaths site, final Config config) {
     final DailyRollingFileAppender dst = new DailyRollingFileAppender();
     dst.setName(LOG_NAME);
     dst.setLayout(new MyLayout());
@@ -74,7 +70,7 @@
 
     async = new AsyncAppender();
     async.setBlocking(true);
-    async.setBufferSize(64);
+    async.setBufferSize(config.getInt("core", "asyncLoggingBufferSize", 64));
     async.setLocationInfo(false);
     async.addAppender(dst);
     async.activateOptions();
@@ -91,18 +87,14 @@
 
   @Override
   public void log(final Request req, final Response rsp) {
-    GuiceHelper.runInContext(req, rsp, new Runnable() {
-      @Override
-      public void run() {
-        doLog(req, rsp);
-      }
-    });
+    CurrentUser user = (CurrentUser) req.getAttribute(GetUserFilter.REQ_ATTR_KEY);
+    doLog(req, rsp, user);
   }
 
-  private void doLog(Request req, Response rsp) {
+  private void doLog(Request req, Response rsp, CurrentUser user) {
     final LoggingEvent event = new LoggingEvent( //
         Logger.class.getName(), // fqnOfCategoryClass
-        null, // logger (optional)
+        log, // logger
         System.currentTimeMillis(), // when
         Level.INFO, // level
         "", // message text
@@ -119,7 +111,6 @@
       uri = uri + "?" + qs;
     }
 
-    CurrentUser user = userProvider.get();
     if (user instanceof IdentifiedUser) {
       IdentifiedUser who = (IdentifiedUser) user;
       if (who.getUserName() != null && !who.getUserName().isEmpty()) {
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..0a0a3cc 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
@@ -19,14 +19,12 @@
 
 import com.google.gerrit.launcher.GerritLauncher;
 import com.google.gerrit.lifecycle.LifecycleListener;
-import com.google.gerrit.reviewdb.AuthType;
-import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.reviewdb.client.AuthType;
 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.Injector;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 import com.google.inject.servlet.GuiceFilter;
 import com.google.inject.servlet.GuiceServletContextListener;
@@ -107,7 +105,7 @@
 
   @Inject
   JettyServer(@GerritServerConfig final Config cfg, final SitePaths site,
-      final JettyEnv env, final Provider<CurrentUser> userProvider)
+      final JettyEnv env)
       throws MalformedURLException, IOException {
     this.site = site;
 
@@ -118,7 +116,7 @@
     Handler app = makeContext(env, cfg);
     if (cfg.getBoolean("httpd", "requestlog", !reverseProxy)) {
       RequestLogHandler handler = new RequestLogHandler();
-      handler.setRequestLog(new HttpLog(site, userProvider));
+      handler.setRequestLog(new HttpLog(site, cfg));
       handler.setHandler(app);
       app = handler;
     }
@@ -143,7 +141,7 @@
     final int acceptors = cfg.getInt("httpd", "acceptorThreads", 2);
     final AuthType authType = ConfigUtil.getEnum(cfg, "auth", null, "type", AuthType.OPENID);
 
-    reverseProxy = true;
+    reverseProxy = isReverseProxied(listenUrls);
     final Connector[] connectors = new Connector[listenUrls.length];
     for (int idx = 0; idx < listenUrls.length; idx++) {
       final URI u = listenUrls[idx];
@@ -158,7 +156,6 @@
       }
 
       if ("http".equals(u.getScheme())) {
-        reverseProxy = false;
         defaultPort = 80;
         c = new SelectChannelConnector();
       } else if ("https".equals(u.getScheme())) {
@@ -177,7 +174,6 @@
           ssl.setNeedClientAuth(true);
         }
 
-        reverseProxy = false;
         defaultPort = 443;
         c = ssl;
 
@@ -235,7 +231,16 @@
     return connectors;
   }
 
-  private URI[] listenURLs(final Config cfg) {
+  static boolean isReverseProxied(final URI[] listenUrls) {
+    for (URI u : listenUrls) {
+      if ("http".equals(u.getScheme()) || "https".equals(u.getScheme())) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  static URI[] listenURLs(final Config cfg) {
     String[] urls = cfg.getStringList("httpd", null, "listenurl");
     if (urls.length == 0) {
       urls = new String[] {"http://*:8080/"};
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/ProjectQoSFilter.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/ProjectQoSFilter.java
index 6870ca4..ee7c794 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/ProjectQoSFilter.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/http/jetty/ProjectQoSFilter.java
@@ -22,10 +22,10 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.git.QueueProvider;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.git.WorkQueue.CancelableRunnable;
 import com.google.gerrit.sshd.CommandExecutorQueueProvider;
-import com.google.gerrit.sshd.QueueProvider;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -142,13 +142,7 @@
   }
 
   private WorkQueue.Executor getExecutor() {
-    WorkQueue.Executor executor;
-    if (userProvider.get().isBatchUser()) {
-      executor = queue.getBatchQueue();
-    } else {
-      executor = queue.getInteractiveQueue();
-    }
-    return executor;
+    return queue.getQueue(userProvider.get().getCapabilities().getQueueType());
   }
 
   @Override
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAuth.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAuth.java
index a5c7d9b..f809c73 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAuth.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitAuth.java
@@ -17,7 +17,8 @@
 import static com.google.gerrit.pgm.init.InitUtil.dnOf;
 
 import com.google.gerrit.pgm.util.ConsoleUI;
-import com.google.gerrit.reviewdb.AuthType;
+import com.google.gerrit.reviewdb.client.AuthType;
+import com.google.gwtjsonrpc.server.SignedToken;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -80,5 +81,9 @@
         break;
       }
     }
+
+    if (auth.getSecure("registerEmailPrivateKey") == null) {
+      auth.setSecure("registerEmailPrivateKey", SignedToken.generateRandomKey());
+    }
   }
 }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitFlags.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitFlags.java
index 992c616..5d71b48 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitFlags.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitFlags.java
@@ -30,9 +30,6 @@
   /** Recursively delete the site path if initialization fails. */
   public boolean deleteOnFailure;
 
-  /** Run the Git project importer after initialization. */
-  public boolean importProjects;
-
   /** Run the daemon (and open the web UI in a browser) after initialization. */
   public boolean autoStart;
 
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitGitManager.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitGitManager.java
index 2d95577..f0cd31f 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitGitManager.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitGitManager.java
@@ -25,14 +25,11 @@
 /** Initialize the GitRepositoryManager configuration section. */
 @Singleton
 class InitGitManager implements InitStep {
-  private final InitFlags flags;
   private final ConsoleUI ui;
   private final Section gerrit;
 
   @Inject
-  InitGitManager(final InitFlags flags, final ConsoleUI ui,
-      final Section.Factory sections) {
-    this.flags = flags;
+  InitGitManager(final ConsoleUI ui, final Section.Factory sections) {
     this.ui = ui;
     this.gerrit = sections.get("gerrit");
   }
@@ -44,11 +41,7 @@
     if (d == null) {
       throw die("gerrit.basePath is required");
     }
-    if (d.exists()) {
-      if (!flags.importProjects && d.list() != null && d.list().length > 0) {
-        flags.importProjects = ui.yesno(true, "Import existing repositories");
-      }
-    } else if (!d.mkdirs()) {
+    if (!d.exists() && !d.mkdirs()) {
       throw die("Cannot create " + d);
     }
   }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitHttpd.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitHttpd.java
index 8b4803f..98d2e47 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitHttpd.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitHttpd.java
@@ -21,7 +21,7 @@
 import static com.google.gerrit.pgm.init.InitUtil.toURI;
 
 import com.google.gerrit.pgm.util.ConsoleUI;
-import com.google.gerrit.reviewdb.AuthType;
+import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gwtjsonrpc.server.SignedToken;
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitSendEmail.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitSendEmail.java
index 7a3556e..c5732e9 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitSendEmail.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitSendEmail.java
@@ -28,12 +28,14 @@
 class InitSendEmail implements InitStep {
   private final ConsoleUI ui;
   private final Section sendemail;
+  private final SitePaths site;
 
   @Inject
   InitSendEmail(final ConsoleUI ui, final SitePaths site,
       final Section.Factory sections) {
     this.ui = ui;
     this.sendemail = sections.get("sendemail");
+    this.site = site;
   }
 
   public void run() {
@@ -49,7 +51,9 @@
             true);
 
     String username = null;
-    if ((enc != null && enc != Encryption.NONE) || !isLocal(hostname)) {
+    if (site.gerrit_config.exists()) {
+      username = sendemail.get("smtpUser");
+    } else if ((enc != null && enc != Encryption.NONE) || !isLocal(hostname)) {
       username = username();
     }
     sendemail.string("SMTP username", "smtpUser", username);
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/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/init/Section.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Section.java
index 005904c..02ed991 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Section.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Section.java
@@ -126,7 +126,7 @@
   }
 
   String password(final String username, final String password) {
-    final String ov = flags.sec.getString(section, null, password);
+    final String ov = getSecure(password);
 
     String user = flags.sec.getString(section, null, username);
     if (user == null) {
@@ -149,15 +149,23 @@
 
     final String nv = ui.password("%s's password", user);
     if (!eq(ov, nv)) {
-      if (nv != null) {
-        flags.sec.setString(section, null, password, nv);
-      } else {
-        flags.sec.unset(section, null, password);
-      }
+      setSecure(password, nv);
     }
     return nv;
   }
 
+  String getSecure(String name) {
+    return flags.sec.getString(section, null, name);
+  }
+
+  void setSecure(String name, String value) {
+    if (value != null) {
+      flags.sec.setString(section, null, name, value);
+    } else {
+      flags.sec.unset(section, null, name);
+    }
+  }
+
   private static boolean eq(final String a, final String b) {
     if (a == null && b == null) {
       return true;
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/AbstractProgram.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/AbstractProgram.java
index 5d40923..833b4d8 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/AbstractProgram.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/AbstractProgram.java
@@ -32,10 +32,7 @@
   private boolean running = true;
 
   @Option(name = "--show-stack-trace", usage = "display stack trace on failure")
-  private boolean showStackTrace;
-
-  @Option(name = "--help", usage = "display this help text", aliases = {"-h"})
-  private boolean help;
+  protected boolean showStackTrace;
 
   private String getName() {
     String n = getClass().getName();
@@ -52,21 +49,15 @@
     try {
       clp.parseArgument(argv);
     } catch (CmdLineException err) {
-      if (!help) {
+      if (!clp.wasHelpRequestedByOption()) {
         System.err.println("fatal: " + err.getMessage());
         return 1;
       }
     }
 
-    if (help) {
-      final StringWriter msg = new StringWriter();
-      msg.write(getName());
-      clp.printSingleLineUsage(msg, null);
-      msg.write('\n');
-
-      msg.write('\n');
-      clp.printUsage(msg, null);
-      msg.write('\n');
+    if (clp.wasHelpRequestedByOption()) {
+      StringWriter msg = new StringWriter();
+      clp.printDetailedUsage(getName(), msg);
       System.err.println(msg.toString());
       return 1;
     }
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 8e3306b..4893579 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,9 +20,11 @@
 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.gwtorm.client.OrmException;
+import com.google.gerrit.server.schema.SchemaModule;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.AbstractModule;
 import com.google.inject.CreationException;
 import com.google.inject.Guice;
@@ -162,6 +164,8 @@
     });
     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/java/com/google/inject/servlet/GuiceHelper.java b/gerrit-pgm/src/main/java/com/google/inject/servlet/GuiceHelper.java
deleted file mode 100644
index 5b6cc09..0000000
--- a/gerrit-pgm/src/main/java/com/google/inject/servlet/GuiceHelper.java
+++ /dev/null
@@ -1,36 +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.inject.servlet;
-
-import com.google.inject.servlet.GuiceFilter.Context;
-
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-public class GuiceHelper {
-  public static void runInContext(HttpServletRequest req,
-      HttpServletResponse rsp, Runnable thunk) {
-    Context previous = GuiceFilter.localContext.get();
-    try {
-      GuiceFilter.localContext.set(new Context(req, rsp));
-      thunk.run();
-    } finally {
-      GuiceFilter.localContext.set(previous);
-    }
-  }
-
-  private GuiceHelper() {
-  }
-}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestAccountsEnum.java b/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/ProtoGenHeader.txt
similarity index 75%
copy from gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestAccountsEnum.java
copy to gerrit-pgm/src/main/resources/com/google/gerrit/pgm/ProtoGenHeader.txt
index 2ef2d44..757e8e2 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestAccountsEnum.java
+++ b/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/ProtoGenHeader.txt
@@ -11,12 +11,12 @@
 // WITHOUT 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@@)
 
-package com.google.gerrit.httpd.rpc;
+syntax = "proto2";
 
-public enum SuggestAccountsEnum {
-  ALL,
-  SAME_GROUP,
-  VISIBLE_GROUP,
-  OFF;
-}
+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 9aba73e..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
@@ -54,70 +54,22 @@
 
 get_config() {
   if test -f "$GERRIT_CONFIG" ; then
-    if type git >/dev/null 2>&1 ; then
-      if test "x$1" = x--int ; then
-        # Git might not be able to expand "8g" properly.  If it gives
-        # us 0 back retry for the raw string and expand ourselves.
-        #
-        n=`git config --file "$GERRIT_CONFIG" --int "$2"`
-        if test x0 = "x$n" ; then
-          n=`git config --file "$GERRIT_CONFIG" --get "$2"`
-          case "$n" in
-          *g) n=`expr ${n%%g} \* 1024`m ;;
-          *k) n=`expr ${n%%k} \* 1024` ;;
-          *)  : ;;
-          esac
-        fi
-        echo "$n"
-      else
-        git config --file "$GERRIT_CONFIG" $1 "$2"
-      fi
-
-    else
-      # This is a very crude parser for the git configuration file.
-      # Its not perfect but it can at least pull some basic values
-      # from a reasonably standard format.
+    if test "x$1" = x--int ; then
+      # Git might not be able to expand "8g" properly.  If it gives
+      # us 0 back retry for the raw string and expand ourselves.
       #
-      s=`echo "$2" | cut -d. -f1`
-      k=`echo "$2" | cut -d. -f2`
-      i=0
-      while read n ; do
+      n=`git config --file "$GERRIT_CONFIG" --int "$2"`
+      if test x0 = "x$n" ; then
+        n=`git config --file "$GERRIT_CONFIG" --get "$2"`
         case "$n" in
-        '['$s']') i=1 ;;
-        '['*']' ) i=0 ;;
+        *g) n=`expr ${n%%g} \* 1024`m ;;
+        *k) n=`expr ${n%%k} \* 1024` ;;
+        *)  : ;;
         esac
-        test $i || continue
-
-        case "$n" in
-        *[' 	']$k[' 	']*=*) : ;;
-        [' 	']$k=*) : ;;
-        $k[' 	']*=*) : ;;
-        $k=*) : ;;
-        *) continue ;;
-        esac
-
-        n=${n#*=}
-
-        if test "x$1" = x--bool ; then
-          case "$n" in
-          true|on|1|yes) n=true ;;
-          false|off|0|no) n=false ;;
-          *)
-            echo >&2 "error: $2=$n not supported, assuming false."
-            n=false
-            ;;
-          esac
-        fi
-
-        if test "x$1" = x--int ; then
-          case "$n" in
-          *g) n=`expr ${n%%g} \* 1024`m ;;
-          *k) n=`expr ${n%%k} \* 1024` ;;
-          *)  : ;;
-          esac
-        fi
-        echo "$n" 
-      done <"$GERRIT_CONFIG"
+      fi
+      echo "$n"
+    else
+      git config --file "$GERRIT_CONFIG" $1 "$2"
     fi
   fi
 }
@@ -174,6 +126,16 @@
 GERRIT_INSTALL_TRACE_FILE=etc/gerrit.config
 
 ##################################################
+# No git in PATH? Needed for gerrit.confg parsing
+##################################################
+if type git >/dev/null 2>&1 ; then
+  : OK
+else
+  echo >&2 "** ERROR: Cannot find git in PATH"
+  exit 1
+fi
+
+##################################################
 # Try to determine GERRIT_SITE if not set
 ##################################################
 if test -z "$GERRIT_SITE" ; then
@@ -418,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/.settings/org.eclipse.core.resources.prefs b/gerrit-prettify/.settings/org.eclipse.core.resources.prefs
index 82eb859..e7d6680 100644
--- a/gerrit-prettify/.settings/org.eclipse.core.resources.prefs
+++ b/gerrit-prettify/.settings/org.eclipse.core.resources.prefs
@@ -1,3 +1,5 @@
-#Tue Sep 02 16:59:24 PDT 2008
+#Thu Jul 28 11:02:35 PDT 2011
 eclipse.preferences.version=1
+encoding//src/main/java=UTF-8
+encoding//src/main/resources=UTF-8
 encoding/<project>=UTF-8
diff --git a/gerrit-prettify/.settings/org.eclipse.jdt.core.prefs b/gerrit-prettify/.settings/org.eclipse.jdt.core.prefs
index 04afc7f..5f73a7f 100644
--- a/gerrit-prettify/.settings/org.eclipse.jdt.core.prefs
+++ b/gerrit-prettify/.settings/org.eclipse.jdt.core.prefs
@@ -1,4 +1,4 @@
-#Tue May 12 17:44:13 PDT 2009
+#Thu Jul 28 11:02:35 PDT 2011
 eclipse.preferences.version=1
 org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
 org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
@@ -9,6 +9,7 @@
 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
diff --git a/gerrit-prettify/pom.xml b/gerrit-prettify/pom.xml
index 060ffdd..f5bd3d6 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.1-SNAPSHOT</version>
+    <version>2.4-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-prettify</artifactId>
@@ -56,4 +56,20 @@
       <scope>provided</scope>
     </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-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-prettify/src/main/java/com/google/gerrit/prettify/common/PrettyFormatter.java b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettyFormatter.java
index 5d1592d..c9a9ab8 100644
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettyFormatter.java
+++ b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/PrettyFormatter.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.prettify.common;
 
-import com.google.gerrit.reviewdb.AccountDiffPreference;
+import com.google.gerrit.reviewdb.client.AccountDiffPreference;
 import com.google.gwtexpui.safehtml.client.SafeHtml;
 import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
 
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/.settings/org.eclipse.core.resources.prefs b/gerrit-reviewdb/.settings/org.eclipse.core.resources.prefs
index 82eb859..e7d6680 100644
--- a/gerrit-reviewdb/.settings/org.eclipse.core.resources.prefs
+++ b/gerrit-reviewdb/.settings/org.eclipse.core.resources.prefs
@@ -1,3 +1,5 @@
-#Tue Sep 02 16:59:24 PDT 2008
+#Thu Jul 28 11:02:35 PDT 2011
 eclipse.preferences.version=1
+encoding//src/main/java=UTF-8
+encoding//src/main/resources=UTF-8
 encoding/<project>=UTF-8
diff --git a/gerrit-reviewdb/.settings/org.eclipse.jdt.core.prefs b/gerrit-reviewdb/.settings/org.eclipse.jdt.core.prefs
index 04afc7f..5f73a7f 100644
--- a/gerrit-reviewdb/.settings/org.eclipse.jdt.core.prefs
+++ b/gerrit-reviewdb/.settings/org.eclipse.jdt.core.prefs
@@ -1,4 +1,4 @@
-#Tue May 12 17:44:13 PDT 2009
+#Thu Jul 28 11:02:35 PDT 2011
 eclipse.preferences.version=1
 org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
 org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
@@ -9,6 +9,7 @@
 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
diff --git a/gerrit-reviewdb/pom.xml b/gerrit-reviewdb/pom.xml
index d81b068..24d6a1b 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.1-SNAPSHOT</version>
+    <version>2.4-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-reviewdb</artifactId>
@@ -38,4 +38,20 @@
       <artifactId>gwtorm</artifactId>
     </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-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountPatchReviewAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountPatchReviewAccess.java
deleted file mode 100644
index 91e8837..0000000
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountPatchReviewAccess.java
+++ /dev/null
@@ -1,31 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.reviewdb;
-
-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 AccountPatchReviewAccess
-    extends Access<AccountPatchReview, AccountPatchReview.Key> {
-  @PrimaryKey("key")
-  AccountPatchReview get(AccountPatchReview.Key id) throws OrmException;
-
-  @Query("WHERE key.accountId = ? AND key.patchKey.patchSetId = ?")
-  ResultSet<AccountPatchReview> byReviewer(Account.Id who, PatchSet.Id ps) throws OrmException;
-
-}
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
deleted file mode 100644
index 377aa59..0000000
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ChangeMessageAccess.java
+++ /dev/null
@@ -1,30 +0,0 @@
-// Copyright (C) 2008 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.reviewdb;
-
-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 ChangeMessageAccess extends
-    Access<ChangeMessage, ChangeMessage.Key> {
-  @PrimaryKey("key")
-  ChangeMessage get(ChangeMessage.Key id) throws OrmException;
-
-  @Query("WHERE key.changeId = ? ORDER BY writtenOn")
-  ResultSet<ChangeMessage> byChange(Change.Id id) throws OrmException;
-}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ProjectAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ProjectAccess.java
deleted file mode 100644
index b9adada..0000000
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ProjectAccess.java
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright (C) 2008 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.reviewdb;
-
-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 ProjectAccess extends Access<Project, Project.NameKey> {
-  @PrimaryKey("name")
-  Project get(Project.NameKey name) throws OrmException;
-
-  @Query("ORDER BY name")
-  ResultSet<Project> all() throws OrmException;
-
-  @Query("WHERE name.name >= ? AND name.name <= ? ORDER BY name LIMIT ?")
-  ResultSet<Project> suggestByName(String nameA, String nameB, int limit)
-      throws OrmException;
-}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/RefRight.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/RefRight.java
deleted file mode 100644
index 97ee219..0000000
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/RefRight.java
+++ /dev/null
@@ -1,235 +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.reviewdb;
-
-import com.google.gwtorm.client.Column;
-import com.google.gwtorm.client.CompoundKey;
-import com.google.gwtorm.client.StringKey;
-
-import java.util.Comparator;
-
-/** Grant to use an {@link ApprovalCategory} in the scope of a git ref. */
-public final class RefRight {
-  /** Pattern that matches all references in a project. */
-  public static final String ALL = "refs/*";
-
-  /** Prefix that triggers a regular expression pattern. */
-  public static final String REGEX_PREFIX = "^";
-
-  public static class RefPattern extends
-      StringKey<com.google.gwtorm.client.Key<?>> {
-    private static final long serialVersionUID = 1L;
-
-    @Column(id = 1)
-    protected String pattern;
-
-    protected RefPattern() {
-    }
-
-    public RefPattern(final String pattern) {
-      this.pattern = pattern;
-    }
-
-    @Override
-    public String get() {
-      return pattern;
-    }
-
-    @Override
-    protected void set(String pattern) {
-      this.pattern = pattern;
-    }
-  }
-
-  public static class Key extends CompoundKey<Project.NameKey> {
-    private static final long serialVersionUID = 1L;
-
-    @Column(id = 1)
-    protected Project.NameKey projectName;
-
-    @Column(id = 2)
-    protected RefPattern refPattern;
-
-    @Column(id = 3)
-    protected ApprovalCategory.Id categoryId;
-
-    @Column(id = 4)
-    protected AccountGroup.Id groupId;
-
-    protected Key() {
-      projectName = new Project.NameKey();
-      refPattern = new RefPattern();
-      categoryId = new ApprovalCategory.Id();
-      groupId = new AccountGroup.Id();
-    }
-
-    public Key(final Project.NameKey projectName, final RefPattern refPattern,
-        final ApprovalCategory.Id categoryId, final AccountGroup.Id groupId) {
-      this.projectName = projectName;
-      this.refPattern = refPattern;
-      this.categoryId = categoryId;
-      this.groupId = groupId;
-    }
-
-    @Override
-    public Project.NameKey getParentKey() {
-      return projectName;
-    }
-
-    public Project.NameKey getProjectNameKey() {
-      return projectName;
-    }
-
-    public String getRefPattern() {
-      return refPattern.get();
-    }
-
-    public void setGroupId(AccountGroup.Id groupId) {
-      this.groupId = groupId;
-    }
-
-    @Override
-    public com.google.gwtorm.client.Key<?>[] members() {
-      return new com.google.gwtorm.client.Key<?>[] {refPattern, categoryId,
-          groupId};
-    }
-  }
-
-  @Column(id = 1, name = Column.NONE)
-  protected Key key;
-
-  @Column(id = 2)
-  protected short minValue;
-
-  @Column(id = 3)
-  protected short maxValue;
-
-  protected RefRight() {
-  }
-
-  public RefRight(RefRight.Key key) {
-    this.key = key;
-  }
-
-  public RefRight(final RefRight refRight, final AccountGroup.Id groupId) {
-    this(new RefRight.Key(refRight.getKey().projectName,
-        refRight.getKey().refPattern, refRight.getKey().categoryId, groupId));
-    setMinValue(refRight.getMinValue());
-    setMaxValue(refRight.getMaxValue());
-  }
-
-  public RefRight.Key getKey() {
-    return key;
-  }
-
-  public String getRefPattern() {
-    if (isExclusive()) {
-      return key.refPattern.get().substring(1);
-    }
-    return key.refPattern.get();
-  }
-
-  public String getRefPatternForDisplay() {
-    return key.refPattern.get();
-  }
-
-  public Project.NameKey getProjectNameKey() {
-    return getKey().getProjectNameKey();
-  }
-
-  public boolean isExclusive() {
-    return key.refPattern.get().startsWith("-");
-  }
-
-  public ApprovalCategory.Id getApprovalCategoryId() {
-    return key.categoryId;
-  }
-
-  public AccountGroup.Id getAccountGroupId() {
-    return key.groupId;
-  }
-
-  public short getMinValue() {
-    return minValue;
-  }
-
-  public void setMinValue(final short m) {
-    minValue = m;
-  }
-
-  public short getMaxValue() {
-    return maxValue;
-  }
-
-  public void setMaxValue(final short m) {
-    maxValue = m;
-  }
-
-  @Override
-  public String toString() {
-    StringBuilder s = new StringBuilder();
-    s.append("{group :");
-    s.append(getAccountGroupId().get());
-    s.append(", proj :");
-    s.append(getProjectNameKey().get());
-    s.append(", cat :");
-    s.append(getApprovalCategoryId().get());
-    s.append(", pattern :");
-    s.append(getRefPatternForDisplay());
-    s.append(", min :");
-    s.append(getMinValue());
-    s.append(", max :");
-    s.append(getMaxValue());
-    s.append("}");
-    return s.toString();
-  }
-
-  @Override
-  public int hashCode() {
-    return getKey().hashCode();
-  }
-
-  @Override
-  public boolean equals(Object o) {
-    if (o instanceof RefRight) {
-      RefRight a = this;
-      RefRight b = (RefRight) o;
-      return a.getKey().equals(b.getKey())
-          && a.getMinValue() == b.getMinValue()
-          && a.getMaxValue() == b.getMaxValue();
-    }
-    return false;
-  }
-
-  public static final Comparator<RefRight> REF_PATTERN_ORDER =
-      new Comparator<RefRight>() {
-
-    @Override
-    public int compare(RefRight a, RefRight b) {
-      int aLength = a.getRefPattern().length();
-      int bLength = b.getRefPattern().length();
-      if (bLength == aLength) {
-        ApprovalCategory.Id aCat = a.getApprovalCategoryId();
-        ApprovalCategory.Id bCat = b.getApprovalCategoryId();
-        if (aCat.get().equals(bCat.get())) {
-          return a.getRefPattern().compareTo(b.getRefPattern());
-        }
-        return a.getApprovalCategoryId().get()
-            .compareTo(b.getApprovalCategoryId().get());
-      }
-      return bLength - aLength;
-    }
-  };
-}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/RefRightAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/RefRightAccess.java
deleted file mode 100644
index a42ff2c..0000000
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/RefRightAccess.java
+++ /dev/null
@@ -1,33 +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.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 RefRightAccess extends Access<RefRight, RefRight.Key> {
-  @PrimaryKey("key")
-  RefRight get(RefRight.Key refRight) throws OrmException;
-
-  @Query("WHERE key.projectName = ?")
-  ResultSet<RefRight> byProject(Project.NameKey project) throws OrmException;
-
-  @Query("WHERE key.categoryId = ? AND key.groupId = ?")
-  ResultSet<RefRight> byCategoryGroup(ApprovalCategory.Id cat,
-      AccountGroup.Id group) throws OrmException;
-}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/ReviewDB.gwt.xml b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ReviewDB.gwt.xml
similarity index 95%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/ReviewDB.gwt.xml
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ReviewDB.gwt.xml
index c5f8912..4402826 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/ReviewDB.gwt.xml
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ReviewDB.gwt.xml
@@ -15,5 +15,4 @@
 -->
 <module>
   <inherits name='com.google.gwtorm.GWTORM'/>
-  <source path='reviewdb' />
 </module>
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AbstractAgreement.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AbstractAgreement.java
similarity index 96%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AbstractAgreement.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AbstractAgreement.java
index 987fc4b..0ed7410 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AbstractAgreement.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AbstractAgreement.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb;
+package com.google.gerrit.reviewdb.client;
 
 import java.sql.Timestamp;
 
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Account.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Account.java
similarity index 97%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Account.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Account.java
index be2cb20..b23aa86 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Account.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Account.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb;
+package com.google.gerrit.reviewdb.client;
 
 import com.google.gwtorm.client.Column;
 import com.google.gwtorm.client.IntKey;
@@ -146,7 +146,8 @@
   /**
    * Create a new account.
    *
-   * @param newId unique id, see {@link ReviewDb#nextAccountId()}.
+   * @param newId unique id, see
+   *        {@link com.google.gerrit.reviewdb.server.ReviewDb#nextAccountId()}.
    */
   public Account(final Account.Id newId) {
     accountId = newId;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountAgreement.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountAgreement.java
similarity index 95%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountAgreement.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountAgreement.java
index 39ed66d..baa9b5c 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountAgreement.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountAgreement.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb;
+package com.google.gerrit.reviewdb.client;
 
 import com.google.gwtorm.client.Column;
 import com.google.gwtorm.client.CompoundKey;
@@ -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/AccountDiffPreference.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountDiffPreference.java
similarity index 86%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountDiffPreference.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountDiffPreference.java
index 38a2359..3b04725 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountDiffPreference.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountDiffPreference.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb;
+package com.google.gerrit.reviewdb.client;
 
 import com.google.gwtorm.client.Column;
 
@@ -65,6 +65,7 @@
     p.setIntralineDifference(true);
     p.setShowTabs(true);
     p.setContext(DEFAULT_CONTEXT);
+    p.setManualReview(false);
     return p;
   }
 
@@ -102,6 +103,15 @@
   @Column(id = 11)
   protected boolean skipUncommented;
 
+  @Column(id = 12)
+  protected boolean expandAllComments;
+
+  @Column(id = 13)
+  protected boolean retainHeader;
+
+  @Column(id = 14)
+  protected boolean manualReview;
+
   protected AccountDiffPreference() {
   }
 
@@ -120,7 +130,10 @@
     this.showTabs = p.showTabs;
     this.skipDeleted = p.skipDeleted;
     this.skipUncommented = p.skipUncommented;
+    this.expandAllComments = p.expandAllComments;
     this.context = p.context;
+    this.retainHeader = p.retainHeader;
+    this.manualReview = p.manualReview;
   }
 
   public Account.Id getAccountId() {
@@ -209,4 +222,28 @@
   public void setSkipUncommented(boolean skip) {
     skipUncommented = skip;
   }
+
+  public boolean isExpandAllComments() {
+    return expandAllComments;
+  }
+
+  public void setExpandAllComments(boolean expand) {
+    expandAllComments = expand;
+  }
+
+  public boolean isRetainHeader() {
+    return retainHeader;
+  }
+
+  public void setRetainHeader(boolean retain) {
+    retainHeader = retain;
+  }
+
+  public boolean isManualReview() {
+    return manualReview;
+  }
+
+  public void setManualReview(boolean manual) {
+    manualReview = manual;
+  }
 }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountExternalId.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountExternalId.java
similarity index 98%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountExternalId.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountExternalId.java
index 853ebd5..0705284 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountExternalId.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountExternalId.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb;
+package com.google.gerrit.reviewdb.client;
 
 import com.google.gwtorm.client.Column;
 import com.google.gwtorm.client.StringKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGeneralPreferences.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGeneralPreferences.java
similarity index 97%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGeneralPreferences.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGeneralPreferences.java
index a551ebb..b9ff4fb 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGeneralPreferences.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGeneralPreferences.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb;
+package com.google.gerrit.reviewdb.client;
 
 import com.google.gwtorm.client.Column;
 
@@ -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/AccountGroup.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroup.java
similarity index 79%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGroup.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroup.java
index d2aceaa..8e2541a 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGroup.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroup.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb;
+package com.google.gerrit.reviewdb.client;
 
 import com.google.gwtorm.client.Column;
 import com.google.gwtorm.client.IntKey;
@@ -46,6 +46,39 @@
     }
   }
 
+  /** Globally unique identifier. */
+  public static class UUID extends
+      StringKey<com.google.gwtorm.client.Key<?>> {
+    private static final long serialVersionUID = 1L;
+
+    @Column(id = 1, length = 40)
+    protected String uuid;
+
+    protected UUID() {
+    }
+
+    public UUID(final String n) {
+      uuid = n;
+    }
+
+    @Override
+    public String get() {
+      return uuid;
+    }
+
+    @Override
+    protected void set(String newValue) {
+      uuid = newValue;
+    }
+
+    /** Parse an AccountGroup.UUID out of a string representation. */
+    public static UUID parse(final String str) {
+      final UUID r = new UUID();
+      r.fromString(str);
+      return r;
+    }
+  }
+
   /** Distinguished name, within organization directory server. */
   public static class ExternalNameKey extends
       StringKey<com.google.gwtorm.client.Key<?>> {
@@ -140,6 +173,18 @@
     LDAP;
   }
 
+  /** Common UUID assigned to the "Project Owners" placeholder group. */
+  public static final AccountGroup.UUID PROJECT_OWNERS =
+      new AccountGroup.UUID("global:Project-Owners");
+
+  /** Common UUID assigned to the "Anonymous Users" group. */
+  public static final AccountGroup.UUID ANONYMOUS_USERS =
+      new AccountGroup.UUID("global:Anonymous-Users");
+
+  /** Common UUID assigned to the "Registered Users" group. */
+  public static final AccountGroup.UUID REGISTERED_USERS =
+      new AccountGroup.UUID("global:Registered-Users");
+
   /** Unique name of this group within the system. */
   @Column(id = 1)
   protected NameKey name;
@@ -171,20 +216,20 @@
   @Column(id = 7)
   protected boolean visibleToAll;
 
-  /** Comment and action email notifications by users in this group are only
-   *  sent to change authors. */
-  @Column(id = 8)
-  protected boolean emailOnlyAuthors;
+  /** Globally unique identifier name for this group. */
+  @Column(id = 9)
+  protected UUID groupUUID;
 
   protected AccountGroup() {
   }
 
   public AccountGroup(final AccountGroup.NameKey newName,
-      final AccountGroup.Id newId) {
+      final AccountGroup.Id newId, final AccountGroup.UUID uuid) {
     name = newName;
     groupId = newId;
     ownerGroupId = groupId;
     visibleToAll = false;
+    groupUUID = uuid;
     setType(Type.INTERNAL);
   }
 
@@ -244,11 +289,11 @@
     return visibleToAll;
   }
 
-  public boolean isEmailOnlyAuthors() {
-    return emailOnlyAuthors;
+  public AccountGroup.UUID getGroupUUID() {
+    return groupUUID;
   }
 
-  public void setEmailOnlyAuthors(boolean emailOnlyAuthors) {
-    this.emailOnlyAuthors = emailOnlyAuthors;
+  public void setGroupUUID(AccountGroup.UUID uuid) {
+    groupUUID = uuid;
   }
 }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGroupAgreement.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupAgreement.java
similarity index 95%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGroupAgreement.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupAgreement.java
index e7c4ad0..c712b3d 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGroupAgreement.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupAgreement.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb;
+package com.google.gerrit.reviewdb.client;
 
 import com.google.gwtorm.client.Column;
 import com.google.gwtorm.client.CompoundKey;
@@ -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/AccountGroupInclude.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupInclude.java
similarity index 97%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGroupInclude.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupInclude.java
index 86b0966..4b58689 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGroupInclude.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupInclude.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb;
+package com.google.gerrit.reviewdb.client;
 
 import com.google.gwtorm.client.Column;
 import com.google.gwtorm.client.CompoundKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGroupIncludeAudit.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupIncludeAudit.java
similarity index 93%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGroupIncludeAudit.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupIncludeAudit.java
index e99e480..275c3c3 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGroupIncludeAudit.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupIncludeAudit.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb;
+package com.google.gerrit.reviewdb.client;
 
 import com.google.gwtorm.client.Column;
 import com.google.gwtorm.client.CompoundKey;
@@ -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/AccountGroupMember.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupMember.java
similarity index 97%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGroupMember.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupMember.java
index 1ca9123..4869cf8 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGroupMember.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupMember.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb;
+package com.google.gerrit.reviewdb.client;
 
 import com.google.gwtorm.client.Column;
 import com.google.gwtorm.client.CompoundKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGroupMemberAudit.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupMemberAudit.java
similarity index 93%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGroupMemberAudit.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupMemberAudit.java
index e2ce939..8ba8912 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGroupMemberAudit.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupMemberAudit.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb;
+package com.google.gerrit.reviewdb.client;
 
 import com.google.gwtorm.client.Column;
 import com.google.gwtorm.client.CompoundKey;
@@ -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/AccountGroupName.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupName.java
similarity index 96%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGroupName.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupName.java
index 3b31dc6..701f2c3 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGroupName.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupName.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb;
+package com.google.gerrit.reviewdb.client;
 
 import com.google.gwtorm.client.Column;
 
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountPatchReview.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountPatchReview.java
similarity index 97%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountPatchReview.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountPatchReview.java
index 4d16bb6..a1c3c3c 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountPatchReview.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountPatchReview.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb;
+package com.google.gerrit.reviewdb.client;
 
 import com.google.gwtorm.client.Column;
 import com.google.gwtorm.client.CompoundKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountProjectWatch.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountProjectWatch.java
similarity index 95%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountProjectWatch.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountProjectWatch.java
index c18ae82..93e6fb3 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountProjectWatch.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountProjectWatch.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb;
+package com.google.gerrit.reviewdb.client;
 
 import com.google.gwtorm.client.Column;
 import com.google.gwtorm.client.CompoundKey;
@@ -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/AccountSshKey.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountSshKey.java
similarity index 98%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountSshKey.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountSshKey.java
index 7a79980..8c4ac82 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountSshKey.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountSshKey.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb;
+package com.google.gerrit.reviewdb.client;
 
 import com.google.gwtorm.client.Column;
 import com.google.gwtorm.client.IntKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ApprovalCategory.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/ApprovalCategory.java
similarity index 61%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ApprovalCategory.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/ApprovalCategory.java
index 29ea97e..bb25265 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ApprovalCategory.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/ApprovalCategory.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb;
+package com.google.gerrit.reviewdb.client;
 
 import com.google.gwtorm.client.Column;
 import com.google.gwtorm.client.Key;
@@ -24,33 +24,6 @@
   public static final ApprovalCategory.Id SUBMIT =
       new ApprovalCategory.Id("SUBM");
 
-  /** Id of the special "Read" action (and category). */
-  public static final ApprovalCategory.Id READ =
-      new ApprovalCategory.Id("READ");
-
-  /** Id of the special "Own" category; manages a project. */
-  public static final ApprovalCategory.Id OWN = new ApprovalCategory.Id("OWN");
-
-  /** Id of the special "Push Annotated Tag" action (and category). */
-  public static final ApprovalCategory.Id PUSH_TAG =
-      new ApprovalCategory.Id("pTAG");
-  public static final short PUSH_TAG_SIGNED = 1;
-  public static final short PUSH_TAG_ANNOTATED = 2;
-
-  /** Id of the special "Push Branch" action (and category). */
-  public static final ApprovalCategory.Id PUSH_HEAD =
-      new ApprovalCategory.Id("pHD");
-  public static final short PUSH_HEAD_UPDATE = 1;
-  public static final short PUSH_HEAD_CREATE = 2;
-  public static final short PUSH_HEAD_REPLACE = 3;
-
-  /** Id of the special "Forge Identity" category. */
-  public static final ApprovalCategory.Id FORGE_IDENTITY =
-      new ApprovalCategory.Id("FORG");
-  public static final short FORGE_AUTHOR = 1;
-  public static final short FORGE_COMMITTER = 2;
-  public static final short FORGE_SERVER = 3;
-
   public static class Id extends StringKey<Key<?>> {
     private static final long serialVersionUID = 1L;
 
@@ -73,15 +46,6 @@
     protected void set(String newValue) {
       id = newValue;
     }
-
-    /** True if the right can be assigned on the wild project. */
-    public boolean canBeOnWildProject() {
-      if (OWN.equals(this)) {
-        return false;
-      } else {
-        return true;
-      }
-    }
   }
 
   /** Internal short unique identifier for this category. */
@@ -96,16 +60,7 @@
   @Column(id = 3, length = 4, notNull = false)
   protected String abbreviatedName;
 
-  /**
-   * Order of this category within the Approvals table when presented.
-   * <p>
-   * If < 0 (e.g. -1) this category is not shown in the Approvals table but is
-   * instead considered to be an action that the user might be able to perform,
-   * e.g. "Submit".
-   * <p>
-   * If >= 0 this category is shown in the Approvals table, sorted along with
-   * its siblings by <code>position, name</code>.
-   */
+  /** Order of this category within the Approvals table when presented. */
   @Column(id = 4)
   protected short position;
 
@@ -117,6 +72,9 @@
   @Column(id = 6)
   protected boolean copyMinScore;
 
+  /** Computed name derived from {@link #name}. */
+  protected String labelName;
+
   protected ApprovalCategory() {
   }
 
@@ -136,6 +94,26 @@
 
   public void setName(final String n) {
     name = n;
+    labelName = null;
+  }
+
+  /** Clean version of {@link #getName()}, e.g. "Code Review" is "Code-Review". */
+  public String getLabelName() {
+    if (labelName == null) {
+      StringBuilder r = new StringBuilder();
+      for (int i = 0; i < name.length(); i++) {
+        char c = name.charAt(i);
+        if (('0' <= c && c <= '9') //
+            || ('a' <= c && c <= 'z') //
+            || ('A' <= c && c <= 'Z')) {
+          r.append(c);
+        } else if (c == ' ') {
+          r.append('-');
+        }
+      }
+      labelName = r.toString();
+    }
+    return labelName;
   }
 
   public String getAbbreviatedName() {
@@ -154,14 +132,6 @@
     position = p;
   }
 
-  public boolean isAction() {
-    return position < 0;
-  }
-
-  public boolean isRange() {
-    return !isAction();
-  }
-
   public String getFunctionName() {
     return functionName;
   }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ApprovalCategoryValue.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/ApprovalCategoryValue.java
similarity index 98%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ApprovalCategoryValue.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/ApprovalCategoryValue.java
index 84f0a17..b7761c5 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ApprovalCategoryValue.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/ApprovalCategoryValue.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb;
+package com.google.gerrit.reviewdb.client;
 
 import com.google.gwtorm.client.Column;
 import com.google.gwtorm.client.ShortKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AuthType.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AuthType.java
similarity index 95%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AuthType.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AuthType.java
index 5d69e21..962426b 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AuthType.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AuthType.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb;
+package com.google.gerrit.reviewdb.client;
 
 public enum AuthType {
   /** Login relies upon the OpenID standard: {@link "http://openid.net/"} */
@@ -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/Branch.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Branch.java
similarity index 98%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Branch.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Branch.java
index 27716a8..e1a15e5 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Branch.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Branch.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb;
+package com.google.gerrit.reviewdb.client;
 
 import com.google.gwtorm.client.Column;
 import com.google.gwtorm.client.StringKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Change.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
similarity index 88%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Change.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
index a869423..61e6a97 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Change.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb;
+package com.google.gerrit.reviewdb.client;
 
 import com.google.gwtorm.client.Column;
 import com.google.gwtorm.client.IntKey;
@@ -182,14 +182,16 @@
   /** Minimum database status constant for an open change. */
   private static final char MIN_OPEN = 'a';
   /** Database constant for {@link Status#NEW}. */
-  protected static final char STATUS_NEW = 'n';
+  public static final char STATUS_NEW = 'n';
   /** Database constant for {@link Status#SUBMITTED}. */
-  protected static final char STATUS_SUBMITTED = 's';
+  public static final char STATUS_SUBMITTED = 's';
+  /** Database constant for {@link Status#DRAFT}. */
+  public static final char STATUS_DRAFT = 'd';
   /** Maximum database status constant for an open change. */
   private static final char MAX_OPEN = 'z';
 
   /** Database constant for {@link Status#MERGED}. */
-  protected static final char STATUS_MERGED = 'M';
+  public static final char STATUS_MERGED = 'M';
 
   /**
    * Current state within the basic workflow of the change.
@@ -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>
@@ -355,6 +375,17 @@
   @Column(id = 14, notNull = false)
   protected String topic;
 
+  /**
+   * Null if the change has never been tested.
+   * Empty if it has been tested but against a branch that does
+   * not exist.
+   */
+  @Column(id = 15, notNull = false)
+  protected RevId lastSha1MergeTested;
+
+  @Column(id = 16)
+  protected boolean mergeable;
+
   protected Change() {
   }
 
@@ -367,6 +398,7 @@
     owner = ownedBy;
     dest = forBranch;
     setStatus(Status.NEW);
+    setLastSha1MergeTested(null);
   }
 
   /** Legacy 32 bit integer identity for a change. */
@@ -400,6 +432,10 @@
     lastUpdatedOn = new Timestamp(System.currentTimeMillis());
   }
 
+  public int getNumberOfPatchSets() {
+    return nbrPatchSets;
+  }
+
   public String getSortKey() {
     return sortKey;
   }
@@ -446,6 +482,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);
   }
@@ -466,4 +511,20 @@
   public void setTopic(String topic) {
     this.topic = topic;
   }
+
+  public RevId getLastSha1MergeTested() {
+    return lastSha1MergeTested;
+  }
+
+  public void setLastSha1MergeTested(RevId lastSha1MergeTested) {
+    this.lastSha1MergeTested = lastSha1MergeTested;
+  }
+
+  public boolean isMergeable() {
+    return mergeable;
+  }
+
+  public void setMergeable(boolean mergeable) {
+    this.mergeable = mergeable;
+  }
 }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ChangeMessage.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/ChangeMessage.java
similarity index 84%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ChangeMessage.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/ChangeMessage.java
index 56c36c8..e05f5e7 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ChangeMessage.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/ChangeMessage.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb;
+package com.google.gerrit.reviewdb.client;
 
 import com.google.gwtorm.client.Column;
 import com.google.gwtorm.client.StringKey;
@@ -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/CodedEnum.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/CodedEnum.java
similarity index 94%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/CodedEnum.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/CodedEnum.java
index 5900292..11e7efa 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/CodedEnum.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/CodedEnum.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb;
+package com.google.gerrit.reviewdb.client;
 
 /** Extension of Enum which provides distinct character code values. */
 public interface CodedEnum {
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ContactInformation.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/ContactInformation.java
similarity index 97%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ContactInformation.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/ContactInformation.java
index d28467c..195387c 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ContactInformation.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/ContactInformation.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb;
+package com.google.gerrit.reviewdb.client;
 
 import com.google.gwtorm.client.Column;
 
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ContributorAgreement.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/ContributorAgreement.java
similarity index 95%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ContributorAgreement.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/ContributorAgreement.java
index 9e285b8..12e7459 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ContributorAgreement.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/ContributorAgreement.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb;
+package com.google.gerrit.reviewdb.client;
 
 import com.google.gwtorm.client.Column;
 import com.google.gwtorm.client.IntKey;
@@ -80,7 +80,8 @@
   /**
    * Create a new agreement.
    *
-   * @param newId unique id, see {@link ReviewDb#nextAccountId()}.
+   * @param newId unique id, see
+   *        {@link com.google.gerrit.reviewdb.server.ReviewDb#nextAccountId()}.
    * @param name a short title/name for the agreement.
    */
   public ContributorAgreement(final ContributorAgreement.Id newId,
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/CurrentSchemaVersion.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/CurrentSchemaVersion.java
similarity index 97%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/CurrentSchemaVersion.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/CurrentSchemaVersion.java
index 8878225..183bb1e 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/CurrentSchemaVersion.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/CurrentSchemaVersion.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb;
+package com.google.gerrit.reviewdb.client;
 
 import com.google.gwtorm.client.Column;
 import com.google.gwtorm.client.StringKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Patch.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Patch.java
similarity index 99%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Patch.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Patch.java
index 0153755..0d0ea88 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Patch.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Patch.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb;
+package com.google.gerrit.reviewdb.client;
 
 import com.google.gwtorm.client.Column;
 import com.google.gwtorm.client.StringKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/PatchLineComment.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchLineComment.java
similarity index 96%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/PatchLineComment.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchLineComment.java
index 0828c95..c8a970d 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/PatchLineComment.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchLineComment.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb;
+package com.google.gerrit.reviewdb.client;
 
 import com.google.gwtorm.client.Column;
 import com.google.gwtorm.client.StringKey;
@@ -55,8 +55,8 @@
     }
   }
 
-  protected static final char STATUS_DRAFT = 'd';
-  protected static final char STATUS_PUBLISHED = 'P';
+  public static final char STATUS_DRAFT = 'd';
+  public static final char STATUS_PUBLISHED = 'P';
 
   public static enum Status {
     DRAFT(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/client/PatchSet.java
similarity index 94%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/PatchSet.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSet.java
index c46a849..83ce828 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/PatchSet.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSet.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb;
+package com.google.gerrit.reviewdb.client;
 
 import com.google.gwtorm.client.Column;
 import com.google.gwtorm.client.IntKey;
@@ -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/PatchSetAncestor.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSetAncestor.java
similarity index 97%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/PatchSetAncestor.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSetAncestor.java
index 8310e4d..8412788 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/PatchSetAncestor.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSetAncestor.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb;
+package com.google.gerrit.reviewdb.client;
 
 import com.google.gwtorm.client.Column;
 import com.google.gwtorm.client.IntKey;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/PatchSetApproval.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSetApproval.java
similarity index 94%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/PatchSetApproval.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSetApproval.java
index 341d085..04b2b6f 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/PatchSetApproval.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSetApproval.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb;
+package com.google.gerrit.reviewdb.client;
 
 import com.google.gwtorm.client.Column;
 import com.google.gwtorm.client.CompoundKey;
@@ -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/PatchSetInfo.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSetInfo.java
similarity index 91%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/PatchSetInfo.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSetInfo.java
index 839dbfd..1a00fae 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/PatchSetInfo.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSetInfo.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb;
+package com.google.gerrit.reviewdb.client;
 
 import java.util.List;
 
@@ -51,6 +51,9 @@
   /** List of parents of the patch set. */
   protected List<ParentInfo> parents;
 
+  /** SHA-1 of commit */
+  protected String revId;
+
   protected PatchSetInfo() {
   }
 
@@ -105,4 +108,12 @@
   public List<ParentInfo> getParents() {
     return parents;
   }
+
+  public void setRevId(final String s) {
+    revId = s;
+  }
+
+  public String getRevId() {
+    return revId;
+  }
 }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Project.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Project.java
similarity index 62%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Project.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Project.java
index 409547a..a2fc46f 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/Project.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Project.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb;
+package com.google.gerrit.reviewdb.client;
 
 import com.google.gwtorm.client.Column;
 import com.google.gwtorm.client.StringKey;
@@ -21,7 +21,7 @@
 public final class Project {
   /** Project name key */
   public static class NameKey extends
-      StringKey<com.google.gwtorm.client.Key<?>> {
+      StringKey<com.google.gwtorm.client.Key<?>>{
     private static final long serialVersionUID = 1L;
 
     @Column(id = 1)
@@ -44,6 +44,19 @@
       name = newValue;
     }
 
+    @Override
+    public int hashCode() {
+      return get().hashCode();
+    }
+
+    @Override
+    public boolean equals(Object b) {
+      if (b instanceof NameKey) {
+        return get().equals(((NameKey) b).get());
+      }
+      return false;
+    }
+
     /** Parse a Project.NameKey out of a string representation. */
     public static NameKey parse(final String str) {
       final NameKey r = new NameKey();
@@ -53,65 +66,48 @@
   }
 
   public static enum SubmitType {
-    FAST_FORWARD_ONLY('F'),
+    FAST_FORWARD_ONLY,
 
-    MERGE_IF_NECESSARY('M'),
+    MERGE_IF_NECESSARY,
 
-    MERGE_ALWAYS('A'),
+    MERGE_ALWAYS,
 
-    CHERRY_PICK('C');
-
-    private final char code;
-
-    private SubmitType(final char c) {
-      code = c;
-    }
-
-    public char getCode() {
-      return code;
-    }
-
-    public static SubmitType forCode(final char c) {
-      for (final SubmitType s : SubmitType.values()) {
-        if (s.code == c) {
-          return s;
-        }
-      }
-      return null;
-    }
+    CHERRY_PICK;
   }
 
-  @Column(id = 1)
+  public static enum State {
+    ACTIVE,
+
+    READ_ONLY,
+
+    HIDDEN;
+  }
+
   protected NameKey name;
 
-  @Column(id = 2, length = Integer.MAX_VALUE, notNull = false)
   protected String description;
 
-  @Column(id = 3)
   protected boolean useContributorAgreements;
 
-  @Column(id = 4)
   protected boolean useSignedOffBy;
 
-  @Column(id = 5)
-  protected char submitType;
+  protected SubmitType submitType;
 
-  @Column(id = 6, notNull = false, name = "parent_name")
+  protected State state;
+
   protected NameKey parent;
 
-  @Column(id = 7)
   protected boolean requireChangeID;
 
-  @Column(id = 8)
   protected boolean useContentMerge;
 
   protected Project() {
   }
 
-  public Project(final Project.NameKey newName) {
-    name = newName;
-    useContributorAgreements = true;
-    setSubmitType(SubmitType.MERGE_IF_NECESSARY);
+  public Project(Project.NameKey nameKey) {
+    name = nameKey;
+    submitType = SubmitType.MERGE_IF_NECESSARY;
+    state = State.ACTIVE;
   }
 
   public Project.NameKey getNameKey() {
@@ -119,7 +115,7 @@
   }
 
   public String getName() {
-    return name.get();
+    return name != null ? name.get() : null;
   }
 
   public String getDescription() {
@@ -163,11 +159,19 @@
   }
 
   public SubmitType getSubmitType() {
-    return SubmitType.forCode(submitType);
+    return submitType;
   }
 
   public void setSubmitType(final SubmitType type) {
-    submitType = type.getCode();
+    submitType = type;
+  }
+
+  public State getState() {
+    return state;
+  }
+
+  public void setState(final State newState) {
+    state = newState;
   }
 
   public void copySettingsFrom(final Project update) {
@@ -177,13 +181,48 @@
     useContentMerge = update.useContentMerge;
     requireChangeID = update.requireChangeID;
     submitType = update.submitType;
+    state = update.state;
   }
 
+  /**
+   * Returns the name key of the parent project.
+   *
+   * @return name key of the parent project, <code>null</code> if this project
+   *         is the wild project, <code>null</code> or the name key of the wild
+   *         project if this project is a direct child of the wild project
+   */
   public Project.NameKey getParent() {
     return parent;
   }
 
-  public void setParent(final Project.NameKey parentProjectName) {
-      parent = parentProjectName;
+  /**
+   * Returns the name key of the parent project.
+   *
+   * @param allProjectsName name key of the wild project
+   * @return name key of the parent project, <code>null</code> if this project
+   *         is the wild project
+   */
+  public Project.NameKey getParent(final Project.NameKey allProjectsName) {
+    if (parent != null) {
+      return parent;
+    }
+
+    if (name.equals(allProjectsName)) {
+      return null;
+    }
+
+    return allProjectsName;
+  }
+
+  public String getParentName() {
+    return parent != null ? parent.get() : null;
+  }
+
+  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/RevId.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RevId.java
similarity index 96%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/RevId.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RevId.java
index 7077312..d90a81c 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/RevId.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/RevId.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb;
+package com.google.gerrit.reviewdb.client;
 
 import com.google.gwtorm.client.Column;
 
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/StarredChange.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/StarredChange.java
similarity index 90%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/StarredChange.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/StarredChange.java
index 7e4359b..d427b09 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/StarredChange.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/StarredChange.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb;
+package com.google.gerrit.reviewdb.client;
 
 import com.google.gwtorm.client.Column;
 import com.google.gwtorm.client.CompoundKey;
@@ -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/client/SubmoduleSubscription.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/SubmoduleSubscription.java
new file mode 100644
index 0000000..32a9131
--- /dev/null
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/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.client;
+
+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/SystemConfig.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/SystemConfig.java
similarity index 69%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/SystemConfig.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/SystemConfig.java
index 6ff23ed..0f8d005 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/SystemConfig.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/SystemConfig.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb;
+package com.google.gerrit.reviewdb.client;
 
 import com.google.gwtorm.client.Column;
 import com.google.gwtorm.client.StringKey;
@@ -52,39 +52,43 @@
   @Column(id = 1)
   protected Key singleton;
 
-  /** Private key to sign account identification cookies. */
-  @Column(id = 2, length = 36)
-  public transient String registerEmailPrivateKey;
-
   /**
    * Local filesystem location of header/footer/CSS configuration files
    */
   @Column(id = 3, notNull = false)
   public transient String sitePath;
 
-  /** Identity of the administration group; those with full access. */
-  @Column(id = 4)
+
+  // DO NOT LOOK BELOW THIS LINE. These fields have all been deleted,
+  // but survive to support schema upgrade code.
+
+  /** DEPRECATED DO NOT USE */
+  @Column(id = 2, length = 36, notNull = false)
+  public transient String registerEmailPrivateKey;
+  /** DEPRECATED DO NOT USE */
+  @Column(id = 4, notNull = false)
   public AccountGroup.Id adminGroupId;
-
-  /** Identity of the anonymous group, which permits anyone. */
-  @Column(id = 5)
+  /** DEPRECATED DO NOT USE */
+  @Column(id = 10, notNull = false)
+  public AccountGroup.UUID adminGroupUUID;
+  /** DEPRECATED DO NOT USE */
+  @Column(id = 5, notNull = false)
   public AccountGroup.Id anonymousGroupId;
-
-  /** Identity of the registered users group, which permits anyone. */
-  @Column(id = 6)
+  /** DEPRECATED DO NOT USE */
+  @Column(id = 6, notNull = false)
   public AccountGroup.Id registeredGroupId;
-
-  /** Identity of the project  */
-  @Column(id = 7)
+  /** DEPRECATED DO NOT USE */
+  @Column(id = 7, notNull = false)
   public Project.NameKey wildProjectName;
-
-  /** Identity of the batch users group */
-  @Column(id = 8)
-  public AccountGroup.Id batchUsersGroupId;
-
-  /** Identity of the owner group, which permits any project owner. */
-  @Column(id = 9)
+  /** DEPRECATED DO NOT USE */
+  @Column(id = 9, notNull = false)
   public AccountGroup.Id ownerGroupId;
+  /** DEPRECATED DO NOT USE */
+  @Column(id = 8, notNull = false)
+  public AccountGroup.Id batchUsersGroupId;
+  /** DEPRECATED DO NOT USE */
+  @Column(id = 11, notNull = false)
+  public AccountGroup.UUID batchUsersGroupUUID;
 
   protected SystemConfig() {
   }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/TrackingId.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/TrackingId.java
similarity index 93%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/TrackingId.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/TrackingId.java
index d59e492..e73dd73 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/TrackingId.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/TrackingId.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb;
+package com.google.gerrit.reviewdb.client;
 
 import com.google.gwtorm.client.Column;
 import com.google.gwtorm.client.CompoundKey;
@@ -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/java/com/google/gerrit/reviewdb/UserIdentity.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/UserIdentity.java
similarity index 97%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/UserIdentity.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/UserIdentity.java
index 6eabbda..c7b4cfe 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/UserIdentity.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/UserIdentity.java
@@ -12,7 +12,7 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb;
+package com.google.gerrit.reviewdb.client;
 
 import java.sql.Timestamp;
 
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountAccess.java
similarity index 77%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountAccess.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountAccess.java
index 3c81152..2b23b7a 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountAccess.java
@@ -12,13 +12,15 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb;
+package com.google.gerrit.reviewdb.server;
 
-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;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Account.Id;
+import com.google.gwtorm.server.Access;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.PrimaryKey;
+import com.google.gwtorm.server.Query;
+import com.google.gwtorm.server.ResultSet;
 
 /** Access interface for {@link Account}. */
 public interface AccountAccess extends Access<Account, Account.Id> {
@@ -42,4 +44,7 @@
 
   @Query("LIMIT 1")
   ResultSet<Account> anyAccounts() throws OrmException;
+
+  @Query("ORDER BY accountId LIMIT ?")
+  ResultSet<Account> firstNById(int n) throws OrmException;
 }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountAgreementAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountAgreementAccess.java
similarity index 66%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountAgreementAccess.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountAgreementAccess.java
index f65af5e..86ea372 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountAgreementAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountAgreementAccess.java
@@ -12,13 +12,17 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb;
+package com.google.gerrit.reviewdb.server;
 
-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;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountAgreement;
+import com.google.gerrit.reviewdb.client.Account.Id;
+import com.google.gerrit.reviewdb.client.AccountAgreement.Key;
+import com.google.gwtorm.server.Access;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.PrimaryKey;
+import com.google.gwtorm.server.Query;
+import com.google.gwtorm.server.ResultSet;
 
 public interface AccountAgreementAccess extends
     Access<AccountAgreement, AccountAgreement.Key> {
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountDiffPreferenceAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountDiffPreferenceAccess.java
similarity index 70%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountDiffPreferenceAccess.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountDiffPreferenceAccess.java
index d1d134b..8574758 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountDiffPreferenceAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountDiffPreferenceAccess.java
@@ -12,11 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb;
+package com.google.gerrit.reviewdb.server;
 
-import com.google.gwtorm.client.Access;
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.client.PrimaryKey;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountDiffPreference;
+import com.google.gerrit.reviewdb.client.Account.Id;
+import com.google.gwtorm.server.Access;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.PrimaryKey;
 
 public interface AccountDiffPreferenceAccess extends Access<AccountDiffPreference, Account.Id> {
 
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountExternalIdAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountExternalIdAccess.java
similarity index 74%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountExternalIdAccess.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountExternalIdAccess.java
index 0719035..b263552 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountExternalIdAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountExternalIdAccess.java
@@ -12,13 +12,17 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb;
+package com.google.gerrit.reviewdb.server;
 
-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;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountExternalId;
+import com.google.gerrit.reviewdb.client.Account.Id;
+import com.google.gerrit.reviewdb.client.AccountExternalId.Key;
+import com.google.gwtorm.server.Access;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.PrimaryKey;
+import com.google.gwtorm.server.Query;
+import com.google.gwtorm.server.ResultSet;
 
 public interface AccountExternalIdAccess extends
     Access<AccountExternalId, AccountExternalId.Key> {
@@ -42,4 +46,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/AccountGroupAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupAccess.java
similarity index 70%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGroupAccess.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupAccess.java
index 2530654..9e88244 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGroupAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupAccess.java
@@ -12,19 +12,23 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb;
+package com.google.gerrit.reviewdb.server;
 
-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;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gwtorm.server.Access;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.PrimaryKey;
+import com.google.gwtorm.server.Query;
+import com.google.gwtorm.server.ResultSet;
 
 public interface AccountGroupAccess extends
     Access<AccountGroup, AccountGroup.Id> {
   @PrimaryKey("groupId")
   AccountGroup get(AccountGroup.Id id) throws OrmException;
 
+  @Query("WHERE groupUUID = ?")
+  ResultSet<AccountGroup> byUUID(AccountGroup.UUID uuid) throws OrmException;
+
   @Query("WHERE externalName = ?")
   ResultSet<AccountGroup> byExternalName(AccountGroup.ExternalNameKey name)
       throws OrmException;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGroupAgreementAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupAgreementAccess.java
similarity index 66%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGroupAgreementAccess.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupAgreementAccess.java
index d4a1fcd..ecab70d 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGroupAgreementAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupAgreementAccess.java
@@ -12,13 +12,17 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb;
+package com.google.gerrit.reviewdb.server;
 
-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;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupAgreement;
+import com.google.gerrit.reviewdb.client.AccountGroup.Id;
+import com.google.gerrit.reviewdb.client.AccountGroupAgreement.Key;
+import com.google.gwtorm.server.Access;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.PrimaryKey;
+import com.google.gwtorm.server.Query;
+import com.google.gwtorm.server.ResultSet;
 
 public interface AccountGroupAgreementAccess extends
     Access<AccountGroupAgreement, AccountGroupAgreement.Key> {
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGroupIncludeAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeAccess.java
similarity index 68%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGroupIncludeAccess.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeAccess.java
index 4881635..c8c7b40 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGroupIncludeAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeAccess.java
@@ -12,13 +12,17 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb;
+package com.google.gerrit.reviewdb.server;
 
-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;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupInclude;
+import com.google.gerrit.reviewdb.client.AccountGroup.Id;
+import com.google.gerrit.reviewdb.client.AccountGroupInclude.Key;
+import com.google.gwtorm.server.Access;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.PrimaryKey;
+import com.google.gwtorm.server.Query;
+import com.google.gwtorm.server.ResultSet;
 
 public interface AccountGroupIncludeAccess extends
     Access<AccountGroupInclude, AccountGroupInclude.Key> {
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGroupIncludeAuditAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeAuditAccess.java
similarity index 67%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGroupIncludeAuditAccess.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeAuditAccess.java
index 55b50e8..3cf24f3 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGroupIncludeAuditAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeAuditAccess.java
@@ -12,13 +12,17 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb;
+package com.google.gerrit.reviewdb.server;
 
-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;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupIncludeAudit;
+import com.google.gerrit.reviewdb.client.AccountGroup.Id;
+import com.google.gerrit.reviewdb.client.AccountGroupIncludeAudit.Key;
+import com.google.gwtorm.server.Access;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.PrimaryKey;
+import com.google.gwtorm.server.Query;
+import com.google.gwtorm.server.ResultSet;
 
 public interface AccountGroupIncludeAuditAccess extends
     Access<AccountGroupIncludeAudit, AccountGroupIncludeAudit.Key> {
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGroupMemberAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupMemberAccess.java
similarity index 65%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGroupMemberAccess.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupMemberAccess.java
index 48a20e3..53ecaae 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGroupMemberAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupMemberAccess.java
@@ -12,13 +12,18 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb;
+package com.google.gerrit.reviewdb.server;
 
-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;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupMember;
+import com.google.gerrit.reviewdb.client.AccountGroup.Id;
+import com.google.gerrit.reviewdb.client.AccountGroupMember.Key;
+import com.google.gwtorm.server.Access;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.PrimaryKey;
+import com.google.gwtorm.server.Query;
+import com.google.gwtorm.server.ResultSet;
 
 public interface AccountGroupMemberAccess extends
     Access<AccountGroupMember, AccountGroupMember.Key> {
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGroupMemberAuditAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupMemberAuditAccess.java
similarity index 65%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGroupMemberAuditAccess.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupMemberAuditAccess.java
index b112059..f26996d 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGroupMemberAuditAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupMemberAuditAccess.java
@@ -12,13 +12,18 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb;
+package com.google.gerrit.reviewdb.server;
 
-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;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
+import com.google.gerrit.reviewdb.client.AccountGroup.Id;
+import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit.Key;
+import com.google.gwtorm.server.Access;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.PrimaryKey;
+import com.google.gwtorm.server.Query;
+import com.google.gwtorm.server.ResultSet;
 
 public interface AccountGroupMemberAuditAccess extends
     Access<AccountGroupMemberAudit, AccountGroupMemberAudit.Key> {
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGroupNameAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupNameAccess.java
similarity index 67%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGroupNameAccess.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupNameAccess.java
index 8a2fb6b..d0bc419 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountGroupNameAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupNameAccess.java
@@ -12,13 +12,16 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb;
+package com.google.gerrit.reviewdb.server;
 
-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;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupName;
+import com.google.gerrit.reviewdb.client.AccountGroup.NameKey;
+import com.google.gwtorm.server.Access;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.PrimaryKey;
+import com.google.gwtorm.server.Query;
+import com.google.gwtorm.server.ResultSet;
 
 
 public interface AccountGroupNameAccess extends
@@ -27,7 +30,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/server/AccountPatchReviewAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountPatchReviewAccess.java
new file mode 100644
index 0000000..ef8133b
--- /dev/null
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountPatchReviewAccess.java
@@ -0,0 +1,38 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.reviewdb.server;
+
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountPatchReview;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Account.Id;
+import com.google.gerrit.reviewdb.client.AccountPatchReview.Key;
+import com.google.gwtorm.server.Access;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.PrimaryKey;
+import com.google.gwtorm.server.Query;
+import com.google.gwtorm.server.ResultSet;
+
+public interface AccountPatchReviewAccess
+    extends Access<AccountPatchReview, AccountPatchReview.Key> {
+  @PrimaryKey("key")
+  AccountPatchReview get(AccountPatchReview.Key id) throws OrmException;
+
+  @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/AccountProjectWatchAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountProjectWatchAccess.java
similarity index 64%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountProjectWatchAccess.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountProjectWatchAccess.java
index 254aac9..046d5a5 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountProjectWatchAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountProjectWatchAccess.java
@@ -12,13 +12,19 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb;
+package com.google.gerrit.reviewdb.server;
 
-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;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.Account.Id;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch.Key;
+import com.google.gerrit.reviewdb.client.Project.NameKey;
+import com.google.gwtorm.server.Access;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.PrimaryKey;
+import com.google.gwtorm.server.Query;
+import com.google.gwtorm.server.ResultSet;
 
 public interface AccountProjectWatchAccess extends
     Access<AccountProjectWatch, AccountProjectWatch.Key> {
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountSshKeyAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountSshKeyAccess.java
similarity index 68%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountSshKeyAccess.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountSshKeyAccess.java
index 5c511b1..d756a44 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/AccountSshKeyAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountSshKeyAccess.java
@@ -12,13 +12,16 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb;
+package com.google.gerrit.reviewdb.server;
 
-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;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountSshKey;
+import com.google.gerrit.reviewdb.client.Account.Id;
+import com.google.gwtorm.server.Access;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.PrimaryKey;
+import com.google.gwtorm.server.Query;
+import com.google.gwtorm.server.ResultSet;
 
 public interface AccountSshKeyAccess extends
     Access<AccountSshKey, AccountSshKey.Id> {
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ApprovalCategoryAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ApprovalCategoryAccess.java
similarity index 70%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ApprovalCategoryAccess.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ApprovalCategoryAccess.java
index d3601f2..31e6c62 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ApprovalCategoryAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ApprovalCategoryAccess.java
@@ -12,13 +12,15 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb;
+package com.google.gerrit.reviewdb.server;
 
-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;
+import com.google.gerrit.reviewdb.client.ApprovalCategory;
+import com.google.gerrit.reviewdb.client.ApprovalCategory.Id;
+import com.google.gwtorm.server.Access;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.PrimaryKey;
+import com.google.gwtorm.server.Query;
+import com.google.gwtorm.server.ResultSet;
 
 public interface ApprovalCategoryAccess extends
     Access<ApprovalCategory, ApprovalCategory.Id> {
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ApprovalCategoryValueAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ApprovalCategoryValueAccess.java
similarity index 69%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ApprovalCategoryValueAccess.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ApprovalCategoryValueAccess.java
index e86d652..acdda5e8 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ApprovalCategoryValueAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ApprovalCategoryValueAccess.java
@@ -12,13 +12,16 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb;
+package com.google.gerrit.reviewdb.server;
 
-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;
+import com.google.gerrit.reviewdb.client.ApprovalCategory;
+import com.google.gerrit.reviewdb.client.ApprovalCategoryValue;
+import com.google.gerrit.reviewdb.client.ApprovalCategory.Id;
+import com.google.gwtorm.server.Access;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.PrimaryKey;
+import com.google.gwtorm.server.Query;
+import com.google.gwtorm.server.ResultSet;
 
 public interface ApprovalCategoryValueAccess extends
     Access<ApprovalCategoryValue, ApprovalCategoryValue.Id> {
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ChangeAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ChangeAccess.java
similarity index 78%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ChangeAccess.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ChangeAccess.java
index 9c4605e..4660291 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ChangeAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ChangeAccess.java
@@ -12,13 +12,20 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb;
+package com.google.gerrit.reviewdb.server;
 
-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;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.Change.Id;
+import com.google.gerrit.reviewdb.client.Change.Key;
+import com.google.gerrit.reviewdb.client.Project.NameKey;
+import com.google.gwtorm.server.Access;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.PrimaryKey;
+import com.google.gwtorm.server.Query;
+import com.google.gwtorm.server.ResultSet;
 
 public interface ChangeAccess extends Access<Change, Change.Id> {
   @PrimaryKey("changeId")
@@ -94,6 +101,16 @@
   ResultSet<Change> allClosedNext(char status, String sortKey, int limit)
       throws OrmException;
 
+  @Query("WHERE open = false AND status = ? AND dest = ? AND sortKey > ?"
+      + " ORDER BY sortKey LIMIT ?")
+  ResultSet<Change> byBranchClosedPrev(char status, Branch.NameKey p,
+      String sortKey, int limit) throws OrmException;
+
+  @Query("WHERE open = false AND status = ? AND dest = ? AND sortKey < ?"
+      + " ORDER BY sortKey DESC LIMIT ?")
+  ResultSet<Change> byBranchClosedNext(char status, Branch.NameKey p,
+      String sortKey, int limit) throws OrmException;
+
   @Query
   ResultSet<Change> all() throws OrmException;
 }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ChangeMessageAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ChangeMessageAccess.java
new file mode 100644
index 0000000..6db2675
--- /dev/null
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ChangeMessageAccess.java
@@ -0,0 +1,41 @@
+// Copyright (C) 2008 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.reviewdb.server;
+
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Change.Id;
+import com.google.gerrit.reviewdb.client.ChangeMessage.Key;
+import com.google.gwtorm.server.Access;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.PrimaryKey;
+import com.google.gwtorm.server.Query;
+import com.google.gwtorm.server.ResultSet;
+
+public interface ChangeMessageAccess extends
+    Access<ChangeMessage, ChangeMessage.Key> {
+  @PrimaryKey("key")
+  ChangeMessage get(ChangeMessage.Key id) throws OrmException;
+
+  @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/ContributorAgreementAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ContributorAgreementAccess.java
similarity index 72%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ContributorAgreementAccess.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ContributorAgreementAccess.java
index ae7b41d..3c7f47a 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ContributorAgreementAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ContributorAgreementAccess.java
@@ -12,13 +12,15 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb;
+package com.google.gerrit.reviewdb.server;
 
-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;
+import com.google.gerrit.reviewdb.client.ContributorAgreement;
+import com.google.gerrit.reviewdb.client.ContributorAgreement.Id;
+import com.google.gwtorm.server.Access;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.PrimaryKey;
+import com.google.gwtorm.server.Query;
+import com.google.gwtorm.server.ResultSet;
 
 /** Access interface for {@link ContributorAgreement}. */
 public interface ContributorAgreementAccess extends
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/PatchLineCommentAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchLineCommentAccess.java
similarity index 64%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/PatchLineCommentAccess.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchLineCommentAccess.java
index 26785a8..3be8563 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/PatchLineCommentAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchLineCommentAccess.java
@@ -12,13 +12,19 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb;
+package com.google.gerrit.reviewdb.server;
 
-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;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Change.Id;
+import com.google.gerrit.reviewdb.client.PatchLineComment.Key;
+import com.google.gwtorm.server.Access;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.PrimaryKey;
+import com.google.gwtorm.server.Query;
+import com.google.gwtorm.server.ResultSet;
 
 public interface PatchLineCommentAccess extends
     Access<PatchLineComment, PatchLineComment.Key> {
@@ -28,37 +34,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/PatchSetAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAccess.java
similarity index 71%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/PatchSetAccess.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAccess.java
index fa594f7..c04fa06 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/PatchSetAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAccess.java
@@ -12,13 +12,17 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb;
+package com.google.gerrit.reviewdb.server;
 
-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;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.reviewdb.client.Change.Id;
+import com.google.gwtorm.server.Access;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.PrimaryKey;
+import com.google.gwtorm.server.Query;
+import com.google.gwtorm.server.ResultSet;
 
 public interface PatchSetAccess extends Access<PatchSet, PatchSet.Id> {
   @PrimaryKey("id")
@@ -27,10 +31,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/server/PatchSetAncestorAccess.java
similarity index 64%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/PatchSetAncestorAccess.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAncestorAccess.java
index eeea372..e163bc4 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/PatchSetAncestorAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetAncestorAccess.java
@@ -12,13 +12,17 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb;
+package com.google.gerrit.reviewdb.server;
 
-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;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetAncestor;
+import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.reviewdb.client.PatchSetAncestor.Id;
+import com.google.gwtorm.server.Access;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.PrimaryKey;
+import com.google.gwtorm.server.Query;
+import com.google.gwtorm.server.ResultSet;
 
 public interface PatchSetAncestorAccess extends
     Access<PatchSetAncestor, PatchSetAncestor.Id> {
@@ -28,6 +32,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/PatchSetApprovalAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetApprovalAccess.java
similarity index 74%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/PatchSetApprovalAccess.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetApprovalAccess.java
index 417d264..b30c5bfa 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/PatchSetApprovalAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/PatchSetApprovalAccess.java
@@ -12,13 +12,19 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb;
+package com.google.gerrit.reviewdb.server;
 
-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;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.Change.Id;
+import com.google.gerrit.reviewdb.client.PatchSetApproval.Key;
+import com.google.gwtorm.server.Access;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.PrimaryKey;
+import com.google.gwtorm.server.Query;
+import com.google.gwtorm.server.ResultSet;
 
 public interface PatchSetApprovalAccess extends
     Access<PatchSetApproval, PatchSetApproval.Key> {
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ReviewDb.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDb.java
similarity index 72%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ReviewDb.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDb.java
index 49e07ff..616a656 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/ReviewDb.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDb.java
@@ -12,19 +12,23 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb;
+package com.google.gerrit.reviewdb.server;
 
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.client.Relation;
-import com.google.gwtorm.client.Schema;
-import com.google.gwtorm.client.Sequence;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.SystemConfig;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.Relation;
+import com.google.gwtorm.server.Schema;
+import com.google.gwtorm.server.Sequence;
 
 /**
  * The review service database schema.
  * <p>
  * Root entities that are at the top level of some important data graph:
  * <ul>
- * <li>{@link Project}: Configuration for a single Git repository.</li>
  * <li>{@link Account}: Per-user account registration, preferences, identity.</li>
  * <li>{@link Change}: All review information about a single proposed change.</li>
  * <li>{@link SystemConfig}: Server-wide settings, managed by administrator.</li>
@@ -33,101 +37,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
-  ProjectAccess projects();
-
-  @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
-  RefRightAccess refRights();
-
-  @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/SchemaVersionAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SchemaVersionAccess.java
similarity index 74%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/SchemaVersionAccess.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SchemaVersionAccess.java
index eec1643..07f255c 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/SchemaVersionAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SchemaVersionAccess.java
@@ -12,11 +12,13 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb;
+package com.google.gerrit.reviewdb.server;
 
-import com.google.gwtorm.client.Access;
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.client.PrimaryKey;
+import com.google.gerrit.reviewdb.client.CurrentSchemaVersion;
+import com.google.gerrit.reviewdb.client.CurrentSchemaVersion.Key;
+import com.google.gwtorm.server.Access;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.PrimaryKey;
 
 /** Access interface for {@link CurrentSchemaVersion}. */
 public interface SchemaVersionAccess extends
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/StarredChangeAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/StarredChangeAccess.java
similarity index 65%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/StarredChangeAccess.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/StarredChangeAccess.java
index 9e31dbf..88e703a 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/StarredChangeAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/StarredChangeAccess.java
@@ -12,13 +12,18 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb;
+package com.google.gerrit.reviewdb.server;
 
-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;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.StarredChange;
+import com.google.gerrit.reviewdb.client.Change.Id;
+import com.google.gerrit.reviewdb.client.StarredChange.Key;
+import com.google.gwtorm.server.Access;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.PrimaryKey;
+import com.google.gwtorm.server.Query;
+import com.google.gwtorm.server.ResultSet;
 
 public interface StarredChangeAccess extends
     Access<StarredChange, StarredChange.Key> {
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SubmoduleSubscriptionAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SubmoduleSubscriptionAccess.java
new file mode 100644
index 0000000..e1aa907
--- /dev/null
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SubmoduleSubscriptionAccess.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.reviewdb.server;
+
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.SubmoduleSubscription;
+import com.google.gerrit.reviewdb.client.Branch.NameKey;
+import com.google.gerrit.reviewdb.client.SubmoduleSubscription.Key;
+import com.google.gwtorm.server.Access;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.PrimaryKey;
+import com.google.gwtorm.server.Query;
+import com.google.gwtorm.server.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/SystemConfigAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SystemConfigAccess.java
similarity index 71%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/SystemConfigAccess.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SystemConfigAccess.java
index dfca1ca..3bb07cb 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/SystemConfigAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SystemConfigAccess.java
@@ -12,13 +12,15 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb;
+package com.google.gerrit.reviewdb.server;
 
-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;
+import com.google.gerrit.reviewdb.client.SystemConfig;
+import com.google.gerrit.reviewdb.client.SystemConfig.Key;
+import com.google.gwtorm.server.Access;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.PrimaryKey;
+import com.google.gwtorm.server.Query;
+import com.google.gwtorm.server.ResultSet;
 
 /** Access interface for {@link SystemConfig}. */
 public interface SystemConfigAccess extends
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/TrackingIdAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/TrackingIdAccess.java
similarity index 68%
rename from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/TrackingIdAccess.java
rename to gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/TrackingIdAccess.java
index ad851a4..51babb3 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/TrackingIdAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/TrackingIdAccess.java
@@ -12,13 +12,17 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb;
+package com.google.gerrit.reviewdb.server;
 
-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;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.TrackingId;
+import com.google.gerrit.reviewdb.client.Change.Id;
+import com.google.gerrit.reviewdb.client.TrackingId.Key;
+import com.google.gwtorm.server.Access;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.PrimaryKey;
+import com.google.gwtorm.server.Query;
+import com.google.gwtorm.server.ResultSet;
 
 public interface TrackingIdAccess extends Access<TrackingId, TrackingId.Key> {
   @PrimaryKey("key")
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/server/index_generic.sql
similarity index 93%
rename from gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/index_generic.sql
rename to gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql
index d33d24d..5d0e15c 100644
--- a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/index_generic.sql
+++ b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql
@@ -109,6 +109,10 @@
 CREATE INDEX changes_allClosed
 ON changes (open, status, sort_key);
 
+--    covers:             byBranchClosedPrev, byBranchClosedNext
+CREATE INDEX changes_byBranchClosed
+ON changes (status, dest_project_name, dest_branch_name, sort_key);
+
 CREATE INDEX changes_key
 ON changes (change_key);
 
@@ -158,14 +162,6 @@
 
 
 -- *********************************************************************
--- RefRightAccess
---    @PrimaryKey covers: byProject
---    covers:             byCategoryGroup
-CREATE INDEX ref_rights_byCatGroup
-ON ref_rights (category_id, group_id);
-
-
--- *********************************************************************
 -- TrackingIdAccess
 --
 CREATE INDEX tracking_ids_byTrkId
@@ -178,3 +174,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/server/index_postgres.sql
similarity index 94%
rename from gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/index_postgres.sql
rename to gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
index 8e3cead..97ad126 100644
--- a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/index_postgres.sql
+++ b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
@@ -187,6 +187,11 @@
 CREATE INDEX changes_byProject
 ON changes (dest_project_name);
 
+--    covers:             byBranchClosedPrev, byBranchClosedNext
+CREATE INDEX changes_byBranchClosed
+ON changes (status, dest_project_name, dest_branch_name, sort_key)
+WHERE open = 'N';
+
 CREATE INDEX changes_key
 ON changes (change_key);
 
@@ -240,14 +245,6 @@
 
 
 -- *********************************************************************
--- RefRightAccess
---    @PrimaryKey covers: byProject
---    covers:             byCategoryGroup
-CREATE INDEX ref_rights_byCatGroup
-ON ref_rights (category_id, group_id);
-
-
--- *********************************************************************
 -- TrackingIdAccess
 --
 CREATE INDEX tracking_ids_byTrkId
@@ -260,3 +257,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/mysql_nextval.sql b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/mysql_nextval.sql
similarity index 81%
rename from gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/mysql_nextval.sql
rename to gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/mysql_nextval.sql
index aee1021..2479010 100644
--- a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/mysql_nextval.sql
+++ b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/mysql_nextval.sql
@@ -9,7 +9,6 @@
   MODIFIES SQL DATA
 BEGIN
   INSERT INTO account_id (s) VALUES (NULL);
-  DELETE FROM account_id WHERE s = LAST_INSERT_ID();
   RETURN LAST_INSERT_ID();
 END;
 //
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/.settings/org.eclipse.core.resources.prefs b/gerrit-server/.settings/org.eclipse.core.resources.prefs
index 82eb859..7d5f965 100644
--- a/gerrit-server/.settings/org.eclipse.core.resources.prefs
+++ b/gerrit-server/.settings/org.eclipse.core.resources.prefs
@@ -1,3 +1,7 @@
-#Tue Sep 02 16:59:24 PDT 2008
+#Thu Jul 28 11:02:36 PDT 2011
 eclipse.preferences.version=1
+encoding//src/main/java=UTF-8
+encoding//src/main/resources=UTF-8
+encoding//src/test/java=UTF-8
+encoding//src/test/resources=UTF-8
 encoding/<project>=UTF-8
diff --git a/gerrit-server/pom.xml b/gerrit-server/pom.xml
index e2370c5..58e43cf 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.1-SNAPSHOT</version>
+    <version>2.4-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>
@@ -95,17 +90,17 @@
     </dependency>
 
     <dependency>
-      <groupId>com.google.code.guice</groupId>
+      <groupId>com.google.inject</groupId>
       <artifactId>guice</artifactId>
     </dependency>
 
     <dependency>
-      <groupId>com.google.code.guice</groupId>
+      <groupId>com.google.inject.extensions</groupId>
       <artifactId>guice-servlet</artifactId>
     </dependency>
 
     <dependency>
-      <groupId>com.google.code.guice</groupId>
+      <groupId>com.google.inject.extensions</groupId>
       <artifactId>guice-assistedinject</artifactId>
     </dependency>
 
@@ -128,6 +123,12 @@
 
     <dependency>
       <groupId>com.google.gerrit</groupId>
+      <artifactId>gerrit-util-cli</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>com.google.gerrit</groupId>
       <artifactId>gerrit-util-ssl</artifactId>
       <version>${project.version}</version>
     </dependency>
@@ -158,5 +159,75 @@
       <groupId>dk.brics.automaton</groupId>
       <artifactId>automaton</artifactId>
     </dependency>
+
+    <dependency>
+      <groupId>com.googlecode.prolog-cafe</groupId>
+      <artifactId>PrologCafe</artifactId>
+    </dependency>
   </dependencies>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-antrun-plugin</artifactId>
+        <executions>
+          <execution>
+            <id>prolog-to-java</id>
+            <phase>generate-sources</phase>
+            <goals>
+              <goal>run</goal>
+            </goals>
+            <configuration>
+              <target>
+                <property name="gensrc" location="${project.build.directory}/generated-sources"/>
+
+                <java classname="com.googlecode.prolog_cafe.compiler.Compiler"
+                    fork="true"
+                    failonerror="true"
+                    classpathref="maven.compile.classpath">
+                  <arg value="--show-stack-trace"/>
+                  <arg value="-O"/>
+                  <arg value="-am"/><arg value="${gensrc}/prolog-am"/>
+                  <arg value="-s" /><arg value="${gensrc}/prolog-java"/>
+                  <arg value="src/main/prolog/gerrit_common.pl"/>
+                </java>
+              </target>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+
+      <plugin>
+        <groupId>org.codehaus.mojo</groupId>
+        <artifactId>build-helper-maven-plugin</artifactId>
+        <executions>
+          <execution>
+            <id>add-source</id>
+            <phase>generate-sources</phase>
+            <goals>
+              <goal>add-source</goal>
+            </goals>
+            <configuration>
+              <sources>
+                <source>${project.build.directory}/generated-sources/prolog-java</source>
+              </sources>
+            </configuration>
+          </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 1a4a863..31837b0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHookRunner.java
@@ -16,16 +16,19 @@
 
 import com.google.gerrit.common.data.ApprovalType;
 import com.google.gerrit.common.data.ApprovalTypes;
-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.PatchSet;
-import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.ApprovalCategory;
+import com.google.gerrit.reviewdb.client.ApprovalCategoryValue;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.ContributorAgreement;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 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;
@@ -42,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.server.OrmException;
+import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -63,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;
@@ -103,6 +114,11 @@
     /** Filename of the ref updated hook. */
     private final File refUpdatedHook;
 
+    /** Filename of the cla signed hook. */
+    private final File claSignedHook;
+
+    private final String anonymousCowardName;
+
     /** Repository Manager. */
     private final GitRepositoryManager repoManager;
 
@@ -129,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;
@@ -149,6 +166,7 @@
         changeAbandonedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "changeAbandonedHook", "change-abandoned")).getPath());
         changeRestoredHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "changeRestoredHook", "change-restored")).getPath());
         refUpdatedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "refUpdatedHook", "ref-updated")).getPath());
+        claSignedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "claSignedHook", "cla-signed")).getPath());
     }
 
     public void addChangeListener(ChangeListener listener, IdentifiedUser user) {
@@ -205,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);
@@ -229,19 +242,12 @@
         addArg(args, "--commit", event.patchSet.revision);
         addArg(args, "--patchset", event.patchSet.number);
 
-        runHook(openRepository(change), patchsetCreatedHook, args);
+        runHook(change.getProject(), 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);
@@ -257,7 +263,7 @@
             }
         }
 
-        fireEvent(change, event);
+        fireEvent(change, event, db);
 
         final List<String> args = new ArrayList<String>();
         addArg(args, "--change", event.change.id);
@@ -271,23 +277,17 @@
             addArg(args, "--" + approval.getKey().get(), Short.toString(approval.getValue().get()));
         }
 
-        runHook(openRepository(change), commentAddedHook, args);
+        runHook(change.getProject(), 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);
@@ -297,23 +297,17 @@
         addArg(args, "--submitter", getDisplayName(account));
         addArg(args, "--commit", event.patchSet.revision);
 
-        runHook(openRepository(change), changeMergedHook, args);
+        runHook(change.getProject(), 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);
@@ -323,23 +317,17 @@
         addArg(args, "--abandoner", getDisplayName(account));
         addArg(args, "--reason", reason == null ? "" : reason);
 
-        runHook(openRepository(change), changeAbandonedHook, args);
+        runHook(change.getProject(), 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);
@@ -349,26 +337,13 @@
         addArg(args, "--restorer", getDisplayName(account));
         addArg(args, "--reason", reason == null ? "" : reason);
 
-        runHook(openRepository(change), changeRestoredHook, args);
+        runHook(change.getProject(), 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();
 
@@ -387,12 +362,23 @@
         addArg(args, "--submitter", getDisplayName(account));
       }
 
-      runHook(openRepository(refName.getParentKey()), refUpdatedHook, args);
+      runHook(refName.getParentKey(), refUpdatedHook, args);
     }
 
-    private void fireEvent(final Change change, final ChangeEvent event) {
+    public void doClaSignupHook(Account account, ContributorAgreement cla) {
+      if (account != null) {
+        final List<String> args = new ArrayList<String>();
+        addArg(args, "--submitter", getDisplayName(account));
+        addArg(args, "--user-id", account.getId().toString());
+        addArg(args, "--cla-id", cla.getId().toString());
+
+        runHook(claSignedHook, args);
+      }
+    }
+
+    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);
           }
       }
@@ -406,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) {
@@ -433,8 +419,10 @@
             Entry<ApprovalCategory.Id, ApprovalCategoryValue.Id> approval) {
         ApprovalAttribute a = new ApprovalAttribute();
         a.type = approval.getKey().get();
-        final ApprovalType at = approvalTypes.getApprovalType(approval.getKey());
-        a.description = at.getCategory().getName();
+        ApprovalType at = approvalTypes.byId(approval.getKey());
+        if (at != null) {
+          a.description = at.getCategory().getName();
+        }
         a.value = Short.toString(approval.getValue().get());
         return a;
     }
@@ -447,47 +435,50 @@
      */
     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;
     }
 
   /**
    * Run a hook.
    *
-   * @param repo repository to run the hook for.
+   * @param project used to open repository to run the hook for.
    * @param hook the hook to execute.
    * @param args Arguments to use to run the hook.
    */
-  private synchronized void runHook(Repository repo, File hook,
+  private synchronized void runHook(Project.NameKey project, File hook,
       List<String> args) {
-    if (repo != null) {
-      if (hook.exists()) {
-        hookQueue.execute(new HookTask(repo, hook, args));
-      } else {
-        repo.close();
-      }
+    if (project != null && hook.exists()) {
+      hookQueue.execute(new HookTask(project, hook, args));
+    }
+  }
+
+  private synchronized void runHook(File hook, List<String> args) {
+    if (hook.exists()) {
+      hookQueue.execute(new HookTask(null, hook, args));
     }
   }
 
   private final class HookTask implements Runnable {
-    private final Repository repo;
+    private final Project.NameKey project;
     private final File hook;
     private final List<String> args;
 
-    private HookTask(Repository repo, File hook, List<String> args) {
-      this.repo = repo;
+    private HookTask(Project.NameKey project, File hook, List<String> args) {
+      this.project = project;
       this.hook = hook;
       this.args = args;
     }
 
     @Override
     public void run() {
+      Repository repo = null;
       try {
         final List<String> argv = new ArrayList<String>(1 + args.size());
         argv.add(hook.getAbsolutePath());
@@ -495,10 +486,17 @@
 
         final ProcessBuilder pb = new ProcessBuilder(argv);
         pb.redirectErrorStream(true);
-        pb.directory(repo.getDirectory());
 
-        final Map<String, String> env = pb.environment();
-        env.put("GIT_DIR", repo.getDirectory().getAbsolutePath());
+        if (project != null) {
+          repo = openRepository(project);
+        }
+
+        if (repo != null) {
+          pb.directory(repo.getDirectory());
+
+          final Map<String, String> env = pb.environment();
+          env.put("GIT_DIR", repo.getDirectory().getAbsolutePath());
+        }
 
         Process ps = pb.start();
         ps.getOutputStream().close();
@@ -520,7 +518,9 @@
       } catch (Throwable err) {
         log.error("Error running hook " + hook.getAbsolutePath(), err);
       } finally {
-        repo.close();
+        if (repo != null) {
+          repo.close();
+        }
       }
     }
 
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..c424a26
--- /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.client.Account;
+import com.google.gerrit.reviewdb.client.ApprovalCategory;
+import com.google.gerrit.reviewdb.client.ApprovalCategoryValue;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.ContributorAgreement;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gwtorm.server.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..5544216
--- /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.client.Account;
+import com.google.gerrit.reviewdb.client.ApprovalCategory;
+import com.google.gerrit.reviewdb.client.ApprovalCategoryValue;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.ContributorAgreement;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Branch.NameKey;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+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/PrologCompiler.java b/gerrit-server/src/main/java/com/google/gerrit/rules/PrologCompiler.java
new file mode 100644
index 0000000..f9aac59
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/rules/PrologCompiler.java
@@ -0,0 +1,318 @@
+// 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.rules;
+
+import com.google.gerrit.common.Version;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import com.googlecode.prolog_cafe.compiler.CompileException;
+import com.googlecode.prolog_cafe.compiler.Compiler;
+
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Locale;
+import java.util.concurrent.Callable;
+import java.util.jar.Attributes;
+import java.util.jar.JarEntry;
+import java.util.jar.JarOutputStream;
+import java.util.jar.Manifest;
+
+import javax.tools.Diagnostic;
+import javax.tools.DiagnosticCollector;
+import javax.tools.JavaCompiler;
+import javax.tools.JavaFileObject;
+import javax.tools.StandardJavaFileManager;
+import javax.tools.ToolProvider;
+
+/**
+ * Helper class for Rulec: does the actual prolog -> java src -> class -> jar work
+ * Finds rules.pl in refs/meta/config branch
+ * Creates rules-(sha1 of rules.pl).jar in (site-path)/cache/rules
+ */
+public class PrologCompiler implements Callable<PrologCompiler.Status> {
+  public interface Factory {
+    PrologCompiler create(Repository git);
+  }
+
+  public static enum Status {
+    NO_RULES, COMPILED;
+  }
+
+  private final File ruleDir;
+  private final Repository git;
+
+  @Inject
+  PrologCompiler(@GerritServerConfig Config config, SitePaths site,
+      @Assisted Repository gitRepository) {
+    File cacheDir = site.resolve(config.getString("cache", null, "directory"));
+    ruleDir = cacheDir != null ? new File(cacheDir, "rules") : null;
+    git = gitRepository;
+  }
+
+  public Status call() throws IOException, CompileException {
+    ObjectId metaConfig = git.resolve(GitRepositoryManager.REF_CONFIG);
+    if (metaConfig == null) {
+      return Status.NO_RULES;
+    }
+
+    ObjectId rulesId = git.resolve(metaConfig.name() + ":rules.pl");
+    if (rulesId == null) {
+      return Status.NO_RULES;
+    }
+
+    if (ruleDir == null) {
+      throw new CompileException("Caching not enabled");
+    }
+    if (!ruleDir.isDirectory() && !ruleDir.mkdir()) {
+      throw new IOException("Cannot create " + ruleDir);
+    }
+
+    File tempDir = File.createTempFile("GerritCodeReview_", ".rulec");
+    if (!tempDir.delete() || !tempDir.mkdir()) {
+      throw new IOException("Cannot create " + tempDir);
+    }
+    try {
+      // Try to make the directory accessible only by this process.
+      // This may help to prevent leaking rule data to outsiders.
+      tempDir.setReadable(true, true);
+      tempDir.setWritable(true, true);
+      tempDir.setExecutable(true, true);
+
+      compileProlog(rulesId, tempDir);
+      compileJava(tempDir);
+
+      File jarFile = new File(ruleDir, "rules-" + rulesId.getName() + ".jar");
+      List<String> classFiles = getRelativePaths(tempDir, ".class");
+      createJar(jarFile, classFiles, tempDir, metaConfig, rulesId);
+
+      return Status.COMPILED;
+    } finally {
+      deleteAllFiles(tempDir);
+    }
+  }
+
+  /** Creates a copy of rules.pl and compiles it into Java sources. */
+  private void compileProlog(ObjectId prolog, File tempDir)
+      throws IOException, CompileException {
+    File tempRules = copyToTempFile(prolog, tempDir);
+    try {
+      Compiler comp = new Compiler();
+      comp.prologToJavaSource(tempRules.getPath(), tempDir.getPath());
+    } finally {
+      tempRules.delete();
+    }
+  }
+
+  private File copyToTempFile(ObjectId blobId, File tempDir)
+      throws IOException, FileNotFoundException, MissingObjectException {
+    // Any leak of tmp caused by this method failing will be cleaned
+    // up by our caller when tempDir is recursively deleted.
+    File tmp = File.createTempFile("rules", ".pl", tempDir);
+    FileOutputStream out = new FileOutputStream(tmp);
+    try {
+      git.open(blobId).copyTo(out);
+    } finally {
+      out.close();
+    }
+    return tmp;
+  }
+
+  /** Compile java src into java .class files */
+  private void compileJava(File tempDir) throws IOException, CompileException {
+    JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
+    if (compiler == null) {
+      throw new CompileException("JDK required (running inside of JRE)");
+    }
+
+    DiagnosticCollector<JavaFileObject> diagnostics =
+        new DiagnosticCollector<JavaFileObject>();
+    StandardJavaFileManager fileManager =
+        compiler.getStandardFileManager(diagnostics, null, null);
+    try {
+      Iterable<? extends JavaFileObject> compilationUnits = fileManager
+        .getJavaFileObjectsFromFiles(getAllFiles(tempDir, ".java"));
+      ArrayList<String> options = new ArrayList<String>();
+      String classpath = getMyClasspath();
+      if (classpath != null) {
+        options.add("-classpath");
+        options.add(classpath);
+      }
+      options.add("-d");
+      options.add(tempDir.getPath());
+      JavaCompiler.CompilationTask task = compiler.getTask(
+          null,
+          fileManager,
+          diagnostics,
+          options,
+          null,
+          compilationUnits);
+      if (!task.call()) {
+        Locale myLocale = Locale.getDefault();
+        StringBuilder msg = new StringBuilder();
+        msg.append("Cannot compile to Java bytecode:");
+        for (Diagnostic<? extends JavaFileObject> err : diagnostics.getDiagnostics()) {
+          msg.append('\n');
+          msg.append(err.getKind());
+          msg.append(": ");
+          if (err.getSource() != null) {
+            msg.append(err.getSource().getName());
+          }
+          msg.append(':');
+          msg.append(err.getLineNumber());
+          msg.append(": ");
+          msg.append(err.getMessage(myLocale));
+        }
+        throw new CompileException(msg.toString());
+      }
+    } finally {
+      fileManager.close();
+    }
+  }
+
+  private String getMyClasspath() {
+    StringBuilder cp = new StringBuilder();
+    appendClasspath(cp, getClass().getClassLoader());
+    return 0 < cp.length() ? cp.toString() : null;
+  }
+
+  private void appendClasspath(StringBuilder cp, ClassLoader classLoader) {
+    if (classLoader.getParent() != null) {
+      appendClasspath(cp, classLoader.getParent());
+    }
+    if (classLoader instanceof URLClassLoader) {
+      for (URL url : ((URLClassLoader) classLoader).getURLs()) {
+        if ("file".equals(url.getProtocol())) {
+          if (0 < cp.length()) {
+            cp.append(File.pathSeparatorChar);
+          }
+          cp.append(url.getPath());
+        }
+      }
+    }
+  }
+
+  /** Takes compiled prolog .class files, puts them into the jar file. */
+  private void createJar(File archiveFile, List<String> toBeJared,
+      File tempDir, ObjectId metaConfig, ObjectId rulesId) throws IOException {
+    long now = System.currentTimeMillis();
+    File tmpjar = File.createTempFile(".rulec_", ".jar", archiveFile.getParentFile());
+    try {
+      Manifest mf = new Manifest();
+      mf.getMainAttributes().put(Attributes.Name.MANIFEST_VERSION, "1.0");
+      mf.getMainAttributes().putValue("Built-by", "Gerrit Code Review " + Version.getVersion());
+      if (git.getDirectory() != null) {
+        mf.getMainAttributes().putValue("Source-Repository", git.getDirectory().getPath());
+      }
+      mf.getMainAttributes().putValue("Source-Commit", metaConfig.name());
+      mf.getMainAttributes().putValue("Source-Blob", rulesId.name());
+
+      FileOutputStream stream = new FileOutputStream(tmpjar);
+      JarOutputStream out = new JarOutputStream(stream, mf);
+      byte buffer[] = new byte[10240];
+      try {
+        for (String path : toBeJared) {
+          JarEntry jarAdd = new JarEntry(path);
+          File f = new File(tempDir, path);
+          jarAdd.setTime(now);
+          out.putNextEntry(jarAdd);
+          if (f.isFile()) {
+            FileInputStream in = new FileInputStream(f);
+            try {
+              while (true) {
+                int nRead = in.read(buffer, 0, buffer.length);
+                if (nRead <= 0) {
+                  break;
+                }
+                out.write(buffer, 0, nRead);
+              }
+            } finally {
+              in.close();
+            }
+          }
+          out.closeEntry();
+        }
+      } finally {
+        out.close();
+      }
+
+      if (!tmpjar.renameTo(archiveFile)) {
+        throw new IOException("Cannot replace " + archiveFile);
+      }
+    } finally {
+      tmpjar.delete();
+    }
+  }
+
+  private List<File> getAllFiles(File dir, String extension) {
+    ArrayList<File> fileList = new ArrayList<File>();
+    getAllFiles(dir, extension, fileList);
+    return fileList;
+  }
+
+  private void getAllFiles(File dir, String extension, List<File> fileList) {
+    for (File f : dir.listFiles()) {
+      if (f.getName().endsWith(extension)) {
+        fileList.add(f);
+      }
+      if (f.isDirectory()) {
+        getAllFiles(f, extension, fileList);
+      }
+    }
+  }
+
+  private List<String> getRelativePaths(File dir, String extension) {
+    ArrayList<String> pathList = new ArrayList<String>();
+    getRelativePaths(dir, extension, "", pathList);
+    return pathList;
+  }
+
+  private void getRelativePaths(File dir, String extension, String path, List<String> pathList) {
+    for (File f : dir.listFiles()) {
+      if (f.getName().endsWith(extension)) {
+        pathList.add(path + f.getName());
+      }
+      if (f.isDirectory()) {
+        getRelativePaths(f, extension, path + f.getName() + "/", pathList);
+      }
+    }
+  }
+
+  private void deleteAllFiles(File dir) {
+    for (File f : dir.listFiles()) {
+      if (f.isDirectory()) {
+        deleteAllFiles(f);
+      } else {
+        f.delete();
+      }
+    }
+    dir.delete();
+  }
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..310b401
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/rules/PrologEnvironment.java
@@ -0,0 +1,142 @@
+// 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.rules;
+
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.assistedinject.Assisted;
+
+import com.googlecode.prolog_cafe.lang.BufferingPrologControl;
+import com.googlecode.prolog_cafe.lang.Prolog;
+import com.googlecode.prolog_cafe.lang.PrologMachineCopy;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Per-thread Prolog interpreter.
+ * <p>
+ * This class is not thread safe.
+ * <p>
+ * A single copy of the Prolog interpreter, for the current thread.
+ */
+public class PrologEnvironment extends BufferingPrologControl {
+
+  private static final Logger log =
+    LoggerFactory.getLogger(PrologEnvironment.class);
+
+  static final int MAX_ARITY = 8;
+
+  public static interface Factory {
+    /**
+     * Construct a new Prolog interpreter.
+     *
+     * @param src the machine to template the new environment from.
+     * @return the new interpreter.
+     */
+    PrologEnvironment create(PrologMachineCopy src);
+  }
+
+  private final Injector injector;
+  private final Map<StoredValue<Object>, Object> storedValues;
+  private List<Runnable> cleanup;
+
+  @Inject
+  PrologEnvironment(Injector i, @Assisted PrologMachineCopy src) {
+    super(src);
+    injector = i;
+    setMaxArity(MAX_ARITY);
+    setEnabled(EnumSet.allOf(Prolog.Feature.class), false);
+    storedValues = new HashMap<StoredValue<Object>, Object>();
+    cleanup = new LinkedList<Runnable>();
+  }
+
+  /** Get the global Guice Injector that configured the environment. */
+  public Injector getInjector() {
+    return injector;
+  }
+
+  /**
+   * Lookup a stored value in the interpreter's hash manager.
+   *
+   * @param <T> type of stored Java object.
+   * @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);
+  }
+
+  /**
+   * Set a stored value on the interpreter's hash manager.
+   *
+   * @param <T> type of stored Java object.
+   * @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);
+  }
+
+  /**
+   * Copy the stored values from another interpreter to this one.
+   * Also gets the cleanup from the child interpreter
+   */
+  public void copyStoredValues(PrologEnvironment child) {
+    storedValues.putAll(child.storedValues);
+    setCleanup(child.cleanup);
+  }
+
+  /**
+   * Assign the environment a cleanup list (in order to use a centralized list)
+   * If this enivronment's list is non-empty, append its cleanup tasks to the
+   * assigning list.
+   */
+  public void setCleanup(List<Runnable> newCleanupList) {
+    newCleanupList.addAll(cleanup);
+    cleanup = newCleanupList;
+  }
+
+  /**
+   * Adds cleanup task to run when close() is called
+   * @param task is run when close() is called
+   */
+  public void addToCleanup(Runnable task) {
+    cleanup.add(task);
+  }
+
+  /**
+   * Release resources stored in interpreter's hash manager.
+   */
+  public void close() {
+    for (final Iterator<Runnable> i = cleanup.iterator(); i.hasNext();) {
+      try {
+        i.next().run();
+      } catch (Throwable err) {
+        log.error("Failed to execute cleanup for PrologEnvironment", err);
+      }
+      i.remove();
+    }
+  }
+}
\ No newline at end of file
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_50.java b/gerrit-server/src/main/java/com/google/gerrit/rules/PrologModule.java
similarity index 72%
copy from gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_50.java
copy to gerrit-server/src/main/java/com/google/gerrit/rules/PrologModule.java
index 4a90c1f..d361790 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_50.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/rules/PrologModule.java
@@ -12,14 +12,13 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.schema;
+package com.google.gerrit.rules;
 
-import com.google.inject.Inject;
-import com.google.inject.Provider;
+import com.google.gerrit.server.config.FactoryModule;
 
-public class Schema_50 extends SchemaVersion {
-  @Inject
-  Schema_50(Provider<Schema_49> prior) {
-    super(prior);
+public class PrologModule extends FactoryModule {
+  @Override
+  protected void configure() {
+    factory(PrologEnvironment.Factory.class);
   }
-}
\ No newline at end of file
+}
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
new file mode 100644
index 0000000..fd72e0c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/rules/RulesCache.java
@@ -0,0 +1,227 @@
+// 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.rules;
+
+import static com.googlecode.prolog_cafe.lang.PrologMachineCopy.save;
+
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import com.googlecode.prolog_cafe.compiler.CompileException;
+import com.googlecode.prolog_cafe.lang.BufferingPrologControl;
+import com.googlecode.prolog_cafe.lang.JavaObjectTerm;
+import com.googlecode.prolog_cafe.lang.Prolog;
+import com.googlecode.prolog_cafe.lang.PrologClassLoader;
+import com.googlecode.prolog_cafe.lang.PrologMachineCopy;
+import com.googlecode.prolog_cafe.lang.SymbolTerm;
+
+import org.eclipse.jgit.errors.LargeObjectException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectLoader;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.util.RawParseUtils;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PushbackReader;
+import java.io.StringReader;
+import java.lang.ref.Reference;
+import java.lang.ref.ReferenceQueue;
+import java.lang.ref.WeakReference;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.EnumSet;
+import java.util.HashMap;
+import java.util.Map;
+
+/**
+ * Manages a cache of compiled Prolog rules.
+ * <p>
+ * Rules are loaded from the {@code site_path/cache/rules/rules-SHA1.jar}, where
+ * {@code SHA1} is the SHA1 of the Prolog {@code rules.pl} in a project's
+ * {@link GitRepositoryManager#REF_CONFIG} branch.
+ */
+@Singleton
+public class RulesCache {
+  /** Maximum size of a dynamic Prolog script, in bytes. */
+  private static final int SRC_LIMIT = 128 * 1024;
+
+  /** Default size of the internal Prolog database within each interpreter. */
+  private static final int DB_MAX = 256;
+
+  private static final String[] PACKAGE_LIST = {
+      Prolog.BUILTIN,
+      "gerrit",
+    };
+
+  private final Map<ObjectId, MachineRef> machineCache =
+      new HashMap<ObjectId, MachineRef>();
+
+  private final ReferenceQueue<PrologMachineCopy> dead =
+      new ReferenceQueue<PrologMachineCopy>();
+
+  private static final class MachineRef extends WeakReference<PrologMachineCopy> {
+    final ObjectId key;
+
+    MachineRef(ObjectId key, PrologMachineCopy pcm,
+        ReferenceQueue<PrologMachineCopy> queue) {
+      super(pcm, queue);
+      this.key = key;
+    }
+  }
+
+  private final boolean enableProjectRules;
+  private final File cacheDir;
+  private final File rulesDir;
+  private final GitRepositoryManager gitMgr;
+  private final ClassLoader systemLoader;
+  private final PrologMachineCopy defaultMachine;
+
+  @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;
+
+    systemLoader = getClass().getClassLoader();
+    defaultMachine = save(newEmptyMachine(systemLoader));
+  }
+
+  /**
+   * Locate a cached Prolog machine state, or create one if not available.
+   *
+   * @return a Prolog machine, after loading the specified rules.
+   * @throws CompileException the machine cannot be created.
+   */
+  public synchronized PrologMachineCopy loadMachine(
+      Project.NameKey project,
+      ObjectId rulesId)
+      throws CompileException {
+    if (!enableProjectRules || project == null || rulesId == null) {
+      return defaultMachine;
+    }
+
+    Reference<? extends PrologMachineCopy> ref = machineCache.get(rulesId);
+    if (ref != null) {
+      PrologMachineCopy pmc = ref.get();
+      if (pmc != null) {
+        return pmc;
+      }
+
+      machineCache.remove(rulesId);
+      ref.enqueue();
+    }
+
+    gc();
+
+    PrologMachineCopy pcm = createMachine(project, rulesId);
+    MachineRef newRef = new MachineRef(rulesId, pcm, dead);
+    machineCache.put(rulesId, newRef);
+    return pcm;
+  }
+
+  private void gc() {
+    Reference<?> ref;
+    while ((ref = dead.poll()) != null) {
+      ObjectId key = ((MachineRef) ref).key;
+      if (machineCache.get(key) == ref) {
+        machineCache.remove(key);
+      }
+    }
+  }
+
+  private PrologMachineCopy createMachine(Project.NameKey project,
+      ObjectId rulesId) throws CompileException {
+    // If the rules are available as a complied JAR on local disk, prefer
+    // that over dynamic consult as the bytecode will be faster.
+    //
+    if (rulesDir != null) {
+      File jarFile = new File(rulesDir, "rules-" + rulesId.getName() + ".jar");
+      if (jarFile.isFile()) {
+        URL[] cp = new URL[] {toURL(jarFile)};
+        return save(newEmptyMachine(new URLClassLoader(cp, systemLoader)));
+      }
+    }
+
+    // Dynamically consult the rules into the machine's internal database.
+    //
+    String rules = read(project, rulesId);
+    BufferingPrologControl ctl = newEmptyMachine(systemLoader);
+    PushbackReader in = new PushbackReader(
+        new StringReader(rules),
+        Prolog.PUSHBACK_SIZE);
+
+    if (!ctl.execute(
+        Prolog.BUILTIN, "consult_stream",
+        SymbolTerm.intern("rules.pl"),
+        new JavaObjectTerm(in))) {
+      throw new CompileException("Cannot consult rules of " + project);
+    }
+    return save(ctl);
+  }
+
+  private String read(Project.NameKey project, ObjectId rulesId)
+      throws CompileException {
+    Repository git;
+    try {
+      git = gitMgr.openRepository(project);
+    } catch (RepositoryNotFoundException e) {
+      throw new CompileException("Cannot open repository " + project, e);
+    }
+    try {
+      ObjectLoader ldr = git.open(rulesId, Constants.OBJ_BLOB);
+      byte[] raw = ldr.getCachedBytes(SRC_LIMIT);
+      return RawParseUtils.decode(raw);
+    } catch (LargeObjectException e) {
+      throw new CompileException("rules of " + project + " are too large", e);
+    } catch (RuntimeException e) {
+      throw new CompileException("Cannot load rules of " + project, e);
+    } catch (IOException e) {
+      throw new CompileException("Cannot load rules of " + project, e);
+    } finally {
+      git.close();
+    }
+  }
+
+  private static BufferingPrologControl newEmptyMachine(ClassLoader cl) {
+    BufferingPrologControl ctl = new BufferingPrologControl();
+    ctl.setMaxArity(PrologEnvironment.MAX_ARITY);
+    ctl.setMaxDatabaseSize(DB_MAX);
+    ctl.setPrologClassLoader(new PrologClassLoader(cl));
+    ctl.setEnabled(EnumSet.allOf(Prolog.Feature.class), false);
+
+    // Bootstrap the interpreter and ensure there is clean state.
+    ctl.initialize(PACKAGE_LIST);
+    return ctl;
+  }
+
+  private static URL toURL(File jarFile) throws CompileException {
+    try {
+      return jarFile.toURI().toURL();
+    } catch (MalformedURLException e) {
+      throw new CompileException("Cannot create URL for " + jarFile, e);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValue.java b/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValue.java
new file mode 100644
index 0000000..e17346b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValue.java
@@ -0,0 +1,91 @@
+// 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.rules;
+
+import com.googlecode.prolog_cafe.lang.Prolog;
+import com.googlecode.prolog_cafe.lang.SystemException;
+
+/**
+ * Defines a value cached in a {@link PrologEnvironment}.
+ *
+ * @see StoredValues
+ */
+public class StoredValue<T> {
+  /** Construct a new unique key that does not match any other key. */
+  public static <T> StoredValue<T> create() {
+    return new StoredValue<T>();
+  }
+
+  /** Construct a key based on a Java Class object, useful for singletons. */
+  public static <T> StoredValue<T> create(Class<T> clazz) {
+    return new StoredValue<T>(clazz);
+  }
+
+  private final Object key;
+
+  /**
+   * Initialize a stored value key using any Java Object.
+   *
+   * @param key unique identity of the stored value. This will be the hash key
+   *        in the Prolog Environments's hash map.
+   */
+  public StoredValue(Object key) {
+    this.key = key;
+  }
+
+  /**
+   * Initializes a stored value key with a new unique key.
+   */
+  public StoredValue() {
+    key = this;
+  }
+
+  /** Look up the value in the engine, or return null. */
+  public T getOrNull(Prolog engine) {
+    return get((PrologEnvironment) engine.control);
+  }
+  /** Get the value from the engine, or throw SystemException. */
+  public T get(Prolog engine) {
+    T obj = getOrNull(engine);
+    if (obj == null) {
+      //unless createValue() is overridden, will return null
+      obj = createValue(engine);
+      if (obj == null) {
+        throw new SystemException("No " + key + " available");
+      }
+      set(engine, obj);
+    }
+    return obj;
+  }
+
+  public void set(Prolog engine, T obj) {
+    set((PrologEnvironment) engine.control, obj);
+  }
+
+  /** Perform {@link #getOrNull(Prolog)} on the environment's interpreter. */
+  public T get(PrologEnvironment env) {
+    return env.get(this);
+  }
+
+  /** Set the value into the environment's interpreter. */
+  public void set(PrologEnvironment env, T obj) {
+    env.set(this, obj);
+  }
+
+  /** Creates a value to store, returns null by default. */
+  protected T createValue(Prolog engine) {
+    return null;
+  }
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..8ab9471
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/rules/StoredValues.java
@@ -0,0 +1,107 @@
+// 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.rules;
+
+import static com.google.gerrit.rules.StoredValue.create;
+
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetInfo;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.patch.PatchList;
+import com.google.gerrit.server.patch.PatchListCache;
+import com.google.gerrit.server.patch.PatchListKey;
+import com.google.gerrit.server.patch.PatchSetInfoFactory;
+import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
+import com.google.gerrit.server.project.ChangeControl;
+
+import com.googlecode.prolog_cafe.lang.Prolog;
+import com.googlecode.prolog_cafe.lang.SystemException;
+
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+
+public final class StoredValues {
+  public static final StoredValue<ReviewDb> REVIEW_DB = create(ReviewDb.class);
+  public static final StoredValue<Change> CHANGE = create(Change.class);
+  public static final StoredValue<PatchSet.Id> PATCH_SET_ID = create(PatchSet.Id.class);
+  public static final StoredValue<ChangeControl> CHANGE_CONTROL = create(ChangeControl.class);
+
+  public static final StoredValue<PatchSetInfo> PATCH_SET_INFO = new StoredValue<PatchSetInfo>() {
+    @Override
+    public PatchSetInfo createValue(Prolog engine) {
+      PatchSet.Id patchSetId = StoredValues.PATCH_SET_ID.get(engine);
+      PrologEnvironment env = (PrologEnvironment) engine.control;
+      PatchSetInfoFactory patchInfoFactory =
+          env.getInjector().getInstance(PatchSetInfoFactory.class);
+      try {
+        return patchInfoFactory.get(REVIEW_DB.get(engine), patchSetId);
+      } catch (PatchSetInfoNotAvailableException e) {
+        throw new SystemException(e.getMessage());
+      }
+    }
+  };
+
+  public static final StoredValue<PatchList> PATCH_LIST = new StoredValue<PatchList>() {
+    @Override
+    public PatchList createValue(Prolog engine) {
+      PrologEnvironment env = (PrologEnvironment) engine.control;
+      PatchSetInfo psInfo = StoredValues.PATCH_SET_INFO.get(engine);
+      PatchListCache plCache = env.getInjector().getInstance(PatchListCache.class);
+      Change change = StoredValues.CHANGE.get(engine);
+      Project.NameKey projectKey = change.getProject();
+      ObjectId a = null;
+      ObjectId b = ObjectId.fromString(psInfo.getRevId());
+      Whitespace ws = Whitespace.IGNORE_NONE;
+      PatchListKey plKey = new PatchListKey(projectKey, a, b, ws);
+      PatchList patchList = plCache.get(plKey);
+      if (patchList == null) {
+        throw new SystemException("Cannot create " + plKey);
+      }
+      return patchList;
+    }
+  };
+
+  public static final StoredValue<Repository> REPOSITORY = new StoredValue<Repository>() {
+    @Override
+    public Repository createValue(Prolog engine) {
+      PrologEnvironment env = (PrologEnvironment) engine.control;
+      GitRepositoryManager gitMgr =
+        env.getInjector().getInstance(GitRepositoryManager.class);
+      Change change = StoredValues.CHANGE.get(engine);
+      Project.NameKey projectKey = change.getProject();
+      final Repository repo;
+      try {
+        repo = gitMgr.openRepository(projectKey);
+      } catch (RepositoryNotFoundException e) {
+        throw new SystemException(e.getMessage());
+      }
+      env.addToCleanup(new Runnable() {
+        @Override
+        public void run() {
+          repo.close();
+        }
+      });
+      return repo;
+    }
+  };
+
+  private StoredValues() {
+  }
+}
\ No newline at end of file
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/AnonymousUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/AnonymousUser.java
index 1bd2066..5d36b33 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/AnonymousUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/AnonymousUser.java
@@ -14,28 +14,28 @@
 
 package com.google.gerrit.server;
 
-import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.AccountProjectWatch;
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.server.config.AuthConfig;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.account.CapabilityControl;
+import com.google.gerrit.server.account.GroupMembership;
+import com.google.gerrit.server.account.ListGroupMembership;
 import com.google.inject.Inject;
-import com.google.inject.Singleton;
 
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Set;
 
 /** An anonymous user who has not yet authenticated. */
-@Singleton
 public class AnonymousUser extends CurrentUser {
   @Inject
-  AnonymousUser(final AuthConfig auth) {
-    super(AccessPath.UNKNOWN, auth);
+  AnonymousUser(CapabilityControl.Factory capabilityControlFactory) {
+    super(capabilityControlFactory, AccessPath.UNKNOWN);
   }
 
   @Override
-  public Set<AccountGroup.Id> getEffectiveGroups() {
-    return authConfig.getAnonymousGroups();
+  public GroupMembership getEffectiveGroups() {
+    return new ListGroupMembership(Collections.singleton(AccountGroup.ANONYMOUS_USERS));
   }
 
   @Override
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..3417111
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
@@ -0,0 +1,74 @@
+// 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.common.data.ApprovalType;
+import com.google.gerrit.common.data.ApprovalTypes;
+import com.google.gerrit.reviewdb.client.ApprovalCategory;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.server.OrmException;
+
+import java.io.IOException;
+import java.util.Collections;
+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);
+  }
+
+  /**
+   * Moves the PatchSetApprovals to the last PatchSet on the change while
+   * keeping the vetos.
+   *
+   * @param db The review database
+   * @param change Change to update
+   * @param approvalTypes The approval types
+   * @throws OrmException
+   * @throws IOException
+   */
+  public static void copyVetosToLatestPatchSet(final ReviewDb db, Change change,
+      ApprovalTypes approvalTypes) throws OrmException, IOException {
+    PatchSet.Id source;
+    if (change.getNumberOfPatchSets() > 1) {
+      source = new PatchSet.Id(change.getId(), change.getNumberOfPatchSets() - 1);
+    } else {
+      throw new IOException("Previous patch set could not be found");
+    }
+
+    PatchSet.Id dest = change.currPatchSetId();
+    for (PatchSetApproval a : db.patchSetApprovals().byPatchSet(source)) {
+      // ApprovalCategory.SUBMIT is still in db but not relevant in git-store
+      if (!ApprovalCategory.SUBMIT.equals(a.getCategoryId())) {
+        final ApprovalType type = approvalTypes.byId(a.getCategoryId());
+        if (type.getCategory().isCopyMinScore() && type.isMaxNegative(a)) {
+          db.patchSetApprovals().insert(
+              Collections.singleton(new PatchSetApproval(dest, a)));
+        }
+      }
+    }
+  }
+}
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 6886d2c..9dc572d 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,34 +14,38 @@
 
 package com.google.gerrit.server;
 
-import static com.google.gerrit.reviewdb.ApprovalCategory.SUBMIT;
-
 import com.google.gerrit.common.ChangeHookRunner;
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.ChangeMessage;
-import com.google.gerrit.reviewdb.PatchSet;
-import com.google.gerrit.reviewdb.PatchSetApproval;
-import com.google.gerrit.reviewdb.PatchSetInfo;
-import com.google.gerrit.reviewdb.RevId;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gerrit.reviewdb.TrackingId;
-import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.common.data.ApprovalTypes;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Change.Status;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetAncestor;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.PatchSetInfo;
+import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.reviewdb.client.TrackingId;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.config.TrackingFooter;
 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.EmailException;
+import com.google.gerrit.server.mail.RebasedPatchSetSender;
+import com.google.gerrit.server.mail.ReplacePatchSetSender;
+import com.google.gerrit.server.mail.ReplyToChangeSender;
+import com.google.gerrit.server.mail.RevertedSender;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.NoSuchChangeException;
-import com.google.gerrit.server.mail.AbandonedSender;
-import com.google.gerrit.server.mail.EmailException;
-import com.google.gerrit.server.mail.RevertedSender;
-import com.google.gwtorm.client.AtomicUpdate;
-import com.google.gwtorm.client.OrmConcurrencyException;
-import com.google.gwtorm.client.OrmException;
-
+import com.google.gwtorm.server.AtomicUpdate;
+import com.google.gwtorm.server.OrmConcurrencyException;
+import com.google.gwtorm.server.OrmException;
 
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.MissingObjectException;
@@ -50,8 +54,11 @@
 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.merge.MergeStrategy;
+import org.eclipse.jgit.merge.ThreeWayMerger;
 import org.eclipse.jgit.revwalk.FooterLine;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
@@ -60,6 +67,7 @@
 
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
@@ -162,113 +170,260 @@
     db.trackingIds().delete(toDelete);
   }
 
-  public static void submit(final PatchSet.Id patchSetId,
-      final IdentifiedUser user, final ReviewDb db,
-      final MergeOp.Factory opFactory, final MergeQueue merger)
+  public static void testMerge(MergeOp.Factory opFactory, Change change) {
+    opFactory.create(change.getDest()).verifyMergeability(change);
+  }
+
+  public static void insertAncestors(ReviewDb db, 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 =
+          new PatchSetAncestor(new PatchSetAncestor.Id(id, p + 1));
+      a.setAncestorRevision(new RevId(src.getParent(p).getId().getName()));
+      toInsert.add(a);
+    }
+    db.patchSetAncestors().insert(toInsert);
+  }
+
+  /**
+   * Rebases a commit
+   *
+   * @param git Repository to find commits in
+   * @param original The commit to rebase
+   * @param base Base to rebase against
+   * @return CommitBuilder the newly rebased commit
+   * @throws IOException Merged failed
+   */
+  public static CommitBuilder rebaseCommit(Repository git, RevCommit original,
+      RevCommit base, PersonIdent committerIdent) throws IOException {
+
+    if (original.getParentCount() == 0) {
+      throw new IOException(
+          "Commits with no parents cannot be rebased (is this the initial commit?).");
+    }
+
+    if (original.getParentCount() > 1) {
+      throw new IOException(
+          "Patch sets with multiple parents cannot be rebased (merge commits)."
+              + " Parents: " + Arrays.toString(original.getParents()));
+    }
+
+    final RevCommit parentCommit = original.getParent(0);
+
+    if (base.equals(parentCommit)) {
+      throw new IOException("Change is already up to date.");
+    }
+
+    final ThreeWayMerger merger = MergeStrategy.RESOLVE.newMerger(git, true);
+    merger.setBase(parentCommit);
+    merger.merge(original, base);
+
+    if (merger.getResultTreeId() == null) {
+      throw new IOException(
+          "The rebase failed since conflicts occured during the merge.");
+    }
+
+    final CommitBuilder rebasedCommitBuilder = new CommitBuilder();
+
+    rebasedCommitBuilder.setTreeId(merger.getResultTreeId());
+    rebasedCommitBuilder.setParentId(base);
+    rebasedCommitBuilder.setAuthor(original.getAuthorIdent());
+    rebasedCommitBuilder.setMessage(original.getFullMessage());
+    rebasedCommitBuilder.setCommitter(committerIdent);
+
+    return rebasedCommitBuilder;
+  }
+
+  public static void rebaseChange(final PatchSet.Id patchSetId,
+      final IdentifiedUser user, final ReviewDb db,
+      RebasedPatchSetSender.Factory rebasedPatchSetSenderFactory,
+      final ChangeHookRunner hooks, GitRepositoryManager gitManager,
+      final PatchSetInfoFactory patchSetInfoFactory,
+      final ReplicationQueue replication, PersonIdent myIdent,
+      final ChangeControl.Factory changeControlFactory,
+      final ApprovalTypes approvalTypes) throws NoSuchChangeException,
+      EmailException, OrmException, MissingObjectException,
+      IncorrectObjectTypeException, IOException,
+      PatchSetInfoNotAvailableException, InvalidChangeOperationException {
+
     final Change.Id changeId = patchSetId.getParentKey();
-    final PatchSetApproval approval = createSubmitApproval(patchSetId, user, db);
+    final ChangeControl changeControl =
+        changeControlFactory.validateFor(changeId);
 
-    db.patchSetApprovals().upsert(Collections.singleton(approval));
+    if (!changeControl.canRebase()) {
+      throw new InvalidChangeOperationException(
+          "Cannot rebase: New patch sets are not allowed to be added to change: "
+              + changeId.toString());
+    }
 
-    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);
+    Change change = changeControl.getChange();
+    final Repository git = gitManager.openRepository(change.getProject());
+    try {
+      final RevWalk revWalk = new RevWalk(git);
+      try {
+        final PatchSet originalPatchSet = db.patchSets().get(patchSetId);
+        RevCommit branchTipCommit = null;
+
+        List<PatchSetAncestor> patchSetAncestors =
+            db.patchSetAncestors().ancestorsOf(patchSetId).toList();
+        if (patchSetAncestors.size() > 1) {
+          throw new IOException(
+              "The patch set you are trying to rebase is dependent on several other patch sets: "
+                  + patchSetAncestors.toString());
         }
-        return change;
-      }
-    });
+        if (patchSetAncestors.size() == 1) {
+          List<PatchSet> depPatchSetList = db.patchSets()
+                  .byRevision(patchSetAncestors.get(0).getAncestorRevision())
+                  .toList();
+          if (!depPatchSetList.isEmpty()) {
+            PatchSet depPatchSet = depPatchSetList.get(0);
 
-    if (updatedChange.getStatus() == Change.Status.SUBMITTED) {
-      merger.merge(opFactory, updatedChange.getDest());
-    }
-  }
+            Change.Id depChangeId = depPatchSet.getId().getParentKey();
+            Change depChange = db.changes().get(depChangeId);
 
-  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());
+            if (depChange.getStatus() == Status.ABANDONED) {
+              throw new IOException("Cannot rebase against an abandoned change: "
+                  + depChange.getKey().toString());
+            }
+            if (depChange.getStatus().isOpen()) {
+              PatchSet latestDepPatchSet =
+                  db.patchSets().get(depChange.currentPatchSetId());
+              if (!depPatchSet.getId().equals(depChange.currentPatchSetId())) {
+                branchTipCommit =
+                    revWalk.parseCommit(ObjectId
+                        .fromString(latestDepPatchSet.getRevision().get()));
+              } else {
+                throw new IOException(
+                    "Change is already based on the latest patch set of the dependent change.");
+              }
+            }
+          }
+        }
 
-    final PatchSetApproval.Key akey =
-        new PatchSetApproval.Key(patchSetId, user.getAccountId(), SUBMIT);
+        if (branchTipCommit == null) {
+          // We are dependent on a merged PatchSet or have no PatchSet
+          // dependencies at all.
+          Ref destRef = git.getRef(change.getDest().get());
+          if (destRef == null) {
+            throw new IOException(
+                "The destination branch does not exist: "
+                    + change.getDest().get());
+          }
+          branchTipCommit = revWalk.parseCommit(destRef.getObjectId());
+        }
 
-    for (final PatchSetApproval approval : allApprovals) {
-      if (akey.equals(approval.getKey())) {
-        approval.setValue((short) 1);
-        approval.setGranted();
-        return approval;
-      }
-    }
-    return new PatchSetApproval(akey, (short) 1);
-  }
+        final RevCommit originalCommit =
+            revWalk.parseCommit(ObjectId.fromString(originalPatchSet
+                .getRevision().get()));
 
-  public static void abandon(final PatchSet.Id patchSetId,
-      final IdentifiedUser user, final String message, final ReviewDb db,
-      final AbandonedSender.Factory abandonedSenderFactory,
-      final ChangeHookRunner hooks) throws NoSuchChangeException,
-      EmailException, OrmException {
-    final Change.Id changeId = patchSetId.getParentKey();
-    final PatchSet patch = db.patchSets().get(patchSetId);
-    if (patch == null) {
-      throw new NoSuchChangeException(changeId);
-    }
+        CommitBuilder rebasedCommitBuilder =
+            rebaseCommit(git, originalCommit, branchTipCommit, myIdent);
 
-    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 ObjectInserter oi = git.newObjectInserter();
+        final ObjectId rebasedCommitId;
+        try {
+          rebasedCommitId = oi.insert(rebasedCommitBuilder);
+          oi.flush();
+        } finally {
+          oi.release();
+        }
 
-    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;
+        Change updatedChange =
+            db.changes().atomicUpdate(changeId, new AtomicUpdate<Change>() {
+              @Override
+              public Change update(Change change) {
+                if (change.getStatus().isOpen()) {
+                  change.nextPatchSetId();
+                  return change;
+                } else {
+                  return null;
+                }
+              }
+            });
+
+        if (updatedChange == null) {
+          throw new InvalidChangeOperationException("Change is closed: "
+              + change.toString());
         } else {
-          return null;
+          change = updatedChange;
         }
+
+        final PatchSet rebasedPatchSet = new PatchSet(change.currPatchSetId());
+        rebasedPatchSet.setCreatedOn(change.getCreatedOn());
+        rebasedPatchSet.setUploader(user.getAccountId());
+        rebasedPatchSet.setRevision(new RevId(rebasedCommitId.getName()));
+
+        insertAncestors(db, rebasedPatchSet.getId(),
+            revWalk.parseCommit(rebasedCommitId));
+
+        db.patchSets().insert(Collections.singleton(rebasedPatchSet));
+        final PatchSetInfo info =
+            patchSetInfoFactory.get(db, rebasedPatchSet.getId());
+
+        change =
+            db.changes().atomicUpdate(change.getId(),
+                new AtomicUpdate<Change>() {
+                  @Override
+                  public Change update(Change change) {
+                    change.setCurrentPatchSet(info);
+                    ChangeUtil.updated(change);
+                    return change;
+                  }
+                });
+
+        final RefUpdate ru = git.updateRef(rebasedPatchSet.getRefName());
+        ru.setNewObjectId(rebasedCommitId);
+        ru.disableRefLog();
+        if (ru.update(revWalk) != RefUpdate.Result.NEW) {
+          throw new IOException("Failed to create ref "
+              + rebasedPatchSet.getRefName() + " in " + git.getDirectory()
+              + ": " + ru.getResult());
+        }
+
+        replication.scheduleUpdate(change.getProject(), ru.getName());
+
+        ApprovalsUtil.copyVetosToLatestPatchSet(db, change, approvalTypes);
+
+        final ChangeMessage cmsg =
+            new ChangeMessage(new ChangeMessage.Key(changeId,
+                ChangeUtil.messageUUID(db)), user.getAccountId(), patchSetId);
+        cmsg.setMessage("Patch Set " + patchSetId.get() + ": Rebased");
+        db.changeMessages().insert(Collections.singleton(cmsg));
+
+        final Set<Account.Id> oldReviewers = new HashSet<Account.Id>();
+        final Set<Account.Id> oldCC = new HashSet<Account.Id>();
+
+        for (PatchSetApproval a : db.patchSetApprovals().byChange(change.getId())) {
+          if (a.getValue() != 0) {
+            oldReviewers.add(a.getAccountId());
+          } else {
+            oldCC.add(a.getAccountId());
+          }
+        }
+
+        final ReplacePatchSetSender cm =
+            rebasedPatchSetSenderFactory.create(change);
+        cm.setFrom(user.getAccountId());
+        cm.setPatchSet(rebasedPatchSet);
+        cm.addReviewers(oldReviewers);
+        cm.addExtraCC(oldCC);
+        cm.send();
+
+        hooks.doPatchsetCreatedHook(change, rebasedPatchSet, db);
+      } finally {
+        revWalk.release();
       }
-    });
-
-    if (updatedChange != null) {
-      db.changeMessages().insert(Collections.singleton(cmsg));
-
-      final List<PatchSetApproval> approvals =
-          db.patchSetApprovals().byChange(changeId).toList();
-      for (PatchSetApproval a : approvals) {
-        a.cache(updatedChange);
-      }
-      db.patchSetApprovals().update(approvals);
-
-      // Email the reviewers
-      final AbandonedSender cm = abandonedSenderFactory.create(updatedChange);
-      cm.setFrom(user.getAccountId());
-      cm.setChangeMessage(cmsg);
-      cm.send();
+    } finally {
+      git.close();
     }
-
-    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,
@@ -346,7 +501,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");
@@ -360,68 +515,92 @@
       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 AbandonedSender.Factory abandonedSenderFactory,
-      final ChangeHookRunner hooks) throws NoSuchChangeException,
-      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);
-    }
-    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;
-        }
-      }
-    });
-
-    if (updatedChange != null) {
-      db.changeMessages().insert(Collections.singleton(cmsg));
-
-      final List<PatchSetApproval> approvals =
-          db.patchSetApprovals().byChange(changeId).toList();
-      for (PatchSetApproval a : approvals) {
-        a.cache(updatedChange);
-      }
-      db.patchSetApprovals().update(approvals);
-
-      // Email the reviewers
-      final AbandonedSender cm = abandonedSenderFactory.create(updatedChange);
-      cm.setFrom(user.getAccountId());
-      cm.setChangeMessage(cmsg);
-      cm.send();
+    for (PatchSet ps : db.patchSets().byChange(changeId)) {
+      // These should all be draft patch sets.
+      deleteOnlyDraftPatchSet(ps, change, gitManager, replication, db);
     }
 
-    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));
+  }
+
+  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));
+
+    ApprovalsUtil.syncChangeStatus(db, change);
+
+    // Email the reviewers
+    final ReplyToChangeSender cm = senderFactory.create(change);
+    cm.setFrom(user.getAccountId());
+    cm.setChangeMessage(cmsg);
+    cm.send();
   }
 
   public static String sortKey(long lastUpdated, int id){
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/CurrentUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/CurrentUser.java
index 0d8d19a..f057c3a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/CurrentUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/CurrentUser.java
@@ -14,10 +14,10 @@
 
 package com.google.gerrit.server;
 
-import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.AccountProjectWatch;
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.server.config.AuthConfig;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.account.CapabilityControl;
+import com.google.gerrit.server.account.GroupMembership;
 import com.google.inject.servlet.RequestScoped;
 
 import java.util.Collection;
@@ -32,12 +32,16 @@
  * @see IdentifiedUser
  */
 public abstract class CurrentUser {
+  private final CapabilityControl.Factory capabilityControlFactory;
   private final AccessPath accessPath;
-  protected final AuthConfig authConfig;
 
-  protected CurrentUser(final AccessPath accessPath, final AuthConfig authConfig) {
+  private CapabilityControl capabilities;
+
+  protected CurrentUser(
+      CapabilityControl.Factory capabilityControlFactory,
+      AccessPath accessPath) {
+    this.capabilityControlFactory = capabilityControlFactory;
     this.accessPath = accessPath;
-    this.authConfig = authConfig;
   }
 
   /** How this user is accessing the Gerrit Code Review application. */
@@ -56,7 +60,7 @@
    *
    * @return active groups for this user.
    */
-  public abstract Set<AccountGroup.Id> getEffectiveGroups();
+  public abstract GroupMembership getEffectiveGroups();
 
   /** Set of changes starred by this user. */
   public abstract Set<Change.Id> getStarredChanges();
@@ -64,12 +68,18 @@
   /** Filters selecting changes the user wants to monitor. */
   public abstract Collection<AccountProjectWatch> getNotificationFilters();
 
-  /** Is the user a non-interactive user? */
-  public boolean isBatchUser() {
-    return getEffectiveGroups().contains(authConfig.getBatchUsersGroup());
+  /** Unique name of the user on this server, if one has been assigned. */
+  public String getUserName() {
+    return null;
   }
 
-  public final boolean isAdministrator() {
-    return getEffectiveGroups().contains(authConfig.getAdministratorsGroup());
+  /** Capabilities available to this user account. */
+  public CapabilityControl getCapabilities() {
+    CapabilityControl ctl = capabilities;
+    if (ctl == null) {
+      ctl = capabilityControlFactory.create(this);
+      capabilities = ctl;
+    }
+    return ctl;
   }
 }
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 e46957d..6e519c4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/IdentifiedUser.java
@@ -14,20 +14,23 @@
 
 package com.google.gerrit.server;
 
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.AccountDiffPreference;
-import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.AccountProjectWatch;
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gerrit.reviewdb.StarredChange;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountDiffPreference;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.StarredChange;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
-import com.google.gerrit.server.account.GroupIncludeCache;
+import com.google.gerrit.server.account.CapabilityControl;
+import com.google.gerrit.server.account.GroupMembership;
+import com.google.gerrit.server.account.MaterializedGroupMembership;
 import com.google.gerrit.server.account.Realm;
+import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.CanonicalWebUrl;
-import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.OutOfScopeException;
 import com.google.inject.Provider;
@@ -43,13 +46,14 @@
 import java.net.MalformedURLException;
 import java.net.SocketAddress;
 import java.net.URL;
+import java.util.AbstractSet;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Date;
 import java.util.HashSet;
-import java.util.LinkedList;
+import java.util.Iterator;
 import java.util.List;
-import java.util.Queue;
 import java.util.Set;
 import java.util.TimeZone;
 
@@ -60,22 +64,29 @@
   /** Create an IdentifiedUser, ignoring any per-request state. */
   @Singleton
   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;
-    private final GroupIncludeCache groupIncludeCache;
+    private final MaterializedGroupMembership.Factory groupMembershipFactory;
 
     @Inject
-    GenericFactory(final AuthConfig authConfig,
+    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) {
+        final MaterializedGroupMembership.Factory groupMembershipFactory) {
+      this.capabilityControlFactory = capabilityControlFactory;
       this.authConfig = authConfig;
+      this.anonymousCowardName = anonymousCowardName;
       this.canonicalUrl = canonicalUrl;
       this.realm = realm;
       this.accountCache = accountCache;
-      this.groupIncludeCache = groupIncludeCache;
+      this.groupMembershipFactory = groupMembershipFactory;
     }
 
     public IdentifiedUser create(final Account.Id id) {
@@ -83,14 +94,16 @@
     }
 
     public IdentifiedUser create(Provider<ReviewDb> db, Account.Id id) {
-      return new IdentifiedUser(AccessPath.UNKNOWN, authConfig, canonicalUrl,
-          realm, accountCache, groupIncludeCache, null, db, id);
+      return new IdentifiedUser(capabilityControlFactory, AccessPath.UNKNOWN,
+          authConfig, anonymousCowardName, canonicalUrl, realm, accountCache,
+          groupMembershipFactory, null, db, id);
     }
 
     public IdentifiedUser create(AccessPath accessPath,
         Provider<SocketAddress> remotePeerProvider, Account.Id id) {
-      return new IdentifiedUser(accessPath, authConfig, canonicalUrl, realm,
-          accountCache, groupIncludeCache, remotePeerProvider, null, id);
+      return new IdentifiedUser(capabilityControlFactory, accessPath,
+          authConfig, anonymousCowardName, canonicalUrl, realm, accountCache,
+          groupMembershipFactory, remotePeerProvider, null, id);
     }
   }
 
@@ -102,28 +115,35 @@
    */
   @Singleton
   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;
-    private final GroupIncludeCache groupIncludeCache;
+    private final MaterializedGroupMembership.Factory groupMembershipFactory;
 
     private final Provider<SocketAddress> remotePeerProvider;
     private final Provider<ReviewDb> dbProvider;
 
     @Inject
-    RequestFactory(final AuthConfig authConfig,
+    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,
+        final MaterializedGroupMembership.Factory groupMembershipFactory,
 
         final @RemotePeer Provider<SocketAddress> remotePeerProvider,
         final Provider<ReviewDb> dbProvider) {
+      this.capabilityControlFactory = capabilityControlFactory;
       this.authConfig = authConfig;
+      this.anonymousCowardName = anonymousCowardName;
       this.canonicalUrl = canonicalUrl;
       this.realm = realm;
       this.accountCache = accountCache;
-      this.groupIncludeCache = groupIncludeCache;
+      this.groupMembershipFactory = groupMembershipFactory;
 
       this.remotePeerProvider = remotePeerProvider;
       this.dbProvider = dbProvider;
@@ -131,18 +151,43 @@
 
     public IdentifiedUser create(final AccessPath accessPath,
         final Account.Id id) {
-      return new IdentifiedUser(accessPath, authConfig, canonicalUrl, realm,
-          accountCache, groupIncludeCache, remotePeerProvider, dbProvider, id);
+      return new IdentifiedUser(capabilityControlFactory, accessPath,
+          authConfig, anonymousCowardName, canonicalUrl, realm, accountCache,
+          groupMembershipFactory, remotePeerProvider, dbProvider, id);
     }
   }
 
   private static final Logger log =
       LoggerFactory.getLogger(IdentifiedUser.class);
 
+  private static final Set<AccountGroup.UUID> registeredGroups =
+      new AbstractSet<AccountGroup.UUID>() {
+        private final List<AccountGroup.UUID> groups =
+            Collections.unmodifiableList(Arrays.asList(new AccountGroup.UUID[] {
+                AccountGroup.ANONYMOUS_USERS, AccountGroup.REGISTERED_USERS}));
+
+        @Override
+        public boolean contains(Object o) {
+          return groups.contains(o);
+        }
+
+        @Override
+        public Iterator<AccountGroup.UUID> iterator() {
+          return groups.iterator();
+        }
+
+        @Override
+        public int size() {
+          return groups.size();
+        }
+      };
+
   private final Provider<String> canonicalUrl;
   private final Realm realm;
   private final AccountCache accountCache;
-  private final GroupIncludeCache groupIncludeCache;
+  private final MaterializedGroupMembership.Factory groupMembershipFactory;
+  private final AuthConfig authConfig;
+  private final String anonymousCowardName;
 
   @Nullable
   private final Provider<SocketAddress> remotePeerProvider;
@@ -154,21 +199,27 @@
 
   private AccountState state;
   private Set<String> emailAddresses;
-  private Set<AccountGroup.Id> effectiveGroups;
+  private GroupMembership effectiveGroups;
   private Set<Change.Id> starredChanges;
   private Collection<AccountProjectWatch> notificationFilters;
 
-  private IdentifiedUser(final AccessPath accessPath,
-      final AuthConfig authConfig, final Provider<String> canonicalUrl,
+  private IdentifiedUser(
+      CapabilityControl.Factory capabilityControlFactory,
+      final AccessPath accessPath,
+      final AuthConfig authConfig,
+      final String anonymousCowardName,
+      final Provider<String> canonicalUrl,
       final Realm realm, final AccountCache accountCache,
-      final GroupIncludeCache groupIncludeCache,
+      final MaterializedGroupMembership.Factory groupMembershipFactory,
       @Nullable final Provider<SocketAddress> remotePeerProvider,
       @Nullable final Provider<ReviewDb> dbProvider, final Account.Id id) {
-    super(accessPath, authConfig);
+    super(capabilityControlFactory, accessPath);
     this.canonicalUrl = canonicalUrl;
     this.realm = realm;
     this.accountCache = accountCache;
-    this.groupIncludeCache = groupIncludeCache;
+    this.groupMembershipFactory = groupMembershipFactory;
+    this.authConfig = authConfig;
+    this.anonymousCowardName = anonymousCowardName;
     this.remotePeerProvider = remotePeerProvider;
     this.dbProvider = dbProvider;
     this.accountId = id;
@@ -187,6 +238,7 @@
   }
 
   /** @return the user's user name; null if one has not been selected/assigned. */
+  @Override
   public String getUserName() {
     return state().getUserName();
   }
@@ -217,39 +269,18 @@
   }
 
   @Override
-  public Set<AccountGroup.Id> getEffectiveGroups() {
+  public GroupMembership getEffectiveGroups() {
     if (effectiveGroups == null) {
-      Set<AccountGroup.Id> seedGroups;
-
       if (authConfig.isIdentityTrustable(state().getExternalIds())) {
-        seedGroups = realm.groups(state());
+        effectiveGroups = realm.groups(state());
       } else {
-        seedGroups = authConfig.getRegisteredGroups();
+        effectiveGroups = groupMembershipFactory.create(registeredGroups);
       }
-
-      effectiveGroups = getIncludedGroups(seedGroups);
     }
 
     return effectiveGroups;
   }
 
-  private Set<AccountGroup.Id> getIncludedGroups(Set<AccountGroup.Id> seedGroups) {
-    Set<AccountGroup.Id> includes = new HashSet<AccountGroup.Id> (seedGroups);
-    Queue<AccountGroup.Id> groupQueue = new LinkedList<AccountGroup.Id> (seedGroups);
-
-    while (groupQueue.size() > 0) {
-      AccountGroup.Id id = groupQueue.remove();
-
-      for (final AccountGroup.Id groupId : groupIncludeCache.getByInclude(id)) {
-        if (includes.add(groupId)) {
-          groupQueue.add(groupId);
-        }
-      }
-    }
-
-    return Collections.unmodifiableSet(includes);
-  }
-
   @Override
   public Set<Change.Id> getStarredChanges() {
     if (starredChanges == null) {
@@ -301,7 +332,7 @@
       name = ua.getPreferredEmail();
     }
     if (name == null || name.isEmpty()) {
-      name = "Anonymous Coward";
+      name = anonymousCowardName;
     }
 
     String user = getUserName();
@@ -361,7 +392,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/PeerDaemonUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/PeerDaemonUser.java
index 26dec09..352f540 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/PeerDaemonUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/PeerDaemonUser.java
@@ -14,17 +14,17 @@
 
 package com.google.gerrit.server;
 
-import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.AccountProjectWatch;
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.server.config.AuthConfig;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.account.CapabilityControl;
+import com.google.gerrit.server.account.GroupMembership;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
 import java.net.SocketAddress;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.HashSet;
 import java.util.Set;
 
 /** Identity of a peer daemon process that isn't this JVM. */
@@ -36,22 +36,18 @@
     PeerDaemonUser create(@Assisted SocketAddress peer);
   }
 
-  private final Set<AccountGroup.Id> effectiveGroups;
   private final SocketAddress peer;
 
   @Inject
-  protected PeerDaemonUser(AuthConfig authConfig, @Assisted SocketAddress peer) {
-    super(AccessPath.SSH_COMMAND, authConfig);
-
-    final HashSet<AccountGroup.Id> g = new HashSet<AccountGroup.Id>();
-    g.add(authConfig.getAdministratorsGroup());
-    this.effectiveGroups = Collections.unmodifiableSet(g);
+  protected PeerDaemonUser(CapabilityControl.Factory capabilityControlFactory,
+      @Assisted SocketAddress peer) {
+    super(capabilityControlFactory, AccessPath.SSH_COMMAND);
     this.peer = peer;
   }
 
   @Override
-  public Set<AccountGroup.Id> getEffectiveGroups() {
-    return effectiveGroups;
+  public GroupMembership getEffectiveGroups() {
+    return GroupMembership.EMPTY;
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ReplicationUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/ReplicationUser.java
index 5c44bfe..fc6ac9c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ReplicationUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ReplicationUser.java
@@ -14,51 +14,39 @@
 
 package com.google.gerrit.server;
 
-import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.AccountProjectWatch;
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.server.config.AuthConfig;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.account.CapabilityControl;
+import com.google.gerrit.server.account.GroupMembership;
+import com.google.gerrit.server.account.ListGroupMembership;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
 import java.util.Collection;
 import java.util.Collections;
-import java.util.HashSet;
 import java.util.Set;
 
 public class ReplicationUser extends CurrentUser {
   /** Magic set of groups enabling read of any project and reference. */
-  public static final Set<AccountGroup.Id> EVERYTHING_VISIBLE =
-      Collections.unmodifiableSet(new HashSet<AccountGroup.Id>(0));
+  public static final GroupMembership EVERYTHING_VISIBLE =
+      new ListGroupMembership(Collections.<AccountGroup.UUID>emptySet());
 
   public interface Factory {
-    ReplicationUser create(@Assisted Set<AccountGroup.Id> authGroups);
+    ReplicationUser create(@Assisted GroupMembership authGroups);
   }
 
-  private final Set<AccountGroup.Id> effectiveGroups;
+  private final GroupMembership effectiveGroups;
 
   @Inject
-  protected ReplicationUser(AuthConfig authConfig,
-      @Assisted Set<AccountGroup.Id> authGroups) {
-    super(AccessPath.REPLICATION, authConfig);
-
-    if (authGroups == EVERYTHING_VISIBLE) {
-      effectiveGroups = EVERYTHING_VISIBLE;
-
-    } else if (authGroups.isEmpty()) {
-      effectiveGroups = Collections.emptySet();
-
-    } else {
-      effectiveGroups = copy(authGroups);
-    }
-  }
-
-  private static Set<AccountGroup.Id> copy(Set<AccountGroup.Id> groups) {
-    return Collections.unmodifiableSet(new HashSet<AccountGroup.Id>(groups));
+  protected ReplicationUser(CapabilityControl.Factory capabilityControlFactory,
+      @Assisted GroupMembership authGroups) {
+    super(capabilityControlFactory, AccessPath.REPLICATION);
+    effectiveGroups = authGroups;
   }
 
   @Override
-  public Set<AccountGroup.Id> getEffectiveGroups() {
+  public GroupMembership getEffectiveGroups() {
     return effectiveGroups;
   }
 
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/AccountByEmailCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCache.java
index 1eb1a4f..406ab52 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCache.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.account;
 
-import com.google.gerrit.reviewdb.Account;
+import com.google.gerrit.reviewdb.client.Account;
 
 import java.util.Set;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java
index 64046fa..72fb2e8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountByEmailCacheImpl.java
@@ -14,13 +14,13 @@
 
 package com.google.gerrit.server.account;
 
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.AccountExternalId;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountExternalId;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.cache.Cache;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.cache.EntryCreator;
-import com.google.gwtorm.client.SchemaFactory;
+import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Module;
 import com.google.inject.Singleton;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCache.java
index 1b3626b..056babd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCache.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.account;
 
-import com.google.gerrit.reviewdb.Account;
+import com.google.gerrit.reviewdb.client.Account;
 
 /** Caches important (but small) account state to avoid database hits. */
 public interface AccountCache {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java
index 52ccc66..819ec31 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountCacheImpl.java
@@ -14,17 +14,16 @@
 
 package com.google.gerrit.server.account;
 
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.AccountExternalId;
-import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.AccountGroupMember;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountExternalId;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupMember;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.cache.Cache;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.cache.EntryCreator;
-import com.google.gerrit.server.config.AuthConfig;
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.client.SchemaFactory;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Module;
 import com.google.inject.Singleton;
@@ -90,18 +89,13 @@
 
   static class ByIdLoader extends EntryCreator<Account.Id, AccountState> {
     private final SchemaFactory<ReviewDb> schema;
-    private final Set<AccountGroup.Id> registered;
-    private final Set<AccountGroup.Id> anonymous;
     private final GroupCache groupCache;
     private final Cache<String, Account.Id> byName;
 
     @Inject
-    ByIdLoader(SchemaFactory<ReviewDb> sf, AuthConfig auth,
-        GroupCache groupCache,
+    ByIdLoader(SchemaFactory<ReviewDb> sf, GroupCache groupCache,
         @Named(BYUSER_NAME) Cache<String, Account.Id> byUsername) {
       this.schema = sf;
-      this.registered = auth.getRegisteredGroups();
-      this.anonymous = auth.getAnonymousGroups();
       this.groupCache = groupCache;
       this.byName = byUsername;
     }
@@ -133,21 +127,18 @@
           Collections.unmodifiableCollection(db.accountExternalIds().byAccount(
               who).toList());
 
-      Set<AccountGroup.Id> internalGroups = new HashSet<AccountGroup.Id>();
+      Set<AccountGroup.UUID> internalGroups = new HashSet<AccountGroup.UUID>();
       for (AccountGroupMember g : db.accountGroupMembers().byAccount(who)) {
         final AccountGroup.Id groupId = g.getAccountGroupId();
         final AccountGroup group = groupCache.get(groupId);
         if (group != null && group.getType() == AccountGroup.Type.INTERNAL) {
-          internalGroups.add(groupId);
+          internalGroups.add(group.getGroupUUID());
         }
       }
 
-      if (internalGroups.isEmpty()) {
-        internalGroups = registered;
-      } else {
-        internalGroups.addAll(registered);
-        internalGroups = Collections.unmodifiableSet(internalGroups);
-      }
+      internalGroups.add(AccountGroup.REGISTERED_USERS);
+      internalGroups.add(AccountGroup.ANONYMOUS_USERS);
+      internalGroups = Collections.unmodifiableSet(internalGroups);
 
       return new AccountState(account, internalGroups, externalIds);
     }
@@ -156,6 +147,8 @@
     public AccountState missing(final Account.Id accountId) {
       final Account account = new Account(accountId);
       final Collection<AccountExternalId> ids = Collections.emptySet();
+      final Set<AccountGroup.UUID> anonymous =
+          Collections.singleton(AccountGroup.ANONYMOUS_USERS);
       return new AccountState(account, anonymous, ids);
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountControl.java
new file mode 100644
index 0000000..b297ed8
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountControl.java
@@ -0,0 +1,121 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.util.Set;
+
+/** Access control management for one account's access to other accounts. */
+public class AccountControl {
+  public static class Factory {
+    private final GroupControl.Factory groupControlFactory;
+    private final Provider<CurrentUser> user;
+    private final IdentifiedUser.GenericFactory userFactory;
+    private final AccountVisibility accountVisibility;
+
+    @Inject
+    Factory(final GroupControl.Factory groupControlFactory,
+        final Provider<CurrentUser> user,
+        final IdentifiedUser.GenericFactory userFactory,
+        final AccountVisibility accountVisibility) {
+      this.groupControlFactory = groupControlFactory;
+      this.user = user;
+      this.userFactory = userFactory;
+      this.accountVisibility = accountVisibility;
+    }
+
+    public AccountControl get() {
+      return new AccountControl(groupControlFactory, user.get(), userFactory,
+          accountVisibility);
+    }
+  }
+
+  private final GroupControl.Factory groupControlFactory;
+  private final CurrentUser currentUser;
+  private final IdentifiedUser.GenericFactory userFactory;
+  private final AccountVisibility accountVisibility;
+
+  AccountControl(final GroupControl.Factory groupControlFactory,
+        final CurrentUser currentUser,
+        final IdentifiedUser.GenericFactory userFactory,
+        final AccountVisibility accountVisibility) {
+    this.groupControlFactory = groupControlFactory;
+    this.currentUser = currentUser;
+    this.userFactory = userFactory;
+    this.accountVisibility = accountVisibility;
+  }
+
+  /**
+   * Returns true if the otherUser is allowed to see the current user, based
+   * on the account visibility policy. Depending on the group membership
+   * realms supported, this may not be able to determine SAME_GROUP or
+   * VISIBLE_GROUP correctly (defaulting to not being visible). This is because
+   * {@link GroupMembership#getKnownGroups()} may only return a subset of the
+   * effective groups.
+   */
+  public boolean canSee(final Account otherUser) {
+    // Special case: I can always see myself.
+    if (currentUser instanceof IdentifiedUser
+        && ((IdentifiedUser) currentUser).getAccountId()
+            .equals(otherUser.getId())) {
+      return true;
+    }
+
+    switch (accountVisibility) {
+      case ALL:
+        return true;
+      case SAME_GROUP: {
+        Set<AccountGroup.UUID> usersGroups = groupsOf(otherUser);
+        usersGroups.remove(AccountGroup.ANONYMOUS_USERS);
+        usersGroups.remove(AccountGroup.REGISTERED_USERS);
+        if (currentUser.getEffectiveGroups().containsAnyOf(usersGroups)) {
+          return true;
+        }
+        break;
+      }
+      case VISIBLE_GROUP: {
+        Set<AccountGroup.UUID> usersGroups = groupsOf(otherUser);
+        usersGroups.remove(AccountGroup.ANONYMOUS_USERS);
+        usersGroups.remove(AccountGroup.REGISTERED_USERS);
+        for (AccountGroup.UUID usersGroup : usersGroups) {
+          try {
+            if (groupControlFactory.controlFor(usersGroup).isVisible()) {
+              return true;
+            }
+          } catch (NoSuchGroupException e) {
+            continue;
+          }
+        }
+        break;
+      }
+      case NONE:
+        break;
+      default:
+        throw new IllegalStateException("Bad AccountVisibility " + accountVisibility);
+    }
+    return false;
+  }
+
+  private Set<AccountGroup.UUID> groupsOf(Account account) {
+    return userFactory.create(account.getId()).getEffectiveGroups().getKnownGroups();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountInfoCacheFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountInfoCacheFactory.java
index bb6e278..e3cf023 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountInfoCacheFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountInfoCacheFactory.java
@@ -16,7 +16,7 @@
 
 import com.google.gerrit.common.data.AccountInfo;
 import com.google.gerrit.common.data.AccountInfoCache;
-import com.google.gerrit.reviewdb.Account;
+import com.google.gerrit.reviewdb.client.Account;
 import com.google.inject.Inject;
 
 import java.util.ArrayList;
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 5cb8f36..6c216a8 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
@@ -15,18 +15,22 @@
 package com.google.gerrit.server.account;
 
 import com.google.gerrit.common.auth.openid.OpenIdUrls;
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.errors.InvalidUserNameException;
 import com.google.gerrit.common.errors.NameAlreadyUsedException;
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.AccountExternalId;
-import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.AccountGroupMember;
-import com.google.gerrit.reviewdb.AccountGroupMemberAudit;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountExternalId;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupMember;
+import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.config.AuthConfig;
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.client.SchemaFactory;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -51,6 +55,7 @@
   private final Realm realm;
   private final IdentifiedUser.GenericFactory userFactory;
   private final ChangeUserName.Factory changeUserNameFactory;
+  private final ProjectCache projectCache;
   private final AtomicBoolean firstAccount;
 
   @Inject
@@ -58,7 +63,8 @@
       final AccountCache byIdCache, final AccountByEmailCache byEmailCache,
       final AuthConfig authConfig, final Realm accountMapper,
       final IdentifiedUser.GenericFactory userFactory,
-      final ChangeUserName.Factory changeUserNameFactory) throws OrmException {
+      final ChangeUserName.Factory changeUserNameFactory,
+      final ProjectCache projectCache) throws OrmException {
     this.schema = schema;
     this.byIdCache = byIdCache;
     this.byEmailCache = byEmailCache;
@@ -66,6 +72,7 @@
     this.realm = accountMapper;
     this.userFactory = userFactory;
     this.changeUserNameFactory = changeUserNameFactory;
+    this.projectCache = projectCache;
 
     firstAccount = new AtomicBoolean();
     final ReviewDb db = schema.open();
@@ -275,9 +282,16 @@
       // is going to be the site's administrator and just make them that
       // to bootstrap the authentication database.
       //
-      final AccountGroup.Id admin = authConfig.getAdministratorsGroup();
+      Permission admin = projectCache.getAllProjects()
+          .getConfig()
+          .getAccessSection(AccessSection.GLOBAL_CAPABILITIES)
+          .getPermission(GlobalCapability.ADMINISTRATE_SERVER);
+
+      final AccountGroup.UUID uuid = admin.getRules().get(0).getGroup().getUUID();
+      final AccountGroup g = db.accountGroups().byUUID(uuid).iterator().next();
+      final AccountGroup.Id adminId = g.getId();
       final AccountGroupMember m =
-          new AccountGroupMember(new AccountGroupMember.Key(newId, admin));
+          new AccountGroupMember(new AccountGroupMember.Key(newId, adminId));
       db.accountGroupMembersAudit().insert(
           Collections.singleton(new AccountGroupMemberAudit(m, newId)));
       db.accountGroupMembers().insert(Collections.singleton(m));
@@ -368,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/AccountResolver.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResolver.java
index 3dce94e..3a2ac56 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResolver.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResolver.java
@@ -14,10 +14,10 @@
 
 package com.google.gerrit.server.account;
 
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.AccountExternalId;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gwtorm.client.OrmException;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountExternalId;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountState.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountState.java
index 9393227..488370e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountState.java
@@ -14,11 +14,11 @@
 
 package com.google.gerrit.server.account;
 
-import static com.google.gerrit.reviewdb.AccountExternalId.SCHEME_USERNAME;
+import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_USERNAME;
 
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.AccountExternalId;
-import com.google.gerrit.reviewdb.AccountGroup;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountExternalId;
+import com.google.gerrit.reviewdb.client.AccountGroup;
 
 import java.util.Collection;
 import java.util.HashSet;
@@ -26,11 +26,11 @@
 
 public class AccountState {
   private final Account account;
-  private final Set<AccountGroup.Id> internalGroups;
+  private final Set<AccountGroup.UUID> internalGroups;
   private final Collection<AccountExternalId> externalIds;
 
   public AccountState(final Account account,
-      final Set<AccountGroup.Id> actualGroups,
+      final Set<AccountGroup.UUID> actualGroups,
       final Collection<AccountExternalId> externalIds) {
     this.account = account;
     this.internalGroups = actualGroups;
@@ -89,7 +89,7 @@
   }
 
   /** The set of groups maintained directly within the Gerrit database. */
-  public Set<AccountGroup.Id> getInternalGroups() {
+  public Set<AccountGroup.UUID> getInternalGroups() {
     return internalGroups;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountVisibility.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountVisibility.java
new file mode 100644
index 0000000..7452da3
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountVisibility.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+/** Visibility level of other accounts to a given user. */
+public enum AccountVisibility {
+  /** All accounts are visible to all users. */
+  ALL,
+
+  /** Accounts sharing a group with the given user. */
+  SAME_GROUP,
+
+  /** Accounts in a group that is visible to the given user. */
+  VISIBLE_GROUP,
+
+  /**
+   * Other accounts are not visible to the given user unless they are explicitly
+   * collaborating on a change.
+   */
+  NONE;
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountVisibilityProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountVisibilityProvider.java
new file mode 100644
index 0000000..d7f7a19
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountVisibilityProvider.java
@@ -0,0 +1,59 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.server.config.GerritServerConfig;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.lib.Config;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class AccountVisibilityProvider implements Provider<AccountVisibility> {
+  private static final Logger log =
+      LoggerFactory.getLogger(AccountVisibilityProvider.class);
+
+  private final AccountVisibility accountVisibility;
+
+  @Inject
+  AccountVisibilityProvider(@GerritServerConfig Config cfg) {
+    AccountVisibility av;
+    if (cfg.getString("accounts", null, "visibility") != null) {
+      av = cfg.getEnum("accounts", null, "visibility", AccountVisibility.ALL);
+    } else if (cfg.getString("suggest", null, "accounts") != null) {
+      try {
+        av = cfg.getEnum("suggest", null, "accounts", AccountVisibility.ALL);
+        log.warn(String.format(
+            "Using legacy value %s for suggest.accounts;"
+            + " use accounts.visibility=%s instead",
+            av, av));
+      } catch (IllegalArgumentException err) {
+        // If suggest.accounts is a valid boolean, it's a new-style config, and
+        // we should use the default here. Invalid values are caught in
+        // SuggestServiceImpl so we don't worry about them here.
+        av = AccountVisibility.ALL;
+      }
+    } else {
+      av = AccountVisibility.ALL;
+    }
+    accountVisibility = av;
+  }
+
+  @Override
+  public AccountVisibility get() {
+    return accountVisibility;
+  }
+}
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 b3ca18a..35ab3af 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
@@ -14,10 +14,10 @@
 
 package com.google.gerrit.server.account;
 
-import static com.google.gerrit.reviewdb.AccountExternalId.SCHEME_GERRIT;
-import static com.google.gerrit.reviewdb.AccountExternalId.SCHEME_MAILTO;
+import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_GERRIT;
+import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_MAILTO;
 
-import com.google.gerrit.reviewdb.AccountExternalId;
+import com.google.gerrit.reviewdb.client.AccountExternalId;
 
 /**
  * Information for {@link AccountManager#authenticate(AuthRequest)}.
@@ -52,11 +52,12 @@
     return r;
   }
 
-  private final String externalId;
+  private String externalId;
   private String password;
   private String displayName;
   private String emailAddress;
   private String userName;
+  private boolean skipAuthentication;
 
   public AuthRequest(final String externalId) {
     this.externalId = externalId;
@@ -77,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;
   }
@@ -108,4 +117,12 @@
   public void setUserName(final String user) {
     userName = user;
   }
+
+  public boolean isSkipAuthentication() {
+    return skipAuthentication;
+  }
+
+  public void setSkipAuthentication(boolean skip) {
+    skipAuthentication = skip;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthResult.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthResult.java
index df65796..b94e41a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthResult.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthResult.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.server.account;
 
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.AccountExternalId;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountExternalId;
 
 /** Result from {@link AccountManager#authenticate(AuthRequest)}. */
 public class AuthResult {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityCollection.java
new file mode 100644
index 0000000..796b44e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityCollection.java
@@ -0,0 +1,113 @@
+// 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.AccessSection;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.common.data.PermissionRange;
+import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/** Caches active {@link GlobalCapability} set for a site. */
+public class CapabilityCollection {
+  private final Map<String, List<PermissionRule>> permissions;
+
+  public final List<PermissionRule> administrateServer;
+  public final List<PermissionRule> emailReviewers;
+  public final List<PermissionRule> priority;
+  public final List<PermissionRule> queryLimit;
+
+  public CapabilityCollection(AccessSection section) {
+    if (section == null) {
+      section = new AccessSection(AccessSection.GLOBAL_CAPABILITIES);
+    }
+
+    Map<String, List<PermissionRule>> tmp =
+        new HashMap<String, List<PermissionRule>>();
+    for (Permission permission : section.getPermissions()) {
+      for (PermissionRule rule : permission.getRules()) {
+        if (!permission.getName().equals(GlobalCapability.EMAIL_REVIEWERS)
+            && rule.getAction() == PermissionRule.Action.DENY) {
+          continue;
+        }
+
+        List<PermissionRule> r = tmp.get(permission.getName());
+        if (r == null) {
+          r = new ArrayList<PermissionRule>(2);
+          tmp.put(permission.getName(), r);
+        }
+        r.add(rule);
+      }
+    }
+    configureDefaults(tmp, section);
+
+    Map<String, List<PermissionRule>> res =
+        new HashMap<String, List<PermissionRule>>();
+    for (Map.Entry<String, List<PermissionRule>> e : tmp.entrySet()) {
+      List<PermissionRule> rules = e.getValue();
+      if (rules.size() == 1) {
+        res.put(e.getKey(), Collections.singletonList(rules.get(0)));
+      } else {
+        res.put(e.getKey(), Collections.unmodifiableList(
+            Arrays.asList(rules.toArray(new PermissionRule[rules.size()]))));
+      }
+    }
+    permissions = Collections.unmodifiableMap(res);
+
+    administrateServer = getPermission(GlobalCapability.ADMINISTRATE_SERVER);
+    emailReviewers = getPermission(GlobalCapability.EMAIL_REVIEWERS);
+    priority = getPermission(GlobalCapability.PRIORITY);
+    queryLimit = getPermission(GlobalCapability.QUERY_LIMIT);
+  }
+
+  public List<PermissionRule> getPermission(String permissionName) {
+    List<PermissionRule> r = permissions.get(permissionName);
+    return r != null ? r : Collections.<PermissionRule> emptyList();
+  }
+
+  private static final GroupReference anonymous = new GroupReference(
+      AccountGroup.ANONYMOUS_USERS,
+      "Anonymous Users");
+
+  private static void configureDefaults(Map<String, List<PermissionRule>> out,
+      AccessSection section) {
+    configureDefault(out, section, GlobalCapability.QUERY_LIMIT, anonymous);
+  }
+
+  private static void configureDefault(Map<String, List<PermissionRule>> out,
+      AccessSection section, String capName, GroupReference group) {
+    if (doesNotDeclare(section, capName)) {
+      PermissionRange.WithDefaults range = GlobalCapability.getRange(capName);
+      if (range != null) {
+        PermissionRule rule = new PermissionRule(group);
+        rule.setRange(range.getDefaultMin(), range.getDefaultMax());
+        out.put(capName, Collections.singletonList(rule));
+      }
+    }
+  }
+
+  private static boolean doesNotDeclare(AccessSection section, String capName) {
+    return section.getPermission(capName) == null;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
new file mode 100644
index 0000000..eb42921
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
@@ -0,0 +1,263 @@
+// 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.common.base.Function;
+import com.google.common.base.Predicate;
+import com.google.common.base.Predicates;
+import com.google.common.collect.Iterables;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.common.data.PermissionRange;
+import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.common.data.PermissionRule.Action;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.PeerDaemonUser;
+import com.google.gerrit.server.git.QueueProvider;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/** Access control management for server-wide capabilities. */
+public class CapabilityControl {
+  public static interface Factory {
+    public CapabilityControl create(CurrentUser user);
+  }
+
+  private final CapabilityCollection capabilities;
+  private final CurrentUser user;
+  private final Map<String, List<PermissionRule>> effective;
+
+  private Boolean canAdministrateServer;
+  private Boolean canEmailReviewers;
+
+  @Inject
+  CapabilityControl(ProjectCache projectCache, @Assisted CurrentUser currentUser) {
+    capabilities = projectCache.getAllProjects().getCapabilityCollection();
+    user = currentUser;
+    effective = new HashMap<String, List<PermissionRule>>();
+  }
+
+  /** Identity of the user the control will compute for. */
+  public CurrentUser getCurrentUser() {
+    return user;
+  }
+
+  /** @return true if the user can administer this server. */
+  public boolean canAdministrateServer() {
+    if (canAdministrateServer == null) {
+      canAdministrateServer = user instanceof PeerDaemonUser
+          || matchAny(capabilities.administrateServer, ALLOWED_RULE);
+    }
+    return canAdministrateServer;
+  }
+
+  /** @return true if the user can create an account for another user. */
+  public boolean canCreateAccount() {
+    return canPerform(GlobalCapability.CREATE_ACCOUNT)
+      || canAdministrateServer();
+  }
+
+  /** @return true if the user can create a group. */
+  public boolean canCreateGroup() {
+    return canPerform(GlobalCapability.CREATE_GROUP)
+      || canAdministrateServer();
+  }
+
+  /** @return true if the user can create a group. */
+  public boolean canCreateProject() {
+    return canPerform(GlobalCapability.CREATE_PROJECT)
+      || canAdministrateServer();
+  }
+
+  /** @return true if the user can email reviewers. */
+  public boolean canEmailReviewers() {
+    if (canEmailReviewers == null) {
+      canEmailReviewers =
+          matchAny(capabilities.emailReviewers, ALLOWED_RULE)
+          || !matchAny(capabilities.emailReviewers, Predicates.not(ALLOWED_RULE));
+
+    }
+    return canEmailReviewers;
+  }
+
+  /** @return true if the user can kill any running task. */
+  public boolean canKillTask() {
+    return canPerform(GlobalCapability.KILL_TASK)
+      || canAdministrateServer();
+  }
+
+  /** @return true if the user can view the server caches. */
+  public boolean canViewCaches() {
+    return canPerform(GlobalCapability.VIEW_CACHES)
+      || canAdministrateServer();
+  }
+
+  /** @return true if the user can flush the server's caches. */
+  public boolean canFlushCaches() {
+    return canPerform(GlobalCapability.FLUSH_CACHES)
+      || canAdministrateServer();
+  }
+
+  /** @return true if the user can view open connections. */
+  public boolean canViewConnections() {
+    return canPerform(GlobalCapability.VIEW_CONNECTIONS)
+        || canAdministrateServer();
+  }
+
+  /** @return true if the user can view the entire queue. */
+  public boolean canViewQueue() {
+    return canPerform(GlobalCapability.VIEW_QUEUE)
+      || canAdministrateServer();
+  }
+
+  /** @return true if the user can force replication to any configured destination. */
+  public boolean canStartReplication() {
+    return canPerform(GlobalCapability.START_REPLICATION)
+        || canAdministrateServer();
+  }
+
+  /** @return which priority queue the user's tasks should be submitted to. */
+  public QueueProvider.QueueType getQueueType() {
+    // If a non-generic group (that is not Anonymous Users or Registered Users)
+    // grants us INTERACTIVE permission, use the INTERACTIVE queue even if
+    // BATCH was otherwise granted. This allows site administrators to grant
+    // INTERACTIVE to Registered Users, and BATCH to 'CI Servers' and have
+    // the 'CI Servers' actually use the BATCH queue while everyone else gets
+    // to use the INTERACTIVE queue without additional grants.
+    //
+    GroupMembership groups = user.getEffectiveGroups();
+    boolean batch = false;
+    for (PermissionRule r : capabilities.priority) {
+      if (match(groups, r)) {
+        switch (r.getAction()) {
+          case INTERACTIVE:
+            if (!isGenericGroup(r.getGroup())) {
+              return QueueProvider.QueueType.INTERACTIVE;
+            }
+            break;
+
+          case BATCH:
+            batch = true;
+            break;
+        }
+      }
+    }
+
+    if (batch) {
+      // If any of our groups matched to the BATCH queue, use it.
+      return QueueProvider.QueueType.BATCH;
+    } else {
+      return QueueProvider.QueueType.INTERACTIVE;
+    }
+  }
+
+  private static boolean isGenericGroup(GroupReference group) {
+    return AccountGroup.ANONYMOUS_USERS.equals(group.getUUID())
+        || AccountGroup.REGISTERED_USERS.equals(group.getUUID());
+  }
+
+  /** True if the user has this permission. Works only for non labels. */
+  public boolean canPerform(String permissionName) {
+    return !access(permissionName).isEmpty();
+  }
+
+  /** The range of permitted values associated with a label permission. */
+  public PermissionRange getRange(String permission) {
+    if (GlobalCapability.hasRange(permission)) {
+      return toRange(permission, access(permission));
+    }
+    return null;
+  }
+
+  private static PermissionRange toRange(String permissionName,
+      List<PermissionRule> ruleList) {
+    int min = 0;
+    int max = 0;
+    for (PermissionRule rule : ruleList) {
+      min = Math.min(min, rule.getMin());
+      max = Math.max(max, rule.getMax());
+    }
+    return new PermissionRange(permissionName, min, max);
+  }
+
+  /** Rules for the given permission, or the empty list. */
+  private List<PermissionRule> access(String permissionName) {
+    List<PermissionRule> rules = effective.get(permissionName);
+    if (rules != null) {
+      return rules;
+    }
+
+    rules = capabilities.getPermission(permissionName);
+
+    if (rules.isEmpty()) {
+      effective.put(permissionName, rules);
+      return rules;
+    }
+
+    GroupMembership groups = user.getEffectiveGroups();
+    if (rules.size() == 1) {
+      if (!match(groups, rules.get(0))) {
+        rules = Collections.emptyList();
+      }
+      effective.put(permissionName, rules);
+      return rules;
+    }
+
+    List<PermissionRule> mine = new ArrayList<PermissionRule>(rules.size());
+    for (PermissionRule rule : rules) {
+      if (match(groups, rule)) {
+        mine.add(rule);
+      }
+    }
+
+    if (mine.isEmpty()) {
+      mine = Collections.emptyList();
+    }
+    effective.put(permissionName, mine);
+    return mine;
+  }
+
+  private static final Predicate<PermissionRule> ALLOWED_RULE = new Predicate<PermissionRule>() {
+    @Override
+    public boolean apply(PermissionRule rule) {
+      return rule.getAction() == Action.ALLOW;
+    }
+  };
+
+  private boolean matchAny(Iterable<PermissionRule> rules, Predicate<PermissionRule> predicate) {
+    Iterable<AccountGroup.UUID> ids = Iterables.transform(
+        Iterables.filter(rules, predicate),
+        new Function<PermissionRule, AccountGroup.UUID>() {
+          @Override
+          public AccountGroup.UUID apply(PermissionRule rule) {
+            return rule.getGroup().getUUID();
+          }
+        });
+    return user.getEffectiveGroups().containsAnyOf(ids);
+  }
+
+  private static boolean match(GroupMembership groups,
+      PermissionRule rule) {
+    return groups.contains(rule.getGroup().getUUID());
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/ChangeUserName.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/ChangeUserName.java
index e875a19..d6f45f5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/ChangeUserName.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/ChangeUserName.java
@@ -14,18 +14,18 @@
 
 package com.google.gerrit.server.account;
 
-import static com.google.gerrit.reviewdb.AccountExternalId.SCHEME_USERNAME;
+import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_USERNAME;
 
 import com.google.gerrit.common.errors.InvalidUserNameException;
 import com.google.gerrit.common.errors.NameAlreadyUsedException;
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.AccountExternalId;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountExternalId;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.ssh.SshKeyCache;
-import com.google.gwtjsonrpc.client.VoidResult;
-import com.google.gwtorm.client.OrmDuplicateKeyException;
-import com.google.gwtorm.client.OrmException;
+import com.google.gwtjsonrpc.common.VoidResult;
+import com.google.gwtorm.server.OrmDuplicateKeyException;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/ClearPassword.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/ClearPassword.java
index 1fc87fe..255c248 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/ClearPassword.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/ClearPassword.java
@@ -15,10 +15,10 @@
 package com.google.gerrit.server.account;
 
 import com.google.gerrit.common.errors.NoSuchEntityException;
-import com.google.gerrit.reviewdb.AccountExternalId;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.client.AccountExternalId;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
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 a836f54..56a95ba 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
@@ -14,22 +14,27 @@
 
 package com.google.gerrit.server.account;
 
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.AccountGroup;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.inject.Inject;
 
 import java.util.Collections;
+import java.util.List;
 import java.util.Set;
 
-public final class DefaultRealm implements Realm {
+public class DefaultRealm implements Realm {
   private final EmailExpander emailExpander;
   private final AccountByEmailCache byEmail;
+  private final MaterializedGroupMembership.Factory groupMembershipFactory;
 
   @Inject
   DefaultRealm(final EmailExpander emailExpander,
-      final AccountByEmailCache byEmail) {
+      final AccountByEmailCache byEmail,
+      final MaterializedGroupMembership.Factory groupMembershipFactory) {
     this.emailExpander = emailExpander;
     this.byEmail = byEmail;
+    this.groupMembershipFactory = groupMembershipFactory;
   }
 
   @Override
@@ -47,12 +52,17 @@
   }
 
   @Override
+  public AuthRequest link(ReviewDb db, Account.Id to, AuthRequest who) {
+    return who;
+  }
+
+  @Override
   public void onCreateAccount(final AuthRequest who, final Account account) {
   }
 
   @Override
-  public Set<AccountGroup.Id> groups(final AccountState who) {
-    return who.getInternalGroups();
+  public GroupMembership groups(final AccountState who) {
+    return groupMembershipFactory.create(who.getInternalGroups());
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GeneratePassword.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GeneratePassword.java
index 1b30503..bbab126 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GeneratePassword.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GeneratePassword.java
@@ -15,10 +15,10 @@
 package com.google.gerrit.server.account;
 
 import com.google.gerrit.common.errors.NoSuchEntityException;
-import com.google.gerrit.reviewdb.AccountExternalId;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.client.AccountExternalId;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
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 978d9c2..b092ac4 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
@@ -14,19 +14,36 @@
 
 package com.google.gerrit.server.account;
 
-import com.google.gerrit.reviewdb.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroup;
 
 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 d948aef..d29a5e5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupCacheImpl.java
@@ -14,28 +14,36 @@
 
 package com.google.gerrit.server.account;
 
-import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.AccountGroupName;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupName;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.cache.Cache;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.cache.EntryCreator;
-import com.google.gerrit.server.config.AuthConfig;
-import com.google.gwtorm.client.SchemaFactory;
+import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Module;
 import com.google.inject.Singleton;
 import com.google.inject.TypeLiteral;
 import com.google.inject.name.Named;
 
+import java.util.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
 public class GroupCacheImpl implements GroupCache {
   private static final String BYID_NAME = "groups";
   private static final String BYNAME_NAME = "groups_byname";
+  private static final String BYUUID_NAME = "groups_byuuid";
   private static final String BYEXT_NAME = "groups_byext";
+  private static final String BYNAME_LIST = "groups_byname_list";
 
   public static Module module() {
     return new CacheModule() {
@@ -49,11 +57,19 @@
             new TypeLiteral<Cache<AccountGroup.NameKey, AccountGroup>>() {};
         core(byName, BYNAME_NAME).populateWith(ByNameLoader.class);
 
+        final TypeLiteral<Cache<AccountGroup.UUID, AccountGroup>> byUUID =
+            new TypeLiteral<Cache<AccountGroup.UUID, AccountGroup>>() {};
+        core(byUUID, BYUUID_NAME).populateWith(ByUUIDLoader.class);
+
         final TypeLiteral<Cache<AccountGroup.ExternalNameKey, Collection<AccountGroup>>> byExternalName =
             new TypeLiteral<Cache<AccountGroup.ExternalNameKey, Collection<AccountGroup>>>() {};
         core(byExternalName, BYEXT_NAME) //
             .populateWith(ByExternalNameLoader.class);
 
+        final TypeLiteral<Cache<ListKey, SortedSet<AccountGroup.NameKey>>> listType =
+          new TypeLiteral<Cache<ListKey, SortedSet<AccountGroup.NameKey>>>() {};
+        core(listType, BYNAME_LIST).populateWith(Lister.class);
+
         bind(GroupCacheImpl.class);
         bind(GroupCache.class).to(GroupCacheImpl.class);
       }
@@ -62,16 +78,24 @@
 
   private final Cache<AccountGroup.Id, AccountGroup> byId;
   private final Cache<AccountGroup.NameKey, AccountGroup> byName;
+  private final Cache<AccountGroup.UUID, AccountGroup> byUUID;
   private final Cache<AccountGroup.ExternalNameKey, Collection<AccountGroup>> byExternalName;
+  private final Cache<ListKey,SortedSet<AccountGroup.NameKey>> list;
+  private final Lock listLock;
 
   @Inject
   GroupCacheImpl(
       @Named(BYID_NAME) Cache<AccountGroup.Id, AccountGroup> byId,
       @Named(BYNAME_NAME) Cache<AccountGroup.NameKey, AccountGroup> byName,
-      @Named(BYEXT_NAME) Cache<AccountGroup.ExternalNameKey, Collection<AccountGroup>> byExternalName) {
+      @Named(BYUUID_NAME) Cache<AccountGroup.UUID, AccountGroup> byUUID,
+      @Named(BYEXT_NAME) Cache<AccountGroup.ExternalNameKey, Collection<AccountGroup>> byExternalName,
+      @Named(BYNAME_LIST) final Cache<ListKey, SortedSet<AccountGroup.NameKey>> list) {
     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) {
@@ -81,30 +105,70 @@
   public void evict(final AccountGroup group) {
     byId.remove(group.getId());
     byName.remove(group.getNameKey());
+    byUUID.remove(group.getGroupUUID());
     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) {
     return byName.get(name);
   }
 
+  public AccountGroup get(final AccountGroup.UUID uuid) {
+    return byUUID.get(uuid);
+  }
+
   public Collection<AccountGroup> get(
       final AccountGroup.ExternalNameKey externalName) {
     return byExternalName.get(externalName);
   }
 
+  @Override
+  public 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;
-    private final AccountGroup.Id administrators;
 
     @Inject
-    ByIdLoader(final SchemaFactory<ReviewDb> sf, final AuthConfig authConfig) {
+    ByIdLoader(final SchemaFactory<ReviewDb> sf) {
       schema = sf;
-      administrators = authConfig.getAdministratorsGroup();
     }
 
     @Override
@@ -126,9 +190,8 @@
     public AccountGroup missing(final AccountGroup.Id key) {
       final AccountGroup.NameKey name =
           new AccountGroup.NameKey("Deleted Group" + key.toString());
-      final AccountGroup g = new AccountGroup(name, key);
+      final AccountGroup g = new AccountGroup(name, key, null);
       g.setType(AccountGroup.Type.SYSTEM);
-      g.setOwnerGroupId(administrators);
       return g;
     }
   }
@@ -160,6 +223,32 @@
     }
   }
 
+  static class ByUUIDLoader extends
+      EntryCreator<AccountGroup.UUID, AccountGroup> {
+    private final SchemaFactory<ReviewDb> schema;
+
+    @Inject
+    ByUUIDLoader(final SchemaFactory<ReviewDb> sf) {
+      schema = sf;
+    }
+
+    @Override
+    public AccountGroup createEntry(final AccountGroup.UUID uuid)
+        throws Exception {
+      final ReviewDb db = schema.open();
+      try {
+        List<AccountGroup> r = db.accountGroups().byUUID(uuid).toList();
+        if (r.size() == 1) {
+          return r.get(0);
+        } else {
+          return null;
+        }
+      } finally {
+        db.close();
+      }
+    }
+  }
+
   static class ByExternalNameLoader extends
       EntryCreator<AccountGroup.ExternalNameKey, Collection<AccountGroup>> {
     private final SchemaFactory<ReviewDb> schema;
@@ -180,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/schema/Schema_49.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupComparator.java
similarity index 64%
rename from gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_49.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/account/GroupComparator.java
index 0977ee9..13800b5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_49.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupComparator.java
@@ -12,15 +12,16 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.schema;
+package com.google.gerrit.server.account;
 
-import com.google.inject.Inject;
-import com.google.inject.Provider;
+import com.google.gerrit.reviewdb.client.AccountGroup;
 
-public class Schema_49 extends SchemaVersion {
+import java.util.Comparator;
 
-  @Inject
-  Schema_49(Provider<Schema_48> prior) {
-    super(prior);
+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/GroupControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java
index 602b593..f7451a8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupControl.java
@@ -15,9 +15,10 @@
 package com.google.gerrit.server.account;
 
 import com.google.gerrit.common.errors.NoSuchGroupException;
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.AccountGroup;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
@@ -39,11 +40,20 @@
       if (group == null) {
         throw new NoSuchGroupException(groupId);
       }
-      return new GroupControl(user.get(), group);
+      return new GroupControl(groupCache, user.get(), group);
+    }
+
+    public GroupControl controlFor(final AccountGroup.UUID groupId)
+        throws NoSuchGroupException {
+      final AccountGroup group = groupCache.get(groupId);
+      if (group == null) {
+        throw new NoSuchGroupException(groupId);
+      }
+      return new GroupControl(groupCache, user.get(), group);
     }
 
     public GroupControl controlFor(final AccountGroup group) {
-      return new GroupControl(user.get(), group);
+      return new GroupControl(groupCache, user.get(), group);
     }
 
     public GroupControl validateFor(final AccountGroup.Id groupId)
@@ -56,10 +66,13 @@
     }
   }
 
+  private final GroupCache groupCache;
   private final CurrentUser user;
   private final AccountGroup group;
+  private Boolean isOwner;
 
-  GroupControl(final CurrentUser who, final AccountGroup gc) {
+  GroupControl(GroupCache g, CurrentUser who, AccountGroup gc) {
+    groupCache = g;
     user = who;
     group = gc;
   }
@@ -74,36 +87,50 @@
 
   /** Can this user see this group exists? */
   public boolean isVisible() {
-    return group.isVisibleToAll() || isOwner();
+    return group.isVisibleToAll()
+      || user.getEffectiveGroups().contains(group.getGroupUUID())
+      || isOwner();
   }
 
   public boolean isOwner() {
-    final AccountGroup.Id owner = group.getOwnerGroupId();
-    return getCurrentUser().getEffectiveGroups().contains(owner)
-        || getCurrentUser().isAdministrator();
+    if (isOwner == null) {
+      AccountGroup g = groupCache.get(group.getOwnerGroupId());
+      AccountGroup.UUID ownerUUID = g != null ? g.getGroupUUID() : null;
+      isOwner = getCurrentUser().getEffectiveGroups().contains(ownerUUID)
+             || getCurrentUser().getCapabilities().canAdministrateServer();
+    }
+    return isOwner;
   }
 
-  public boolean canAddMember(final Account.Id id) {
+  public boolean canAddMember(Account.Id id) {
     return isOwner();
   }
 
-  public boolean canRemoveMember(final Account.Id id) {
+  public boolean canRemoveMember(Account.Id id) {
     return isOwner();
   }
 
   public boolean canSeeMember(Account.Id id) {
-    return isVisible();
+    if (user instanceof IdentifiedUser
+        && ((IdentifiedUser) user).getAccountId().equals(id)) {
+      return true;
+    }
+    return canSeeMembers();
   }
 
-  public boolean canAddGroup(final AccountGroup.Id id) {
+  public boolean canAddGroup(AccountGroup.Id id) {
     return isOwner();
   }
 
-  public boolean canRemoveGroup(final AccountGroup.Id id) {
+  public boolean canRemoveGroup(AccountGroup.Id id) {
     return isOwner();
   }
 
   public boolean canSeeGroup(AccountGroup.Id id) {
-    return isVisible();
+    return canSeeMembers();
+  }
+
+  private boolean canSeeMembers() {
+    return group.isVisibleToAll() || isOwner();
   }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/GroupDetailFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupDetailFactory.java
similarity index 86%
rename from gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/GroupDetailFactory.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/account/GroupDetailFactory.java
index da6ab65..7f88134 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/GroupDetailFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupDetailFactory.java
@@ -12,21 +12,16 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.httpd.rpc.account;
+package com.google.gerrit.server.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.Account;
-import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.AccountGroupInclude;
-import com.google.gerrit.reviewdb.AccountGroupMember;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gerrit.server.account.AccountInfoCacheFactory;
-import com.google.gerrit.server.account.GroupCache;
-import com.google.gerrit.server.account.GroupControl;
-import com.google.gerrit.server.account.GroupInfoCacheFactory;
-import com.google.gwtorm.client.OrmException;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupInclude;
+import com.google.gerrit.reviewdb.client.AccountGroupMember;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -34,9 +29,10 @@
 import java.util.Collections;
 import java.util.Comparator;
 import java.util.List;
+import java.util.concurrent.Callable;
 
-class GroupDetailFactory extends Handler<GroupDetail> {
-  interface Factory {
+public class GroupDetailFactory implements Callable<GroupDetail> {
+  public interface Factory {
     GroupDetailFactory create(AccountGroup.Id groupId);
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCache.java
index 3088806..432a8b8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCache.java
@@ -14,14 +14,14 @@
 
 package com.google.gerrit.server.account;
 
-import com.google.gerrit.reviewdb.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroup;
 
 import java.util.Collection;
 
 /** Tracks group inclusions in memory for efficient access. */
 public interface GroupIncludeCache {
-  public Collection<AccountGroup.Id> getByInclude(AccountGroup.Id groupId);
+  public Collection<AccountGroup.UUID> getByInclude(AccountGroup.UUID groupId);
 
-  public void evictInclude(AccountGroup.Id groupId);
+  public void evictInclude(AccountGroup.UUID groupId);
 }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
index 76e9231..791d0f5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupIncludeCacheImpl.java
@@ -14,23 +14,24 @@
 
 package com.google.gerrit.server.account;
 
-import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.AccountGroupInclude;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupInclude;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.cache.Cache;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.cache.EntryCreator;
-import com.google.gwtorm.client.SchemaFactory;
-
+import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Module;
 import com.google.inject.Singleton;
 import com.google.inject.TypeLiteral;
 import com.google.inject.name.Named;
 
-import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
 
 /** Tracks group inclusions in memory for efficient access. */
 @Singleton
@@ -41,8 +42,8 @@
     return new CacheModule() {
       @Override
       protected void configure() {
-        final TypeLiteral<Cache<AccountGroup.Id, Collection<AccountGroup.Id>>> byInclude =
-            new TypeLiteral<Cache<AccountGroup.Id, Collection<AccountGroup.Id>>>() {};
+        final TypeLiteral<Cache<AccountGroup.UUID, Collection<AccountGroup.UUID>>> byInclude =
+            new TypeLiteral<Cache<AccountGroup.UUID, Collection<AccountGroup.UUID>>>() {};
         core(byInclude, BYINCLUDE_NAME).populateWith(ByIncludeLoader.class);
 
         bind(GroupIncludeCacheImpl.class);
@@ -51,23 +52,24 @@
     };
   }
 
-  private final Cache<AccountGroup.Id, Collection<AccountGroup.Id>> byInclude;
+  private final Cache<AccountGroup.UUID, Collection<AccountGroup.UUID>> byInclude;
 
   @Inject
   GroupIncludeCacheImpl(
-      @Named(BYINCLUDE_NAME) Cache<AccountGroup.Id, Collection<AccountGroup.Id>> byInclude) {
+      @Named(BYINCLUDE_NAME) Cache<AccountGroup.UUID, Collection<AccountGroup.UUID>> byInclude) {
     this.byInclude = byInclude;
   }
 
-  public Collection<AccountGroup.Id> getByInclude(final AccountGroup.Id groupId) {
+  public Collection<AccountGroup.UUID> getByInclude(AccountGroup.UUID groupId) {
     return byInclude.get(groupId);
   }
 
-  public void evictInclude(AccountGroup.Id groupId) {
+  public void evictInclude(AccountGroup.UUID groupId) {
     byInclude.remove(groupId);
   }
 
-  static class ByIncludeLoader extends EntryCreator<AccountGroup.Id, Collection<AccountGroup.Id>> {
+  static class ByIncludeLoader extends
+      EntryCreator<AccountGroup.UUID, Collection<AccountGroup.UUID>> {
     private final SchemaFactory<ReviewDb> schema;
 
     @Inject
@@ -76,14 +78,23 @@
     }
 
     @Override
-    public Collection<AccountGroup.Id> createEntry(final AccountGroup.Id key) throws Exception {
+    public Collection<AccountGroup.UUID> createEntry(final AccountGroup.UUID key) throws Exception {
       final ReviewDb db = schema.open();
       try {
-        ArrayList<AccountGroup.Id> groupArray = new ArrayList<AccountGroup.Id> ();
-        for (AccountGroupInclude agi : db.accountGroupIncludes().byInclude(key)) {
-          groupArray.add(agi.getGroupId());
+        List<AccountGroup> group = db.accountGroups().byUUID(key).toList();
+        if (group.size() != 1) {
+          return Collections.emptyList();
         }
 
+        Set<AccountGroup.Id> ids = new HashSet<AccountGroup.Id>();
+        for (AccountGroupInclude agi : db.accountGroupIncludes().byInclude(group.get(0).getId())) {
+          ids.add(agi.getGroupId());
+        }
+
+        Set<AccountGroup.UUID> groupArray = new HashSet<AccountGroup.UUID> ();
+        for (AccountGroup g : db.accountGroups().get(ids)) {
+          groupArray.add(g.getGroupUUID());
+        }
         return Collections.unmodifiableCollection(groupArray);
       } finally {
         db.close();
@@ -91,7 +102,7 @@
     }
 
     @Override
-    public Collection<AccountGroup.Id> missing(final AccountGroup.Id key) {
+    public Collection<AccountGroup.UUID> missing(final AccountGroup.UUID key) {
       return Collections.emptyList();
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupInfoCacheFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupInfoCacheFactory.java
index a3e592f..19d953d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupInfoCacheFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupInfoCacheFactory.java
@@ -16,7 +16,7 @@
 
 import com.google.gerrit.common.data.GroupInfo;
 import com.google.gerrit.common.data.GroupInfoCache;
-import com.google.gerrit.reviewdb.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.inject.Inject;
 
 import java.util.ArrayList;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembers.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembers.java
new file mode 100644
index 0000000..84282b5
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembers.java
@@ -0,0 +1,124 @@
+// 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.NoSuchGroupException;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupInclude;
+import com.google.gerrit.reviewdb.client.AccountGroupMember;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.Set;
+
+public class GroupMembers {
+  public interface Factory {
+    GroupMembers create();
+  }
+
+  private final GroupCache groupCache;
+  private final GroupDetailFactory.Factory groupDetailFactory;
+  private final AccountCache accountCache;
+  private final ProjectControl.GenericFactory projectControl;
+  private final IdentifiedUser currentUser;
+
+  @Inject
+  GroupMembers(final GroupCache groupCache,
+      final GroupDetailFactory.Factory groupDetailFactory,
+      final AccountCache accountCache,
+      final ProjectControl.GenericFactory projectControl,
+      final IdentifiedUser currentUser) {
+    this.groupCache = groupCache;
+    this.groupDetailFactory = groupDetailFactory;
+    this.accountCache = accountCache;
+    this.projectControl = projectControl;
+    this.currentUser = currentUser;
+  }
+
+  public Set<Account> listAccounts(final AccountGroup.UUID groupUUID,
+      final Project.NameKey project) throws NoSuchGroupException,
+      NoSuchProjectException, OrmException {
+    return listAccounts(groupUUID, project, new HashSet<AccountGroup.UUID>());
+  }
+
+  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();
+    }
+
+    final Set<AccountGroup.UUID> ownerGroups =
+        projectControl.controlFor(project, currentUser).getProjectState()
+            .getOwners();
+
+    final HashSet<Account> projectOwners = new HashSet<Account>();
+    for (final AccountGroup.UUID ownerGroup : ownerGroups) {
+      if (!seen.contains(ownerGroup)) {
+        projectOwners.addAll(listAccounts(ownerGroup, project, seen));
+      }
+    }
+    return projectOwners;
+  }
+
+  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();
+
+    final Set<Account> members = new HashSet<Account>();
+    if (groupDetail.members != null) {
+      for (final AccountGroupMember member : groupDetail.members) {
+        members.add(accountCache.get(member.getAccountId()).getAccount());
+      }
+    }
+    if (groupDetail.includes != null) {
+      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));
+        }
+      }
+    }
+    return members;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembership.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembership.java
new file mode 100644
index 0000000..9bb571e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembership.java
@@ -0,0 +1,51 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import com.google.gerrit.reviewdb.client.AccountGroup;
+
+import java.util.Collections;
+import java.util.Set;
+
+/**
+ * Implementations of GroupMembership provide methods to test
+ * the presence of a user in a particular group.
+ */
+public interface GroupMembership {
+
+  public static final GroupMembership EMPTY =
+      new ListGroupMembership(Collections.<AccountGroup.UUID>emptySet());
+
+  /**
+   * Returns {@code true} when the user this object was created for is a member
+   * of the specified group.
+   */
+  boolean contains(AccountGroup.UUID groupId);
+
+  /**
+   * Returns {@code true} when the user this object was created for is a member
+   * of any of the specified group.
+   */
+  boolean containsAnyOf(Iterable<AccountGroup.UUID> groupIds);
+
+  /**
+   * Returns the set of groups that can be determined by the implementation.
+   * This may not return all groups the {@link #contains(AccountGroup.UUID)}
+   * would return {@code true} for, but will at least contain all top level
+   * groups. This restriction stems from the API of some group systems, which
+   * make it expensive to enumate the members of a group.
+   */
+  Set<AccountGroup.UUID> getKnownGroups();
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupUUID.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupUUID.java
new file mode 100644
index 0000000..b871c68
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupUUID.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2010 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import com.google.gerrit.reviewdb.client.AccountGroup;
+
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
+
+import java.security.MessageDigest;
+
+public class GroupUUID {
+  public static AccountGroup.UUID make(String groupName, PersonIdent creator) {
+    MessageDigest md = Constants.newMessageDigest();
+    md.update(Constants.encode("group " + groupName + "\n"));
+    md.update(Constants.encode("creator " + creator.toExternalString() + "\n"));
+    return new AccountGroup.UUID(ObjectId.fromRaw(md.digest()).name());
+  }
+
+  private GroupUUID() {
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/ListGroupMembership.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/ListGroupMembership.java
new file mode 100644
index 0000000..237d381
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/ListGroupMembership.java
@@ -0,0 +1,52 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+
+import java.util.Set;
+
+/**
+ * GroupMembership over an explicit list.
+ */
+public class ListGroupMembership implements GroupMembership {
+
+  private final Set<AccountGroup.UUID> groups;
+
+  public ListGroupMembership(Iterable<AccountGroup.UUID> groupIds) {
+    this.groups = ImmutableSet.copyOf(groupIds);
+  }
+
+  @Override
+  public boolean contains(AccountGroup.UUID groupId) {
+    return groups.contains(groupId);
+  }
+
+  @Override
+  public boolean containsAnyOf(Iterable<AccountGroup.UUID> groupIds) {
+    for (AccountGroup.UUID groupId : groupIds) {
+      if (contains(groupId)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  @Override
+  public Set<AccountGroup.UUID> getKnownGroups() {
+    return ImmutableSet.copyOf(groups);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/MaterializedGroupMembership.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/MaterializedGroupMembership.java
new file mode 100644
index 0000000..81ff656
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/MaterializedGroupMembership.java
@@ -0,0 +1,93 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import java.util.Collections;
+import java.util.Queue;
+import java.util.Set;
+
+/**
+ * Creates a GroupMembership object from materialized collection of groups.
+ */
+public class MaterializedGroupMembership implements GroupMembership {
+  public interface Factory {
+    MaterializedGroupMembership create(Iterable<AccountGroup.UUID> groupIds);
+  }
+
+  private final GroupIncludeCache groupIncludeCache;
+  private final Set<AccountGroup.UUID> includes;
+  private final Queue<AccountGroup.UUID> groupQueue;
+
+  @Inject
+  MaterializedGroupMembership(
+      GroupIncludeCache groupIncludeCache,
+      @Assisted Iterable<AccountGroup.UUID> seedGroups) {
+    this.groupIncludeCache = groupIncludeCache;
+    this.includes = Sets.newHashSet(seedGroups);
+    this.groupQueue = Lists.newLinkedList(seedGroups);
+  }
+
+  @Override
+  public boolean contains(AccountGroup.UUID id) {
+    if (id == null) {
+      return false;
+    }
+    if (includes.contains(id)) {
+      return true;
+    }
+    return findIncludedGroup(Collections.singleton(id));
+  }
+
+  @Override
+  public boolean containsAnyOf(Iterable<AccountGroup.UUID> ids) {
+    Set<AccountGroup.UUID> query = Sets.newHashSet();
+    for (AccountGroup.UUID groupId : ids) {
+      if (includes.contains(groupId)) {
+        return true;
+      }
+      query.add(groupId);
+    }
+
+    return findIncludedGroup(query);
+  }
+
+  private boolean findIncludedGroup(Set<AccountGroup.UUID> query) {
+    boolean found = false;
+    while (!found && !groupQueue.isEmpty()) {
+      AccountGroup.UUID id = groupQueue.remove();
+
+      for (final AccountGroup.UUID groupId : groupIncludeCache.getByInclude(id)) {
+        if (includes.add(groupId)) {
+          groupQueue.add(groupId);
+          found |= query.contains(groupId);
+        }
+      }
+    }
+
+    return found;
+  }
+
+  @Override
+  public Set<AccountGroup.UUID> getKnownGroups() {
+    findIncludedGroup(Collections.<AccountGroup.UUID>emptySet()); // find all
+    return Sets.newHashSet(includes);
+  }
+}
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 aab1cda..5f94df2 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
@@ -15,22 +15,27 @@
 package com.google.gerrit.server.account;
 
 import com.google.gerrit.common.errors.NameAlreadyUsedException;
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.AccountGroupInclude;
-import com.google.gerrit.reviewdb.AccountGroupIncludeAudit;
-import com.google.gerrit.reviewdb.AccountGroupMember;
-import com.google.gerrit.reviewdb.AccountGroupMemberAudit;
-import com.google.gerrit.reviewdb.AccountGroupName;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.common.errors.PermissionDeniedException;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupInclude;
+import com.google.gerrit.reviewdb.client.AccountGroupIncludeAudit;
+import com.google.gerrit.reviewdb.client.AccountGroupMember;
+import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
+import com.google.gerrit.reviewdb.client.AccountGroupName;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gwtorm.client.OrmDuplicateKeyException;
-import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.server.OrmDuplicateKeyException;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 
+import org.eclipse.jgit.lib.PersonIdent;
+
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.List;
 
 public class PerformCreateGroup {
@@ -43,14 +48,21 @@
   private final AccountCache accountCache;
   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) {
+      final GroupIncludeCache groupIncludeCache,
+      final IdentifiedUser currentUser,
+      @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;
   }
 
   /**
@@ -71,17 +83,28 @@
    *         error
    * @throws NameAlreadyUsedException is thrown in case a group with the given
    *         name already exists
+   * @throws PermissionDeniedException user cannot create a group.
    */
   public AccountGroup.Id createGroup(final String groupName,
       final String groupDescription, final boolean visibleToAll,
       final AccountGroup.Id ownerGroupId,
       final Collection<? extends Account.Id> initialMembers,
       final Collection<? extends AccountGroup.Id> initialGroups)
-      throws OrmException, NameAlreadyUsedException {
+      throws OrmException, NameAlreadyUsedException, PermissionDeniedException {
+    if (!currentUser.getCapabilities().canCreateGroup()) {
+      throw new PermissionDeniedException(String.format(
+        "%s does not have \"Create Group\" capability.",
+        currentUser.getUserName()));
+    }
+
     final AccountGroup.Id groupId =
         new AccountGroup.Id(db.nextAccountGroupId());
     final AccountGroup.NameKey nameKey = new AccountGroup.NameKey(groupName);
-    final AccountGroup group = new AccountGroup(nameKey, groupId);
+    final AccountGroup.UUID uuid = GroupUUID.make(groupName,
+        currentUser.newCommitterIdent(
+            serverIdent.getWhen(),
+            serverIdent.getTimeZone()));
+    final AccountGroup group = new AccountGroup(nameKey, groupId, uuid);
     group.setVisibleToAll(visibleToAll);
     if (ownerGroupId != null) {
       group.setOwnerGroupId(ownerGroupId);
@@ -105,6 +128,8 @@
       addGroups(groupId, initialGroups);
     }
 
+    groupCache.onCreateGroup(nameKey);
+
     return groupId;
   }
 
@@ -149,8 +174,9 @@
     db.accountGroupIncludes().insert(includeList);
     db.accountGroupIncludesAudit().insert(includesAudit);
 
-    for (AccountGroup.Id includeId : groups) {
-      groupIncludeCache.evictInclude(includeId);
+    for (AccountGroup group : db.accountGroups().get(
+        new HashSet<AccountGroup.Id>(groups))) {
+      groupIncludeCache.evictInclude(group.getGroupUUID());
     }
   }
 }
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..6db232a
--- /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.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupName;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.git.RenameGroupOp;
+import com.google.gwtorm.server.OrmDuplicateKeyException;
+import com.google.gwtorm.server.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 e42d4ae..fc7c0be 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
@@ -14,8 +14,9 @@
 
 package com.google.gerrit.server.account;
 
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.AccountGroup;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 
 import java.util.Set;
 
@@ -25,9 +26,12 @@
 
   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.Id> groups(AccountState who);
+  public GroupMembership groups(AccountState who);
 
   /**
    * Locate an account whose local username is the given account name.
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..3112ed4
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/VisibleGroups.java
@@ -0,0 +1,146 @@
+// 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.client.AccountGroup;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.LinkedList;
+import java.util.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));
+  }
+
+  /**
+   * Returns visible list of known groups for the user. Depending on the group
+   * membership realms supported, this may only return a subset of the effective
+   * groups.
+   * @See GroupMembership#getKnownGroups()
+   */
+  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().getKnownGroups();
+      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/schema/Schema_49.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/AuthenticationUnavailableException.java
similarity index 61%
copy from gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_49.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/auth/AuthenticationUnavailableException.java
index 0977ee9..b0b6142 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_49.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/AuthenticationUnavailableException.java
@@ -12,15 +12,15 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.schema;
+package com.google.gerrit.server.auth;
 
-import com.google.inject.Inject;
-import com.google.inject.Provider;
+import com.google.gerrit.server.account.AccountException;
 
-public class Schema_49 extends SchemaVersion {
+/** A query to the authentication server failed */
+public class AuthenticationUnavailableException extends AccountException {
+  private static final long serialVersionUID = 1L;
 
-  @Inject
-  Schema_49(Provider<Schema_48> prior) {
-    super(prior);
+  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 a9ea853..e81bfc2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/Helper.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/Helper.java
@@ -14,10 +14,11 @@
 
 package com.google.gerrit.server.auth.ldap;
 
-import com.google.gerrit.common.data.ParamertizedString;
-import com.google.gerrit.reviewdb.AccountGroup;
+import com.google.gerrit.common.data.ParameterizedString;
+import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.account.AccountException;
 import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.util.ssl.BlindSSLSocketFactory;
 import com.google.inject.Inject;
@@ -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;
   }
 
@@ -134,7 +148,7 @@
     }
   }
 
-  Set<AccountGroup.Id> queryForGroups(final DirContext ctx,
+  Set<AccountGroup.UUID> queryForGroups(final DirContext ctx,
       final String username, LdapQuery.Result account)
       throws NamingException, AccountException {
     final LdapSchema schema = getSchema(ctx);
@@ -179,12 +193,12 @@
       }
     }
 
-    final Set<AccountGroup.Id> actual = new HashSet<AccountGroup.Id>();
+    final Set<AccountGroup.UUID> actual = new HashSet<AccountGroup.UUID>();
     for (String dn : groupDNs) {
       for (AccountGroup group : groupCache
           .get(new AccountGroup.ExternalNameKey(dn))) {
         if (group.getType() == AccountGroup.Type.LDAP) {
-          actual.add(group.getId());
+          actual.add(group.getGroupUUID());
         }
       }
     }
@@ -224,16 +238,16 @@
   class LdapSchema {
     final LdapType type;
 
-    final ParamertizedString accountFullName;
-    final ParamertizedString accountEmailAddress;
-    final ParamertizedString accountSshUserName;
+    final ParameterizedString accountFullName;
+    final ParameterizedString accountEmailAddress;
+    final ParameterizedString accountSshUserName;
     final String accountMemberField;
     final List<LdapQuery> accountQueryList;
 
     boolean groupNeedsAccount;
     final List<String> groupBases;
     final SearchScope groupScope;
-    final ParamertizedString groupPattern;
+    final ParameterizedString groupPattern;
     final List<LdapQuery> groupMemberQueryList;
 
     LdapSchema(final DirContext ctx) {
@@ -255,7 +269,7 @@
       for (String groupBase : groupBases) {
         if (groupMemberPattern != null) {
           final LdapQuery groupMemberQuery =
-              new LdapQuery(groupBase, groupScope, new ParamertizedString(
+              new LdapQuery(groupBase, groupScope, new ParameterizedString(
                   groupMemberPattern), Collections.<String> emptySet());
           if (groupMemberQuery.getParameters().isEmpty()) {
             throw new IllegalArgumentException(
@@ -303,7 +317,7 @@
 
       for (String accountBase : LdapRealm.requiredList(config, "accountBase")) {
         final LdapQuery accountQuery =
-            new LdapQuery(accountBase, accountScope, new ParamertizedString(
+            new LdapQuery(accountBase, accountScope, new ParameterizedString(
                 accountPattern), accountAtts);
         if (accountQuery.getParameters().isEmpty()) {
           throw new IllegalArgumentException(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapModule.java
index 810df28..6eb2f54 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapModule.java
@@ -16,8 +16,8 @@
 
 import static java.util.concurrent.TimeUnit.HOURS;
 
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.AccountGroup;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.account.Realm;
 import com.google.gerrit.server.cache.Cache;
 import com.google.gerrit.server.cache.CacheModule;
@@ -32,8 +32,8 @@
 
   @Override
   protected void configure() {
-    final TypeLiteral<Cache<String, Set<AccountGroup.Id>>> groups =
-        new TypeLiteral<Cache<String, Set<AccountGroup.Id>>>() {};
+    final TypeLiteral<Cache<String, Set<AccountGroup.UUID>>> groups =
+        new TypeLiteral<Cache<String, Set<AccountGroup.UUID>>>() {};
     core(groups, GROUP_CACHE).maxAge(1, HOURS) //
         .populateWith(LdapRealm.MemberLoader.class);
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapQuery.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapQuery.java
index 7d1e37d..8a6dfeb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapQuery.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapQuery.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.auth.ldap;
 
-import com.google.gerrit.common.data.ParamertizedString;
+import com.google.gerrit.common.data.ParameterizedString;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -38,11 +38,11 @@
 
   private final String base;
   private final SearchScope searchScope;
-  private final ParamertizedString pattern;
+  private final ParameterizedString pattern;
   private final String[] returnAttributes;
 
   LdapQuery(final String base, final SearchScope searchScope,
-      final ParamertizedString pattern, final Set<String> returnAttributes) {
+      final ParameterizedString pattern, final Set<String> returnAttributes) {
     this.base = base;
     this.searchScope = searchScope;
 
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 de33b44..e085d1e 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
@@ -14,27 +14,31 @@
 
 package com.google.gerrit.server.auth.ldap;
 
-import static com.google.gerrit.reviewdb.AccountExternalId.SCHEME_GERRIT;
+import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_GERRIT;
 
-import com.google.gerrit.common.data.ParamertizedString;
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.AccountExternalId;
-import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.AuthType;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.common.collect.Iterables;
+import com.google.gerrit.common.data.ParameterizedString;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountExternalId;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AuthType;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.account.AccountException;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.account.EmailExpander;
+import com.google.gerrit.server.account.GroupMembership;
+import com.google.gerrit.server.account.MaterializedGroupMembership;
 import com.google.gerrit.server.account.Realm;
+import com.google.gerrit.server.auth.AuthenticationUnavailableException;
 import com.google.gerrit.server.auth.ldap.Helper.LdapSchema;
 import com.google.gerrit.server.cache.Cache;
 import com.google.gerrit.server.cache.EntryCreator;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.client.SchemaFactory;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 import com.google.inject.name.Named;
@@ -49,6 +53,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,22 +72,27 @@
   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.Id>> membershipCache;
+  private final Cache<String, Set<AccountGroup.UUID>> membershipCache;
+  private final MaterializedGroupMembership.Factory groupMembershipFactory;
 
   @Inject
   LdapRealm(
       final Helper helper,
       final AuthConfig authConfig,
       final EmailExpander emailExpander,
-      @Named(LdapModule.GROUP_CACHE) final Cache<String, Set<AccountGroup.Id>> membershipCache,
+      @Named(LdapModule.GROUP_CACHE) final Cache<String, Set<AccountGroup.UUID>> membershipCache,
       @Named(LdapModule.USERNAME_CACHE) final Cache<String, Account.Id> usernameCache,
-      @GerritServerConfig final Config config) {
+      @GerritServerConfig final Config config,
+      final MaterializedGroupMembership.Factory groupMembershipFactory) {
     this.helper = helper;
     this.authConfig = authConfig;
     this.emailExpander = emailExpander;
     this.usernameCache = usernameCache;
     this.membershipCache = membershipCache;
+    this.config = config;
+    this.groupMembershipFactory = groupMembershipFactory;
 
     this.readOnlyAccountFields = new HashSet<Account.FieldName>();
 
@@ -148,14 +158,14 @@
     return v;
   }
 
-  static ParamertizedString paramString(Config c, String n, String d) {
+  static ParameterizedString paramString(Config c, String n, String d) {
     String expression = optdef(c, n, d);
     if (expression == null) {
       return null;
     } else if (expression.contains("${")) {
-      return new ParamertizedString(expression);
+      return new ParameterizedString(expression);
     } else {
-      return new ParamertizedString("${" + expression + "}");
+      return new ParameterizedString("${" + expression + "}");
     }
   }
 
@@ -164,7 +174,7 @@
     return !readOnlyAccountFields.contains(field);
   }
 
-  private static String apply(ParamertizedString p, LdapQuery.Result m)
+  private static String apply(ParameterizedString p, LdapQuery.Result m)
       throws NamingException {
     if (p == null) {
       return null;
@@ -181,6 +191,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;
@@ -193,7 +207,7 @@
         final Helper.LdapSchema schema = helper.getSchema(ctx);
         final LdapQuery.Result m = helper.findAccount(schema, ctx, username);
 
-        if (authConfig.getAuthType() == AuthType.LDAP) {
+        if (authConfig.getAuthType() == AuthType.LDAP && !who.isSkipAuthentication()) {
           // We found the user account, but we need to verify
           // the password matches it before we can continue.
           //
@@ -231,24 +245,27 @@
       }
     } 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());
   }
 
   @Override
-  public Set<AccountGroup.Id> groups(final AccountState who) {
-    final HashSet<AccountGroup.Id> r = new HashSet<AccountGroup.Id>();
-    r.addAll(membershipCache.get(findId(who.getExternalIds())));
-    r.addAll(who.getInternalGroups());
-    return r;
+  public GroupMembership groups(final AccountState who) {
+    return groupMembershipFactory.create(Iterables.concat(
+        membershipCache.get(findId(who.getExternalIds())),
+        who.getInternalGroups()));
   }
 
-
   private static String findId(final Collection<AccountExternalId> ids) {
     for (final AccountExternalId i : ids) {
       if (i.isScheme(AccountExternalId.SCHEME_GERRIT)) {
@@ -273,8 +290,8 @@
       final DirContext ctx = helper.open();
       try {
         final LdapSchema schema = helper.getSchema(ctx);
-        final ParamertizedString filter =
-            ParamertizedString.asis(schema.groupPattern
+        final ParameterizedString filter =
+            ParameterizedString.asis(schema.groupPattern
                 .replace(GROUPNAME, name).toString());
         for (String groupBase : schema.groupBases) {
           final LdapQuery query =
@@ -324,7 +341,7 @@
     }
   }
 
-  static class MemberLoader extends EntryCreator<String, Set<AccountGroup.Id>> {
+  static class MemberLoader extends EntryCreator<String, Set<AccountGroup.UUID>> {
     private final Helper helper;
 
     @Inject
@@ -333,7 +350,7 @@
     }
 
     @Override
-    public Set<AccountGroup.Id> createEntry(final String username)
+    public Set<AccountGroup.UUID> createEntry(final String username)
         throws Exception {
       final DirContext ctx = helper.open();
       try {
@@ -348,7 +365,7 @@
     }
 
     @Override
-    public Set<AccountGroup.Id> missing(final String key) {
+    public Set<AccountGroup.UUID> missing(final String key) {
       return Collections.emptySet();
     }
   }
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
new file mode 100644
index 0000000..bafdc49
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/cache/ConcurrentHashMapCache.java
@@ -0,0 +1,48 @@
+// Copyright (C) 2011 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.package com.google.gerrit.server.git;
+
+package com.google.gerrit.server.cache;
+
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ * An infinitely sized cache backed by java.util.ConcurrentHashMap.
+ * <p>
+ * This cache type is only suitable for unit tests, as it has no upper limit on
+ * number of items held in the cache. No upper limit can result in memory leaks
+ * in production servers.
+ */
+public class ConcurrentHashMapCache<K, V> implements Cache<K, V> {
+  private final ConcurrentHashMap<K, V> map = new ConcurrentHashMap<K, V>();
+
+  @Override
+  public V get(K key) {
+    return map.get(key);
+  }
+
+  @Override
+  public void put(K key, V value) {
+    map.put(key, value);
+  }
+
+  @Override
+  public void remove(K key) {
+    map.remove(key);
+  }
+
+  @Override
+  public void removeAll() {
+    map.clear();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/cache/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..1fac8c5
--- /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.client.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.mail.AbandonedSender;
+import com.google.gerrit.server.mail.EmailException;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gwtorm.server.AtomicUpdate;
+import com.google.gwtorm.server.OrmException;
+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..268e118
--- /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.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.ReplicationQueue;
+import com.google.gerrit.server.patch.PatchSetInfoFactory;
+import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gwtorm.server.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..028feac
--- /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.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gwtorm.server.AtomicUpdate;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import java.util.concurrent.Callable;
+
+public class 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..7232755
--- /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.client.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.mail.EmailException;
+import com.google.gerrit.server.mail.RestoredSender;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gwtorm.server.AtomicUpdate;
+import com.google.gwtorm.server.OrmException;
+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..abd3582
--- /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.client.ApprovalCategory.SUBMIT;
+
+import com.google.gerrit.common.data.ReviewResult;
+import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.server.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.ChangeControl;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gwtorm.server.AtomicUpdate;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.Callable;
+
+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/schema/Schema_20.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/AllProjectsName.java
similarity index 67%
rename from gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_20.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/config/AllProjectsName.java
index 4d8dca7..198d5c5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_20.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/AllProjectsName.java
@@ -12,14 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.schema;
+package com.google.gerrit.server.config;
 
-import com.google.inject.Inject;
-import com.google.inject.Provider;
+import com.google.gerrit.reviewdb.client.Project;
 
-class Schema_20 extends SchemaVersion {
-  @Inject
-  Schema_20(Provider<Schema_19> prior) {
-    super(prior);
+/** Special name of the project that all projects derive from. */
+@SuppressWarnings("serial")
+public class AllProjectsName extends Project.NameKey {
+  public AllProjectsName(String name) {
+    super(name);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/WildProjectNameProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/AllProjectsNameProvider.java
similarity index 62%
rename from gerrit-server/src/main/java/com/google/gerrit/server/config/WildProjectNameProvider.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/config/AllProjectsNameProvider.java
index ee8df40..73074b7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/WildProjectNameProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/AllProjectsNameProvider.java
@@ -14,20 +14,26 @@
 
 package com.google.gerrit.server.config;
 
-import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.SystemConfig;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-public class WildProjectNameProvider implements Provider<Project.NameKey> {
-  private final Project.NameKey name;
+import org.eclipse.jgit.lib.Config;
+
+public class AllProjectsNameProvider implements Provider<AllProjectsName> {
+  public static final String DEFAULT = "All-Projects";
+
+  private final AllProjectsName name;
 
   @Inject
-  WildProjectNameProvider(final SystemConfig config) {
-    name = config.wildProjectName;
+  AllProjectsNameProvider(@GerritServerConfig Config cfg) {
+    String n = cfg.getString("gerrit", null, "allProjects");
+    if (n == null || n.isEmpty()) {
+      n = DEFAULT;
+    }
+    name = new AllProjectsName(n);
   }
 
-  public Project.NameKey get() {
+  public AllProjectsName get() {
     return name;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectCreatorGroups.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/AnonymousCowardName.java
similarity index 78%
rename from gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectCreatorGroups.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/config/AnonymousCowardName.java
index 7db2cd3..197d64e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectCreatorGroups.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/AnonymousCowardName.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2010 The Android Open Source Project
+// 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.
@@ -14,17 +14,14 @@
 
 package com.google.gerrit.server.config;
 
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
 import com.google.inject.BindingAnnotation;
 
 import java.lang.annotation.Retention;
 
-import static java.lang.annotation.RetentionPolicy.RUNTIME;
-
-/**
- * Marker on a {@code Set&lt;AccountGroup.Id>} for the configured groups with
- * permission to create projects.
- */
+/** Special name for a user that hasn't set a name. */
 @Retention(RUNTIME)
 @BindingAnnotation
-public @interface ProjectCreatorGroups {
-}
\ No newline at end of file
+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/ApprovalTypesProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ApprovalTypesProvider.java
index db5bceb..ed9416d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ApprovalTypesProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ApprovalTypesProvider.java
@@ -16,11 +16,11 @@
 
 import com.google.gerrit.common.data.ApprovalType;
 import com.google.gerrit.common.data.ApprovalTypes;
-import com.google.gerrit.reviewdb.ApprovalCategory;
-import com.google.gerrit.reviewdb.ApprovalCategoryValue;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.client.SchemaFactory;
+import com.google.gerrit.reviewdb.client.ApprovalCategory;
+import com.google.gerrit.reviewdb.client.ApprovalCategoryValue;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.ProvisionException;
@@ -39,8 +39,7 @@
 
   @Override
   public ApprovalTypes get() {
-    List<ApprovalType> approvalTypes = new ArrayList<ApprovalType>(2);
-    List<ApprovalType> actionTypes = new ArrayList<ApprovalType>(2);
+    List<ApprovalType> types = new ArrayList<ApprovalType>(2);
 
     try {
       final ReviewDb db = schema.open();
@@ -48,12 +47,7 @@
         for (final ApprovalCategory c : db.approvalCategories().all()) {
           final List<ApprovalCategoryValue> values =
               db.approvalCategoryValues().byCategory(c.getId()).toList();
-          final ApprovalType type = new ApprovalType(c, values);
-          if (type.getCategory().isAction()) {
-            actionTypes.add(type);
-          } else {
-            approvalTypes.add(type);
-          }
+          types.add(new ApprovalType(c, values));
         }
       } finally {
         db.close();
@@ -62,8 +56,6 @@
       throw new ProvisionException("Cannot query approval categories", e);
     }
 
-    approvalTypes = Collections.unmodifiableList(approvalTypes);
-    actionTypes = Collections.unmodifiableList(actionTypes);
-    return new ApprovalTypes(approvalTypes, actionTypes);
+    return new ApprovalTypes(Collections.unmodifiableList(types));
   }
 }
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 16a6a7c..a0f0d36 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
@@ -15,10 +15,8 @@
 package com.google.gerrit.server.config;
 
 import com.google.gerrit.common.auth.openid.OpenIdProviderPattern;
-import com.google.gerrit.reviewdb.AccountExternalId;
-import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.AuthType;
-import com.google.gerrit.reviewdb.SystemConfig;
+import com.google.gerrit.reviewdb.client.AccountExternalId;
+import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gwtjsonrpc.server.SignedToken;
 import com.google.gwtjsonrpc.server.XsrfException;
 import com.google.inject.Inject;
@@ -29,15 +27,15 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.HashSet;
 import java.util.List;
-import java.util.Set;
+import java.util.concurrent.TimeUnit;
 
 /** Authentication related settings from {@code gerrit.config}. */
 @Singleton
 public class AuthConfig {
   private final AuthType authType;
   private final String httpHeader;
+  private final boolean trustContainerAuth;
   private final String logoutUrl;
   private final List<OpenIdProviderPattern> trustedOpenIDs;
   private final List<OpenIdProviderPattern> allowedOpenIDs;
@@ -45,15 +43,10 @@
   private final boolean cookieSecure;
   private final SignedToken emailReg;
 
-  private final AccountGroup.Id administratorGroup;
-  private final Set<AccountGroup.Id> anonymousGroups;
-  private final Set<AccountGroup.Id> registeredGroups;
-  private final AccountGroup.Id batchUsersGroup;
-
   private final boolean allowGoogleAccountUpgrade;
 
   @Inject
-  AuthConfig(@GerritServerConfig final Config cfg, final SystemConfig s)
+  AuthConfig(@GerritServerConfig final Config cfg)
       throws XsrfException {
     authType = toType(cfg);
     httpHeader = cfg.getString("auth", null, "httpheader");
@@ -62,15 +55,18 @@
     allowedOpenIDs = toPatterns(cfg, "allowedOpenID");
     cookiePath = cfg.getString("auth", null, "cookiepath");
     cookieSecure = cfg.getBoolean("auth", "cookiesecure", false);
-    emailReg = new SignedToken(5 * 24 * 60 * 60, s.registerEmailPrivateKey);
+    trustContainerAuth = cfg.getBoolean("auth", "trustContainerAuth", false);
 
-    final HashSet<AccountGroup.Id> r = new HashSet<AccountGroup.Id>(2);
-    r.add(s.anonymousGroupId);
-    r.add(s.registeredGroupId);
-    registeredGroups = Collections.unmodifiableSet(r);
-    anonymousGroups = Collections.singleton(s.anonymousGroupId);
-    administratorGroup = s.adminGroupId;
-    batchUsersGroup = s.batchUsersGroupId;
+    String key = cfg.getString("auth", null, "registerEmailPrivateKey");
+    if (key != null && !key.isEmpty()) {
+      int age = (int) ConfigUtil.getTimeUnit(cfg,
+          "auth", null, "maxRegisterEmailTokenAge",
+          TimeUnit.SECONDS.convert(12, TimeUnit.HOURS),
+          TimeUnit.SECONDS);
+      emailReg = new SignedToken(age, key);
+    } else {
+      emailReg = null;
+    }
 
     if (authType == AuthType.OPENID) {
       allowGoogleAccountUpgrade =
@@ -126,31 +122,16 @@
     return allowGoogleAccountUpgrade;
   }
 
-  /** Identity of the magic group with full powers. */
-  public AccountGroup.Id getAdministratorsGroup() {
-    return administratorGroup;
-  }
-
-  /** Identity of the group whose service is degraded to lower priority. */
-  public AccountGroup.Id getBatchUsersGroup() {
-    return batchUsersGroup;
-  }
-
-  /** Groups that all users, including anonymous users, belong to. */
-  public Set<AccountGroup.Id> getAnonymousGroups() {
-    return anonymousGroups;
-  }
-
-  /** Groups that all users who have created an account belong to. */
-  public Set<AccountGroup.Id> getRegisteredGroups() {
-    return registeredGroups;
-  }
-
   /** OpenID identities which the server permits for authentication. */
   public List<OpenIdProviderPattern> getAllowedOpenIDs() {
     return allowedOpenIDs;
   }
 
+  /** Whether git-over-http should trust authentication done by container. */
+  public boolean isTrustContainerAuth() {
+    return trustContainerAuth;
+  }
+
   public boolean isIdentityTrustable(final Collection<AccountExternalId> ids) {
     switch (getAuthType()) {
       case DEVELOPMENT_BECOME_ANY_ACCOUNT:
@@ -159,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/AuthConfigModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfigModule.java
index 33ea3fb..e5d4ba8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfigModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/AuthConfigModule.java
@@ -16,14 +16,12 @@
 
 import static com.google.inject.Scopes.SINGLETON;
 
-import com.google.gerrit.reviewdb.SystemConfig;
 import com.google.inject.AbstractModule;
 
 /** Creates {@link AuthConfig} from {@link GerritServerConfig}. */
 public class AuthConfigModule extends AbstractModule {
   @Override
   protected void configure() {
-    bind(SystemConfig.class).toProvider(SystemConfigProvider.class).in(SINGLETON);
     bind(AuthConfig.class).in(SINGLETON);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/CanonicalWebUrlModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/CanonicalWebUrlModule.java
index 1c70c78..8d66d33 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/CanonicalWebUrlModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/CanonicalWebUrlModule.java
@@ -14,8 +14,6 @@
 
 package com.google.gerrit.server.config;
 
-import static com.google.inject.Scopes.SINGLETON;
-
 import com.google.inject.AbstractModule;
 import com.google.inject.Provider;
 
@@ -31,7 +29,6 @@
     // running in an HTTP environment.
     //
     final Class<? extends Provider<String>> provider = provider();
-    bind(provider).in(SINGLETON);
     bind(String.class).annotatedWith(CanonicalWebUrl.class)
         .toProvider(provider);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigUtil.java
index f24dcc5..e76249a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigUtil.java
@@ -16,11 +16,15 @@
 
 import static org.eclipse.jgit.util.StringUtils.equalsIgnoreCase;
 
-import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.AccountGroupName;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.client.SchemaFactory;
+import com.google.common.base.Function;
+import com.google.common.base.Predicates;
+import com.google.common.collect.Iterables;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupName;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.OrmRuntimeException;
+import com.google.gwtorm.server.SchemaFactory;
 
 import org.eclipse.jgit.lib.Config;
 import org.slf4j.Logger;
@@ -28,7 +32,9 @@
 import java.lang.reflect.InvocationTargetException;
 import java.text.MessageFormat;
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.HashSet;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Set;
 import java.util.concurrent.TimeUnit;
@@ -310,25 +316,48 @@
    * @return the actual groups resolved from the database. If no groups are
    *         found, returns an empty {@code Set}, never {@code null}.
    */
-  public static Set<AccountGroup.Id> groupsFor(
+  public static Set<AccountGroup.UUID> groupsFor(
       SchemaFactory<ReviewDb> dbfactory, String[] groupNames, Logger log,
       String groupNotFoundWarning) {
-    final Set<AccountGroup.Id> result = new HashSet<AccountGroup.Id>();
+    final Set<AccountGroup.UUID> result = new HashSet<AccountGroup.UUID>();
     try {
       final ReviewDb db = dbfactory.open();
       try {
-        for (String name : groupNames) {
-          AccountGroupName group =
-              db.accountGroupNames().get(new AccountGroup.NameKey(name));
-          if (group == null) {
-            log.warn(MessageFormat.format(groupNotFoundWarning, name));
+        List<AccountGroupName> groups = db.accountGroupNames().get(
+            Iterables.transform(Arrays.asList(groupNames),
+                new Function<String, AccountGroup.NameKey>() {
+                  @Override
+                  public AccountGroup.NameKey apply(String name) {
+                    return new AccountGroup.NameKey(name);
+                  }
+            })).toList();
+
+        Iterator<AccountGroup> ags = db.accountGroups().get(
+            Iterables.transform(Iterables.filter(groups, Predicates.notNull()),
+                new Function<AccountGroupName, AccountGroup.Id>() {
+                  @Override
+                  public AccountGroup.Id apply(AccountGroupName group) {
+                    return group.getId();
+                  }
+            })).iterator();
+
+        for (int i = 0; i < groupNames.length; i++) {
+          if (groups.get(i) == null) {
+            log.warn(MessageFormat.format(groupNotFoundWarning, groupNames[i]));
+            continue;
+          }
+          AccountGroup ag = ags.next();
+          if (ag == null) {
+            log.warn(MessageFormat.format(groupNotFoundWarning, groupNames[i]));
           } else {
-            result.add(group.getId());
+            result.add(ag.getGroupUUID());
           }
         }
       } finally {
         db.close();
       }
+    } catch (OrmRuntimeException e) {
+      log.error("Database error, cannot load groups", e);
     } catch (OrmException e) {
       log.error("Database error, cannot load groups", e);
     }
@@ -345,7 +374,7 @@
    * @return the actual groups resolved from the database. If no groups are
    *         found, returns an empty {@code Set}, never {@code null}.
    */
-  public static Set<AccountGroup.Id> groupsFor(
+  public static Set<AccountGroup.UUID> groupsFor(
       SchemaFactory<ReviewDb> dbfactory, String[] groupNames, Logger log) {
     return groupsFor(dbfactory, groupNames, log,
         "Group \"{0}\" not in database, skipping.");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/DownloadSchemeConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/DownloadSchemeConfig.java
index 6f67032..ecfe4f5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/DownloadSchemeConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/DownloadSchemeConfig.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.server.config;
 
-import com.google.gerrit.reviewdb.SystemConfig;
-import com.google.gerrit.reviewdb.AccountGeneralPreferences.DownloadScheme;
+import com.google.gerrit.reviewdb.client.SystemConfig;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/FactoryModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/FactoryModule.java
index 331f471..a0cf2ab 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/FactoryModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/FactoryModule.java
@@ -15,11 +15,7 @@
 package com.google.gerrit.server.config;
 
 import com.google.inject.AbstractModule;
-import com.google.inject.Key;
-import com.google.inject.assistedinject.FactoryProvider;
-
-import java.lang.reflect.Method;
-import java.lang.reflect.Modifier;
+import com.google.inject.assistedinject.FactoryModuleBuilder;
 
 public abstract class FactoryModule extends AbstractModule {
   /**
@@ -42,63 +38,9 @@
    * Just pass {@code Foo.Factory.class} to this method. The factory will be
    * generated to return its one return type as declared in the creation method.
    *
-   * @param <F>
    * @param factory interface which specifies the bean factory method.
    */
-  protected <F> void factory(final Class<F> factory) {
-    factory(Key.get(factory), factory);
-  }
-
-  /**
-   * Register an assisted injection factory.
-   * <p>
-   * This function provides an automatic way to define a factory that creates a
-   * concrete type through assited injection. For example to configure the
-   * following assisted injection case:
-   *
-   * <pre>
-   * public class Foo {
-   *   public interface Factory {
-   *     Foo create(int a);
-   *   }
-   *   &#064;Inject
-   *   Foo(Logger log, @Assisted int a) {...}
-   * }
-   * </pre>
-   *
-   * Just pass {@code Foo.Factory.class} to this method. The factory will be
-   * generated to return its one return type as declared in the creation method.
-   *
-   * @param <F>
-   * @param key key to bind with in Guice bindings.
-   * @param factory interface which specifies the bean factory method.
-   */
-  protected <F> void factory(final Key<F> key, final Class<F> factory) {
-    final Method[] methods = factory.getDeclaredMethods();
-    switch (methods.length) {
-      case 1: {
-        final Class<?> result = methods[0].getReturnType();
-        if (isAbstract(result)) {
-          addError("Factory " + factory.getName() + " returns abstract result.");
-        } else {
-          bind(key).toProvider(FactoryProvider.newFactory(factory, result));
-        }
-        break;
-      }
-
-      case 0:
-        addError("Factory " + factory.getName() + " has no create method.");
-        break;
-
-      default:
-        addError("Factory " + factory.getName()
-            + " has more than one create method.");
-        break;
-    }
-  }
-
-  private static boolean isAbstract(final Class<?> result) {
-    return result.isInterface()
-        || (result.getModifiers() & Modifier.ABSTRACT) == Modifier.ABSTRACT;
+  protected void factory(final Class<?> factory) {
+    install(new FactoryModuleBuilder().build(factory));
   }
 }
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 d6cc95d..99dd54a 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,104 +17,62 @@
 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.reviewdb.Project;
-import com.google.gerrit.server.AnonymousUser;
+import com.google.gerrit.reviewdb.client.AuthType;
+import com.google.gerrit.rules.PrologModule;
+import com.google.gerrit.rules.RulesCache;
 import com.google.gerrit.server.FileTypeRegistry;
-import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.GerritPersonIdentProvider;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.MimeUtilFileTypeRegistry;
 import com.google.gerrit.server.ReplicationUser;
 import com.google.gerrit.server.account.AccountByEmailCacheImpl;
 import com.google.gerrit.server.account.AccountCacheImpl;
 import com.google.gerrit.server.account.AccountInfoCacheFactory;
+import com.google.gerrit.server.account.AccountVisibility;
+import com.google.gerrit.server.account.AccountVisibilityProvider;
+import com.google.gerrit.server.account.CapabilityControl;
 import com.google.gerrit.server.account.DefaultRealm;
 import com.google.gerrit.server.account.EmailExpander;
 import com.google.gerrit.server.account.GroupCacheImpl;
 import com.google.gerrit.server.account.GroupIncludeCacheImpl;
 import com.google.gerrit.server.account.GroupInfoCacheFactory;
+import com.google.gerrit.server.account.MaterializedGroupMembership;
 import com.google.gerrit.server.account.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.GitRepositoryManager;
-import com.google.gerrit.server.git.LocalDiskRepositoryManager;
+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;
 import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.PermissionCollection;
 import com.google.gerrit.server.project.ProjectCacheImpl;
 import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.project.ProjectNode;
 import com.google.gerrit.server.project.ProjectState;
-import com.google.gerrit.server.project.RefControl;
+import com.google.gerrit.server.project.SectionSortCache;
 import com.google.gerrit.server.tools.ToolsCatalog;
 import com.google.gerrit.server.util.IdGenerator;
 import com.google.gerrit.server.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 org.eclipse.jgit.lib.PersonIdent;
-
-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) {
@@ -131,55 +89,61 @@
         install(new LdapModule());
         break;
 
+      case CUSTOM_EXTENSION:
+        break;
+
       default:
         bind(Realm.class).to(DefaultRealm.class);
         break;
     }
 
-    bind(Project.NameKey.class).annotatedWith(WildProjectName.class)
-        .toProvider(WildProjectNameProvider.class).in(SINGLETON);
     bind(ApprovalTypes.class).toProvider(ApprovalTypesProvider.class).in(
         SINGLETON);
     bind(EmailExpander.class).toProvider(EmailExpanderProvider.class).in(
         SINGLETON);
-    bind(AnonymousUser.class);
-
-    bind(PersonIdent.class).annotatedWith(GerritPersonIdent.class).toProvider(
-        GerritPersonIdentProvider.class);
 
     bind(IdGenerator.class);
-    bind(CachePool.class);
+    bind(RulesCache.class);
     install(AccountByEmailCacheImpl.module());
     install(AccountCacheImpl.module());
     install(GroupCacheImpl.module());
     install(GroupIncludeCacheImpl.module());
     install(PatchListCacheImpl.module());
     install(ProjectCacheImpl.module());
+    install(SectionSortCache.module());
     install(TagCache.module());
     install(new AccessControlModule());
+    install(new GitModule());
+    install(new PrologModule());
 
     factory(AccountInfoCacheFactory.Factory.class);
+    factory(CapabilityControl.Factory.class);
     factory(GroupInfoCacheFactory.Factory.class);
+    factory(ProjectNode.Factory.class);
     factory(ProjectState.Factory.class);
-    factory(RefControl.Factory.class);
+    factory(MaterializedGroupMembership.Factory.class);
+    bind(PermissionCollection.Factory.class);
+    bind(AccountVisibility.class)
+        .toProvider(AccountVisibilityProvider.class)
+        .in(SINGLETON);
 
-    bind(GitRepositoryManager.class).to(LocalDiskRepositoryManager.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);
@@ -187,15 +151,5 @@
     bind(ProjectControl.GenericFactory.class);
     factory(FunctionState.Factory.class);
     factory(ReplicationUser.Factory.class);
-
-    install(new LifecycleModule() {
-      @Override
-      protected void configure() {
-        listener().to(LocalDiskRepositoryManager.Lifecycle.class);
-        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 f6dad7d..71bcc6c 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
@@ -16,27 +16,47 @@
 
 import static com.google.inject.Scopes.SINGLETON;
 
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.RequestCleanup;
+import com.google.gerrit.server.account.AccountControl;
 import com.google.gerrit.server.account.AccountResolver;
 import com.google.gerrit.server.account.GroupControl;
+import com.google.gerrit.server.account.GroupDetailFactory;
+import com.google.gerrit.server.account.GroupMembers;
 import com.google.gerrit.server.account.PerformCreateGroup;
+import com.google.gerrit.server.account.PerformRenameGroup;
+import com.google.gerrit.server.account.VisibleGroups;
+import com.google.gerrit.server.changedetail.AbandonChange;
+import com.google.gerrit.server.changedetail.DeleteDraftPatchSet;
+import com.google.gerrit.server.changedetail.PublishDraft;
+import com.google.gerrit.server.changedetail.RestoreChange;
+import com.google.gerrit.server.changedetail.Submit;
+import com.google.gerrit.server.git.AsyncReceiveCommits;
 import com.google.gerrit.server.git.CreateCodeReviewNotes;
 import com.google.gerrit.server.git.MergeOp;
-import com.google.gerrit.server.git.ReceiveCommits;
+import com.google.gerrit.server.git.MetaDataUpdate;
+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.RebasedPatchSetSender;
 import com.google.gerrit.server.mail.ReplacePatchSetSender;
+import com.google.gerrit.server.mail.RestoredSender;
 import com.google.gerrit.server.mail.RevertedSender;
+import com.google.gerrit.server.patch.AddReviewer;
 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.ListProjects;
+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;
@@ -49,31 +69,51 @@
     bind(ReviewDb.class).toProvider(RequestScopedReviewDbProvider.class).in(
         RequestScoped.class);
     bind(IdentifiedUser.RequestFactory.class).in(SINGLETON);
+    bind(MetaDataUpdate.User.class).in(RequestScoped.class);
     bind(AccountResolver.class);
     bind(ChangeQueryRewriter.class);
+    bind(ListProjects.class);
 
+    bind(AnonymousUser.class).in(RequestScoped.class);
+    bind(PerRequestProjectControlCache.class).in(RequestScoped.class);
     bind(ChangeControl.Factory.class).in(SINGLETON);
     bind(GroupControl.Factory.class).in(SINGLETON);
     bind(ProjectControl.Factory.class).in(SINGLETON);
+    bind(AccountControl.Factory.class).in(SINGLETON);
 
     factory(ChangeQueryBuilder.Factory.class);
-    factory(ReceiveCommits.Factory.class);
+    factory(SubmoduleOp.Factory.class);
     factory(MergeOp.Factory.class);
     factory(CreateCodeReviewNotes.Factory.class);
+    install(new AsyncReceiveCommits.Module());
 
     // Not really per-request, but dammit, I don't know where else to
     // easily park this stuff.
     //
+    factory(AbandonChange.Factory.class);
+    factory(AddReviewer.Factory.class);
     factory(AddReviewerSender.Factory.class);
     factory(CreateChangeSender.Factory.class);
+    factory(DeleteDraftPatchSet.Factory.class);
     factory(PublishComments.Factory.class);
+    factory(PublishDraft.Factory.class);
     factory(ReplacePatchSetSender.Factory.class);
+    factory(RebasedPatchSetSender.Factory.class);
     factory(AbandonedSender.Factory.class);
+    factory(RemoveReviewer.Factory.class);
+    factory(RestoreChange.Factory.class);
+    factory(RestoredSender.Factory.class);
     factory(RevertedSender.Factory.class);
     factory(CommentSender.Factory.class);
     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(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/GitReceivePackGroupsProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitReceivePackGroupsProvider.java
index 9af6d62..9992f18 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GitReceivePackGroupsProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitReceivePackGroupsProvider.java
@@ -14,29 +14,25 @@
 
 package com.google.gerrit.server.config;
 
-import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gwtorm.client.SchemaFactory;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 
 import org.eclipse.jgit.lib.Config;
 
 import java.util.Collections;
-import java.util.HashSet;
 
 public class GitReceivePackGroupsProvider extends GroupSetProvider {
   @Inject
   public GitReceivePackGroupsProvider(@GerritServerConfig Config config,
-      AuthConfig authConfig, SchemaFactory<ReviewDb> db) {
+      SchemaFactory<ReviewDb> db) {
     super(config, db, "receive", null, "allowGroup");
 
     // If no group was set, default to "registered users"
     //
     if (groupIds.isEmpty()) {
-      HashSet<AccountGroup.Id> all = new HashSet<AccountGroup.Id>();
-      all.addAll(authConfig.getRegisteredGroups());
-      all.removeAll(authConfig.getAnonymousGroups());
-      groupIds = Collections.unmodifiableSet(all);
+      groupIds = Collections.singleton(AccountGroup.REGISTERED_USERS);
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GitUploadPackGroupsProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitUploadPackGroupsProvider.java
index bfb09a5..76d8844 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GitUploadPackGroupsProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GitUploadPackGroupsProvider.java
@@ -14,9 +14,9 @@
 
 package com.google.gerrit.server.config;
 
-import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gwtorm.client.SchemaFactory;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 
 import org.eclipse.jgit.lib.Config;
@@ -27,15 +27,15 @@
 public class GitUploadPackGroupsProvider extends GroupSetProvider {
   @Inject
   public GitUploadPackGroupsProvider(@GerritServerConfig Config config,
-      AuthConfig authConfig, SchemaFactory<ReviewDb> db) {
+      SchemaFactory<ReviewDb> db) {
     super(config, db, "upload", null, "allowGroup");
 
     // If no group was set, default to "registered users" and "anonymous"
     //
     if (groupIds.isEmpty()) {
-      HashSet<AccountGroup.Id> all = new HashSet<AccountGroup.Id>();
-      all.addAll(authConfig.getRegisteredGroups());
-      all.addAll(authConfig.getAnonymousGroups());
+      HashSet<AccountGroup.UUID> all = new HashSet<AccountGroup.UUID>();
+      all.add(AccountGroup.REGISTERED_USERS);
+      all.add(AccountGroup.ANONYMOUS_USERS);
       groupIds = Collections.unmodifiableSet(all);
     }
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GroupSetProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GroupSetProvider.java
index 373fdb5..15711af 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/GroupSetProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/GroupSetProvider.java
@@ -17,9 +17,9 @@
 import static com.google.gerrit.server.config.ConfigUtil.groupsFor;
 import static java.util.Collections.unmodifiableSet;
 
-import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gwtorm.client.SchemaFactory;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
@@ -30,11 +30,11 @@
 import java.util.Set;
 
 public abstract class GroupSetProvider implements
-    Provider<Set<AccountGroup.Id>> {
+    Provider<Set<AccountGroup.UUID>> {
   private static final Logger log =
       LoggerFactory.getLogger(GroupSetProvider.class);
 
-  protected Set<AccountGroup.Id> groupIds;
+  protected Set<AccountGroup.UUID> groupIds;
 
   @Inject
   protected GroupSetProvider(@GerritServerConfig Config config,
@@ -44,7 +44,7 @@
   }
 
   @Override
-  public Set<AccountGroup.Id> get() {
+  public Set<AccountGroup.UUID> get() {
     return groupIds;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectCreatorGroupsProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectCreatorGroupsProvider.java
deleted file mode 100644
index 381c914..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectCreatorGroupsProvider.java
+++ /dev/null
@@ -1,47 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.config;
-
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gerrit.reviewdb.SystemConfig;
-import com.google.gwtorm.client.SchemaFactory;
-import com.google.inject.Inject;
-
-import org.eclipse.jgit.lib.Config;
-
-import java.util.Collections;
-
-/**
- * Provider of the group(s) which are allowed to create new projects. Currently
- * only supports {@code createGroup} declarations in the {@code "*"} repository,
- * like so:
- *
- * <pre>
- * [repository &quot;*&quot;]
- *     createGroup = Registered Users
- *     createGroup = Administrators
- * </pre>
- */
-public class ProjectCreatorGroupsProvider extends GroupSetProvider {
-  @Inject
-  public ProjectCreatorGroupsProvider(@GerritServerConfig final Config config,
-      final SystemConfig systemConfig, final SchemaFactory<ReviewDb> db) {
-    super(config, db, "repository", "*", "createGroup");
-
-    if (groupIds.isEmpty()) {
-      groupIds = Collections.singleton(systemConfig.adminGroupId);
-    }
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroups.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroups.java
index 4ce1258..876c51f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroups.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroups.java
@@ -14,12 +14,12 @@
 
 package com.google.gerrit.server.config;
 
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
 import com.google.inject.BindingAnnotation;
 
 import java.lang.annotation.Retention;
 
-import static java.lang.annotation.RetentionPolicy.RUNTIME;
-
 /**
  * Marker on a {@code Set&lt;AccountGroup.Id>} for the configured groups which
  * should become owners of a created project.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java
index c457d73..b279086 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectOwnerGroupsProvider.java
@@ -14,15 +14,12 @@
 
 package com.google.gerrit.server.config;
 
-import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gwtorm.client.SchemaFactory;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 
 import org.eclipse.jgit.lib.Config;
 
-import java.util.Set;
-
 /**
  * Provider of the group(s) which should become owners of a newly created
  * project. Currently only supports {@code ownerGroup} declarations in the
@@ -37,12 +34,7 @@
 public class ProjectOwnerGroupsProvider extends GroupSetProvider {
   @Inject
   public ProjectOwnerGroupsProvider(
-      @ProjectCreatorGroups final Set<AccountGroup.Id> creatorGroups,
       @GerritServerConfig final Config config, final SchemaFactory<ReviewDb> db) {
     super(config, db, "repository", "*", "ownerGroup");
-
-    if (groupIds.isEmpty()) {
-      groupIds = creatorGroups;
-    }
   }
 }
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..4518a2e 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
@@ -14,10 +14,10 @@
 
 package com.google.gerrit.server.config;
 
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.RequestCleanup;
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.jdbc.Database;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.ProvisionException;
@@ -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/SitePathFromSystemConfigProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePathFromSystemConfigProvider.java
deleted file mode 100644
index 5b5dcc1..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/SitePathFromSystemConfigProvider.java
+++ /dev/null
@@ -1,37 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.config;
-
-import com.google.gerrit.reviewdb.SystemConfig;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-import java.io.File;
-
-/** Provides {@link java.io.File} annotated with {@link SitePath}. */
-public class SitePathFromSystemConfigProvider implements Provider<File> {
-  private final File path;
-
-  @Inject
-  SitePathFromSystemConfigProvider(final SystemConfig config) {
-    final String p = config.sitePath;
-    path = new File(p != null && p.length() > 0 ? p : ".").getAbsoluteFile();
-  }
-
-  @Override
-  public File get() {
-    return path;
-  }
-}
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/config/SystemConfigProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/SystemConfigProvider.java
deleted file mode 100644
index ba3e0fc..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/SystemConfigProvider.java
+++ /dev/null
@@ -1,87 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.config;
-
-import com.google.gerrit.reviewdb.CurrentSchemaVersion;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gerrit.reviewdb.SystemConfig;
-import com.google.gerrit.server.schema.Current;
-import com.google.gerrit.server.schema.SchemaVersion;
-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.ProvisionException;
-
-import java.util.List;
-
-/** Loads the {@link SystemConfig} from the database. */
-public class SystemConfigProvider implements Provider<SystemConfig> {
-  private final SchemaFactory<ReviewDb> schema;
-
-  @Current
-  private final Provider<SchemaVersion> version;
-
-  @Inject
-  public SystemConfigProvider(final SchemaFactory<ReviewDb> schemaFactory,
-      @Current final Provider<SchemaVersion> version) {
-    this.schema = schemaFactory;
-    this.version = version;
-  }
-
-  @Override
-  public SystemConfig get() {
-    try {
-      final ReviewDb db = schema.open();
-      try {
-        final CurrentSchemaVersion sVer = getSchemaVersion(db);
-        final int eVer = version.get().getVersionNbr();
-
-        if (sVer == null) {
-          throw new OrmException("Schema not yet initialized."
-              + "  Run init to initialize the schema.");
-        }
-        if (sVer.versionNbr != eVer) {
-          throw new OrmException("Unsupported schema version "
-              + sVer.versionNbr + "; expected schema version " + eVer
-              + ".  Run init to upgrade.");
-        }
-
-        final List<SystemConfig> all = db.systemConfig().all().toList();
-        switch (all.size()) {
-          case 1:
-            return all.get(0);
-          case 0:
-            throw new OrmException("system_config table is empty");
-          default:
-            throw new OrmException("system_config must have exactly 1 row;"
-                + " found " + all.size() + " rows instead");
-        }
-      } finally {
-        db.close();
-      }
-    } catch (OrmException e) {
-      throw new ProvisionException("Cannot read system_config", e);
-    }
-  }
-
-  private CurrentSchemaVersion getSchemaVersion(final ReviewDb db) {
-    try {
-      return db.schemaVersion().get(new CurrentSchemaVersion.Key());
-    } catch (OrmException e) {
-      return null;
-    }
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/TrackingFootersProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/TrackingFootersProvider.java
index 06ef0dc..c10c0eb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/TrackingFootersProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/TrackingFootersProvider.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.config;
 
-import com.google.gerrit.reviewdb.TrackingId;
+import com.google.gerrit.reviewdb.client.TrackingId;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/WildProjectName.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/WildProjectName.java
deleted file mode 100644
index 3df550a..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/WildProjectName.java
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.config;
-
-import static java.lang.annotation.RetentionPolicy.RUNTIME;
-
-import com.google.gerrit.reviewdb.Project;
-import com.google.inject.BindingAnnotation;
-
-import java.lang.annotation.Retention;
-
-/**
- * Marker on a {@link Project.NameKey} for the current wildcard project.
- * <p>
- * This is the name of the project whose rights inherit into every other project
- * in the system.
- */
-@Retention(RUNTIME)
-@BindingAnnotation
-public @interface WildProjectName {
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/contact/ContactStore.java b/gerrit-server/src/main/java/com/google/gerrit/server/contact/ContactStore.java
index 84c7c75..0ab21f9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/contact/ContactStore.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/contact/ContactStore.java
@@ -15,8 +15,8 @@
 package com.google.gerrit.server.contact;
 
 import com.google.gerrit.common.errors.ContactInformationStoreException;
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.ContactInformation;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.ContactInformation;
 
 public interface ContactStore {
   boolean isEnabled();
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..d5cd11a 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
@@ -14,39 +14,47 @@
 
 package com.google.gerrit.server.contact;
 
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
-import com.google.gwtorm.client.SchemaFactory;
+import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 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..bc88cd2 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
@@ -15,13 +15,13 @@
 package com.google.gerrit.server.contact;
 
 import com.google.gerrit.common.errors.ContactInformationStoreException;
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.AccountExternalId;
-import com.google.gerrit.reviewdb.ContactInformation;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountExternalId;
+import com.google.gerrit.reviewdb.client.ContactInformation;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.UrlEncoded;
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.client.SchemaFactory;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.ProvisionException;
 import com.google.inject.Singleton;
 
@@ -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..e210865
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/contact/HttpContactStoreConnection.java
@@ -0,0 +1,68 @@
+// Copyright 2011 Google Inc. All Rights Reserved.
+
+package com.google.gerrit.server.contact;
+
+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.FactoryModuleBuilder;
+
+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() {
+        install(new FactoryModuleBuilder()
+            .implement(ContactStoreConnection.class, HttpContactStoreConnection.class)
+            .build(ContactStoreConnection.Factory.class));
+      }
+    };
+  }
+
+  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/contact/NoContactStore.java b/gerrit-server/src/main/java/com/google/gerrit/server/contact/NoContactStore.java
index e219186..a625c0c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/contact/NoContactStore.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/contact/NoContactStore.java
@@ -15,8 +15,8 @@
 package com.google.gerrit.server.contact;
 
 import com.google.gerrit.common.errors.ContactInformationStoreException;
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.ContactInformation;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.ContactInformation;
 
 class NoContactStore implements ContactStore {
   @Override
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 79a7e5b..9810f59 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
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.events;
 
-import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.reviewdb.client.Change;
 
 import java.util.List;
 
@@ -27,13 +27,19 @@
     public String subject;
     public AccountAttribute owner;
     public String url;
+    public String commitMessage;
 
+    public Long createdOn;
     public Long lastUpdated;
     public String sortKey;
     public Boolean open;
     public Change.Status status;
+    public List<MessageAttribute> comments;
 
     public List<TrackingIdAttribute> trackingIds;
     public PatchSetAttribute currentPatchSet;
     public List<PatchSetAttribute> patchSets;
+
+    public List<DependencyAttribute> dependsOn;
+    public List<DependencyAttribute> neededBy;
 }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/CodedEnum.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/DependencyAttribute.java
similarity index 67%
copy from gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/CodedEnum.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/events/DependencyAttribute.java
index 5900292..47fbdac 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/CodedEnum.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/DependencyAttribute.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2009 The Android Open Source Project
+// Copyright (C) 2012 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,9 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.reviewdb;
+package com.google.gerrit.server.events;
 
-/** Extension of Enum which provides distinct character code values. */
-public interface CodedEnum {
-  char getCode();
+public class DependencyAttribute {
+  public String id;
+  public String number;
+  public String revision;
+  public String ref;
+  public Boolean isCurrentPatchSet;
 }
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 573e6bb..4d34b71 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/EventFactory.java
@@ -16,18 +16,27 @@
 
 import com.google.gerrit.common.data.ApprovalType;
 import com.google.gerrit.common.data.ApprovalTypes;
-import com.google.gerrit.reviewdb.Account;
-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.TrackingId;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetAncestor;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.reviewdb.client.TrackingId;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.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.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
-import com.google.inject.internal.Nullable;
 
 import org.eclipse.jgit.lib.ObjectId;
 
@@ -35,19 +44,26 @@
 import java.util.Collection;
 import java.util.Map;
 
+import javax.annotation.Nullable;
+
 @Singleton
 public class EventFactory {
   private final AccountCache accountCache;
   private final Provider<String> urlProvider;
   private final ApprovalTypes approvalTypes;
+  private final PatchListCache patchListCache;
+  private final SchemaFactory<ReviewDb> schema;
 
   @Inject
   EventFactory(AccountCache accountCache,
       @CanonicalWebUrl @Nullable Provider<String> urlProvider,
-      ApprovalTypes approvalTypes) {
+      ApprovalTypes approvalTypes,
+      PatchListCache patchListCache, SchemaFactory<ReviewDb> schema) {
     this.accountCache = accountCache;
     this.urlProvider = urlProvider;
     this.approvalTypes = approvalTypes;
+    this.patchListCache = patchListCache;
+    this.schema = schema;
   }
 
   /**
@@ -94,12 +110,68 @@
    * @param change
    */
   public void extend(ChangeAttribute a, Change change) {
+    a.createdOn = change.getCreatedOn().getTime() / 1000L;
     a.lastUpdated = change.getLastUpdatedOn().getTime() / 1000L;
     a.sortKey = change.getSortKey();
     a.open = change.getStatus().isOpen();
     a.status = change.getStatus();
   }
 
+  public void addDependencies(ChangeAttribute ca, Change change) {
+    ca.dependsOn = new ArrayList<DependencyAttribute>();
+    ca.neededBy = new ArrayList<DependencyAttribute>();
+    try {
+      final ReviewDb db = schema.open();
+      try {
+        final PatchSet.Id psId = change.currentPatchSetId();
+        for (PatchSetAncestor a : db.patchSetAncestors().ancestorsOf(psId)) {
+          for (PatchSet p :
+              db.patchSets().byRevision(a.getAncestorRevision())) {
+            Change c = db.changes().get(p.getId().getParentKey());
+            ca.dependsOn.add(newDependsOn(c, p));
+          }
+        }
+
+        final RevId revId = db.patchSets().get(psId).getRevision();
+        for (PatchSetAncestor a : db.patchSetAncestors().descendantsOf(revId)) {
+          final PatchSet p = db.patchSets().get(a.getPatchSet());
+          final Change c = db.changes().get(p.getId().getParentKey());
+          ca.neededBy.add(newNeededBy(c, p));
+        }
+      } finally {
+        db.close();
+      }
+    } catch (OrmException e) {
+      // Squash DB exceptions and leave dependency lists partially filled.
+    }
+    // Remove empty lists so a confusing label won't be displayed in the output.
+    if (ca.dependsOn.isEmpty()) {
+      ca.dependsOn = null;
+    }
+    if (ca.neededBy.isEmpty()) {
+      ca.neededBy = null;
+    }
+  }
+
+  private DependencyAttribute newDependsOn(Change c, PatchSet ps) {
+    DependencyAttribute d = newDependencyAttribute(c, ps);
+    d.isCurrentPatchSet = c.currPatchSetId().equals(ps.getId());
+    return d;
+  }
+
+  private DependencyAttribute newNeededBy(Change c, PatchSet ps) {
+    return newDependencyAttribute(c, ps);
+  }
+
+  private DependencyAttribute newDependencyAttribute(Change c, PatchSet ps) {
+    DependencyAttribute d = new DependencyAttribute();
+    d.number = c.getId().toString();
+    d.id = c.getKey().toString();
+    d.revision = ps.getRevision().get();
+    d.ref = ps.getRefName();
+    return d;
+  }
+
   public void addTrackingIds(ChangeAttribute a, Collection<TrackingId> ids) {
     if (!ids.isEmpty()) {
       a.trackingIds = new ArrayList<TrackingIdAttribute>(ids.size());
@@ -109,12 +181,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) {
@@ -123,6 +206,48 @@
           addApprovals(psa, p.getId(), approvals);
         }
         ca.patchSets.add(psa);
+        if (includeFiles && change != null) {
+          addPatchSetFileNames(psa, change, p);
+        }
+      }
+    }
+  }
+
+  public void addPatchSetComments(PatchSetAttribute patchSetAttribute,
+      Collection<PatchLineComment> patchLineComments) {
+    for (PatchLineComment comment : patchLineComments) {
+      if (comment.getKey().getParentKey().getParentKey().get()
+          == Integer.parseInt(patchSetAttribute.number)) {
+        if (patchSetAttribute.comments == null) {
+          patchSetAttribute.comments =
+            new ArrayList<PatchSetCommentAttribute>();
+        }
+        patchSetAttribute.comments.add(asPatchSetLineAttribute(comment));
+      }
+    }
+  }
+
+  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()) {
+      ca.comments = new ArrayList<MessageAttribute>();
+      for (ChangeMessage message : messages) {
+        ca.comments.add(asMessageAttribute(message));
       }
     }
   }
@@ -147,6 +272,7 @@
     p.number = Integer.toString(patchSet.getPatchSetId());
     p.ref = patchSet.getRefName();
     p.uploader = asAccountAttribute(patchSet.getUploader());
+    p.createdOn = patchSet.getCreatedOn().getTime() / 1000L;
     return p;
   }
 
@@ -212,13 +338,30 @@
     a.by = asAccountAttribute(approval.getAccountId());
     a.grantedOn = approval.getGranted().getTime() / 1000L;
 
-    ApprovalType at = approvalTypes.getApprovalType(approval.getCategoryId());
+    ApprovalType at = approvalTypes.byId(approval.getCategoryId());
     if (at != null) {
       a.description = at.getCategory().getName();
     }
     return a;
   }
 
+  public MessageAttribute asMessageAttribute(ChangeMessage message) {
+    MessageAttribute a = new MessageAttribute();
+    a.timestamp = message.getWrittenOn().getTime() / 1000L;
+    a.reviewer = asAccountAttribute(message.getAuthor());
+    a.message = message.getMessage();
+    return a;
+  }
+
+  public PatchSetCommentAttribute asPatchSetLineAttribute(PatchLineComment c) {
+    PatchSetCommentAttribute a = new PatchSetCommentAttribute();
+    a.reviewer = asAccountAttribute(c.getAuthor());
+    a.file = c.getKey().getParentKey().get();
+    a.line = c.getLine();
+    a.message = c.getMessage();
+    return a;
+  }
+
   /** Get a link to the change; null if the server doesn't know its own address. */
   private String getChangeUrl(final Change change) {
     if (change != null && urlProvider.get() != null) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestAccountsEnum.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/MessageAttribute.java
similarity index 78%
rename from gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestAccountsEnum.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/events/MessageAttribute.java
index 2ef2d44..71b38b5 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestAccountsEnum.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/MessageAttribute.java
@@ -12,11 +12,10 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.httpd.rpc;
+package com.google.gerrit.server.events;
 
-public enum SuggestAccountsEnum {
-  ALL,
-  SAME_GROUP,
-  VISIBLE_GROUP,
-  OFF;
+public class MessageAttribute {
+    public Long timestamp;
+    public AccountAttribute reviewer;
+    public String message;
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestAccountsEnum.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/PatchAttribute.java
similarity index 76%
copy from gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestAccountsEnum.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/events/PatchAttribute.java
index 2ef2d44..3802fdd 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestAccountsEnum.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/PatchAttribute.java
@@ -12,11 +12,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.httpd.rpc;
+package com.google.gerrit.server.events;
 
-public enum SuggestAccountsEnum {
-  ALL,
-  SAME_GROUP,
-  VISIBLE_GROUP,
-  OFF;
+import com.google.gerrit.reviewdb.client.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 5de4d6f..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
@@ -21,6 +21,9 @@
     public String revision;
     public String ref;
     public AccountAttribute uploader;
+    public Long createdOn;
 
     public List<ApprovalAttribute> approvals;
+    public List<PatchSetCommentAttribute> comments;
+    public List<PatchAttribute> files;
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestAccountsEnum.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/PatchSetCommentAttribute.java
similarity index 75%
copy from gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestAccountsEnum.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/events/PatchSetCommentAttribute.java
index 2ef2d44..e0c8c13 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestAccountsEnum.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/PatchSetCommentAttribute.java
@@ -12,11 +12,11 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.httpd.rpc;
+package com.google.gerrit.server.events;
 
-public enum SuggestAccountsEnum {
-  ALL,
-  SAME_GROUP,
-  VISIBLE_GROUP,
-  OFF;
+public class PatchSetCommentAttribute {
+    public String file;
+    public Integer line;
+    public AccountAttribute reviewer;
+    public String message;
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/AsyncReceiveCommits.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/AsyncReceiveCommits.java
new file mode 100644
index 0000000..c8d741b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/AsyncReceiveCommits.java
@@ -0,0 +1,184 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git;
+
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.config.ConfigUtil;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.git.WorkQueue.Executor;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.util.RequestScopePropagator;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.FactoryModuleBuilder;
+import com.google.inject.Inject;
+
+import com.google.inject.name.Named;
+import com.google.inject.PrivateModule;
+import com.google.inject.Provides;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.transport.PreReceiveHook;
+import org.eclipse.jgit.transport.ReceiveCommand;
+import org.eclipse.jgit.transport.ReceiveCommand.Result;
+import org.eclipse.jgit.transport.ReceivePack;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.OutputStream;
+import java.util.Collection;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.TimeUnit;
+
+/** Hook that delegates to {@link ReceiveCommits} in a worker thread. */
+public class AsyncReceiveCommits implements PreReceiveHook {
+  private static final Logger log =
+      LoggerFactory.getLogger(AsyncReceiveCommits.class);
+
+  private static final String TIMEOUT_NAME = "ReceiveCommitsOverallTimeout";
+
+  public interface Factory {
+    AsyncReceiveCommits create(ProjectControl projectControl,
+        Repository repository);
+  }
+
+  public static class Module extends PrivateModule {
+    @Override
+    public void configure() {
+      install(new FactoryModuleBuilder()
+          .build(AsyncReceiveCommits.Factory.class));
+      expose(AsyncReceiveCommits.Factory.class);
+      // Don't expose the binding for ReceiveCommits.Factory. All callers should
+      // be using AsyncReceiveCommits.Factory instead.
+      install(new FactoryModuleBuilder()
+          .build(ReceiveCommits.Factory.class));
+    }
+
+    @Provides
+    @Singleton
+    @Named(TIMEOUT_NAME)
+    long getTimeoutMillis(@GerritServerConfig final Config cfg) {
+      return ConfigUtil.getTimeUnit(
+          cfg, "receive", null, "timeout",
+          TimeUnit.MINUTES.toMillis(2),
+          TimeUnit.MILLISECONDS);
+    }
+  }
+
+  private class Worker implements ProjectRunnable {
+    private final Collection<ReceiveCommand> commands;
+
+    private Worker(final Collection<ReceiveCommand> commands) {
+      this.commands = commands;
+    }
+
+    @Override
+    public void run() {
+      rc.processCommands(commands, progress);
+    }
+
+    @Override
+    public Project.NameKey getProjectNameKey() {
+      return rc.getProject().getNameKey();
+    }
+
+    @Override
+    public String getRemoteName() {
+      return null;
+    }
+
+    @Override
+    public boolean hasCustomizedPrint() {
+      return true;
+    }
+
+    @Override
+    public String toString() {
+      return "receive-commits";
+    }
+  }
+
+  private class MessageSenderOutputStream extends OutputStream {
+    @Override
+    public void write(int b) {
+      rc.getMessageSender().sendBytes(new byte[]{(byte)b});
+    }
+
+    @Override
+    public void write(byte[] what, int off, int len) {
+      rc.getMessageSender().sendBytes(what, off, len);
+    }
+
+    @Override
+    public void write(byte[] what) {
+      rc.getMessageSender().sendBytes(what);
+    }
+
+    @Override
+    public void flush() {
+      rc.getMessageSender().flush();
+    }
+  }
+
+  private final ReceiveCommits rc;
+  private final Executor executor;
+  private final RequestScopePropagator scopePropagator;
+  private final MultiProgressMonitor progress;
+  private final long timeoutMillis;
+
+  @Inject
+  AsyncReceiveCommits(final ReceiveCommits.Factory factory,
+      @ReceiveCommitsExecutor final Executor executor,
+      final RequestScopePropagator scopePropagator,
+      @Named(TIMEOUT_NAME) final long timeoutMillis,
+      @Assisted final ProjectControl projectControl,
+      @Assisted final Repository repo) {
+    this.executor = executor;
+    this.scopePropagator = scopePropagator;
+    rc = factory.create(projectControl, repo);
+    rc.getReceivePack().setPreReceiveHook(this);
+
+    progress = new MultiProgressMonitor(
+        new MessageSenderOutputStream(), "Processing changes");
+    this.timeoutMillis = timeoutMillis;
+  }
+
+  @Override
+  public void onPreReceive(final ReceivePack rp,
+      final Collection<ReceiveCommand> commands) {
+    try {
+      progress.waitFor(
+          executor.submit(scopePropagator.wrap(new Worker(commands))),
+          timeoutMillis, TimeUnit.MILLISECONDS);
+    } catch (ExecutionException e) {
+      log.warn("Error in ReceiveCommits", e);
+      rc.addError("internal error while processing changes");
+      // ReceiveCommits has tried its best to catch errors, so anything at this
+      // point is very bad.
+      for (final ReceiveCommand c : commands) {
+        if (c.getResult() == Result.NOT_ATTEMPTED) {
+          c.setResult(Result.REJECTED_OTHER_REASON, "internal error");
+        }
+      }
+    } finally {
+      rc.sendMessages();
+    }
+  }
+
+  public ReceiveCommits getReceiveCommits() {
+    return rc;
+  }
+}
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 a7a969f..86e0740 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeMergeQueue.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeMergeQueue.java
@@ -17,21 +17,20 @@
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static java.util.concurrent.TimeUnit.SECONDS;
 
-import com.google.gerrit.reviewdb.Branch;
-import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.RemotePeer;
-import com.google.gerrit.server.RequestCleanup;
 import com.google.gerrit.server.config.GerritRequestModule;
 import com.google.gerrit.server.ssh.SshInfo;
+import com.google.gerrit.server.util.RequestScopePropagator;
 import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
-import com.google.inject.Key;
 import com.google.inject.OutOfScopeException;
 import com.google.inject.Provider;
-import com.google.inject.Scope;
+import com.google.inject.Singleton;
 import com.google.inject.servlet.RequestScoped;
 
 import com.jcraft.jsch.HostKey;
@@ -46,6 +45,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);
@@ -65,7 +65,9 @@
     Injector child = parent.createChildInjector(new AbstractModule() {
       @Override
       protected void configure() {
-        bindScope(RequestScoped.class, MyScope.REQUEST);
+        bindScope(RequestScoped.class, PerThreadRequestScope.REQUEST);
+        bind(RequestScopePropagator.class)
+            .to(PerThreadRequestScope.Propagator.class);
         install(new GerritRequestModule());
 
         bind(CurrentUser.class).to(IdentifiedUser.class);
@@ -186,8 +188,8 @@
 
   private void mergeImpl(Branch.NameKey branch) {
     try {
-      MyScope ctx = new MyScope();
-      MyScope old = MyScope.set(ctx);
+      PerThreadRequestScope ctx = new PerThreadRequestScope();
+      PerThreadRequestScope old = PerThreadRequestScope.set(ctx);
       try {
         try {
           bgFactory.get().create(branch).merge();
@@ -195,7 +197,7 @@
           ctx.cleanup.run();
         }
       } finally {
-        MyScope.set(old);
+        PerThreadRequestScope.set(old);
       }
     } catch (Throwable e) {
       log.error("Merge attempt for " + branch + " failed", e);
@@ -261,65 +263,4 @@
       return "recheck " + project.get() + " " + dest.getShortName();
     }
   }
-
-  private static class MyScope {
-    private static final ThreadLocal<MyScope> current =
-        new ThreadLocal<MyScope>();
-
-    private static MyScope getContext() {
-      final MyScope ctx = current.get();
-      if (ctx == null) {
-        throw new OutOfScopeException("Not in command/request");
-      }
-      return ctx;
-    }
-
-    static MyScope set(MyScope ctx) {
-      MyScope old = current.get();
-      current.set(ctx);
-      return old;
-    }
-
-    static final Scope REQUEST = new Scope() {
-      public <T> Provider<T> scope(final Key<T> key, final Provider<T> creator) {
-        return new Provider<T>() {
-          public T get() {
-            return getContext().get(key, creator);
-          }
-
-          @Override
-          public String toString() {
-            return String.format("%s[%s]", creator, REQUEST);
-          }
-        };
-      }
-
-      @Override
-      public String toString() {
-        return "MergeQueue.REQUEST";
-      }
-    };
-
-    private static final Key<RequestCleanup> RC_KEY =
-        Key.get(RequestCleanup.class);
-
-    private final RequestCleanup cleanup;
-    private final Map<Key<?>, Object> map;
-
-    MyScope() {
-      cleanup = new RequestCleanup();
-      map = new HashMap<Key<?>, Object>();
-      map.put(RC_KEY, cleanup);
-    }
-
-    synchronized <T> T get(Key<T> key, Provider<T> creator) {
-      @SuppressWarnings("unchecked")
-      T t = (T) map.get(key);
-      if (t == null) {
-        t = creator.get();
-        map.put(key, t);
-      }
-      return t;
-    }
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/CodeReviewCommit.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/CodeReviewCommit.java
index 863c0bd..86d79e0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/CodeReviewCommit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/CodeReviewCommit.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.server.git;
 
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.PatchSet;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
 
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.ObjectId;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/CommitMergeStatus.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/CommitMergeStatus.java
index 788d90b..8790351 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/CommitMergeStatus.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/CommitMergeStatus.java
@@ -16,32 +16,50 @@
 
 enum CommitMergeStatus {
   /** */
-  CLEAN_MERGE,
+  CLEAN_MERGE("Change has been successfully merged into the git repository."),
 
   /** */
-  CLEAN_PICK,
+  CLEAN_PICK("Change has been successfully cherry-picked"),
 
   /** */
-  ALREADY_MERGED,
+  ALREADY_MERGED(""),
 
   /** */
-  PATH_CONFLICT,
+  PATH_CONFLICT("Your change could not be merged due to a path conflict.\n"
+                  + "\n"
+                  + "Please merge (or rebase) the change locally and upload the resolution for review."),
 
   /** */
-  MISSING_DEPENDENCY,
+  MISSING_DEPENDENCY(""),
 
   /** */
-  NO_PATCH_SET,
+  NO_PATCH_SET(""),
 
   /** */
-  REVISION_GONE,
+  REVISION_GONE(""),
 
   /** */
-  CRISS_CROSS_MERGE,
+  CRISS_CROSS_MERGE("Your change requires a recursive merge to resolve.\n"
+                  + "\n"
+                  + "Please merge (or rebase) the change locally and upload the resolution for review."),
 
   /** */
-  CANNOT_CHERRY_PICK_ROOT,
+  CANNOT_CHERRY_PICK_ROOT("Cannot cherry-pick an initial commit onto an existing branch.\n"
+                  + "\n"
+                  + "Please merge the change locally and upload the merge commit for review."),
 
   /** */
-  NOT_FAST_FORWARD;
-}
+  NOT_FAST_FORWARD("Project policy requires all submissions to be a fast-forward.\n"
+                  + "\n"
+                  + "Please rebase the change locally and upload again for review.");
+
+  private String message;
+
+  CommitMergeStatus(String message){
+    this.message = message;
+  }
+
+  public String getMessage(){
+    return message;
+  }
+}
\ No newline at end of file
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 976ec2e..6fea8f1 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
@@ -16,16 +16,18 @@
 
 import static com.google.gerrit.server.git.GitRepositoryManager.REFS_NOTES_REVIEW;
 
+import com.google.gerrit.common.data.ApprovalType;
 import com.google.gerrit.common.data.ApprovalTypes;
-import com.google.gerrit.reviewdb.ApprovalCategory;
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.PatchSetApproval;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.client.ApprovalCategory;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.config.CanonicalWebUrl;
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.client.ResultSet;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -75,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;
@@ -94,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);
@@ -184,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());
@@ -197,10 +203,13 @@
         } else if (ApprovalCategory.SUBMIT.equals(a.getCategoryId())) {
           submit = a;
         } else {
-          formatter.appendApproval(
-              approvalTypes.getApprovalType(a.getCategoryId()).getCategory(),
-              a.getValue(),
-              accountCache.get(a.getAccountId()).getAccount());
+          ApprovalType type = approvalTypes.byId(a.getCategoryId());
+          if (type != null) {
+            formatter.appendApproval(
+                type.getCategory(),
+                a.getValue(),
+                accountCache.get(a.getAccountId()).getAccount());
+          }
         }
       }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_50.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/GitModule.java
similarity index 63%
copy from gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_50.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/git/GitModule.java
index 4a90c1f..7947d30 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_50.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/GitModule.java
@@ -12,14 +12,16 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.schema;
+package com.google.gerrit.server.git;
 
-import com.google.inject.Inject;
-import com.google.inject.Provider;
+import com.google.gerrit.server.config.FactoryModule;
 
-public class Schema_50 extends SchemaVersion {
-  @Inject
-  Schema_50(Provider<Schema_49> prior) {
-    super(prior);
+/** Configures the Git support. */
+public class GitModule extends FactoryModule {
+  @Override
+  protected void configure() {
+    factory(RenameGroupOp.Factory.class);
+    factory(MetaDataUpdate.InternalFactory.class);
+    bind(MetaDataUpdate.Server.class);
   }
-}
\ No newline at end of file
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/GitProjectImporter.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/GitProjectImporter.java
deleted file mode 100644
index 3f2ff0d..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/GitProjectImporter.java
+++ /dev/null
@@ -1,121 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.git;
-
-import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gerrit.reviewdb.Project.SubmitType;
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.client.SchemaFactory;
-import com.google.inject.Inject;
-
-import org.eclipse.jgit.lib.RepositoryCache.FileKey;
-import org.eclipse.jgit.util.FS;
-
-import java.io.File;
-import java.io.IOException;
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.Set;
-
-/** Imports all projects found within the repository manager. */
-public class GitProjectImporter {
-  public interface Messages {
-    void info(String msg);
-    void warning(String msg);
-  }
-
-  private final LocalDiskRepositoryManager repositoryManager;
-  private final SchemaFactory<ReviewDb> schema;
-  private Messages messages;
-
-  @Inject
-  GitProjectImporter(final LocalDiskRepositoryManager repositoryManager,
-      final SchemaFactory<ReviewDb> schema) {
-    this.repositoryManager = repositoryManager;
-    this.schema = schema;
-  }
-
-  public void run(final Messages msg) throws OrmException, IOException {
-    messages = msg;
-    messages.info("Scanning " + repositoryManager.getBasePath());
-    final ReviewDb db = schema.open();
-    try {
-      final HashSet<String> have = new HashSet<String>();
-      for (Project p : db.projects().all()) {
-        have.add(p.getName());
-      }
-      importProjects(repositoryManager.getBasePath(), "", db, have);
-    } finally {
-      db.close();
-    }
-  }
-
-  private void importProjects(final File dir, final String prefix,
-      final ReviewDb db, final Set<String> have) throws OrmException,
-      IOException {
-    final File[] ls = dir.listFiles();
-    if (ls == null) {
-      return;
-    }
-
-    for (File f : ls) {
-      String name = f.getName();
-      if (".".equals(name) || "..".equals(name)) {
-        continue;
-      }
-
-      if (FileKey.isGitRepository(f, FS.DETECTED)) {
-        if (name.equals(".git")) {
-          if ("".equals(prefix)) {
-            // If the git base path is itself a git repository working
-            // directory, this is a bit nonsensical for Gerrit Code Review.
-            // Skip the path and do the next one.
-            messages.warning("Skipping " + f.getAbsolutePath());
-            continue;
-          }
-          name = prefix.substring(0, prefix.length() - 1);
-
-        } else if (name.endsWith(".git")) {
-          name = prefix + name.substring(0, name.length() - 4);
-
-        } else {
-          name = prefix + name;
-          if (!have.contains(name)) {
-            messages.warning("Importing non-standard name '" + name + "'");
-          }
-        }
-
-        if (have.contains(name)) {
-          continue;
-        }
-
-        final Project.NameKey nameKey = new Project.NameKey(name);
-        final Project p = new Project(nameKey);
-
-        p.setDescription(repositoryManager.getProjectDescription(nameKey));
-        p.setSubmitType(SubmitType.MERGE_IF_NECESSARY);
-        p.setUseContributorAgreements(false);
-        p.setUseSignedOffBy(false);
-        p.setUseContentMerge(false);
-        p.setRequireChangeID(false);
-        db.projects().insert(Collections.singleton(p));
-
-      } else if (f.isDirectory()) {
-        importProjects(f, prefix + f.getName() + "/", db, have);
-      }
-    }
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/GitRepositoryManager.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/GitRepositoryManager.java
index a19b722..a2b0495 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/GitRepositoryManager.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/GitRepositoryManager.java
@@ -14,14 +14,14 @@
 
 package com.google.gerrit.server.git;
 
-import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.inject.Singleton;
 
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Repository;
 
 import java.io.IOException;
-
+import java.util.SortedSet;
 
 /**
  * Manages Git repositories for the Gerrit server process.
@@ -37,6 +37,21 @@
   /** Note tree listing commits we refuse {@code refs/meta/reject-commits} */
   public static final String REF_REJECT_COMMITS = "refs/meta/reject-commits";
 
+  /** Configuration settings for a project {@code refs/meta/config} */
+  public static final String REF_CONFIG = "refs/meta/config";
+
+  /**
+   * Prefix applied to merge commit base nodes.
+   * <p>
+   * References in this directory should take the form
+   * {@code refs/cache-automerge/xx/yyyy...} where xx is
+   * the first two digits of the merge commit's object
+   * name, and yyyyy... is the remaining 38. The reference
+   * should point to a treeish that is the automatic merge
+   * result of the merge commit's parents.
+   */
+  public static final String REFS_CACHE_AUTOMERGE = "refs/cache-automerge/";
+
   /**
    * Get (or open) a repository by name.
    *
@@ -55,11 +70,15 @@
    * @param name the repository name, relative to the base directory.
    * @return the cached Repository instance. Caller must call {@code close()}
    *         when done to decrement the resource handle.
-   * @throws RepositoryNotFoundException the name does not denote an existing
-   *         repository, or the name cannot be read as a repository.
+   * @throws RepositoryCaseMismatchException the name collides with an existing
+   *         repository name, but only in case of a character within the name.
+   * @throws RepositoryNotFoundException the name is invalid.
    */
   public abstract Repository createRepository(Project.NameKey name)
-      throws RepositoryNotFoundException;
+      throws RepositoryCaseMismatchException, RepositoryNotFoundException;
+
+  /** @return set of all known projects, sorted by natural NameKey order. */
+  public abstract SortedSet<Project.NameKey> list();
 
   /**
    * Read the {@code GIT_DIR/description} file for gitweb.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/LargeObjectException.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/LargeObjectException.java
new file mode 100644
index 0000000..d08b8768
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/LargeObjectException.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git;
+
+/**
+ * Wrapper for {@link org.eclipse.jgit.errors.LargeObjectException}. Since
+ * org.eclipse.jgit.errors.LargeObjectException is a {@link RuntimeException}
+ * the GerritJsonServlet would treat it as internal failure and as result the
+ * web ui would just show 'Internal Server Error'. Wrapping
+ * org.eclipse.jgit.errors.LargeObjectException into a normal {@link Exception}
+ * allows to display a proper error message.
+ */
+public class LargeObjectException extends Exception {
+
+  private static final long serialVersionUID = 1L;
+
+  public LargeObjectException(final String message,
+      final org.eclipse.jgit.errors.LargeObjectException cause) {
+    super(message, cause);
+  }
+}
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 7e98348..9fa45e1 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,18 +15,22 @@
 package com.google.gerrit.server.git;
 
 import com.google.gerrit.lifecycle.LifecycleListener;
-import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
+import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ConfigConstants;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.lib.RepositoryCache;
 import org.eclipse.jgit.lib.RepositoryCache.FileKey;
+import org.eclipse.jgit.lib.StoredConfig;
 import org.eclipse.jgit.storage.file.LockFile;
 import org.eclipse.jgit.storage.file.WindowCache;
 import org.eclipse.jgit.storage.file.WindowCacheConfig;
@@ -39,6 +43,11 @@
 import java.io.File;
 import java.io.FileNotFoundException;
 import java.io.IOException;
+import java.util.Collections;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
 
 /** Manages Git repositories stored on the local filesystem. */
 @Singleton
@@ -49,6 +58,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;
 
@@ -70,6 +93,8 @@
   }
 
   private final File basePath;
+  private final Lock namesUpdateLock;
+  private volatile SortedSet<Project.NameKey> names;
 
   @Inject
   LocalDiskRepositoryManager(final SitePaths site,
@@ -78,6 +103,8 @@
     if (basePath == null) {
       throw new IllegalStateException("gerrit.basePath must be configured");
     }
+    namesUpdateLock = new ReentrantLock(true /* fair */);
+    names = list();
   }
 
   /** @return base directory under which all projects are stored. */
@@ -94,9 +121,11 @@
     if (isUnreasonableName(name)) {
       throw new RepositoryNotFoundException("Invalid name: " + name);
     }
-
+    if (!names.contains(name)) {
+      throw new RepositoryNotFoundException(gitDirOf(name));
+    }
+    final FileKey loc = FileKey.lenient(gitDirOf(name), FS.DETECTED);
     try {
-      final FileKey loc = FileKey.lenient(gitDirOf(name), FS.DETECTED);
       return RepositoryCache.open(loc);
     } catch (IOException e1) {
       final RepositoryNotFoundException e2;
@@ -107,76 +136,109 @@
   }
 
   public Repository createRepository(final Project.NameKey name)
-      throws RepositoryNotFoundException {
+      throws RepositoryNotFoundException, RepositoryCaseMismatchException {
     if (isUnreasonableName(name)) {
       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);
+
+      if (!names.contains(name)) {
+        throw new RepositoryCaseMismatchException(name);
       }
-      return RepositoryCache.open(loc, false);
+    } 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 */);
+
+      StoredConfig config = db.getConfig();
+      config.setBoolean(ConfigConstants.CONFIG_CORE_SECTION,
+        null, ConfigConstants.CONFIG_KEY_LOGALLREFUPDATES, true);
+      config.save();
+
+      onCreateProject(name);
+
+      return db;
     } catch (IOException e1) {
       final RepositoryNotFoundException e2;
-      e2 = new RepositoryNotFoundException("Cannot open repository " + name);
+      e2 = new RepositoryNotFoundException("Cannot create repository " + name);
       e2.initCause(e1);
       throw e2;
     }
   }
 
+  private void onCreateProject(final Project.NameKey newProjectName) {
+    namesUpdateLock.lock();
+    try {
+      SortedSet<Project.NameKey> n = new TreeSet<Project.NameKey>(names);
+      n.add(newProjectName);
+      names = Collections.unmodifiableSortedSet(n);
+    } finally {
+      namesUpdateLock.unlock();
+    }
+  }
+
   public String getProjectDescription(final Project.NameKey name)
       throws RepositoryNotFoundException, IOException {
     final Repository e = openRepository(name);
     try {
-      final File d = new File(e.getDirectory(), "description");
-
-      String description;
-      try {
-        description = RawParseUtils.decode(IO.readFully(d));
-      } catch (FileNotFoundException err) {
-        return null;
-      }
-
-      if (description != null) {
-        description = description.trim();
-        if (description.isEmpty()) {
-          description = null;
-        }
-        if (UNNAMED.equals(description)) {
-          description = null;
-        }
-      }
-      return description;
+      return getProjectDescription(e);
     } finally {
       e.close();
     }
   }
 
+  private String getProjectDescription(final Repository e) throws IOException {
+    final File d = new File(e.getDirectory(), "description");
+
+    String description;
+    try {
+      description = RawParseUtils.decode(IO.readFully(d));
+    } catch (FileNotFoundException err) {
+      return null;
+    }
+
+    if (description != null) {
+      description = description.trim();
+      if (description.isEmpty()) {
+        description = null;
+      }
+      if (UNNAMED.equals(description)) {
+        description = null;
+      }
+    }
+    return description;
+  }
+
   public void setProjectDescription(final Project.NameKey name,
       final String description) {
     // Update git's description file, in case gitweb is being used
     //
     try {
-      final Repository e;
-      final LockFile f;
-
-      e = openRepository(name);
+      final Repository e = openRepository(name);
       try {
-        f = new LockFile(new File(e.getDirectory(), "description"), FS.DETECTED);
+        final String old = getProjectDescription(e);
+        if ((old == null && description == null)
+            || (old != null && old.equals(description))) {
+          return;
+        }
+
+        final LockFile f = new LockFile(new File(e.getDirectory(), "description"), FS.DETECTED);
         if (f.lock()) {
           String d = description;
           if (d != null) {
@@ -204,6 +266,7 @@
     final String name = nameKey.get();
 
     if (name.length() == 0) return true; // no empty paths
+    if (name.charAt(name.length() -1) == '/') return true; // no suffix
 
     if (name.indexOf('\\') >= 0) return true; // no windows/dos stlye paths
     if (name.charAt(0) == '/') return true; // no absolute paths
@@ -216,4 +279,60 @@
 
     return false; // is a reasonable name
   }
+
+  @Override
+  public SortedSet<Project.NameKey> list() {
+    // The results of this method are cached by ProjectCacheImpl. Control only
+    // enters here if the cache was flushed by the administrator to force
+    // scanning the filesystem. Don't rely on the cached names collection.
+    namesUpdateLock.lock();
+    try {
+      SortedSet<Project.NameKey> n = new TreeSet<Project.NameKey>();
+      scanProjects(basePath, "", n);
+      names = Collections.unmodifiableSortedSet(n);
+      return n;
+    } finally {
+      namesUpdateLock.unlock();
+    }
+  }
+
+  private void scanProjects(final File dir, final String prefix,
+      final SortedSet<Project.NameKey> names) {
+    final File[] ls = dir.listFiles();
+    if (ls == null) {
+      return;
+    }
+
+    for (File f : ls) {
+      String fileName = f.getName();
+      if (FileKey.isGitRepository(f, FS.DETECTED)) {
+        Project.NameKey nameKey = getProjectName(prefix, fileName);
+        if (isUnreasonableName(nameKey)) {
+          log.warn("Ignoring unreasonably named repository " + f.getAbsolutePath());
+        } else {
+          names.add(nameKey);
+        }
+
+      } else if (f.isDirectory()) {
+        scanProjects(f, prefix + f.getName() + "/", names);
+      }
+    }
+  }
+
+  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);
+  }
 }
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 f29c52e..4773680 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,37 +17,41 @@
 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.reviewdb.Account;
-import com.google.gerrit.reviewdb.ApprovalCategory;
-import com.google.gerrit.reviewdb.Branch;
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.ChangeMessage;
-import com.google.gerrit.reviewdb.PatchSet;
-import com.google.gerrit.reviewdb.PatchSetApproval;
-import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.RevId;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.common.data.Capable;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.ApprovalCategory;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetAncestor;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.config.CanonicalWebUrl;
-import com.google.gerrit.server.mail.EmailException;
 import com.google.gerrit.server.mail.MergeFailSender;
 import com.google.gerrit.server.mail.MergedSender;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.util.RequestScopePropagator;
 import com.google.gerrit.server.workflow.CategoryFunction;
 import com.google.gerrit.server.workflow.FunctionState;
-import com.google.gwtorm.client.AtomicUpdate;
-import com.google.gwtorm.client.OrmConcurrencyException;
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.client.SchemaFactory;
+import com.google.gwtorm.server.AtomicUpdate;
+import com.google.gwtorm.server.OrmConcurrencyException;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
@@ -137,6 +141,7 @@
   private final ApprovalTypes approvalTypes;
   private final PatchSetInfoFactory patchSetInfoFactory;
   private final IdentifiedUser.GenericFactory identifiedUserFactory;
+  private final ChangeControl.GenericFactory changeControlFactory;
   private final MergeQueue mergeQueue;
 
   private final PersonIdent myIdent;
@@ -145,8 +150,8 @@
   private final List<CodeReviewCommit> toMerge;
   private List<Change> submitted;
   private final Map<Change.Id, CodeReviewCommit> commits;
-  private ReviewDb schema;
-  private Repository db;
+  private ReviewDb db;
+  private Repository repo;
   private RevWalk rw;
   private RevFlag CAN_MERGE;
   private CodeReviewCommit branchTip;
@@ -154,10 +159,13 @@
   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;
+  private final WorkQueue workQueue;
+  private final RequestScopePropagator requestScopePropagator;
 
   @Inject
   MergeOp(final GitRepositoryManager grm, final SchemaFactory<ReviewDb> sf,
@@ -167,11 +175,14 @@
       @CanonicalWebUrl @Nullable final Provider<String> cwu,
       final ApprovalTypes approvalTypes, final PatchSetInfoFactory psif,
       final IdentifiedUser.GenericFactory iuf,
+      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,
+      final WorkQueue workQueue,
+      final RequestScopePropagator requestScopePropagator) {
     repoManager = grm;
     schemaFactory = sf;
     functionState = fs;
@@ -183,48 +194,106 @@
     this.approvalTypes = approvalTypes;
     patchSetInfoFactory = psif;
     identifiedUserFactory = iuf;
+    this.changeControlFactory = changeControlFactory;
     this.mergeQueue = mergeQueue;
     this.hooks = hooks;
     this.accountCache = accountCache;
     this.tagCache = tagCache;
     codeReviewNotesFactory = crnf;
-
+    this.subOpFactory = subOpFactory;
+    this.workQueue = workQueue;
+    this.requestScopePropagator = requestScopePropagator;
     this.myIdent = myIdent;
     destBranch = branch;
     toMerge = new ArrayList<CodeReviewCommit>();
     commits = new HashMap<Change.Id, CodeReviewCommit>();
   }
 
-  public void merge() throws MergeException {
+  public void verifyMergeability(Change change) {
+    try {
+      setDestProject();
+      openRepository();
+      final Ref destBranchRef = repo.getRef(destBranch.get());
+      submitted = new ArrayList<Change>();
+      submitted.add(change);
+
+      // Test mergeability of the change if the last merged sha1
+      // in the branch is different from the last sha1
+      // the change was tested against.
+      if ((destBranchRef == null && change.getLastSha1MergeTested() == null)
+          || change.getLastSha1MergeTested() == null
+          || (destBranchRef != null && !destBranchRef.getObjectId().getName()
+              .equals(change.getLastSha1MergeTested().get()))) {
+        openSchema();
+        preMerge();
+
+        // update sha1 tested merge.
+        if (destBranchRef != null) {
+          change.setLastSha1MergeTested(new RevId(destBranchRef
+              .getObjectId().getName()));
+        } else {
+          change.setLastSha1MergeTested(new RevId(""));
+        }
+        change.setMergeable(isMergeable(change));
+        db.changes().update(Collections.singleton(change));
+      }
+    } catch (MergeException e) {
+      log.error("Test merge attempt for change: " + change.getId()
+          + " failed", e);
+    } catch (OrmException e) {
+      log.error("Test merge attempt for change: " + change.getId()
+          + " failed: Not able to query the database", e);
+    } catch (IOException e) {
+      log.error("Test merge attempt for change: " + change.getId()
+          + " failed", e);
+    } finally {
+      if (db != null) {
+        db.close();
+      }
+      db = null;
+    }
+  }
+
+  private void setDestProject() throws MergeException {
     final ProjectState pe = projectCache.get(destBranch.getParentKey());
     if (pe == null) {
       throw new MergeException("No such project: " + destBranch.getParentKey());
     }
     destProject = pe.getProject();
+  }
 
-    try {
-      schema = schemaFactory.open();
-    } catch (OrmException e) {
-      throw new MergeException("Cannot open database", e);
+  private void openSchema() throws OrmException {
+    if (db == null) {
+      db = schemaFactory.open();
     }
+  }
+
+  public void merge() throws MergeException {
+    setDestProject();
     try {
-      mergeImpl();
+      openSchema();
+      openRepository();
+      submitted = db.changes().submitted(destBranch).toList();
+      preMerge();
+      updateBranch();
+      updateChangeStatus();
+      updateSubscriptions();
+    } catch (OrmException e) {
+      throw new MergeException("Cannot query the database", e);
     } finally {
       if (rw != null) {
         rw.release();
       }
-      if (db != null) {
-        db.close();
+      if (repo != null) {
+        repo.close();
       }
-      schema.close();
-      schema = null;
+      db.close();
+      db = null;
     }
   }
 
-  private void mergeImpl() throws MergeException {
-    openRepository();
+  private void preMerge() throws MergeException, OrmException {
     openBranch();
-    listPendingSubmits();
     validateChangeList();
     mergeTip = branchTip;
     switch (destProject.getSubmitType()) {
@@ -241,20 +310,18 @@
         markCleanMerges();
         break;
     }
-    updateBranch();
-    updateChangeStatus();
   }
 
   private void openRepository() throws MergeException {
     final Project.NameKey name = destBranch.getParentKey();
     try {
-      db = repoManager.openRepository(name);
+      repo = repoManager.openRepository(name);
     } catch (RepositoryNotFoundException notGit) {
       final String m = "Repository \"" + name.get() + "\" unknown.";
       throw new MergeException(m, notGit);
     }
 
-    rw = new RevWalk(db) {
+    rw = new RevWalk(repo) {
       @Override
       protected RevCommit createCommit(final AnyObjectId id) {
         return new CodeReviewCommit(id);
@@ -269,7 +336,7 @@
     alreadyAccepted = new HashSet<RevCommit>();
 
     try {
-      branchUpdate = db.updateRef(destBranch.get());
+      branchUpdate = repo.updateRef(destBranch.get());
       if (branchUpdate.getOldObjectId() != null) {
         branchTip =
             (CodeReviewCommit) rw.parseCommit(branchUpdate.getOldObjectId());
@@ -278,7 +345,7 @@
         branchTip = null;
       }
 
-      for (final Ref r : db.getAllRefs().values()) {
+      for (final Ref r : repo.getAllRefs().values()) {
         if (r.getName().startsWith(Constants.R_HEADS)
             || r.getName().startsWith(Constants.R_TAGS)) {
           try {
@@ -293,17 +360,9 @@
     }
   }
 
-  private void listPendingSubmits() throws MergeException {
-    try {
-      submitted = schema.changes().submitted(destBranch).toList();
-    } catch (OrmException e) {
-      throw new MergeException("Cannot query the database", e);
-    }
-  }
-
   private void validateChangeList() throws MergeException {
     final Set<ObjectId> tips = new HashSet<ObjectId>();
-    for (final Ref r : db.getAllRefs().values()) {
+    for (final Ref r : repo.getAllRefs().values()) {
       tips.add(r.getObjectId());
     }
 
@@ -318,7 +377,7 @@
 
       final PatchSet ps;
       try {
-        ps = schema.patchSets().get(chg.currentPatchSetId());
+        ps = db.patchSets().get(chg.currentPatchSetId());
       } catch (OrmException e) {
         throw new MergeException("Cannot query the database", e);
       }
@@ -447,11 +506,11 @@
       // Settings for this project allow us to try and
       // automatically resolve conflicts within files if needed.
       // Use ResolveMerge and instruct to operate in core.
-      m = MergeStrategy.RESOLVE.newMerger(db, true);
+      m = MergeStrategy.RESOLVE.newMerger(repo, true);
     } else {
       // No auto conflict resolving allowed. If any of the
       // affected files was modified, merge will fail.
-      m = MergeStrategy.SIMPLE_TWO_WAY_IN_CORE.newMerger(db);
+      m = MergeStrategy.SIMPLE_TWO_WAY_IN_CORE.newMerger(repo);
     }
 
     try {
@@ -549,7 +608,7 @@
       final List<CodeReviewCommit> codeReviewCommits) {
     PatchSetApproval submitter = null;
     for (final CodeReviewCommit c : codeReviewCommits) {
-      PatchSetApproval s = getSubmitter(c.patchsetId);
+      PatchSetApproval s = getSubmitter(db, c.patchsetId);
       if (submitter == null
           || (s != null && s.getGranted().compareTo(submitter.getGranted()) > 0)) {
         submitter = s;
@@ -609,7 +668,7 @@
         if (c.patchsetId != null) {
           c.statusCode = CommitMergeStatus.CLEAN_MERGE;
           if (branchUpdate.getRefLogIdent() == null) {
-            setRefLogIdent(getSubmitter(c.patchsetId));
+            setRefLogIdent(getSubmitter(db, c.patchsetId));
           }
         }
       }
@@ -625,7 +684,7 @@
     }
   }
 
-  private void cherryPickChanges() throws MergeException {
+  private void cherryPickChanges() throws MergeException, OrmException {
     while (!toMerge.isEmpty()) {
       final CodeReviewCommit n = toMerge.remove(0);
       final ThreeWayMerger m;
@@ -634,11 +693,11 @@
         // Settings for this project allow us to try and
         // automatically resolve conflicts within files if needed.
         // Use ResolveMerge and instruct to operate in core.
-        m = MergeStrategy.RESOLVE.newMerger(db, true);
+        m = MergeStrategy.RESOLVE.newMerger(repo, true);
       } else {
         // No auto conflict resolving allowed. If any of the
         // affected files was modified, merge will fail.
-        m = MergeStrategy.SIMPLE_TWO_WAY_IN_CORE.newMerger(db);
+        m = MergeStrategy.SIMPLE_TWO_WAY_IN_CORE.newMerger(repo);
       }
 
       try {
@@ -709,7 +768,7 @@
   }
 
   private void writeCherryPickCommit(final Merger m, final CodeReviewCommit n)
-      throws IOException {
+      throws IOException, OrmException {
     rw.parseBody(n);
 
     final List<FooterLine> footers = n.getFooterLines();
@@ -750,9 +809,10 @@
     }
 
     PatchSetApproval submitAudit = null;
+    List<PatchSetApproval> approvalList = null;
     try {
-      final List<PatchSetApproval> approvalList =
-          schema.patchSetApprovals().byPatchSet(n.patchsetId).toList();
+      approvalList =
+          db.patchSetApprovals().byPatchSet(n.patchsetId).toList();
       Collections.sort(approvalList, new Comparator<PatchSetApproval>() {
         public int compare(final PatchSetApproval a, final PatchSetApproval b) {
           return a.getGranted().compareTo(b.getGranted());
@@ -808,7 +868,7 @@
           tag = "Tested-by";
         } else {
           final ApprovalType at =
-              approvalTypes.getApprovalType(a.getCategoryId());
+              approvalTypes.byId(a.getCategoryId());
           if (at == null) {
             // A deprecated/deleted approval type, ignore it.
             continue;
@@ -836,14 +896,63 @@
 
     final ObjectId id = commit(m, mergeCommit);
     final CodeReviewCommit newCommit = (CodeReviewCommit) rw.parseCommit(id);
+
+    n.change =
+        db.changes().atomicUpdate(n.change.getId(),
+            new AtomicUpdate<Change>() {
+              @Override
+              public Change update(Change change) {
+                change.nextPatchSetId();
+                return change;
+              }
+            });
+
+    final PatchSet ps = new PatchSet(n.change.currPatchSetId());
+    ps.setCreatedOn(new Timestamp(System.currentTimeMillis()));
+    ps.setUploader(submitAudit.getAccountId());
+    ps.setRevision(new RevId(id.getName()));
+    insertAncestors(ps.getId(), newCommit);
+    db.patchSets().insert(Collections.singleton(ps));
+
+    n.change =
+        db.changes().atomicUpdate(n.change.getId(),
+            new AtomicUpdate<Change>() {
+              @Override
+              public Change update(Change change) {
+                change.setCurrentPatchSet(patchSetInfoFactory.get(newCommit,
+                    ps.getId()));
+                return change;
+              }
+            });
+
+    if (approvalList != null) {
+      for (PatchSetApproval a : approvalList) {
+        db.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);
+    }
+    db.patchSetAncestors().insert(toInsert);
+  }
+
   private ObjectId commit(final Merger m, final CommitBuilder mergeCommit)
       throws IOException, UnsupportedEncodingException {
     ObjectInserter oi = m.getObjectInserter();
@@ -885,6 +994,17 @@
 
   private void updateBranch() throws MergeException {
     if (mergeTip != null && (branchTip == null || branchTip != mergeTip)) {
+      if (GitRepositoryManager.REF_CONFIG.equals(branchUpdate.getName())) {
+        try {
+          ProjectConfig cfg = new ProjectConfig(destProject.getNameKey());
+          cfg.load(repo, mergeTip);
+        } catch (Exception e) {
+          throw new MergeException("Submit would store invalid"
+              + " project configuration " + mergeTip.name() + " for "
+              + destProject.getName(), e);
+        }
+      }
+
       branchUpdate.setForceUpdate(false);
       branchUpdate.setNewObjectId(mergeTip);
       branchUpdate.setRefLogMessage("merged", true);
@@ -898,11 +1018,19 @@
                   branchUpdate.getOldObjectId(),
                   mergeTip);
             }
+
+            if (GitRepositoryManager.REF_CONFIG.equals(branchUpdate.getName())) {
+              projectCache.evict(destProject);
+              ProjectState ps = projectCache.get(destProject.getNameKey());
+              repoManager.setProjectDescription(destProject.getNameKey(), //
+                  ps.getProject().getDescription());
+            }
+
             replication.scheduleUpdate(destBranch.getParentKey(), branchUpdate
                 .getName());
 
             Account account = null;
-            final PatchSetApproval submitter = getSubmitter(mergeTip.patchsetId);
+            final PatchSetApproval submitter = getSubmitter(db, mergeTip.patchsetId);
             if (submitter != null) {
               account = accountCache.get(submitter.getAccountId()).getAccount();
             }
@@ -918,7 +1046,21 @@
     }
   }
 
-  private void updateChangeStatus() throws MergeException {
+  private boolean isMergeable(Change c) {
+    final CodeReviewCommit commit = commits.get(c.getId());
+    final CommitMergeStatus s = commit != null ? commit.statusCode : null;
+    boolean isMergeable = false;
+    if (s != null
+        && (s.equals(CommitMergeStatus.CLEAN_MERGE)
+            || s.equals(CommitMergeStatus.CLEAN_PICK) || s
+            .equals(CommitMergeStatus.ALREADY_MERGED))) {
+      isMergeable = true;
+    }
+
+    return isMergeable;
+  }
+
+  private void updateChangeStatus() {
     List<CodeReviewCommit> merged = new ArrayList<CodeReviewCommit>();
 
     for (final Change c : submitted) {
@@ -931,20 +1073,17 @@
         continue;
       }
 
+      final String txt = s.getMessage();
+
       switch (s) {
         case CLEAN_MERGE: {
-          final String txt =
-              "Change has been successfully merged into the git repository.";
           setMerged(c, message(c, txt));
           merged.add(commit);
           break;
         }
 
         case CLEAN_PICK: {
-          final String txt =
-              "Change has been successfully cherry-picked as " + commit.name()
-                  + ".";
-          setMerged(c, message(c, txt));
+          setMerged(c, message(c, txt + " as " + commit.name()));
           merged.add(commit);
           break;
         }
@@ -954,44 +1093,19 @@
           merged.add(commit);
           break;
 
-        case PATH_CONFLICT: {
-          final String txt =
-              "Your change could not be merged due to a path conflict.\n"
-                  + "\n"
-                  + "Please merge (or rebase) the change locally and upload the resolution for review.";
-          setNew(c, message(c, txt));
-          break;
-        }
-
-        case CRISS_CROSS_MERGE: {
-          final String txt =
-              "Your change requires a recursive merge to resolve.\n"
-                  + "\n"
-                  + "Please merge (or rebase) the change locally and upload the resolution for review.";
-          setNew(c, message(c, txt));
-          break;
-        }
-
-        case CANNOT_CHERRY_PICK_ROOT: {
-          final String txt =
-              "Cannot cherry-pick an initial commit onto an existing branch.\n"
-                  + "\n"
-                  + "Please merge the change locally and upload the merge commit for review.";
-          setNew(c, message(c, txt));
-          break;
-        }
-
+        case PATH_CONFLICT:
+        case CRISS_CROSS_MERGE:
+        case CANNOT_CHERRY_PICK_ROOT:
         case NOT_FAST_FORWARD: {
-          final String txt =
-              "Project policy requires all submissions to be a fast-forward.\n"
-                  + "\n"
-                  + "Please rebase the change locally and upload again for review.";
           setNew(c, message(c, txt));
           break;
         }
 
         case MISSING_DEPENDENCY: {
-          dependencyError(commit);
+          final Capable capable = isSubmitStillPossible(commit);
+          if (capable != Capable.OK) {
+            sendMergeFail(c, message(c, capable.getMessage()), false);
+          }
           break;
         }
 
@@ -1002,7 +1116,7 @@
     }
 
     CreateCodeReviewNotes codeReviewNotes =
-        codeReviewNotesFactory.create(schema, db);
+        codeReviewNotesFactory.create(db, repo);
     try {
       codeReviewNotes.create(merged, computeAuthor(merged));
     } catch (CodeReviewNoteCreationException e) {
@@ -1012,7 +1126,23 @@
         GitRepositoryManager.REFS_NOTES_REVIEW);
   }
 
-  private void dependencyError(final CodeReviewCommit commit) {
+  private void updateSubscriptions() {
+    if (mergeTip != null && (branchTip == null || branchTip != mergeTip)) {
+      SubmoduleOp subOp =
+          subOpFactory.create(destBranch, mergeTip, rw, repo, 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;
     if (commit.missing == null) {
       commit.missing = new ArrayList<CodeReviewCommit>();
@@ -1047,7 +1177,7 @@
       // this change submitted. Reschedule an attempt in a bit.
       //
       mergeQueue.recheckAfter(destBranch, waitUntil - now, MILLISECONDS);
-
+      capable = Capable.OK;
     } else if (submitStillPossible) {
       // It would be possible to submit the change if the missing
       // dependencies are also submitted. Perhaps the user just
@@ -1071,9 +1201,7 @@
         }
         txt = m.toString();
       }
-
-      sendMergeFail(c, message(c, txt), false);
-
+      capable = new Capable(txt);
     } else {
       // It is impossible to submit this change as-is. The author
       // needs to rebase it in order to work around the missing
@@ -1103,20 +1231,21 @@
       }
       m.append("\n");
       m.append("Please rebase the change and upload a replacement commit.");
-
-      setNew(c, message(c, m.toString()));
+      capable = new Capable(m.toString());
     }
+
+    return capable;
   }
 
   private void loadChangeInfo(final CodeReviewCommit commit) {
     if (commit.patchsetId == null) {
       try {
         List<PatchSet> matches =
-            schema.patchSets().byRevision(new RevId(commit.name())).toList();
+            db.patchSets().byRevision(new RevId(commit.name())).toList();
         if (matches.size() == 1) {
           final PatchSet ps = matches.get(0);
           commit.patchsetId = ps.getId();
-          commit.change = schema.changes().get(ps.getId().getParentKey());
+          commit.change = db.changes().get(ps.getId().getParentKey());
         }
       } catch (OrmException e) {
       }
@@ -1126,7 +1255,7 @@
   private boolean isAlreadySent(final Change c, final String prefix) {
     try {
       final List<ChangeMessage> msgList =
-          schema.changeMessages().byChange(c.getId()).toList();
+          db.changeMessages().byChange(c.getId()).toList();
       if (msgList.size() > 0) {
         final ChangeMessage last = msgList.get(msgList.size() - 1);
         if (last.getAuthor() == null && last.getMessage().startsWith(prefix)) {
@@ -1150,24 +1279,26 @@
   private ChangeMessage message(final Change c, final String body) {
     final String uuid;
     try {
-      uuid = ChangeUtil.messageUUID(schema);
+      uuid = ChangeUtil.messageUUID(db);
     } catch (OrmException e) {
       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;
   }
 
-  private PatchSetApproval getSubmitter(PatchSet.Id c) {
+  private static PatchSetApproval getSubmitter(ReviewDb reviewDb,
+      PatchSet.Id c) {
     if (c == null) {
       return null;
     }
     PatchSetApproval submitter = null;
     try {
       final List<PatchSetApproval> approvals =
-          schema.patchSetApprovals().byPatchSet(c).toList();
+          reviewDb.patchSetApprovals().byPatchSet(c).toList();
       for (PatchSetApproval a : approvals) {
         if (a.getValue() > 0
             && ApprovalCategory.SUBMIT.equals(a.getCategoryId())) {
@@ -1182,21 +1313,28 @@
     return submitter;
   }
 
-  private void setMerged(Change c, ChangeMessage msg) {
+  private void setMerged(final Change c, final 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>() {
+      db.changes().atomicUpdate(changeId, new AtomicUpdate<Change>() {
         @Override
         public Change update(Change c) {
           c.setStatus(Change.Status.MERGED);
+          // It could be possible that the change being merged
+          // has never had its mergeability tested. So we insure
+          // merged changes has mergeable field true.
+          c.setMergeable(true);
           if (!merged.equals(c.currentPatchSetId())) {
             // Uncool; the patch set changed after we merged it.
             // Go back to the patch set that was actually merged.
             //
             try {
-              c.setCurrentPatchSet(patchSetInfoFactory.get(merged));
+              c.setCurrentPatchSet(patchSetInfoFactory.get(db, merged));
             } catch (PatchSetInfoNotAvailableException e1) {
               log.error("Cannot read merged patch set " + merged, e1);
             }
@@ -1220,8 +1358,12 @@
     try {
       c.setStatus(Change.Status.MERGED);
       final List<PatchSetApproval> approvals =
-          schema.patchSetApprovals().byChange(changeId).toList();
-      final FunctionState fs = functionState.create(c, merged, approvals);
+          db.patchSetApprovals().byChange(changeId).toList();
+      final FunctionState fs = functionState.create(
+          changeControlFactory.controlFor(
+              c,
+              identifiedUserFactory.create(c.getOwner())),
+              merged, approvals);
       for (ApprovalType at : approvalTypes.getApprovalTypes()) {
         CategoryFunction.forCategory(at.getCategory()).run(at, fs);
       }
@@ -1236,7 +1378,9 @@
         }
         a.cache(c);
       }
-      schema.patchSetApprovals().update(approvals);
+      db.patchSetApprovals().update(approvals);
+    } catch (NoSuchChangeException err) {
+      log.warn("Cannot normalize approvals for change " + changeId, err);
     } catch (OrmException err) {
       log.warn("Cannot normalize approvals for change " + changeId, err);
     }
@@ -1246,29 +1390,53 @@
         msg.setAuthor(submitter.getAccountId());
       }
       try {
-        schema.changeMessages().insert(Collections.singleton(msg));
+        db.changeMessages().insert(Collections.singleton(msg));
       } catch (OrmException err) {
         log.warn("Cannot store message on change", err);
       }
     }
 
-    try {
-      final MergedSender cm = mergedSenderFactory.create(c);
-      if (submitter != null) {
-        cm.setFrom(submitter.getAccountId());
+    final PatchSetApproval from = submitter;
+    workQueue.getDefaultQueue()
+        .submit(requestScopePropagator.wrap(new Runnable() {
+      @Override
+      public void run() {
+        PatchSet patchSet;
+        try {
+          ReviewDb reviewDb = schemaFactory.open();
+          try {
+            patchSet = reviewDb.patchSets().get(c.currentPatchSetId());
+          } finally {
+            reviewDb.close();
+          }
+        } catch (Exception e) {
+          log.error("Cannot send email for submitted patch set " + c.getId(), e);
+          return;
+        }
+
+        try {
+          final MergedSender cm = mergedSenderFactory.create(c);
+          if (from != null) {
+            cm.setFrom(from.getAccountId());
+          }
+          cm.setPatchSet(patchSet);
+          cm.send();
+        } catch (Exception e) {
+          log.error("Cannot send email for submitted patch set " + c.getId(), e);
+        }
       }
-      cm.setPatchSet(schema.patchSets().get(c.currentPatchSetId()));
-      cm.send();
-    } catch (OrmException e) {
-      log.error("Cannot send email for submitted patch set " + c.getId(), e);
-    } catch (EmailException e) {
-      log.error("Cannot send email for submitted patch set " + c.getId(), e);
-    }
+
+      @Override
+      public String toString() {
+        return "send-email merged";
+      }
+    }));
+
 
     try {
       hooks.doChangeMergedHook(c, //
           accountCache.get(submitter.getAccountId()).getAccount(), //
-          schema.patchSets().get(c.currentPatchSetId()));
+          db.patchSets().get(c.currentPatchSetId()), db);
     } catch (OrmException ex) {
       log.error("Cannot run hook for submitted patch set " + c.getId(), ex);
     }
@@ -1278,16 +1446,17 @@
     sendMergeFail(c, msg, true);
   }
 
-  private void sendMergeFail(Change c, ChangeMessage msg, final boolean makeNew) {
+  private void sendMergeFail(final Change c, final ChangeMessage msg,
+      final boolean makeNew) {
     try {
-      schema.changeMessages().insert(Collections.singleton(msg));
+      db.changeMessages().insert(Collections.singleton(msg));
     } catch (OrmException err) {
       log.warn("Cannot record merge failure message", err);
     }
 
     if (makeNew) {
       try {
-        schema.changes().atomicUpdate(c.getId(), new AtomicUpdate<Change>() {
+        db.changes().atomicUpdate(c.getId(), new AtomicUpdate<Change>() {
           @Override
           public Change update(Change c) {
             if (c.getStatus().isOpen()) {
@@ -1303,25 +1472,48 @@
       }
     } else {
       try {
-        ChangeUtil.touch(c, schema);
+        ChangeUtil.touch(c, db);
       } catch (OrmException err) {
         log.warn("Cannot update change timestamp", err);
       }
     }
 
-    try {
-      final MergeFailSender cm = mergeFailSenderFactory.create(c);
-      final PatchSetApproval submitter = getSubmitter(c.currentPatchSetId());
-      if (submitter != null) {
-        cm.setFrom(submitter.getAccountId());
+    workQueue.getDefaultQueue()
+        .submit(requestScopePropagator.wrap(new Runnable() {
+      @Override
+      public void run() {
+        PatchSet patchSet;
+        PatchSetApproval submitter;
+        try {
+          ReviewDb reviewDb = schemaFactory.open();
+          try {
+            patchSet = reviewDb.patchSets().get(c.currentPatchSetId());
+            submitter = getSubmitter(reviewDb, c.currentPatchSetId());
+          } finally {
+            reviewDb.close();
+          }
+        } catch (Exception e) {
+          log.error("Cannot send email notifications about merge failure", e);
+          return;
+        }
+
+        try {
+          final MergeFailSender cm = mergeFailSenderFactory.create(c);
+          if (submitter != null) {
+            cm.setFrom(submitter.getAccountId());
+          }
+          cm.setPatchSet(patchSet);
+          cm.setChangeMessage(msg);
+          cm.send();
+        } catch (Exception e) {
+          log.error("Cannot send email notifications about merge failure", e);
+        }
       }
-      cm.setPatchSet(schema.patchSets().get(c.currentPatchSetId()));
-      cm.setChangeMessage(msg);
-      cm.send();
-    } catch (OrmException e) {
-      log.error("Cannot send email notifications about merge failure", e);
-    } catch (EmailException e) {
-      log.error("Cannot send email notifications about merge failure", e);
-    }
+
+      @Override
+      public String toString() {
+        return "send-email merge-failed";
+      }
+    }));
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeQueue.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeQueue.java
index 39017eb..1071768 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeQueue.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeQueue.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.git;
 
-import com.google.gerrit.reviewdb.Branch;
+import com.google.gerrit.reviewdb.client.Branch;
 
 import java.util.concurrent.TimeUnit;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MetaDataUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MetaDataUpdate.java
new file mode 100644
index 0000000..fbe9d16
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MetaDataUpdate.java
@@ -0,0 +1,128 @@
+// Copyright (C) 2011 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git;
+
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+
+/** Helps with the updating of a {@link VersionedMetaData}. */
+public class MetaDataUpdate {
+  public static class User {
+    private final InternalFactory factory;
+    private final GitRepositoryManager mgr;
+    private final PersonIdent serverIdent;
+    private final PersonIdent userIdent;
+
+    @Inject
+    User(InternalFactory factory, GitRepositoryManager mgr,
+        @GerritPersonIdent PersonIdent serverIdent, IdentifiedUser currentUser) {
+      this.factory = factory;
+      this.mgr = mgr;
+      this.serverIdent = serverIdent;
+      this.userIdent = currentUser.newCommitterIdent( //
+          serverIdent.getWhen(), //
+          serverIdent.getTimeZone());
+    }
+
+    public PersonIdent getUserPersonIdent() {
+      return userIdent;
+    }
+
+    public MetaDataUpdate create(Project.NameKey name)
+        throws RepositoryNotFoundException {
+      MetaDataUpdate md = factory.create(name, mgr.openRepository(name));
+      md.getCommitBuilder().setAuthor(userIdent);
+      md.getCommitBuilder().setCommitter(serverIdent);
+      return md;
+    }
+  }
+
+  public static class Server {
+    private final InternalFactory factory;
+    private final GitRepositoryManager mgr;
+    private final PersonIdent serverIdent;
+
+    @Inject
+    Server(InternalFactory factory, GitRepositoryManager mgr,
+        @GerritPersonIdent PersonIdent serverIdent) {
+      this.factory = factory;
+      this.mgr = mgr;
+      this.serverIdent = serverIdent;
+    }
+
+    public MetaDataUpdate create(Project.NameKey name)
+        throws RepositoryNotFoundException {
+      MetaDataUpdate md = factory.create(name, mgr.openRepository(name));
+      md.getCommitBuilder().setAuthor(serverIdent);
+      md.getCommitBuilder().setCommitter(serverIdent);
+      return md;
+    }
+  }
+
+  interface InternalFactory {
+    MetaDataUpdate create(@Assisted Project.NameKey projectName,
+        @Assisted Repository db);
+  }
+
+  private final ReplicationQueue replication;
+  private final Project.NameKey projectName;
+  private final Repository db;
+  private final CommitBuilder commit;
+
+  @Inject
+  public MetaDataUpdate(ReplicationQueue replication,
+      @Assisted Project.NameKey projectName, @Assisted Repository db) {
+    this.replication = replication;
+    this.projectName = projectName;
+    this.db = db;
+    this.commit = new CommitBuilder();
+  }
+
+  /** Set the commit message used when committing the update. */
+  public void setMessage(String message) {
+    getCommitBuilder().setMessage(message);
+  }
+
+  /** Close the cached Repository handle. */
+  public void close() {
+    getRepository().close();
+  }
+
+  Project.NameKey getProjectName() {
+    return projectName;
+  }
+
+  Repository getRepository() {
+    return db;
+  }
+
+  public CommitBuilder getCommitBuilder() {
+    return commit;
+  }
+
+  void replicate(String ref) {
+    if (replication.isEnabled()) {
+      replication.scheduleUpdate(projectName, ref);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MultiProgressMonitor.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MultiProgressMonitor.java
new file mode 100644
index 0000000..dbb849c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MultiProgressMonitor.java
@@ -0,0 +1,325 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git;
+
+import static java.util.concurrent.TimeUnit.NANOSECONDS;
+
+import org.eclipse.jgit.lib.Constants;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+import java.util.concurrent.CancellationException;
+import java.util.concurrent.CopyOnWriteArrayList;
+import java.util.concurrent.ExecutionException;
+import java.util.concurrent.Future;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
+import java.util.List;
+
+/**
+ * Progress reporting interface that multiplexes multiple sub-tasks.
+ * <p>
+ * Output is of the format:
+ * <pre>
+ *   Task: subA: 1, subB: 75% (3/4) (-)\r
+ *   Task: subA: 2, subB: 75% (3/4), subC: 1 (\)\r
+ *   Task: subA: 2, subB: 100% (4/4), subC: 1 (|)\r
+ *   Task: subA: 4, subB: 100% (4/4), subC: 4, done    \n
+ * </pre>
+ * <p>
+ * Callers should try to keep task and sub-task descriptions short, since the
+ * output should fit on one terminal line. (Note that git clients do not accept
+ * terminal control characters, so true multi-line progress messages would be
+ * impossible.)
+ */
+public class MultiProgressMonitor {
+  private static final Logger log =
+      LoggerFactory.getLogger(MultiProgressMonitor.class);
+
+  /** Constant indicating the total work units cannot be predicted. */
+  public static final int UNKNOWN = 0;
+
+  private static final char[] SPINNER_STATES = new char[]{'-', '\\', '|', '/'};
+  private static final char NO_SPINNER = ' ';
+
+  /** Handle for a sub-task. */
+  public class Task {
+    private final String name;
+    private final int total;
+    private volatile int count;
+    private int lastPercent;
+
+    Task(final String subTaskName, final int totalWork) {
+      this.name = subTaskName;
+      this.total = totalWork;
+    }
+
+    /**
+     * Indicate that work has been completed on this sub-task.
+     * <p>
+     * Must be called from the worker thread.
+     *
+     * @param completed number of work units completed.
+     */
+    public void update(final int completed) {
+      count += completed;
+      if (total != UNKNOWN) {
+        int percent = count * 100 / total;
+        if (percent > lastPercent) {
+          lastPercent = percent;
+          wakeUp();
+        }
+      }
+    }
+
+    /**
+     * Indicate that this sub-task is finished.
+     * <p>
+     * Must be called from the worker thread.
+     */
+    public void end() {
+      if (total == UNKNOWN && count > 0) {
+        wakeUp();
+      }
+    }
+  }
+
+  private final OutputStream out;
+  private final String taskName;
+  private final List<Task> tasks = new CopyOnWriteArrayList<Task>();
+  private int spinnerIndex;
+  private char spinnerState = NO_SPINNER;
+  private boolean done;
+  private boolean write = true;
+
+  private final long maxIntervalNanos;
+
+  /**
+   * Create a new progress monitor for multiple sub-tasks.
+   *
+   * @param out stream for writing progress messages.
+   * @param taskName name of the overall task.
+   */
+  public MultiProgressMonitor(final OutputStream out, final String taskName) {
+    this(out, taskName, 500, TimeUnit.MILLISECONDS);
+  }
+
+  /**
+   * Create a new progress monitor for multiple sub-tasks.
+   *
+   * @param out stream for writing progress messages.
+   * @param taskName name of the overall task.
+   * @param maxIntervalTime maximum interval between progress messages.
+   * @param maxIntervalUnit time unit for progress interval.
+   */
+  public MultiProgressMonitor(final OutputStream out, final String taskName,
+      long maxIntervalTime, TimeUnit maxIntervalUnit) {
+    this.out = out;
+    this.taskName = taskName;
+    maxIntervalNanos = NANOSECONDS.convert(maxIntervalTime, maxIntervalUnit);
+  }
+
+  /**
+   * Wait for a task managed by a {@link Future}, with no timeout.
+   *
+   * @see #waitFor(Future, long, TimeUnit)
+   */
+  public void waitFor(final Future<?> workerFuture) throws ExecutionException {
+    waitFor(workerFuture, 0, null);
+  }
+
+  /**
+   * Wait for a task managed by a {@link Future}.
+   * <p>
+   * Must be called from the main thread, <em>not</em> the worker thread. Once
+   * the worker thread calls {@link #end()}, the future has an additional
+   * <code>maxInterval</code> to finish before it is forcefully cancelled and
+   * {@link ExecutionException} is thrown.
+   *
+   * @param workerFuture a future that returns when the worker thread is
+   *     finished.
+   * @param timeoutTime overall timeout for the task; the future is forcefully
+   *     cancelled if the task exceeds the timeout. Non-positive values indicate
+   *     no timeout.
+   * @param timeoutUnit unit for overall task timeout.
+   * @throws ExecutionException if this thread or the worker thread was
+   *     interrupted, the worker was cancelled, or the worker timed out.
+   */
+  public void waitFor(final Future<?> workerFuture, final long timeoutTime,
+      final TimeUnit timeoutUnit) throws ExecutionException {
+    long overallStart = System.nanoTime();
+    long deadline;
+    if (timeoutTime > 0) {
+      deadline = overallStart + NANOSECONDS.convert(timeoutTime, timeoutUnit);
+    } else {
+      deadline = 0;
+    }
+
+    synchronized (this) {
+      long left = maxIntervalNanos;
+      while (!done) {
+        long start = System.nanoTime();
+        try {
+          NANOSECONDS.timedWait(this, left);
+        } catch (InterruptedException e) {
+          throw new ExecutionException(e);
+        }
+
+        // Send an update on every wakeup (manual or spurious), but only move
+        // the spinner every maxInterval.
+        long now = System.nanoTime();
+
+        if (deadline > 0 && now > deadline) {
+          log.warn(String.format(
+              "MultiProgressMonitor worker killed after %sms",
+              TimeUnit.MILLISECONDS.convert(now - overallStart, NANOSECONDS)));
+          workerFuture.cancel(true);
+          break;
+        }
+
+        left -= now - start;
+        if (left <= 0) {
+          moveSpinner();
+          left = maxIntervalNanos;
+        }
+        sendUpdate();
+        if (!done && workerFuture.isDone()) {
+          // The worker may not have called end() explicitly, which is likely a
+          // programming error.
+          log.warn("MultiProgressMonitor worker did not call end()"
+              + " before returning");
+          end();
+        }
+      }
+      sendDone();
+    }
+
+    // The loop exits as soon as the worker calls end(), but we give it another
+    // maxInterval to finish up and return.
+    try {
+      workerFuture.get(maxIntervalNanos, NANOSECONDS);
+    } catch (InterruptedException e) {
+      throw new ExecutionException(e);
+    } catch (CancellationException e) {
+      throw new ExecutionException(e);
+    } catch (TimeoutException e) {
+      workerFuture.cancel(true);
+      throw new ExecutionException(e);
+    }
+  }
+
+  private synchronized void wakeUp() {
+    notifyAll();
+  }
+
+  /**
+   * Begin a sub-task.
+   *
+   * @param subTask sub-task name.
+   * @param subTaskWork total work units in sub-task, or {@link #UNKNOWN}.
+   * @return sub-task handle.
+   */
+  public Task beginSubTask(final String subTask, final int subTaskWork) {
+    Task task = new Task(subTask, subTaskWork);
+    tasks.add(task);
+    return task;
+  }
+
+  /**
+   * End the overall task.
+   * <p>
+   * Must be called from the worker thread.
+   */
+  public synchronized void end() {
+    done = true;
+    wakeUp();
+  }
+
+  private void sendDone() {
+    spinnerState = NO_SPINNER;
+    StringBuilder s = format();
+    boolean any = false;
+    for (Task t : tasks) {
+      if (t.count != 0) {
+        any = true;
+        break;
+      }
+    }
+    if (any) {
+      s.append(",");
+    }
+    s.append(" done    \n");
+    send(s);
+  }
+
+  private void moveSpinner() {
+    spinnerIndex = (spinnerIndex + 1) % SPINNER_STATES.length;
+    spinnerState = SPINNER_STATES[spinnerIndex];
+  }
+
+  private void sendUpdate() {
+    send(format());
+  }
+
+  private StringBuilder format() {
+    StringBuilder s = new StringBuilder().append("\r").append(taskName)
+        .append(':');
+
+    if (!tasks.isEmpty()) {
+      boolean first = true;
+      for (Task t : tasks) {
+        int count = t.count;
+        if (count == 0) {
+          continue;
+        }
+
+        if (!first) {
+          s.append(',');
+        } else {
+          first = false;
+        }
+
+        s.append(' ').append(t.name).append(": ");
+        if (t.total == UNKNOWN) {
+          s.append(count);
+        } else {
+          s.append(String.format("%d%% (%d/%d)",
+              count * 100 / t.total,
+              count, t.total));
+        }
+      }
+    }
+
+    if (spinnerState != NO_SPINNER) {
+      // Don't output a spinner until the alarm fires for the first time.
+      s.append(" (").append(spinnerState).append(')');
+    }
+    return s;
+  }
+
+  private void send(StringBuilder s) {
+    if (write) {
+      try {
+        out.write(Constants.encode(s.toString()));
+        out.flush();
+      } catch (IOException e) {
+        write = false;
+      }
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/NoReplication.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/NoReplication.java
new file mode 100644
index 0000000..c5ee262
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/NoReplication.java
@@ -0,0 +1,37 @@
+// Copyright (C) 2011 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git;
+
+import com.google.gerrit.reviewdb.client.Project;
+
+/** A disabled {@link ReplicationQueue}. */
+public final class NoReplication implements ReplicationQueue {
+  @Override
+  public boolean isEnabled() {
+    return false;
+  }
+
+  @Override
+  public void scheduleUpdate(Project.NameKey project, String ref) {
+  }
+
+  @Override
+  public void scheduleFullSync(Project.NameKey project, String urlMatch) {
+  }
+
+  @Override
+  public void replicateNewProject(Project.NameKey project, String head) {
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/PerThreadRequestScope.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/PerThreadRequestScope.java
new file mode 100644
index 0000000..057e80d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/PerThreadRequestScope.java
@@ -0,0 +1,99 @@
+// Copyright (C) 2009 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git;
+
+import com.google.gerrit.server.RequestCleanup;
+import com.google.gerrit.server.util.ThreadLocalRequestScopePropagator;
+import com.google.inject.Key;
+import com.google.inject.OutOfScopeException;
+import com.google.inject.Provider;
+import com.google.inject.Scope;
+
+import java.util.HashMap;
+import java.util.Map;
+
+class PerThreadRequestScope {
+  static class Propagator
+      extends ThreadLocalRequestScopePropagator<PerThreadRequestScope> {
+    Propagator() {
+      super(REQUEST, current);
+    }
+
+    @Override
+    protected PerThreadRequestScope continuingContext(
+        PerThreadRequestScope ctx) {
+      return new PerThreadRequestScope();
+    }
+  }
+
+  private static final ThreadLocal<PerThreadRequestScope> current =
+      new ThreadLocal<PerThreadRequestScope>();
+
+  private static PerThreadRequestScope requireContext() {
+    final PerThreadRequestScope ctx = current.get();
+    if (ctx == null) {
+      throw new OutOfScopeException("Not in command/request");
+    }
+    return ctx;
+  }
+
+  static PerThreadRequestScope set(PerThreadRequestScope ctx) {
+    PerThreadRequestScope old = current.get();
+    current.set(ctx);
+    return old;
+  }
+
+  static final Scope REQUEST = new Scope() {
+    public <T> Provider<T> scope(final Key<T> key, final Provider<T> creator) {
+      return new Provider<T>() {
+        public T get() {
+          return requireContext().get(key, creator);
+        }
+
+        @Override
+        public String toString() {
+          return String.format("%s[%s]", creator, REQUEST);
+        }
+      };
+    }
+
+    @Override
+    public String toString() {
+      return "PerThreadRequestScope.REQUEST";
+    }
+  };
+
+  private static final Key<RequestCleanup> RC_KEY =
+      Key.get(RequestCleanup.class);
+
+  final RequestCleanup cleanup;
+  private final Map<Key<?>, Object> map;
+
+  PerThreadRequestScope() {
+    cleanup = new RequestCleanup();
+    map = new HashMap<Key<?>, Object>();
+    map.put(RC_KEY, cleanup);
+  }
+
+  synchronized <T> T get(Key<T> key, Provider<T> creator) {
+    @SuppressWarnings("unchecked")
+    T t = (T) map.get(key);
+    if (t == null) {
+      t = creator.get();
+      map.put(key, t);
+    }
+    return t;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
new file mode 100644
index 0000000..d15095b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
@@ -0,0 +1,506 @@
+// Copyright (C) 2010 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git;
+
+import static com.google.gerrit.common.data.Permission.isPermission;
+
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.Project.State;
+import com.google.gerrit.reviewdb.client.Project.SubmitType;
+import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.common.data.RefConfigSection;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
+
+import java.io.BufferedReader;
+import java.io.IOException;
+import java.io.StringReader;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class ProjectConfig extends VersionedMetaData {
+  private static final String PROJECT_CONFIG = "project.config";
+  private static final String GROUP_LIST = "groups";
+
+  private static final String PROJECT = "project";
+  private static final String KEY_DESCRIPTION = "description";
+
+  private static final String ACCESS = "access";
+  private static final String KEY_INHERIT_FROM = "inheritFrom";
+  private static final String KEY_GROUP_PERMISSIONS = "exclusiveGroupPermissions";
+
+  private static final String CAPABILITY = "capability";
+
+  private static final String RECEIVE = "receive";
+  private static final String KEY_REQUIRE_SIGNED_OFF_BY = "requireSignedOffBy";
+  private static final String KEY_REQUIRE_CHANGE_ID = "requireChangeId";
+  private static final String KEY_REQUIRE_CONTRIBUTOR_AGREEMENT =
+      "requireContributorAgreement";
+
+  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;
+  private Map<AccountGroup.UUID, GroupReference> groupsByUUID;
+  private Map<String, AccessSection> accessSections;
+  private List<ValidationError> validationErrors;
+  private ObjectId rulesId;
+
+  public static ProjectConfig read(MetaDataUpdate update) throws IOException,
+      ConfigInvalidException {
+    ProjectConfig r = new ProjectConfig(update.getProjectName());
+    r.load(update);
+    return r;
+  }
+
+  public static ProjectConfig read(MetaDataUpdate update, ObjectId id)
+      throws IOException, ConfigInvalidException {
+    ProjectConfig r = new ProjectConfig(update.getProjectName());
+    r.load(update, id);
+    return r;
+  }
+
+  public ProjectConfig(Project.NameKey projectName) {
+    this.projectName = projectName;
+  }
+
+  public Project getProject() {
+    return project;
+  }
+
+  public AccessSection getAccessSection(String name) {
+    return getAccessSection(name, false);
+  }
+
+  public AccessSection getAccessSection(String name, boolean create) {
+    AccessSection as = accessSections.get(name);
+    if (as == null && create) {
+      as = new AccessSection(name);
+      accessSections.put(name, as);
+    }
+    return as;
+  }
+
+  public Collection<AccessSection> getAccessSections() {
+    return sort(accessSections.values());
+  }
+
+  public void remove(AccessSection section) {
+    if (section != null) {
+      accessSections.remove(section.getName());
+    }
+  }
+
+  public void replace(AccessSection section) {
+    for (Permission permission : section.getPermissions()) {
+      for (PermissionRule rule : permission.getRules()) {
+        rule.setGroup(resolve(rule.getGroup()));
+      }
+    }
+
+    accessSections.put(section.getName(), section);
+  }
+
+  public GroupReference resolve(AccountGroup group) {
+    return resolve(GroupReference.forGroup(group));
+  }
+
+  public GroupReference resolve(GroupReference group) {
+    if (group != null) {
+      GroupReference ref = groupsByUUID.get(group.getUUID());
+      if (ref != null) {
+        return ref;
+      }
+      groupsByUUID.put(group.getUUID(), group);
+    }
+    return group;
+  }
+
+  /** @return the group reference, if the group is used by at least one rule. */
+  public GroupReference getGroup(AccountGroup.UUID uuid) {
+    return groupsByUUID.get(uuid);
+  }
+
+  /**
+   * @return the project's rules.pl ObjectId, if present in the branch.
+   *    Null if it doesn't exist.
+   */
+  public ObjectId getRulesId() {
+    return rulesId;
+  }
+
+  /**
+   * Check all GroupReferences use current group name, repairing stale ones.
+   *
+   * @param groupCache cache to use when looking up group information by UUID.
+   * @return true if one or more group names was stale.
+   */
+  public boolean updateGroupNames(GroupCache groupCache) {
+    boolean dirty = false;
+    for (GroupReference ref : groupsByUUID.values()) {
+      AccountGroup g = groupCache.get(ref.getUUID());
+      if (g != null && !g.getName().equals(ref.getName())) {
+        dirty = true;
+        ref.setName(g.getName());
+      }
+    }
+    return dirty;
+  }
+
+  /**
+   * Get the validation errors, if any were discovered during load.
+   *
+   * @return list of errors; empty list if there are no errors.
+   */
+  public List<ValidationError> getValidationErrors() {
+    if (validationErrors != null) {
+      return Collections.unmodifiableList(validationErrors);
+    } else {
+      return Collections.emptyList();
+    }
+  }
+
+  @Override
+  protected String getRefName() {
+    return GitRepositoryManager.REF_CONFIG;
+  }
+
+  @Override
+  protected void onLoad() throws IOException, ConfigInvalidException {
+    Map<String, GroupReference> groupsByName = readGroupList();
+
+    rulesId = getObjectId("rules.pl");
+    Config rc = readConfig(PROJECT_CONFIG);
+    project = new Project(projectName);
+
+    Project p = project;
+    p.setDescription(rc.getString(PROJECT, null, KEY_DESCRIPTION));
+    if (p.getDescription() == null) {
+      p.setDescription("");
+    }
+    p.setParentName(rc.getString(ACCESS, null, KEY_INHERIT_FROM));
+
+    p.setUseContributorAgreements(getBoolean(rc, RECEIVE, KEY_REQUIRE_CONTRIBUTOR_AGREEMENT, false));
+    p.setUseSignedOffBy(getBoolean(rc, RECEIVE, KEY_REQUIRE_SIGNED_OFF_BY, false));
+    p.setRequireChangeID(getBoolean(rc, RECEIVE, KEY_REQUIRE_CHANGE_ID, false));
+
+    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)) {
+      if (RefConfigSection.isValid(refName)) {
+        AccessSection as = getAccessSection(refName, true);
+
+        for (String varName : rc.getStringList(ACCESS, refName, KEY_GROUP_PERMISSIONS)) {
+          for (String n : varName.split("[, \t]{1,}")) {
+            if (isPermission(n)) {
+              as.getPermission(n, true).setExclusiveGroup(true);
+            }
+          }
+        }
+
+        for (String varName : rc.getNames(ACCESS, refName)) {
+          if (isPermission(varName)) {
+            Permission perm = as.getPermission(varName, true);
+            loadPermissionRules(rc, ACCESS, refName, varName, groupsByName,
+                perm, perm.isLabel());
+          }
+        }
+      }
+    }
+
+    AccessSection capability = null;
+    for (String varName : rc.getNames(CAPABILITY)) {
+      if (GlobalCapability.isCapability(varName)) {
+        if (capability == null) {
+          capability = new AccessSection(AccessSection.GLOBAL_CAPABILITIES);
+          accessSections.put(AccessSection.GLOBAL_CAPABILITIES, capability);
+        }
+        Permission perm = capability.getPermission(varName, true);
+        loadPermissionRules(rc, CAPABILITY, null, varName, groupsByName, perm,
+            GlobalCapability.hasRange(varName));
+      }
+    }
+  }
+
+  private void loadPermissionRules(Config rc, String section,
+      String subsection, String varName,
+      Map<String, GroupReference> groupsByName, Permission perm,
+      boolean useRange) {
+    for (String ruleString : rc.getStringList(section, subsection, varName)) {
+      PermissionRule rule;
+      try {
+        rule = PermissionRule.fromString(ruleString, useRange);
+      } catch (IllegalArgumentException notRule) {
+        error(new ValidationError(PROJECT_CONFIG, "Invalid rule in "
+            + section
+            + (subsection != null ? "." + subsection : "")
+            + "." + varName + ": "
+            + notRule.getMessage()));
+        continue;
+      }
+
+      GroupReference ref = groupsByName.get(rule.getGroup().getName());
+      if (ref == null) {
+        // The group wasn't mentioned in the groups table, so there is
+        // no valid UUID for it. Pool the reference anyway so at least
+        // all rules in the same file share the same GroupReference.
+        //
+        ref = rule.getGroup();
+        groupsByName.put(ref.getName(), ref);
+        error(new ValidationError(PROJECT_CONFIG,
+            "group \"" + ref.getName() + "\" not in " + GROUP_LIST));
+      }
+
+      rule.setGroup(ref);
+      perm.add(rule);
+    }
+  }
+
+  private Map<String, GroupReference> readGroupList() throws IOException {
+    groupsByUUID = new HashMap<AccountGroup.UUID, GroupReference>();
+    Map<String, GroupReference> groupsByName =
+        new HashMap<String, GroupReference>();
+
+    BufferedReader br = new BufferedReader(new StringReader(readUTF8(GROUP_LIST)));
+    String s;
+    for (int lineNumber = 1; (s = br.readLine()) != null; lineNumber++) {
+      if (s.isEmpty() || s.startsWith("#")) {
+        continue;
+      }
+
+      int tab = s.indexOf('\t');
+      if (tab < 0) {
+        error(new ValidationError(GROUP_LIST, lineNumber, "missing tab delimiter"));
+        continue;
+      }
+
+      AccountGroup.UUID uuid = new AccountGroup.UUID(s.substring(0, tab).trim());
+      String name = s.substring(tab + 1).trim();
+      GroupReference ref = new GroupReference(uuid, name);
+
+      groupsByUUID.put(uuid, ref);
+      groupsByName.put(name, ref);
+    }
+    return groupsByName;
+  }
+
+  @Override
+  protected void onSave(CommitBuilder commit) throws IOException,
+      ConfigInvalidException {
+    if (commit.getMessage() == null || "".equals(commit.getMessage())) {
+      commit.setMessage("Updated project configuration\n");
+    }
+
+    Config rc = readConfig(PROJECT_CONFIG);
+    Project p = project;
+
+    if (p.getDescription() != null && !p.getDescription().isEmpty()) {
+      rc.setString(PROJECT, null, KEY_DESCRIPTION, p.getDescription());
+    } else {
+      rc.unset(PROJECT, null, KEY_DESCRIPTION);
+    }
+    set(rc, ACCESS, null, KEY_INHERIT_FROM, p.getParentName());
+
+    set(rc, RECEIVE, null, KEY_REQUIRE_CONTRIBUTOR_AGREEMENT, p.isUseContributorAgreements());
+    set(rc, RECEIVE, null, KEY_REQUIRE_SIGNED_OFF_BY, p.isUseSignedOffBy());
+    set(rc, RECEIVE, null, KEY_REQUIRE_CHANGE_ID, p.isRequireChangeID());
+
+    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) {
+      Set<String> have = new HashSet<String>();
+      for (Permission permission : sort(capability.getPermissions())) {
+        have.add(permission.getName().toLowerCase());
+
+        boolean needRange = GlobalCapability.hasRange(permission.getName());
+        List<String> rules = new ArrayList<String>();
+        for (PermissionRule rule : sort(permission.getRules())) {
+          GroupReference group = rule.getGroup();
+          if (group.getUUID() != null) {
+            keepGroups.add(group.getUUID());
+          }
+          rules.add(rule.asString(needRange));
+        }
+        rc.setStringList(CAPABILITY, null, permission.getName(), rules);
+      }
+      for (String varName : rc.getNames(CAPABILITY)) {
+        if (GlobalCapability.isCapability(varName)
+            && !have.contains(varName.toLowerCase())) {
+          rc.unset(CAPABILITY, null, varName);
+        }
+      }
+    } else {
+      rc.unsetSection(CAPABILITY, null);
+    }
+
+    for (AccessSection as : sort(accessSections.values())) {
+      String refName = as.getName();
+      if (AccessSection.GLOBAL_CAPABILITIES.equals(refName)) {
+        continue;
+      }
+
+      StringBuilder doNotInherit = new StringBuilder();
+      for (Permission perm : sort(as.getPermissions())) {
+        if (perm.getExclusiveGroup()) {
+          if (0 < doNotInherit.length()) {
+            doNotInherit.append(' ');
+          }
+          doNotInherit.append(perm.getName());
+        }
+      }
+      if (0 < doNotInherit.length()) {
+        rc.setString(ACCESS, refName, KEY_GROUP_PERMISSIONS, doNotInherit.toString());
+      } else {
+        rc.unset(ACCESS, refName, KEY_GROUP_PERMISSIONS);
+      }
+
+      Set<String> have = new HashSet<String>();
+      for (Permission permission : sort(as.getPermissions())) {
+        have.add(permission.getName().toLowerCase());
+
+        boolean needRange = permission.isLabel();
+        List<String> rules = new ArrayList<String>();
+        for (PermissionRule rule : sort(permission.getRules())) {
+          GroupReference group = rule.getGroup();
+          if (group.getUUID() != null) {
+            keepGroups.add(group.getUUID());
+          }
+          rules.add(rule.asString(needRange));
+        }
+        rc.setStringList(ACCESS, refName, permission.getName(), rules);
+      }
+
+      for (String varName : rc.getNames(ACCESS, refName)) {
+        if (isPermission(varName) && !have.contains(varName.toLowerCase())) {
+          rc.unset(ACCESS, refName, varName);
+        }
+      }
+    }
+
+    for (String name : rc.getSubsections(ACCESS)) {
+      if (RefConfigSection.isValid(name) && !accessSections.containsKey(name)) {
+        rc.unsetSection(ACCESS, name);
+      }
+    }
+    groupsByUUID.keySet().retainAll(keepGroups);
+
+    saveConfig(PROJECT_CONFIG, rc);
+    saveGroupList();
+  }
+
+  private void saveGroupList() throws IOException {
+    if (groupsByUUID.isEmpty()) {
+      saveFile(GROUP_LIST, null);
+      return;
+    }
+
+    final int uuidLen = 40;
+    StringBuilder buf = new StringBuilder();
+    buf.append(pad(uuidLen, "# UUID"));
+    buf.append('\t');
+    buf.append("Group Name");
+    buf.append('\n');
+
+    buf.append('#');
+    buf.append('\n');
+
+    for (GroupReference g : sort(groupsByUUID.values())) {
+      if (g.getUUID() != null && g.getName() != null) {
+        buf.append(pad(uuidLen, g.getUUID().get()));
+        buf.append('\t');
+        buf.append(g.getName());
+        buf.append('\n');
+      }
+    }
+    saveUTF8(GROUP_LIST, buf.toString());
+  }
+
+  private boolean getBoolean(Config rc, String section, String name,
+      boolean defaultValue) {
+    try {
+      return rc.getBoolean(section, name, defaultValue);
+    } catch (IllegalArgumentException err) {
+      error(new ValidationError(PROJECT_CONFIG, err.getMessage()));
+      return defaultValue;
+    }
+  }
+
+  private <E extends Enum<?>> E getEnum(Config rc, String section,
+      String subsection, String name, E defaultValue) {
+    try {
+      return rc.getEnum(section, subsection, name, defaultValue);
+    } catch (IllegalArgumentException err) {
+      error(new ValidationError(PROJECT_CONFIG, err.getMessage()));
+      return defaultValue;
+    }
+  }
+
+  private void error(ValidationError error) {
+    if (validationErrors == null) {
+      validationErrors = new ArrayList<ValidationError>(4);
+    }
+    validationErrors.add(error);
+  }
+
+  private static String pad(int len, String src) {
+    if (len <= src.length()) {
+      return src;
+    }
+
+    StringBuilder r = new StringBuilder(len);
+    r.append(src);
+    while (r.length() < len) {
+      r.append(' ');
+    }
+    return r.toString();
+  }
+
+  private static <T extends Comparable<? super T>> List<T> sort(Collection<T> m) {
+    ArrayList<T> r = new ArrayList<T>(m);
+    Collections.sort(r);
+    return r;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectRunnable.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectRunnable.java
index a94ba39..32f157d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectRunnable.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectRunnable.java
@@ -13,7 +13,7 @@
 // limitations under the License.
 package com.google.gerrit.server.git;
 
-import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.reviewdb.client.Project;
 
 /** Used to retrieve the project name from an operation **/
 public interface ProjectRunnable extends Runnable {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/PushAllProjectsOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/PushAllProjectsOp.java
index 8985f29..845d037 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/PushAllProjectsOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/PushAllProjectsOp.java
@@ -14,11 +14,8 @@
 
 package com.google.gerrit.server.git;
 
-import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gerrit.server.config.WildProjectName;
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.client.SchemaFactory;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.project.ProjectCache;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -37,20 +34,16 @@
   private static final Logger log =
       LoggerFactory.getLogger(PushAllProjectsOp.class);
 
-  private final SchemaFactory<ReviewDb> schema;
+  private final ProjectCache projectCache;
   private final ReplicationQueue replication;
-  private final Project.NameKey wildProject;
   private final String urlMatch;
 
   @Inject
-  public PushAllProjectsOp(final WorkQueue wq,
-      final SchemaFactory<ReviewDb> sf, final ReplicationQueue rq,
-      @WildProjectName final Project.NameKey wp,
-      @Assisted @Nullable final String urlMatch) {
+  public PushAllProjectsOp(final WorkQueue wq, final ProjectCache projectCache,
+      final ReplicationQueue rq, @Assisted @Nullable final String urlMatch) {
     super(wq);
-    this.schema = sf;
+    this.projectCache = projectCache;
     this.replication = rq;
-    this.wildProject = wp;
     this.urlMatch = urlMatch;
   }
 
@@ -63,17 +56,10 @@
 
   public void run() {
     try {
-      final ReviewDb db = schema.open();
-      try {
-        for (final Project project : db.projects().all()) {
-          if (!project.getNameKey().equals(wildProject)) {
-            replication.scheduleFullSync(project.getNameKey(), urlMatch);
-          }
-        }
-      } finally {
-        db.close();
+      for (final Project.NameKey nameKey : projectCache.all()) {
+        replication.scheduleFullSync(nameKey, urlMatch);
       }
-    } catch (OrmException e) {
+    } catch (RuntimeException e) {
       log.error("Cannot enumerate known projects", e);
     }
   }
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 b28b8396..0868749 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
@@ -14,13 +14,13 @@
 
 package com.google.gerrit.server.git;
 
-import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.Project.NameKey;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.Project.NameKey;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectControl;
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.client.SchemaFactory;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -65,7 +65,7 @@
   }
 
   private static final Logger log = PushReplication.log;
-  static final String MIRROR_ALL = "..all..";
+  static final String ALL_REFS = "..all..";
 
   private final GitRepositoryManager repoManager;
   private final SchemaFactory<ReviewDb> schema;
@@ -77,7 +77,7 @@
   private final Set<String> delta = new HashSet<String>();
   private final Project.NameKey projectName;
   private final URIish uri;
-  private boolean mirror;
+  private boolean pushAllRefs;
 
   private Repository db;
 
@@ -125,10 +125,10 @@
   }
 
   void addRef(final String ref) {
-    if (MIRROR_ALL.equals(ref)) {
+    if (ALL_REFS.equals(ref)) {
       delta.clear();
-      mirror = true;
-    } else if (!mirror) {
+      pushAllRefs = true;
+    } else if (!pushAllRefs) {
       delta.add(ref);
     }
   }
@@ -136,9 +136,9 @@
   public Set<String> getRefs() {
     final Set<String> refs;
 
-    if (mirror) {
+    if (pushAllRefs) {
       refs = new HashSet<String>(1);
-      refs.add(MIRROR_ALL);
+      refs.add(ALL_REFS);
     } else {
       refs = delta;
     }
@@ -147,7 +147,7 @@
   }
 
   public void addRefs(Set<String> refs) {
-    if (!mirror) {
+    if (!pushAllRefs) {
       for (String ref : refs) {
         addRef(ref);
       }
@@ -155,6 +155,16 @@
   }
 
   public void run() {
+    PerThreadRequestScope ctx = new PerThreadRequestScope();
+    PerThreadRequestScope old = PerThreadRequestScope.set(ctx);
+    try {
+      runPushOperation();
+    } finally {
+      PerThreadRequestScope.set(old);
+    }
+  }
+
+  private void runPushOperation() {
     // Lock the queue, and remove ourselves, so we can't be modified once
     // we start replication (instead a new instance, with the same URI, is
     // created and scheduled for a future point in time.)
@@ -206,7 +216,7 @@
 
   @Override
   public String toString() {
-    return (mirror ? "mirror " : "push ") + uri;
+    return "push " + uri;
   }
 
   private void runImpl() throws IOException {
@@ -282,7 +292,7 @@
 
     Map<String, Ref> local = db.getAllRefs();
     if (!pc.allRefsAreVisible()) {
-      if (!mirror) {
+      if (!pushAllRefs) {
         // If we aren't mirroring, reduce the space we need to filter
         // to only the references we will update during this operation.
         //
@@ -304,17 +314,22 @@
         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();
       }
     }
 
+    final boolean noPerms = !pool.isReplicatePermissions();
     final List<RemoteRefUpdate> cmds = new ArrayList<RemoteRefUpdate>();
-    if (mirror) {
+    if (pushAllRefs) {
       final Map<String, Ref> remote = listRemote(tn);
 
       for (final Ref src : local.values()) {
+        if (noPerms && GitRepositoryManager.REF_CONFIG.equals(src.getName())) {
+          continue;
+        }
+
         final RefSpec spec = matchSrc(src.getName());
         if (spec != null) {
           final Ref dst = remote.get(spec.getDestination());
@@ -326,13 +341,15 @@
         }
       }
 
-      for (final Ref ref : remote.values()) {
-        if (!Constants.HEAD.equals(ref.getName())) {
-          final RefSpec spec = matchDst(ref.getName());
-          if (spec != null && !local.containsKey(spec.getSource())) {
-            // No longer on local side, request removal.
-            //
-            delete(cmds, spec);
+      if (config.isMirror()) {
+        for (final Ref ref : remote.values()) {
+          if (!Constants.HEAD.equals(ref.getName())) {
+            final RefSpec spec = matchDst(ref.getName());
+            if (spec != null && !local.containsKey(spec.getSource())) {
+              // No longer on local side, request removal.
+              //
+              delete(cmds, spec);
+            }
           }
         }
       }
@@ -344,9 +361,10 @@
           // If the ref still exists locally, send it, otherwise delete it.
           //
           Ref srcRef = local.get(src);
-          if (srcRef != null) {
+          if (srcRef != null &&
+              !(noPerms && GitRepositoryManager.REF_CONFIG.equals(src))) {
             send(cmds, spec, srcRef);
-          } else {
+          } else if (config.isMirror()) {
             delete(cmds, spec);
           }
         }
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 fd7649a..5cf5f7a 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
@@ -14,27 +14,36 @@
 
 package com.google.gerrit.server.git;
 
-import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.ReplicationUser;
+import com.google.gerrit.server.account.GroupMembership;
+import com.google.gerrit.server.account.ListGroupMembership;
 import com.google.gerrit.server.config.ConfigUtil;
+import com.google.gerrit.server.config.FactoryModule;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.project.PerRequestProjectControlCache;
 import com.google.gerrit.server.project.ProjectControl;
-import com.google.gwtorm.client.SchemaFactory;
+import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.AbstractModule;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
 import com.google.inject.Singleton;
-import com.google.inject.assistedinject.FactoryProvider;
+import com.google.inject.servlet.RequestScoped;
 
 import com.jcraft.jsch.Session;
 
 import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.storage.file.FileRepository;
 import org.eclipse.jgit.transport.JschConfigSessionFactory;
 import org.eclipse.jgit.transport.OpenSshConfig;
 import org.eclipse.jgit.transport.RefSpec;
@@ -58,7 +67,6 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
 import java.util.concurrent.TimeUnit;
 
 /** Manages automatic replication to remote repositories. */
@@ -66,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.
@@ -83,15 +98,18 @@
   private final List<ReplicationConfig> configs;
   private final SchemaFactory<ReviewDb> database;
   private final ReplicationUser.Factory replicationUserFactory;
+  private final GitRepositoryManager gitRepositoryManager;
 
   @Inject
   PushReplication(final Injector i, final WorkQueue wq, final SitePaths site,
-      final ReplicationUser.Factory ruf, final SchemaFactory<ReviewDb> db)
+      final ReplicationUser.Factory ruf, final SchemaFactory<ReviewDb> db,
+      final GitRepositoryManager grm)
       throws ConfigInvalidException, IOException {
     injector = i;
     workQueue = wq;
     database = db;
     replicationUserFactory = ruf;
+    gitRepositoryManager = grm;
     configs = allConfigs(site);
   }
 
@@ -105,7 +123,7 @@
       final String urlMatch) {
     for (final ReplicationConfig cfg : configs) {
       for (final URIish uri : cfg.getURIs(project, urlMatch)) {
-        cfg.schedule(project, PushOp.MIRROR_ALL, uri);
+        cfg.schedule(project, PushOp.ALL_REFS, uri);
       }
     }
   }
@@ -186,7 +204,7 @@
       }
 
       r.add(new ReplicationConfig(injector, workQueue, c, cfg, database,
-          replicationUserFactory));
+          replicationUserFactory, gitRepositoryManager));
     }
     return Collections.unmodifiableList(r);
   }
@@ -254,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
@@ -308,7 +353,7 @@
       }
 
       @Override
-      public synchronized void write(final int b) throws IOException {
+      public synchronized void write(final int b) {
         if (b == '\r') {
           return;
         }
@@ -341,10 +386,13 @@
     private final Map<URIish, PushOp> pending = new HashMap<URIish, PushOp>();
     private final PushOp.Factory opFactory;
     private final ProjectControl.Factory projectControlFactory;
+    private final GitRepositoryManager mgr;
+    private final boolean replicatePermissions;
 
     ReplicationConfig(final Injector injector, final WorkQueue workQueue,
         final RemoteConfig rc, final Config cfg, SchemaFactory<ReviewDb> db,
-        final ReplicationUser.Factory replicationUserFactory) {
+        final ReplicationUser.Factory replicationUserFactory,
+        final GitRepositoryManager gitRepositoryManager) {
 
       remote = rc;
       delay = Math.max(0, getInt(rc, cfg, "replicationdelay", 15));
@@ -356,15 +404,18 @@
 
       String[] authGroupNames =
           cfg.getStringList("remote", rc.getName(), "authGroup");
-      final Set<AccountGroup.Id> authGroups;
+      final GroupMembership authGroups;
       if (authGroupNames.length > 0) {
-        authGroups = ConfigUtil.groupsFor(db, authGroupNames, //
-            log, "Group \"{0}\" not in database, removing from authGroup");
+        authGroups = new ListGroupMembership(ConfigUtil.groupsFor(db, authGroupNames, //
+            log, "Group \"{0}\" not in database, removing from authGroup"));
       } else {
         authGroups = ReplicationUser.EVERYTHING_VISIBLE;
       }
 
       adminUrls = cfg.getStringList("remote", rc.getName(), "adminUrl");
+      replicatePermissions = cfg.getBoolean("remote", rc.getName(),
+              "replicatePermissions", true);
+      mgr = gitRepositoryManager;
 
       final ReplicationUser remoteUser =
           replicationUserFactory.create(authGroups);
@@ -373,18 +424,18 @@
           injector.createChildInjector(new AbstractModule() {
             @Override
             protected void configure() {
+              bindScope(RequestScoped.class, PerThreadRequestScope.REQUEST);
+              bind(PerRequestProjectControlCache.class).in(RequestScoped.class);
               bind(CurrentUser.class).toInstance(remoteUser);
             }
           }).getInstance(ProjectControl.Factory.class);
 
-      opFactory = injector.createChildInjector(new AbstractModule() {
+      opFactory = injector.createChildInjector(new FactoryModule() {
         @Override
         protected void configure() {
-          bind(PushReplication.ReplicationConfig.class).toInstance(
-              ReplicationConfig.this);
+          bind(PushReplication.ReplicationConfig.class).toInstance(ReplicationConfig.this);
           bind(RemoteConfig.class).toInstance(remote);
-          bind(PushOp.Factory.class).toProvider(
-              FactoryProvider.newFactory(PushOp.Factory.class, PushOp.class));
+          factory(PushOp.Factory.class);
         }
       }).getInstance(PushOp.Factory.class);
     }
@@ -396,15 +447,53 @@
 
     void schedule(final Project.NameKey project, final String ref,
         final URIish uri) {
+      PerThreadRequestScope ctx = new PerThreadRequestScope();
+      PerThreadRequestScope old = PerThreadRequestScope.set(ctx);
       try {
-        if (!controlFor(project).isVisible()) {
+        try {
+          if (!controlFor(project).isVisible()) {
+            return;
+          }
+        } catch (NoSuchProjectException e1) {
+          log.error("Internal error: project " + project
+              + " not found during replication");
           return;
         }
-      } catch (NoSuchProjectException e1) {
-        log.error("Internal error: project " + project
-            + " not found during replication");
-        return;
+      } finally {
+        PerThreadRequestScope.set(old);
       }
+
+      if (!replicatePermissions) {
+        PushOp e;
+        synchronized (pending) {
+          e = pending.get(uri);
+        }
+        if (e == null) {
+          Repository git;
+          try {
+            git = mgr.openRepository(project);
+          } catch (RepositoryNotFoundException err) {
+            log.error("Internal error: project " + project
+                + " not found during replication", err);
+            return;
+          }
+          try {
+            Ref head = git.getRef(Constants.HEAD);
+            if (head != null
+                && head.isSymbolic()
+                && GitRepositoryManager.REF_CONFIG.equals(head.getLeaf().getName())) {
+              return;
+            }
+          } catch (IOException err) {
+            log.error("Internal error: cannot check type of project " + project
+                + " during replication", err);
+            return;
+          } finally {
+            git.close();
+          }
+        }
+      }
+
       synchronized (pending) {
         PushOp e = pending.get(uri);
         if (e == null) {
@@ -443,16 +532,6 @@
      * @param pushOp The PushOp instance to be scheduled.
      */
     void reschedule(final PushOp pushOp) {
-      try {
-        if (!controlFor(pushOp.getProjectNameKey()).isVisible()) {
-          return;
-        }
-      } catch (NoSuchProjectException e1) {
-        log.error("Internal error: project " + pushOp.getProjectNameKey()
-            + " not found during replication");
-        return;
-      }
-
       // It locks access to pending variable.
       synchronized (pending) {
         URIish uri = pushOp.getURI();
@@ -521,6 +600,9 @@
     }
 
     boolean wouldPushRef(final String ref) {
+      if (!replicatePermissions && GitRepositoryManager.REF_CONFIG.equals(ref)) {
+        return false;
+      }
       for (final RefSpec s : remote.getPushRefSpecs()) {
         if (s.matchSource(ref)) {
           return true;
@@ -529,6 +611,10 @@
       return false;
     }
 
+    boolean isReplicatePermissions() {
+      return replicatePermissions;
+    }
+
     List<URIish> getURIs(final Project.NameKey project, final String urlMatch) {
       final List<URIish> r = new ArrayList<URIish>(remote.getURIs().size());
       for (URIish uri : remote.getURIs()) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestAccountsEnum.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/QueueProvider.java
similarity index 75%
copy from gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestAccountsEnum.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/git/QueueProvider.java
index 2ef2d44..b539416 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/SuggestAccountsEnum.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/QueueProvider.java
@@ -12,11 +12,12 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.httpd.rpc;
+package com.google.gerrit.server.git;
 
-public enum SuggestAccountsEnum {
-  ALL,
-  SAME_GROUP,
-  VISIBLE_GROUP,
-  OFF;
+public interface QueueProvider {
+  public static enum QueueType {
+    INTERACTIVE, BATCH;
+  }
+
+  public WorkQueue.Executor getQueue(QueueType type);
 }
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 96dd95d..37fbdb2 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,44 +14,53 @@
 
 package com.google.gerrit.server.git;
 
-import com.google.gerrit.common.ChangeHookRunner;
+import static com.google.gerrit.server.git.MultiProgressMonitor.UNKNOWN;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.OK;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_MISSING_OBJECT;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_NONFASTFORWARD;
+import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON;
+
+import com.google.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.AbstractAgreement;
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.AccountAgreement;
-import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.AccountGroupAgreement;
-import com.google.gerrit.reviewdb.ApprovalCategory;
-import com.google.gerrit.reviewdb.Branch;
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.ChangeMessage;
-import com.google.gerrit.reviewdb.ContributorAgreement;
-import com.google.gerrit.reviewdb.PatchSet;
-import com.google.gerrit.reviewdb.PatchSetAncestor;
-import com.google.gerrit.reviewdb.PatchSetApproval;
-import com.google.gerrit.reviewdb.PatchSetInfo;
-import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.RevId;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.ApprovalCategory;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetAncestor;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.PatchSetInfo;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountResolver;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.TrackingFooters;
+import com.google.gerrit.server.git.MultiProgressMonitor.Task;
 import com.google.gerrit.server.mail.CreateChangeSender;
-import com.google.gerrit.server.mail.EmailException;
 import com.google.gerrit.server.mail.MergedSender;
 import com.google.gerrit.server.mail.ReplacePatchSetSender;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
 import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.project.RefControl;
-import com.google.gwtorm.client.AtomicUpdate;
-import com.google.gwtorm.client.OrmException;
+import com.google.gerrit.server.util.MagicBranch;
+import com.google.gerrit.server.util.RequestScopePropagator;
+import com.google.gwtorm.server.AtomicUpdate;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -73,11 +82,10 @@
 import org.eclipse.jgit.revwalk.RevSort;
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.revwalk.filter.RevFilter;
-import org.eclipse.jgit.transport.PostReceiveHook;
-import org.eclipse.jgit.transport.PreReceiveHook;
+import org.eclipse.jgit.transport.AdvertiseRefsHook;
+import org.eclipse.jgit.transport.AdvertiseRefsHookChain;
 import org.eclipse.jgit.transport.ReceiveCommand;
 import org.eclipse.jgit.transport.ReceivePack;
-import org.eclipse.jgit.transport.ReceiveCommand.Result;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -97,11 +105,10 @@
 import javax.annotation.Nullable;
 
 /** Receives change upload using the Git receive-pack protocol. */
-public class ReceiveCommits implements PreReceiveHook, PostReceiveHook {
+public class ReceiveCommits {
   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)?$");
 
@@ -109,21 +116,60 @@
   private static final FooterKey TESTED_BY = new FooterKey("Tested-by");
   private static final FooterKey CHANGE_ID = new FooterKey("Change-Id");
 
-  public interface Factory {
+  interface Factory {
     ReceiveCommits create(ProjectControl projectControl, Repository repository);
   }
 
-  public static class Capable {
-    public static final Capable OK = new Capable("OK");
+  public interface MessageSender {
+    void sendMessage(String what);
+    void sendError(String what);
+    void sendBytes(byte[] what);
+    void sendBytes(byte[] what, int off, int len);
+    void flush();
+  }
 
-    private final String message;
-
-    Capable(String msg) {
-      message = msg;
+  private class ReceivePackMessageSender implements MessageSender {
+    @Override
+    public void sendMessage(String what) {
+      rp.sendMessage(what);
     }
 
-    public String getMessage() {
-      return message;
+    @Override
+    public void sendError(String what) {
+      rp.sendError(what);
+    }
+
+    @Override
+    public void sendBytes(byte[] what) {
+      sendBytes(what, 0, what.length);
+    }
+
+    @Override
+    public void sendBytes(byte[] what, int off, int len) {
+      try {
+        rp.getMessageOutputStream().write(what, off, len);
+      } catch (IOException e) {
+        // Ignore write failures (matching JGit behavior).
+      }
+    }
+
+    @Override
+    public void flush() {
+      try {
+        rp.getMessageOutputStream().flush();
+      } catch (IOException e) {
+        // Ignore write failures (matching JGit behavior).
+      }
+    }
+  }
+
+  private static class Message {
+    private final String message;
+    private final boolean isError;
+
+    private Message(final String message, final boolean isError) {
+      this.message = message;
+      this.isError = isError;
     }
   }
 
@@ -139,23 +185,26 @@
   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;
   private final PersonIdent gerritIdent;
   private final TrackingFooters trackingFooters;
   private final TagCache tagCache;
+  private final WorkQueue workQueue;
+  private final RequestScopePropagator requestScopePropagator;
 
   private final ProjectControl projectControl;
   private final Project project;
   private final Repository repo;
   private final ReceivePack rp;
   private final NoteMap rejectCommits;
-
   private ReceiveCommand newChange;
   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 =
@@ -166,6 +215,15 @@
 
   private String destTopicName;
 
+  private final SubmoduleOp.Factory subOpFactory;
+
+  private final List<Message> messages = new ArrayList<Message>();
+  private Task newProgress;
+  private Task replaceProgress;
+  private Task closeProgress;
+  private Task commandProgress;
+  private MessageSender messageSender;
+
   @Inject
   ReceiveCommits(final ReviewDb db, final ApprovalTypes approvalTypes,
       final AccountResolver accountResolver,
@@ -174,14 +232,19 @@
       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,
       @CanonicalWebUrl @Nullable final String canonicalWebUrl,
       @GerritPersonIdent final PersonIdent gerritIdent,
       final TrackingFooters trackingFooters,
+      final WorkQueue workQueue,
+      final RequestScopePropagator requestScopePropagator,
 
       @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;
@@ -192,10 +255,14 @@
     this.replication = replication;
     this.patchSetInfoFactory = patchSetInfoFactory;
     this.hooks = hooks;
+    this.projectCache = projectCache;
+    this.repoManager = repoManager;
     this.canonicalWebUrl = canonicalWebUrl;
     this.gerritIdent = gerritIdent;
     this.trackingFooters = trackingFooters;
     this.tagCache = tagCache;
+    this.workQueue = workQueue;
+    this.requestScopePropagator = requestScopePropagator;
 
     this.projectControl = projectControl;
     this.project = projectControl.getProject();
@@ -203,6 +270,10 @@
     this.rp = new ReceivePack(repo);
     this.rejectCommits = loadRejectCommitsMap();
 
+    this.subOpFactory = subOpFactory;
+
+    this.messageSender = new ReceivePackMessageSender();
+
     rp.setAllowCreates(true);
     rp.setAllowDeletes(true);
     rp.setAllowNonFastForwards(true);
@@ -210,12 +281,12 @@
 
     if (!projectControl.allRefsAreVisible()) {
       rp.setCheckReferencedObjectsAreReachable(true);
-      rp.setRefFilter(new VisibleRefFilter(tagCache, repo, projectControl, db, false));
+      rp.setAdvertiseRefsHook(new VisibleRefFilter(tagCache, repo, projectControl, db, false));
     }
-    rp.setRefFilter(new ReceiveCommitsRefFilter(rp.getRefFilter()));
-
-    rp.setPreReceiveHook(this);
-    rp.setPostReceiveHook(this);
+    List<AdvertiseRefsHook> advHooks = new ArrayList<AdvertiseRefsHook>(2);
+    advHooks.add(rp.getAdvertiseRefsHook());
+    advHooks.add(new ReceiveCommitsAdvertiseRefsHook());
+    rp.setAdvertiseRefsHook(AdvertiseRefsHookChain.newChain(advHooks));
   }
 
   /** Add reviewers for new (or updated) changes. */
@@ -228,6 +299,22 @@
     ccId.addAll(who);
   }
 
+  /** Set a message sender for this operation. */
+  public void setMessageSender(final MessageSender ms) {
+    messageSender = ms != null ? ms : new ReceivePackMessageSender();
+  }
+
+  MessageSender getMessageSender() {
+    if (messageSender == null) {
+      setMessageSender(null);
+    }
+    return messageSender;
+  }
+
+  Project getProject() {
+    return project;
+  }
+
   /** @return the ReceivePack instance to speak the native Git protocol. */
   public ReceivePack getReceivePack() {
     return rp;
@@ -309,61 +396,50 @@
 
   /** Determine if the user can upload commits. */
   public Capable canUpload() {
-    if (!projectControl.canPushToAtLeastOneRef()) {
-      String reqName = project.getName();
-      return new Capable("Upload denied for project '" + reqName + "'");
+    Capable result = projectControl.canPushToAtLeastOneRef();
+    if (result != Capable.OK) {
+      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 MagicBranch.checkMagicBranchRefs(repo, project);
+  }
 
-    if (project.isUseContributorAgreements()) {
-      try {
-        return verifyActiveContributorAgreement();
-      } catch (OrmException e) {
-        log.error("Cannot query database for agreements", e);
-        return new Capable("Cannot verify contribution agreement");
+  private void addMessage(String message) {
+    messages.add(new Message(message, false));
+  }
+
+  void addError(String error) {
+    messages.add(new Message(error, true));
+  }
+
+  void sendMessages() {
+    for (Message m : messages) {
+      if (m.isError) {
+        messageSender.sendError(m.message);
+      } else {
+        messageSender.sendMessage(m.message);
       }
-    } else {
-      return Capable.OK;
     }
   }
 
-  @Override
-  public void onPreReceive(final ReceivePack arg0,
-      final Collection<ReceiveCommand> commands) {
+  void processCommands(final Collection<ReceiveCommand> commands,
+      final MultiProgressMonitor progress) {
+    newProgress = progress.beginSubTask("new", UNKNOWN);
+    replaceProgress = progress.beginSubTask("updated", UNKNOWN);
+    closeProgress = progress.beginSubTask("closed", UNKNOWN);
+    commandProgress = progress.beginSubTask("refs", UNKNOWN);
+
     parseCommands(commands);
-    if (newChange != null
-        && newChange.getResult() == ReceiveCommand.Result.NOT_ATTEMPTED) {
+    if (newChange != null && newChange.getResult() == NOT_ATTEMPTED) {
       createNewChanges();
     }
-    doReplaces();
-  }
+    newProgress.end();
 
-  @Override
-  public void onPostReceive(final ReceivePack arg0,
-      final Collection<ReceiveCommand> commands) {
+    doReplaces();
+    replaceProgress.end();
+
     for (final ReceiveCommand c : commands) {
-      if (c.getResult() == Result.OK) {
+      if (c.getResult() == OK) {
         switch (c.getType()) {
           case CREATE:
             if (isHead(c)) {
@@ -388,142 +464,42 @@
             break;
         }
 
-        if (!c.getRefName().startsWith(NEW_CHANGE)) {
+        if (isConfig(c)) {
+          projectCache.evict(project);
+          ProjectState ps = projectCache.get(project.getNameKey());
+          repoManager.setProjectDescription(project.getNameKey(), //
+              ps.getProject().getDescription());
+        }
+
+        if (!MagicBranch.isMagicBranch(c.getRefName())) {
           // We only schedule direct refs updates for replication.
           // Change refs are scheduled when they are created.
           //
           replication.scheduleUpdate(project.getNameKey(), c.getRefName());
           Branch.NameKey destBranch = new Branch.NameKey(project.getNameKey(), c.getRefName());
           hooks.doRefUpdatedHook(destBranch, c.getOldId(), c.getNewId(), currentUser.getAccount());
+          commandProgress.update(1);
         }
       }
     }
+    closeProgress.end();
+    commandProgress.end();
+    progress.end();
 
     if (!allNewChanges.isEmpty() && canonicalWebUrl != null) {
       final String url = canonicalWebUrl;
-      rp.sendMessage("");
-      rp.sendMessage("New Changes:");
-      for (final Change.Id c : allNewChanges) {
-        rp.sendMessage("  " + url + c.get());
-      }
-      rp.sendMessage("");
-    }
-  }
-
-  private Capable verifyActiveContributorAgreement() throws OrmException {
-    AbstractAgreement bestAgreement = null;
-    ContributorAgreement bestCla = null;
-
-    OUTER: for (AccountGroup.Id groupId : currentUser.getEffectiveGroups()) {
-      final List<AccountGroupAgreement> temp =
-          db.accountGroupAgreements().byGroup(groupId).toList();
-
-      Collections.reverse(temp);
-
-      for (final AccountGroupAgreement a : temp) {
-        final ContributorAgreement cla =
-            db.contributorAgreements().get(a.getAgreementId());
-        if (cla == null) {
-          continue;
+      addMessage("");
+      addMessage("New Changes:");
+      for (final Change c : allNewChanges) {
+        if (c.getStatus() == Change.Status.DRAFT) {
+          addMessage("  " + url + c.getChangeId() + " [DRAFT]");
         }
-
-        bestAgreement = a;
-        bestCla = cla;
-        break OUTER;
-      }
-    }
-
-    if (bestAgreement == null) {
-      final List<AccountAgreement> temp =
-          db.accountAgreements().byAccount(currentUser.getAccountId()).toList();
-
-      Collections.reverse(temp);
-
-      for (final AccountAgreement a : temp) {
-        final ContributorAgreement cla =
-            db.contributorAgreements().get(a.getAgreementId());
-        if (cla == null) {
-          continue;
+        else {
+          addMessage("  " + url + c.getChangeId());
         }
-
-        bestAgreement = a;
-        bestCla = cla;
-        break;
       }
+      addMessage("");
     }
-
-    if (bestCla != null && !bestCla.isActive()) {
-      final StringBuilder msg = new StringBuilder();
-      msg.append(bestCla.getShortName());
-      msg.append(" contributor agreement is expired.\n");
-      if (canonicalWebUrl != null) {
-        msg.append("\nPlease complete a new agreement");
-        msg.append(":\n\n  ");
-        msg.append(canonicalWebUrl);
-        msg.append("#");
-        msg.append(PageLinks.SETTINGS_AGREEMENTS);
-        msg.append("\n");
-      }
-      msg.append("\n");
-      return new Capable(msg.toString());
-    }
-
-    if (bestCla != null && bestCla.isRequireContactInformation()) {
-      boolean fail = false;
-      fail |= missing(currentUser.getAccount().getFullName());
-      fail |= missing(currentUser.getAccount().getPreferredEmail());
-      fail |= !currentUser.getAccount().isContactFiled();
-
-      if (fail) {
-        final StringBuilder msg = new StringBuilder();
-        msg.append(bestCla.getShortName());
-        msg.append(" contributor agreement requires");
-        msg.append(" current contact information.\n");
-        if (canonicalWebUrl != null) {
-          msg.append("\nPlease review your contact information");
-          msg.append(":\n\n  ");
-          msg.append(canonicalWebUrl);
-          msg.append("#");
-          msg.append(PageLinks.SETTINGS_CONTACT);
-          msg.append("\n");
-        }
-        msg.append("\n");
-        return new Capable(msg.toString());
-      }
-    }
-
-    if (bestAgreement != null) {
-      switch (bestAgreement.getStatus()) {
-        case VERIFIED:
-          return Capable.OK;
-        case REJECTED:
-          return new Capable(bestCla.getShortName()
-              + " contributor agreement was rejected."
-              + "\n       (rejected on " + bestAgreement.getReviewedOn()
-              + ")\n");
-        case NEW:
-          return new Capable(bestCla.getShortName()
-              + " contributor agreement is still pending review.\n");
-      }
-    }
-
-    final StringBuilder msg = new StringBuilder();
-    msg.append(" A Contributor Agreement must be completed before uploading");
-    if (canonicalWebUrl != null) {
-      msg.append(":\n\n  ");
-      msg.append(canonicalWebUrl);
-      msg.append("#");
-      msg.append(PageLinks.SETTINGS_AGREEMENTS);
-      msg.append("\n");
-    } else {
-      msg.append(".");
-    }
-    msg.append("\n");
-    return new Capable(msg.toString());
-  }
-
-  private static boolean missing(final String value) {
-    return value == null || value.trim().equals("");
   }
 
   private Account.Id toAccountId(final String nameOrEmail) throws OrmException,
@@ -538,7 +514,7 @@
 
   private void parseCommands(final Collection<ReceiveCommand> commands) {
     for (final ReceiveCommand cmd : commands) {
-      if (cmd.getResult() != ReceiveCommand.Result.NOT_ATTEMPTED) {
+      if (cmd.getResult() != NOT_ATTEMPTED) {
         // Already rejected by the core receive process.
         //
         continue;
@@ -550,7 +526,7 @@
         continue;
       }
 
-      if (cmd.getRefName().startsWith(NEW_CHANGE)) {
+      if (MagicBranch.isMagicBranch(cmd.getRefName())) {
         parseNewChangeCommand(cmd);
         continue;
       }
@@ -567,24 +543,70 @@
       switch (cmd.getType()) {
         case CREATE:
           parseCreate(cmd);
-          continue;
+          break;
 
         case UPDATE:
           parseUpdate(cmd);
-          continue;
+          break;
 
         case DELETE:
           parseDelete(cmd);
-          continue;
+          break;
 
         case UPDATE_NONFASTFORWARD:
           parseRewind(cmd);
+          break;
+
+        default:
+          reject(cmd);
           continue;
       }
 
-      // Everything else is bogus as far as we are concerned.
-      //
-      reject(cmd);
+      if (cmd.getResult() != NOT_ATTEMPTED) {
+        continue;
+      }
+
+      if (isConfig(cmd)) {
+        if (!projectControl.isOwner()) {
+          reject(cmd, "not project owner");
+          continue;
+        }
+
+        switch (cmd.getType()) {
+          case CREATE:
+          case UPDATE:
+          case UPDATE_NONFASTFORWARD:
+            try {
+              ProjectConfig cfg = new ProjectConfig(project.getNameKey());
+              cfg.load(repo, cmd.getNewId());
+              if (!cfg.getValidationErrors().isEmpty()) {
+                addError("Invalid project configuration:");
+                for (ValidationError err : cfg.getValidationErrors()) {
+                  addError("  " + err.getMessage());
+                }
+                reject(cmd, "invalid project configuration");
+                log.error("User " + currentUser.getUserName()
+                    + " tried to push invalid project configuration "
+                    + cmd.getNewId().name() + " for " + project.getName());
+                continue;
+              }
+            } catch (Exception e) {
+              reject(cmd, "invalid project configuration");
+              log.error("User " + currentUser.getUserName()
+                  + " tried to push invalid project configuration "
+                  + cmd.getNewId().name() + " for " + project.getName(), e);
+              continue;
+            }
+            break;
+
+          case DELETE:
+            break;
+
+          default:
+            reject(cmd);
+            continue;
+        }
+      }
     }
   }
 
@@ -606,10 +628,11 @@
     RefControl ctl = projectControl.controlForRef(cmd.getRefName());
     if (ctl.canCreate(rp.getRevWalk(), obj)) {
       validateNewCommits(ctl, cmd);
-
-      // Let the core receive process handle it
+      if (cmd.getResult() == NOT_ATTEMPTED) {
+        cmd.execute(rp);
+      }
     } else {
-      reject(cmd);
+      reject(cmd, "can not create new references");
     }
   }
 
@@ -621,9 +644,11 @@
       }
 
       validateNewCommits(ctl, cmd);
-      // Let the core receive process handle it
+      if (cmd.getResult() == NOT_ATTEMPTED) {
+        cmd.execute(rp);
+      }
     } else {
-      reject(cmd);
+      reject(cmd, "can not update the reference as a fast forward");
     }
   }
 
@@ -649,9 +674,11 @@
   private void parseDelete(final ReceiveCommand cmd) {
     RefControl ctl = projectControl.controlForRef(cmd.getRefName());
     if (ctl.canDelete()) {
-      // Let the core receive process handle it
+      if (cmd.getResult() == NOT_ATTEMPTED) {
+        cmd.execute(rp);
+      }
     } else {
-      reject(cmd);
+      reject(cmd, "can not delete references");
     }
   }
 
@@ -671,15 +698,18 @@
     RefControl ctl = projectControl.controlForRef(cmd.getRefName());
     if (newObject != null) {
       validateNewCommits(ctl, cmd);
-      if (cmd.getResult() != ReceiveCommand.Result.NOT_ATTEMPTED) {
+      if (cmd.getResult() != NOT_ATTEMPTED) {
         return;
       }
     }
 
     if (ctl.canForceUpdate()) {
-      // Let the core receive process handle it
+      if (cmd.getResult() == NOT_ATTEMPTED) {
+        cmd.execute(rp);
+      }
     } else {
-      cmd.setResult(ReceiveCommand.Result.REJECTED_NONFASTFORWARD);
+      cmd.setResult(REJECTED_NONFASTFORWARD, " need '"
+          + PermissionRule.FORCE_PUSH + "' privilege.");
     }
   }
 
@@ -692,7 +722,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;
     }
@@ -747,7 +777,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;
     }
 
@@ -784,7 +814,7 @@
         walk.setRevFilter(oldRevFilter);
       }
     } catch (IOException e) {
-      newChange.setResult(Result.REJECTED_MISSING_OBJECT);
+      newChange.setResult(REJECTED_MISSING_OBJECT);
       log.error("Invalid pack upload; one or more objects weren't sent", e);
       return;
     }
@@ -959,7 +989,7 @@
       // Should never happen, the core receive process would have
       // identified the missing object earlier before we got control.
       //
-      newChange.setResult(Result.REJECTED_MISSING_OBJECT);
+      newChange.setResult(REJECTED_MISSING_OBJECT);
       log.error("Invalid pack upload; one or more objects weren't sent", e);
       return;
     } catch (OrmException e) {
@@ -985,8 +1015,9 @@
         reject(newChange, "database error");
         return;
       }
+      newProgress.update(1);
     }
-    newChange.setResult(ReceiveCommand.Result.OK);
+    newChange.setResult(OK);
   }
 
   private static boolean isValidChangeId(String idStr) {
@@ -1023,45 +1054,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());
@@ -1073,22 +1118,32 @@
     }
     replication.scheduleUpdate(project.getNameKey(), ru.getName());
 
-    allNewChanges.add(change.getId());
+    allNewChanges.add(change);
 
-    try {
-      final CreateChangeSender cm;
-      cm = createChangeSenderFactory.create(change);
-      cm.setFrom(me);
-      cm.setPatchSet(ps, info);
-      cm.addReviewers(reviewers);
-      cm.addExtraCC(cc);
-      cm.send();
-    } catch (EmailException e) {
-      log.error("Cannot send email for new change " + change.getId(), e);
-    }
+    workQueue.getDefaultQueue()
+        .submit(requestScopePropagator.wrap(new Runnable() {
+      @Override
+      public void run() {
+        try {
+          final CreateChangeSender cm;
+          cm = createChangeSenderFactory.create(change);
+          cm.setFrom(me);
+          cm.setPatchSet(ps, info);
+          cm.addReviewers(reviewers);
+          cm.addExtraCC(cc);
+          cm.send();
+        } catch (Exception e) {
+          log.error("Cannot send email for new change " + change.getId(), e);
+        }
+      }
 
-    ChangeUtil.updateTrackingIds(db, change, trackingFooters, footerLines);
-    hooks.doPatchsetCreatedHook(change, ps);
+      @Override
+      public String toString() {
+        return "send-email newchange";
+      }
+    }));
+
+    hooks.doPatchsetCreatedHook(change, ps, db);
   }
 
   private static boolean isReviewer(final FooterLine candidateFooterLine) {
@@ -1101,7 +1156,8 @@
   private void doReplaces() {
     for (final ReplaceRequest request : replaceByChange.values()) {
       try {
-        doReplace(request);
+        doReplace(request, false);
+        replaceProgress.update(1);
       } catch (IOException err) {
         log.error("Error computing replacement patch for change "
             + request.ontoChange + ", commit " + request.newCommit.name(), err);
@@ -1111,7 +1167,7 @@
             + request.ontoChange + ", commit " + request.newCommit.name(), err);
         reject(request.cmd, "database error");
       }
-      if (request.cmd.getResult() == ReceiveCommand.Result.NOT_ATTEMPTED) {
+      if (request.cmd.getResult() == NOT_ATTEMPTED) {
         log.error("Replacement patch for change " + request.ontoChange
             + ", commit " + request.newCommit.name() + " wasn't attempted."
             + "  This is a bug in the receive process implementation.");
@@ -1120,7 +1176,7 @@
     }
   }
 
-  private PatchSet.Id doReplace(final ReplaceRequest request)
+  private PatchSet.Id doReplace(final ReplaceRequest request, boolean ignoreNoChanges)
       throws IOException, OrmException {
     final RevCommit c = request.newCommit;
     rp.getRevWalk().parseBody(c);
@@ -1216,7 +1272,7 @@
           final boolean parentsEq = parentsEqual(c, prior);
           final boolean authorEq = authorEqual(c, prior);
 
-          if (messageEq && parentsEq && authorEq) {
+          if (messageEq && parentsEq && authorEq && !ignoreNoChanges) {
             reject(request.cmd, "no changes made");
             return null;
           } else {
@@ -1235,7 +1291,7 @@
             if (!parentsEq) {
               msg.append(", was rebased");
             }
-            rp.sendMessage(msg.toString());
+            addMessage(msg.toString());
           }
         }
       } catch (IOException e) {
@@ -1245,135 +1301,160 @@
       }
     }
 
-    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) {
             if (change.getStatus().isOpen()) {
               change.nextPatchSetId();
+              change.setLastSha1MergeTested(null);
               return change;
             } else {
               return null;
             }
           }
         });
-    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;
       }
 
-      final ApprovalType type =
-          approvalTypes.getApprovalType(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.
+      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;
+        }
+      }
+
+      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);
+          }
+        }
+      }
+
+      msg =
+          new ChangeMessage(new ChangeMessage.Key(change.getId(), ChangeUtil
+              .messageUUID(db)), me, ps.getCreatedOn(), ps.getId());
+      msg.setMessage("Uploaded patch set " + ps.getPatchSetId() + ".");
+      db.changeMessages().insert(Collections.singleton(msg));
+      ChangeUtil.updateTrackingIds(db, change, trackingFooters, footerLines);
+      result.msg = msg;
+
+      if (result.mergedIntoRef == null) {
+        // Change should be new, so it can go through review again.
         //
-        db.patchSetApprovals().insert(
-            Collections.singleton(new PatchSetApproval(ps.getId(), a)));
+        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;
+        }
       }
 
-      if (!haveAuthor && authorId != null && a.getAccountId().equals(authorId)) {
-        haveAuthor = true;
-      }
-      if (!haveCommitter && committerId != null
-          && a.getAccountId().equals(committerId)) {
-        haveCommitter = true;
-      }
+      db.commit();
+    } finally {
+      db.rollback();
     }
 
-    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;
-
     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());
@@ -1384,25 +1465,35 @@
           + repo.getDirectory() + ": " + ru.getResult());
     }
     replication.scheduleUpdate(project.getNameKey(), ru.getName());
-    hooks.doPatchsetCreatedHook(result.change, ps);
-    request.cmd.setResult(ReceiveCommand.Result.OK);
+    hooks.doPatchsetCreatedHook(result.change, ps, db);
+    request.cmd.setResult(OK);
 
-    try {
-      final ReplacePatchSetSender cm;
-      cm = replacePatchSetFactory.create(result.change);
-      cm.setFrom(me);
-      cm.setPatchSet(ps, result.info);
-      cm.setChangeMessage(result.msg);
-      cm.addReviewers(reviewers);
-      cm.addExtraCC(cc);
-      cm.addReviewers(oldReviewers);
-      cm.addExtraCC(oldCC);
-      cm.send();
-    } catch (EmailException e) {
-      log.error("Cannot send email for new patch set " + ps.getId(), e);
-    }
+    workQueue.getDefaultQueue()
+        .submit(requestScopePropagator.wrap(new Runnable() {
+      @Override
+      public void run() {
+        try {
+          final ReplacePatchSetSender cm;
+          cm = replacePatchSetFactory.create(result.change);
+          cm.setFrom(me);
+          cm.setPatchSet(ps, result.info);
+          cm.setChangeMessage(result.msg);
+          cm.addReviewers(reviewers);
+          cm.addExtraCC(cc);
+          cm.addReviewers(oldReviewers);
+          cm.addExtraCC(oldCC);
+          cm.send();
+        } catch (Exception e) {
+          log.error("Cannot send email for new patch set " + ps.getId(), e);
+        }
+      }
 
-    ChangeUtil.updateTrackingIds(db, change, trackingFooters, footerLines);
+      @Override
+      public String toString() {
+        return "send-email newpatchset";
+      }
+    }));
+
     sendMergedEmail(result);
     return result != null ? result.info.getKey() : null;
   }
@@ -1531,7 +1622,7 @@
         }
       }
     } catch (IOException err) {
-      cmd.setResult(Result.REJECTED_MISSING_OBJECT);
+      cmd.setResult(REJECTED_MISSING_OBJECT);
       log.error("Invalid pack upload; one or more objects weren't sent", err);
     }
   }
@@ -1611,33 +1702,60 @@
     }
 
     final List<String> idList = c.getFooterLines(CHANGE_ID);
-    if (idList.isEmpty()) {
-      if (project.isRequireChangeID() && (cmd.getRefName().startsWith(NEW_CHANGE)
-  || NEW_PATCHSET.matcher(cmd.getRefName()).matches())) {
-        String errMsg = "missing Change-Id in commit message";
-        reject(cmd, errMsg);
-        rp.sendMessage(getFixedCommitMsgWithChangeId(errMsg, c));
+    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";
+          reject(cmd, errMsg);
+          addMessage(getFixedCommitMsgWithChangeId(errMsg, c));
+          return false;
+        }
+      } else if (idList.size() > 1) {
+        reject(cmd, "multiple Change-Id lines in commit message");
         return false;
-      }
-    } else if (idList.size() > 1) {
-      reject(cmd, "multiple Change-Id lines in commit message");
-      return false;
-    } else {
-      final String v = idList.get(idList.size() - 1).trim();
-      if (!v.matches("^I[0-9a-f]{8,}.*$")) {
-        final String errMsg = "missing or invalid Change-Id line format in commit message";
-        reject(cmd, errMsg);
-        rp.sendMessage(getFixedCommitMsgWithChangeId(errMsg, c));
-        return false;
+      } else {
+        final String v = idList.get(idList.size() - 1).trim();
+        if (!v.matches("^I[0-9a-f]{8,}.*$")) {
+          final String errMsg =
+              "missing or invalid Change-Id line format in commit message";
+          reject(cmd, errMsg);
+          addMessage(getFixedCommitMsgWithChangeId(errMsg, c));
+          return false;
+        }
       }
     }
 
     // Check for banned commits to prevent them from entering the tree again.
     if (rejectCommits.contains(c)) {
-      reject(newChange, "contains banned commit " + c.getName());
+      reject(cmd, "contains banned commit " + c.getName());
       return false;
     }
 
+    // If this is the special project configuration branch, validate the config.
+    if (GitRepositoryManager.REF_CONFIG.equals(ctl.getRefName())) {
+      try {
+        ProjectConfig cfg = new ProjectConfig(project.getNameKey());
+        cfg.load(repo, cmd.getNewId());
+        if (!cfg.getValidationErrors().isEmpty()) {
+          addError("Invalid project configuration:");
+          for (ValidationError err : cfg.getValidationErrors()) {
+            addError("  " + err.getMessage());
+          }
+          reject(cmd, "invalid project configuration");
+          log.error("User " + currentUser.getUserName()
+              + " tried to push invalid project configuration "
+              + cmd.getNewId().name() + " for " + project.getName());
+          return false;
+        }
+      } catch (Exception e) {
+        reject(cmd, "invalid project configuration");
+        log.error("User " + currentUser.getUserName()
+            + " tried to push invalid project configuration "
+            + cmd.getNewId().name() + " for " + project.getName(), e);
+        return false;
+      }
+    }
+
     return true;
   }
 
@@ -1700,7 +1818,7 @@
       sb.append("ERROR:  " + canonicalWebUrl + "#" + PageLinks.SETTINGS_CONTACT + "\n");
     }
     sb.append("\n");
-    getReceivePack().sendMessage(sb.toString());
+    addMessage(sb.toString());
   }
 
   private void warnMalformedMessage(RevCommit c) {
@@ -1712,7 +1830,7 @@
       } catch (IOException err) {
         id = c.abbreviate(6);
       }
-      rp.sendMessage("(W) " + id.name() //
+      addMessage("(W) " + id.name() //
           + ": commit subject >65 characters; use shorter first paragraph");
     }
 
@@ -1733,7 +1851,7 @@
       } catch (IOException err) {
         id = c.abbreviate(6);
       }
-      rp.sendMessage("(W) " + id.name() //
+      addMessage("(W) " + id.name() //
           + ": commit message lines >70 characters; manually wrap lines");
     }
   }
@@ -1757,7 +1875,7 @@
         if (ref != null) {
           rw.parseBody(c);
           closeChange(cmd, PatchSet.Id.fromRef(ref.getName()), c);
-          continue;
+          closeProgress.update(1);
         }
 
         rw.parseBody(c);
@@ -1771,15 +1889,30 @@
       }
 
       for (final ReplaceRequest req : toClose) {
-        final PatchSet.Id psi = doReplace(req);
+        final PatchSet.Id psi = doReplace(req, true);
         if (psi != null) {
           closeChange(req.cmd, psi, req.newCommit);
+          closeProgress.update(1);
         }
       }
+
+      // 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);
     }
   }
 
@@ -1795,7 +1928,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.
@@ -1843,12 +1977,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");
@@ -1864,7 +1993,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));
@@ -1884,18 +2013,33 @@
 
   private void sendMergedEmail(final ReplaceResult result) {
     if (result != null && result.mergedIntoRef != null) {
-      try {
-        final MergedSender cm = mergedSenderFactory.create(result.change);
-        cm.setFrom(currentUser.getAccountId());
-        cm.setPatchSet(result.patchSet, result.info);
-        cm.send();
-      } catch (EmailException e) {
-        final PatchSet.Id psi = result.patchSet.getId();
-        log.error("Cannot send email for submitted patch set " + psi, e);
-      }
+      workQueue.getDefaultQueue()
+          .submit(requestScopePropagator.wrap(new Runnable() {
+        @Override
+        public void run() {
+          try {
+            final MergedSender cm = mergedSenderFactory.create(result.change);
+            cm.setFrom(currentUser.getAccountId());
+            cm.setPatchSet(result.patchSet, result.info);
+            cm.send();
+          } catch (Exception e) {
+            final PatchSet.Id psi = result.patchSet.getId();
+            log.error("Cannot send email for submitted patch set " + psi, e);
+          }
+        }
 
-      hooks.doChangeMergedHook(result.change, currentUser.getAccount(),
-          result.patchSet);
+        @Override
+        public String toString() {
+          return "send-email merged";
+        }
+      }));
+
+      try {
+        hooks.doChangeMergedHook(result.change, currentUser.getAccount(),
+            result.patchSet, db);
+      } catch (OrmException err) {
+        log.error("Cannot open change: " + result.change.getChangeId(), err);
+      }
     }
   }
 
@@ -1917,12 +2061,13 @@
     return new RevId(src.getId().name());
   }
 
-  private static void reject(final ReceiveCommand cmd) {
+  private void reject(final ReceiveCommand cmd) {
     reject(cmd, "prohibited by Gerrit");
   }
 
-  private static void reject(final ReceiveCommand cmd, final String why) {
-    cmd.setResult(ReceiveCommand.Result.REJECTED_OTHER_REASON, why);
+  private void reject(final ReceiveCommand cmd, final String why) {
+    cmd.setResult(REJECTED_OTHER_REASON, why);
+    commandProgress.update(1);
   }
 
   private static boolean isHead(final Ref ref) {
@@ -1932,4 +2077,8 @@
   private static boolean isHead(final ReceiveCommand cmd) {
     return cmd.getRefName().startsWith(Constants.R_HEADS);
   }
+
+  private static boolean isConfig(final ReceiveCommand cmd) {
+    return cmd.getRefName().equals(GitRepositoryManager.REF_CONFIG);
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsAdvertiseRefsHook.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsAdvertiseRefsHook.java
new file mode 100644
index 0000000..e87fe2b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsAdvertiseRefsHook.java
@@ -0,0 +1,47 @@
+// Copyright (C) 2010 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git;
+
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.transport.AdvertiseRefsHook;
+import org.eclipse.jgit.transport.ReceivePack;
+import org.eclipse.jgit.transport.UploadPack;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/** Exposes only the non refs/changes/ reference names. */
+public class ReceiveCommitsAdvertiseRefsHook implements AdvertiseRefsHook {
+  @Override
+  public void advertiseRefs(UploadPack us) {
+    throw new UnsupportedOperationException(
+        "ReceiveCommitsAdvertiseRefsHook cannot be used for UploadPack");
+  }
+
+  @Override
+  public void advertiseRefs(ReceivePack rp) {
+    Map<String, Ref> oldRefs = rp.getAdvertisedRefs();
+    if (oldRefs == null) {
+      oldRefs = rp.getRepository().getAllRefs();
+    }
+    HashMap<String, Ref> r = new HashMap<String, Ref>();
+    for (Map.Entry<String, Ref> e : oldRefs.entrySet()) {
+      if (!e.getKey().startsWith("refs/changes/")) {
+        r.put(e.getKey(), e.getValue());
+      }
+    }
+    rp.setAdvertisedRefs(r, rp.getAdvertisedObjects());
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectCreatorGroups.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsExecutor.java
similarity index 74%
copy from gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectCreatorGroups.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsExecutor.java
index 7db2cd3..2049cea 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ProjectCreatorGroups.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsExecutor.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2010 The Android Open Source Project
+// Copyright (C) 2012 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -12,19 +12,19 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.config;
+package com.google.gerrit.server.git;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
 
 import com.google.inject.BindingAnnotation;
 
 import java.lang.annotation.Retention;
 
-import static java.lang.annotation.RetentionPolicy.RUNTIME;
-
 /**
- * Marker on a {@code Set&lt;AccountGroup.Id>} for the configured groups with
- * permission to create projects.
+ * Marker on the global {@link WorkQueue.Executor} used by
+ * {@link ReceiveCommits}.
  */
 @Retention(RUNTIME)
 @BindingAnnotation
-public @interface ProjectCreatorGroups {
-}
\ No newline at end of file
+public @interface ReceiveCommitsExecutor {
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsExecutorModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsExecutorModule.java
new file mode 100644
index 0000000..063db2d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsExecutorModule.java
@@ -0,0 +1,41 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git;
+
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.git.WorkQueue;
+import com.google.gerrit.server.git.WorkQueue.Executor;
+import com.google.inject.AbstractModule;
+import com.google.inject.Provides;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.lib.Config;
+
+/** Module providing the {@link ReceiveCommitsExecutor}. */
+public class ReceiveCommitsExecutorModule extends AbstractModule {
+  @Override
+  protected void configure() {
+  }
+
+  @Provides
+  @Singleton
+  @ReceiveCommitsExecutor
+  public Executor getReceiveCommitsExecutor(@GerritServerConfig Config config,
+      WorkQueue queues) {
+    int poolSize = config.getInt("receive", null, "threadPoolSize",
+        Runtime.getRuntime().availableProcessors());
+    return queues.createQueue(poolSize, "ReceiveCommits");
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsRefFilter.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsRefFilter.java
deleted file mode 100644
index 730305c..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsRefFilter.java
+++ /dev/null
@@ -1,41 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.git;
-
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.transport.RefFilter;
-
-import java.util.HashMap;
-import java.util.Map;
-
-/** Exposes only the non refs/changes/ reference names. */
-class ReceiveCommitsRefFilter implements RefFilter {
-  private final RefFilter base;
-
-  public ReceiveCommitsRefFilter(RefFilter base) {
-    this.base = base != null ? base : RefFilter.DEFAULT;
-  }
-
-  @Override
-  public Map<String, Ref> filter(Map<String, Ref> refs) {
-    HashMap<String, Ref> r = new HashMap<String, Ref>();
-    for (Map.Entry<String, Ref> e : refs.entrySet()) {
-      if (!e.getKey().startsWith("refs/changes/")) {
-        r.put(e.getKey(), e.getValue());
-      }
-    }
-    return base.filter(r);
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReloadSubmitQueueOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReloadSubmitQueueOp.java
index 356981d..bcc2107 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReloadSubmitQueueOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReloadSubmitQueueOp.java
@@ -14,11 +14,11 @@
 
 package com.google.gerrit.server.git;
 
-import com.google.gerrit.reviewdb.Branch;
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.client.SchemaFactory;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 
 import org.slf4j.Logger;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/RenameGroupOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/RenameGroupOp.java
new file mode 100644
index 0000000..f5e8fa8
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/RenameGroupOp.java
@@ -0,0 +1,153 @@
+// 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.common.data.GroupReference;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.Project.NameKey;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.concurrent.TimeUnit;
+
+public class RenameGroupOp extends DefaultQueueOp {
+  public interface Factory {
+    RenameGroupOp create(@Assisted("author") PersonIdent author,
+        @Assisted AccountGroup.UUID uuid, @Assisted("oldName") String oldName,
+        @Assisted("newName") String newName);
+  }
+
+  private static final int MAX_TRIES = 10;
+  private static final Logger log =
+      LoggerFactory.getLogger(RenameGroupOp.class);
+
+  private final ProjectCache projectCache;
+  private final MetaDataUpdate.Server metaDataUpdateFactory;
+
+  private final PersonIdent author;
+  private final AccountGroup.UUID uuid;
+  private final String oldName;
+  private final String newName;
+  private final List<Project.NameKey> retryOn;
+
+  private boolean tryingAgain;
+
+  @Inject
+  public RenameGroupOp(WorkQueue workQueue, ProjectCache projectCache,
+      MetaDataUpdate.Server metaDataUpdateFactory,
+
+      @Assisted("author") PersonIdent author, @Assisted AccountGroup.UUID uuid,
+      @Assisted("oldName") String oldName, @Assisted("newName") String newName) {
+    super(workQueue);
+    this.projectCache = projectCache;
+    this.metaDataUpdateFactory = metaDataUpdateFactory;
+
+    this.author = author;
+    this.uuid = uuid;
+    this.oldName = oldName;
+    this.newName = newName;
+    this.retryOn = new ArrayList<Project.NameKey>();
+  }
+
+  @Override
+  public void run() {
+    Iterable<NameKey> names = tryingAgain ? retryOn : projectCache.all();
+    for (Project.NameKey projectName : names) {
+      ProjectConfig config = projectCache.get(projectName).getConfig();
+      GroupReference ref = config.getGroup(uuid);
+      if (ref == null || newName.equals(ref.getName())) {
+        continue;
+      }
+
+      try {
+        MetaDataUpdate md = metaDataUpdateFactory.create(projectName);
+        try {
+          rename(md);
+        } finally {
+          md.close();
+        }
+      } catch (RepositoryNotFoundException noProject) {
+        continue;
+      } catch (ConfigInvalidException err) {
+        log.error("Cannot rename group " + oldName + " in " + projectName, err);
+      } catch (IOException err) {
+        log.error("Cannot rename group " + oldName + " in " + projectName, err);
+      }
+    }
+
+    // If one or more projects did not update, wait 5 minutes
+    // and give it another attempt.
+    if (!retryOn.isEmpty() && !tryingAgain) {
+      tryingAgain = true;
+      start(5, TimeUnit.MINUTES);
+    }
+  }
+
+  private void rename(MetaDataUpdate md) throws IOException,
+      ConfigInvalidException {
+    boolean success = false;
+    for (int attempts = 0; !success && attempts < MAX_TRIES; attempts++) {
+      ProjectConfig config = ProjectConfig.read(md);
+
+      // The group isn't referenced, or its name has been fixed already.
+      //
+      GroupReference ref = config.getGroup(uuid);
+      if (ref == null || newName.equals(ref.getName())) {
+        projectCache.evict(config.getProject());
+        return;
+      }
+
+      ref.setName(newName);
+      md.getCommitBuilder().setAuthor(author);
+      md.setMessage("Rename group " + oldName + " to " + newName + "\n");
+      if (config.commit(md)) {
+        projectCache.evict(config.getProject());
+        success = true;
+
+      } else {
+        try {
+          Thread.sleep(25 /* milliseconds */);
+        } catch (InterruptedException wakeUp) {
+          continue;
+        }
+      }
+    }
+
+    if (!success) {
+      if (tryingAgain) {
+        log.warn("Could not rename group " + oldName + " to " + newName
+            + " in " + md.getProjectName().get());
+      } else {
+        retryOn.add(md.getProjectName());
+      }
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "Rename Group " + oldName;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplicationQueue.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplicationQueue.java
index d1c518d..f7bb9f8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplicationQueue.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReplicationQueue.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.git;
 
-import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.reviewdb.client.Project;
 
 /** Manages replication to other nodes. */
 public interface ReplicationQueue {
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..64ba6e8
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/RepositoryCaseMismatchException.java
@@ -0,0 +1,41 @@
+// Copyright (C) 2011 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git;
+
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.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;
+
+  /**
+   * @param projectName name of the project that cannot be created
+   */
+  public RepositoryCaseMismatchException(final Project.NameKey projectName) {
+    super("Name occupied in other case. Project " + projectName.get()
+        + " cannot be created.");
+  }
+}
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 d54dcec..7b669b2 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
@@ -14,10 +14,10 @@
 
 package com.google.gerrit.server.git;
 
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.ApprovalCategory;
-import com.google.gerrit.reviewdb.Branch;
-import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.ApprovalCategory;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
 
 import java.text.DateFormat;
 import java.text.SimpleDateFormat;
@@ -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) {
@@ -51,8 +53,7 @@
 
   void appendApproval(ApprovalCategory category,
       short value, Account user) {
-    // TODO: use category.getLabel() when available
-    sb.append(category.getName().replace(' ', '-'));
+    sb.append(category.getLabelName());
     sb.append(value < 0 ? "-" : "+").append(Math.abs(value)).append(": ");
     appendUserData(user);
     sb.append("\n");
@@ -77,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/schema/Schema_49.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleException.java
similarity index 63%
copy from gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_49.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleException.java
index 0977ee9..d7e8446 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_49.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleException.java
@@ -12,15 +12,17 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.schema;
+package com.google.gerrit.server.git;
 
-import com.google.inject.Inject;
-import com.google.inject.Provider;
+/** Indicates the gitlink's update cannot be processed at this time. */
+class SubmoduleException extends Exception {
+  private static final long serialVersionUID = 1L;
 
-public class Schema_49 extends SchemaVersion {
+  SubmoduleException(final String msg) {
+    super(msg, null);
+  }
 
-  @Inject
-  Schema_49(Provider<Schema_48> prior) {
-    super(prior);
+  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..706ba7d
--- /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.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.SubmoduleSubscription;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.util.SubmoduleSectionParser;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.assistedinject.Assisted;
+
+import org.eclipse.jgit.dircache.DirCache;
+import org.eclipse.jgit.dircache.DirCacheBuilder;
+import org.eclipse.jgit.dircache.DirCacheEditor;
+import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit;
+import org.eclipse.jgit.dircache.DirCacheEntry;
+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/TagCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/TagCache.java
index 053fda8..ac4882f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/TagCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/TagCache.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.git;
 
-import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.cache.Cache;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.inject.Inject;
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 5adfbd1..8830580 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
@@ -17,8 +17,8 @@
 import static org.eclipse.jgit.lib.ObjectIdSerialization.readNotNull;
 import static org.eclipse.jgit.lib.ObjectIdSerialization.writeNotNull;
 
-import com.google.gerrit.reviewdb.PatchSet;
-import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
 
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.lib.AnyObjectId;
@@ -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);
     }
@@ -348,6 +353,8 @@
   }
 
   private static final class CachedRef extends AtomicReference<ObjectId> {
+    private static final long serialVersionUID = 1L;
+
     final int flag;
 
     CachedRef(Ref ref, int flag) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/TagSetHolder.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/TagSetHolder.java
index c6667c5..91c8a5c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/TagSetHolder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/TagSetHolder.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.git;
 
-import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.reviewdb.client.Project;
 
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
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/ValidationError.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ValidationError.java
new file mode 100644
index 0000000..e1ab41d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ValidationError.java
@@ -0,0 +1,41 @@
+// 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 a problem with Git based data. */
+public class ValidationError {
+  private final String message;
+
+  public ValidationError(String file, String message) {
+    this(file + ": " + message);
+  }
+
+  public ValidationError(String file, int line, String message) {
+    this(file + ":" + line + ": " + message);
+  }
+
+  public ValidationError(String message) {
+    this.message = message;
+  }
+
+  public String getMessage() {
+    return message;
+  }
+
+  @Override
+  public String toString() {
+    return "ValidationError[" + message + "]";
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java
new file mode 100644
index 0000000..c34cc54
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java
@@ -0,0 +1,331 @@
+// Copyright (C) 2010 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git;
+
+import org.eclipse.jgit.dircache.DirCache;
+import org.eclipse.jgit.dircache.DirCacheBuilder;
+import org.eclipse.jgit.dircache.DirCacheEditor;
+import org.eclipse.jgit.dircache.DirCacheEditor.DeletePath;
+import org.eclipse.jgit.dircache.DirCacheEditor.PathEdit;
+import org.eclipse.jgit.dircache.DirCacheEntry;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.errors.UnmergedPathException;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.Config;
+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.ObjectLoader;
+import org.eclipse.jgit.lib.ObjectReader;
+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.RevTree;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.eclipse.jgit.util.RawParseUtils;
+
+import java.io.IOException;
+
+/**
+ * Support for metadata stored within a version controlled branch.
+ * <p>
+ * Implementors are responsible for supplying implementations of the onLoad and
+ * onSave methods to read from the repository, or format an update that can
+ * later be written back to the repository.
+ */
+public abstract class VersionedMetaData {
+  private RevCommit revision;
+  private ObjectReader reader;
+  private ObjectInserter inserter;
+  private DirCache newTree;
+
+  /** @return name of the reference storing this configuration. */
+  protected abstract String getRefName();
+
+  protected abstract void onLoad() throws IOException, ConfigInvalidException;
+
+  protected abstract void onSave(CommitBuilder commit) throws IOException,
+      ConfigInvalidException;
+
+  /** @return revision of the metadata that was loaded. */
+  public ObjectId getRevision() {
+    return revision != null ? revision.copy() : null;
+  }
+
+  /** Initialize in-memory as though the repository branch doesn't exist. */
+  public void createInMemory() {
+    try {
+      revision = null;
+      onLoad();
+    } catch (IOException err) {
+      throw new RuntimeException("Unexpected IOException", err);
+    } catch (ConfigInvalidException err) {
+      throw new RuntimeException("Unexpected ConfigInvalidException", err);
+    }
+  }
+
+  /**
+   * Load the current version from the branch.
+   * <p>
+   * The repository is not held after the call completes, allowing the
+   * application to retain this object for long periods of time.
+   *
+   * @param db repository to access.
+   * @throws IOException
+   * @throws ConfigInvalidException
+   */
+  public void load(Repository db) throws IOException, ConfigInvalidException {
+    Ref ref = db.getRef(getRefName());
+    load(db, ref != null ? ref.getObjectId() : null);
+  }
+
+  /**
+   * Load a specific version from the repository.
+   * <p>
+   * This method is primarily useful for applying updates to a specific revision
+   * that was shown to an end-user in the user interface. If there are conflicts
+   * with another user's concurrent changes, these will be automatically
+   * detected at commit time.
+   * <p>
+   * The repository is not held after the call completes, allowing the
+   * application to retain this object for long periods of time.
+   *
+   * @param db repository to access.
+   * @param id revision to load.
+   * @throws IOException
+   * @throws ConfigInvalidException
+   */
+  public void load(Repository db, ObjectId id) throws IOException,
+      ConfigInvalidException {
+    if (id != null) {
+      reader = db.newObjectReader();
+      try {
+        revision = new RevWalk(reader).parseCommit(id);
+        onLoad();
+      } finally {
+        reader.release();
+        reader = null;
+      }
+    } else {
+      // The branch does not yet exist.
+      revision = null;
+      onLoad();
+    }
+  }
+
+  public void load(MetaDataUpdate update) throws IOException,
+      ConfigInvalidException {
+    load(update.getRepository());
+  }
+
+  public void load(MetaDataUpdate update, ObjectId id) throws IOException,
+      ConfigInvalidException {
+    load(update.getRepository(), id);
+  }
+
+  /**
+   * Update this metadata branch, recording a new commit on its reference.
+   *
+   * @param update helper information to define the update that will occur.
+   * @return true if the update was successful, false if it failed because of a
+   *         concurrent update to the same reference.
+   * @throws IOException if there is a storage problem and the update cannot be
+   *         executed as requested.
+   */
+  public boolean commit(MetaDataUpdate update) throws IOException {
+    final Repository db = update.getRepository();
+    final CommitBuilder commit = update.getCommitBuilder();
+
+    reader = db.newObjectReader();
+    inserter = db.newObjectInserter();
+    try {
+      final RevWalk rw = new RevWalk(reader);
+      final RevTree src = revision != null ? rw.parseTree(revision) : null;
+      final ObjectId res = writeTree(src, commit);
+
+      if (res.equals(src)) {
+        // If there are no changes to the content, don't create the commit.
+        return true;
+      }
+
+      commit.setTreeId(res);
+      if (revision != null) {
+        commit.setParentId(revision);
+      }
+
+      RefUpdate ru = db.updateRef(getRefName());
+      if (revision != null) {
+        ru.setExpectedOldObjectId(revision);
+      } else {
+        ru.setExpectedOldObjectId(ObjectId.zeroId());
+      }
+      ru.setNewObjectId(inserter.insert(commit));
+      ru.disableRefLog();
+      inserter.flush();
+
+      switch (ru.update(rw)) {
+        case NEW:
+        case FAST_FORWARD:
+          revision = rw.parseCommit(ru.getNewObjectId());
+          update.replicate(ru.getName());
+          return true;
+
+        case LOCK_FAILURE:
+          return false;
+
+        default:
+          throw new IOException("Cannot update " + ru.getName() + " in "
+              + db.getDirectory() + ": " + ru.getResult());
+      }
+    } catch (ConfigInvalidException e) {
+      throw new IOException("Cannot update " + getRefName() + " in "
+          + db.getDirectory() + ": " + e.getMessage(), e);
+    } finally {
+      inserter.release();
+      inserter = null;
+
+      reader.release();
+      reader = null;
+    }
+  }
+
+  private ObjectId writeTree(RevTree srcTree, CommitBuilder commit)
+      throws IOException, MissingObjectException, IncorrectObjectTypeException,
+      UnmergedPathException, ConfigInvalidException {
+    try {
+      newTree = readTree(srcTree);
+      onSave(commit);
+      return newTree.writeTree(inserter);
+    } finally {
+      newTree = null;
+    }
+  }
+
+  private DirCache readTree(RevTree tree) throws IOException,
+      MissingObjectException, IncorrectObjectTypeException {
+    DirCache dc = DirCache.newInCore();
+    if (tree != null) {
+      DirCacheBuilder b = dc.builder();
+      b.addTree(new byte[0], DirCacheEntry.STAGE_0, reader, tree);
+      b.finish();
+    }
+    return dc;
+  }
+
+  protected Config readConfig(String fileName) throws IOException,
+      ConfigInvalidException {
+    Config rc = new Config();
+    String text = readUTF8(fileName);
+    if (!text.isEmpty()) {
+      try {
+        rc.fromText(text);
+      } catch (ConfigInvalidException err) {
+        throw new ConfigInvalidException("Invalid config file " + fileName
+            + " in commit" + revision.name(), err);
+      }
+    }
+    return rc;
+  }
+
+  protected String readUTF8(String fileName) throws IOException {
+    byte[] raw = readFile(fileName);
+    return raw.length != 0 ? RawParseUtils.decode(raw) : "";
+  }
+
+  protected byte[] readFile(String fileName) throws IOException {
+    if (revision == null) {
+      return new byte[] {};
+    }
+
+    TreeWalk tw = TreeWalk.forPath(reader, fileName, revision.getTree());
+    if (tw != null) {
+      ObjectLoader obj = reader.open(tw.getObjectId(0), Constants.OBJ_BLOB);
+      return obj.getCachedBytes(Integer.MAX_VALUE);
+
+    } else {
+      return new byte[] {};
+    }
+  }
+
+  protected ObjectId getObjectId(String fileName) throws IOException {
+    if (revision == null) {
+      return null;
+    }
+
+    TreeWalk tw = TreeWalk.forPath(reader, fileName, revision.getTree());
+    if (tw != null) {
+     return tw.getObjectId(0);
+    }
+
+    return null;
+  }
+
+  protected static void set(Config rc, String section, String subsection,
+      String name, String value) {
+    if (value != null) {
+      rc.setString(section, subsection, name, value);
+    } else {
+      rc.unset(section, subsection, name);
+    }
+  }
+
+  protected static void set(Config rc, String section, String subsection,
+      String name, boolean value) {
+    if (value) {
+      rc.setBoolean(section, subsection, name, value);
+    } else {
+      rc.unset(section, subsection, name);
+    }
+  }
+
+  protected static <E extends Enum<?>> void set(Config rc, String section,
+      String subsection, String name, E value, E defaultValue) {
+    if (value != defaultValue) {
+      rc.setEnum(section, subsection, name, value);
+    } else {
+      rc.unset(section, subsection, name);
+    }
+  }
+
+  protected void saveConfig(String fileName, Config cfg) throws IOException {
+    saveUTF8(fileName, cfg.toText());
+  }
+
+  protected void saveUTF8(String fileName, String text) throws IOException {
+    saveFile(fileName, text != null ? Constants.encode(text) : null);
+  }
+
+  protected void saveFile(String fileName, byte[] raw) throws IOException {
+    DirCacheEditor editor = newTree.editor();
+    if (raw != null && 0 < raw.length) {
+      final ObjectId blobId = inserter.insert(Constants.OBJ_BLOB, raw);
+      editor.add(new PathEdit(fileName) {
+        @Override
+        public void apply(DirCacheEntry ent) {
+          ent.setFileMode(FileMode.REGULAR_FILE);
+          ent.setObjectId(blobId);
+        }
+      });
+    } else {
+      editor.add(new DeletePath(fileName));
+    }
+    editor.finish();
+  }
+}
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..fc47f10 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/VisibleRefFilter.java
@@ -14,17 +14,18 @@
 
 package com.google.gerrit.server.git;
 
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.PatchSet;
-import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.project.ProjectControl;
-import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.server.OrmException;
 
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.transport.RefFilter;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.AbstractAdvertiseRefsHook;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -36,7 +37,7 @@
 import java.util.Map;
 import java.util.Set;
 
-public class VisibleRefFilter implements RefFilter {
+public class VisibleRefFilter extends AbstractAdvertiseRefsHook {
   private static final Logger log =
       LoggerFactory.getLogger(VisibleRefFilter.class);
 
@@ -58,8 +59,7 @@
     this.showChanges = showChanges;
   }
 
-  @Override
-  public Map<String, Ref> filter(Map<String, Ref> refs) {
+  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 +91,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);
@@ -103,6 +104,16 @@
     return result;
   }
 
+  @Override
+  protected Map<String, Ref> getAdvertisedRefs(
+      Repository repository, RevWalk revWalk) {
+    return filter(repository.getAllRefs());
+  }
+
+  private Map<String, Ref> filter(Map<String, Ref> refs) {
+    return filter(refs, false);
+  }
+
   private Set<Change.Id> visibleChanges() {
     if (!showChanges) {
       return Collections.emptySet();
@@ -112,7 +123,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..987ab7c 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,7 +15,8 @@
 package com.google.gerrit.server.git;
 
 import com.google.gerrit.lifecycle.LifecycleListener;
-import com.google.gerrit.reviewdb.Project.NameKey;
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.reviewdb.client.Project.NameKey;
 import com.google.gerrit.server.util.IdGenerator;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -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/ioutil/BasicSerialization.java b/gerrit-server/src/main/java/com/google/gerrit/server/ioutil/BasicSerialization.java
index 2971906..4210363 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ioutil/BasicSerialization.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ioutil/BasicSerialization.java
@@ -29,7 +29,7 @@
 
 package com.google.gerrit.server.ioutil;
 
-import com.google.gerrit.reviewdb.CodedEnum;
+import com.google.gerrit.reviewdb.client.CodedEnum;
 
 import org.eclipse.jgit.util.IO;
 
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 e679849..7fabcfe1 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
@@ -14,19 +14,22 @@
 
 package com.google.gerrit.server.mail;
 
-import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
 /** Send notice about a change being abandoned by its owner. */
 public class AbandonedSender extends ReplyToChangeSender {
-  public static interface Factory {
+  public static interface Factory extends
+      ReplyToChangeSender.Factory<AbandonedSender> {
     AbandonedSender create(Change change);
   }
 
   @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..f7ace27 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
@@ -14,7 +14,8 @@
 
 package com.google.gerrit.server.mail;
 
-import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.reviewdb.client.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 f5a7870..ff0bb578 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ChangeEmail.java
@@ -14,17 +14,17 @@
 
 package com.google.gerrit.server.mail;
 
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.AccountProjectWatch;
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.ChangeMessage;
-import com.google.gerrit.reviewdb.Patch;
-import com.google.gerrit.reviewdb.PatchSet;
-import com.google.gerrit.reviewdb.PatchSetApproval;
-import com.google.gerrit.reviewdb.PatchSetInfo;
-import com.google.gerrit.reviewdb.StarredChange;
-import com.google.gerrit.reviewdb.AccountProjectWatch.NotifyType;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.PatchSetInfo;
+import com.google.gerrit.reviewdb.client.StarredChange;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.patch.PatchList;
 import com.google.gerrit.server.patch.PatchListEntry;
@@ -34,7 +34,7 @@
 import com.google.gerrit.server.query.QueryParseException;
 import com.google.gerrit.server.query.change.ChangeData;
 import com.google.gerrit.server.query.change.ChangeQueryBuilder;
-import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.server.OrmException;
 
 import java.text.MessageFormat;
 import java.util.ArrayList;
@@ -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;
@@ -69,13 +70,7 @@
 
     /** Is the from user in an email squelching group? */
     final IdentifiedUser user =  args.identifiedUserFactory.create(id);
-    final Set<AccountGroup.Id> gids = user.getEffectiveGroups();
-    for (final AccountGroup.Id gid : gids) {
-      if (args.groupCache.get(gid).isEmailOnlyAuthors()) {
-        emailOnlyAuthors = true;
-        break;
-      }
-    }
+    emailOnlyAuthors = !user.getCapabilities().canEmailReviewers();
   }
 
   public void setPatchSet(final PatchSet ps) {
@@ -135,7 +130,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;
       }
@@ -273,11 +268,11 @@
   }
 
   /** Get the groups which own the project. */
-  protected Set<AccountGroup.Id> getProjectOwners() {
+  protected Set<AccountGroup.UUID> getProjectOwners() {
     final ProjectState r;
 
     r = args.projectCache.get(change.getProject());
-    return r != null ? r.getOwners() : Collections.<AccountGroup.Id> emptySet();
+    return r != null ? r.getOwners() : Collections.<AccountGroup.UUID> emptySet();
   }
 
   /** TO or CC all vested parties (change owner, patch set uploader, author). */
@@ -294,7 +289,7 @@
       //
       for (StarredChange w : args.db.get().starredChanges().byChange(
           change.getId())) {
-        add(RecipientType.BCC, w.getAccountId());
+        super.add(RecipientType.BCC, w.getAccountId());
       }
     } catch (OrmException err) {
       // Just don't BCC everyone. Better to send a partial message to those
@@ -336,7 +331,7 @@
     }
 
     for (AccountProjectWatch w : args.db.get().accountProjectWatches()
-        .byProject(args.wildProject)) {
+        .byProject(args.allProjectsName)) {
       if (!projectWatchers.contains(w.getAccountId())) {
         add(matching, w);
       }
@@ -399,11 +394,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 +410,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..f054ee8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java
@@ -14,9 +14,10 @@
 
 package com.google.gerrit.server.mail;
 
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.Patch;
-import com.google.gerrit.reviewdb.PatchLineComment;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.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 c14ff1b..c6f716d 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
@@ -14,14 +14,16 @@
 
 package com.google.gerrit.server.mail;
 
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.AccountGroupMember;
-import com.google.gerrit.reviewdb.AccountProjectWatch;
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.AccountProjectWatch.NotifyType;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupMember;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
+import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.ssh.SshInfo;
-import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -34,10 +36,14 @@
     public CreateChangeSender create(Change change);
   }
 
+  private final GroupCache groupCache;
+
   @Inject
-  public CreateChangeSender(EmailArguments ea, SshInfo sshInfo,
-      @Assisted Change c) {
-    super(ea, sshInfo, c);
+  public CreateChangeSender(EmailArguments ea,
+      @AnonymousCowardName String anonymousCowardName, SshInfo sshInfo,
+      GroupCache groupCache, @Assisted Change c) {
+    super(ea, anonymousCowardName, sshInfo, c);
+    this.groupCache = groupCache;
   }
 
   @Override
@@ -52,10 +58,13 @@
       // Try to mark interested owners with a TO and not a BCC line.
       //
       final Set<Account.Id> owners = new HashSet<Account.Id>();
-      for (AccountGroup.Id g : getProjectOwners()) {
-        for (AccountGroupMember m : args.db.get().accountGroupMembers()
-            .byGroup(g)) {
-          owners.add(m.getAccountId());
+      for (AccountGroup.UUID uuid : getProjectOwners()) {
+        AccountGroup group = groupCache.get(uuid);
+        if (group != null) {
+          for (AccountGroupMember m : args.db.get().accountGroupMembers()
+              .byGroup(group.getId())) {
+            owners.add(m.getAccountId());
+          }
         }
       }
 
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 4a99eb4..68f78d0 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
@@ -14,15 +14,13 @@
 
 package com.google.gerrit.server.mail;
 
-import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.IdentifiedUser.GenericFactory;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.CanonicalWebUrl;
-import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.config.WildProjectName;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
@@ -32,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 {
@@ -45,12 +45,12 @@
   final PatchSetInfoFactory patchSetInfoFactory;
   final IdentifiedUser.GenericFactory identifiedUserFactory;
   final Provider<String> urlProvider;
-  final Project.NameKey wildProject;
+  final AllProjectsName allProjectsName;
 
   final ChangeQueryBuilder.Factory queryBuilder;
   final Provider<ChangeQueryRewriter> queryRewriter;
   final Provider<ReviewDb> db;
-  final SitePaths site;
+  final RuntimeInstance velocityRuntime;
 
   @Inject
   EmailArguments(GitRepositoryManager server, ProjectCache projectCache,
@@ -59,10 +59,10 @@
       EmailSender emailSender, PatchSetInfoFactory patchSetInfoFactory,
       GenericFactory identifiedUserFactory,
       @CanonicalWebUrl @Nullable Provider<String> urlProvider,
-      @WildProjectName Project.NameKey wildProject,
+      AllProjectsName allProjectsName,
       ChangeQueryBuilder.Factory queryBuilder,
       Provider<ChangeQueryRewriter> queryRewriter, Provider<ReviewDb> db,
-      SitePaths site) {
+      RuntimeInstance velocityRuntime) {
     this.server = server;
     this.projectCache = projectCache;
     this.groupCache = groupCache;
@@ -73,10 +73,10 @@
     this.patchSetInfoFactory = patchSetInfoFactory;
     this.identifiedUserFactory = identifiedUserFactory;
     this.urlProvider = urlProvider;
-    this.wildProject = wildProject;
+    this.allProjectsName = allProjectsName;
     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..ce8c8f9 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
@@ -19,29 +19,34 @@
 import java.io.Writer;
 import java.text.SimpleDateFormat;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.Iterator;
 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..4307854
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailTokenVerifier.java
@@ -0,0 +1,79 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.reviewdb.client.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/FromAddressGenerator.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/FromAddressGenerator.java
index aa948f1..dec3d2c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/FromAddressGenerator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/FromAddressGenerator.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.mail;
 
-import com.google.gerrit.reviewdb.Account;
+import com.google.gerrit.reviewdb.client.Account;
 
 /** Constructs an address to send email from. */
 public interface FromAddressGenerator {
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 afcfccd..bb50374 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/FromAddressGeneratorProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/FromAddressGeneratorProvider.java
@@ -14,10 +14,11 @@
 
 package com.google.gerrit.server.mail;
 
-import com.google.gerrit.common.data.ParamertizedString;
-import com.google.gerrit.reviewdb.Account;
+import com.google.gerrit.common.data.ParameterizedString;
+import com.google.gerrit.reviewdb.client.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) {
 
@@ -41,8 +43,10 @@
     final Address srvAddr = toAddress(myIdent);
 
     if (from == null || "MIXED".equalsIgnoreCase(from)) {
-      ParamertizedString name = new ParamertizedString("${user} (Code Review)");
-      generator = new PatternGen(srvAddr, accountCache, name, srvAddr.email);
+      ParameterizedString name = new ParameterizedString("${user} (Code Review)");
+      generator =
+          new PatternGen(srvAddr, accountCache, anonymousCowardName, name,
+              srvAddr.email);
 
     } else if ("USER".equalsIgnoreCase(from)) {
       generator = new UserGen(accountCache, srvAddr);
@@ -52,11 +56,13 @@
 
     } else {
       final Address a = Address.parse(from);
-      final ParamertizedString name = a.name != null ? new ParamertizedString(a.name) : null;
+      final ParameterizedString name = a.name != null ? new ParameterizedString(a.name) : null;
       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 ParamertizedString namePattern;
+    private final String anonymousCowardName;
+    private final ParameterizedString namePattern;
 
     PatternGen(final Address serverAddress, final AccountCache accountCache,
-        final ParamertizedString namePattern, final String senderEmail) {
+        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..b695fb4 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
@@ -14,7 +14,8 @@
 
 package com.google.gerrit.server.mail;
 
-import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.reviewdb.client.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..3590b8a 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
@@ -16,14 +16,15 @@
 
 import com.google.gerrit.common.data.ApprovalType;
 import com.google.gerrit.common.data.ApprovalTypes;
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.AccountProjectWatch;
-import com.google.gerrit.reviewdb.AccountProjectWatch.NotifyType;
-import com.google.gerrit.reviewdb.ApprovalCategory;
-import com.google.gerrit.reviewdb.ApprovalCategoryValue;
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.PatchSetApproval;
-import com.google.gwtorm.client.OrmException;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch;
+import com.google.gerrit.reviewdb.client.ApprovalCategory;
+import com.google.gerrit.reviewdb.client.ApprovalCategoryValue;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
+import com.google.gerrit.server.config.AnonymousCowardName;
+import com.google.gwtorm.server.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..2e459d4 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
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.server.mail;
 
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.ssh.SshInfo;
 
 import com.jcraft.jsch.HostKey;
@@ -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..de8628f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/OutgoingEmail.java
@@ -14,18 +14,23 @@
 
 package com.google.gerrit.server.mail;
 
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.UserIdentity;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.UserIdentity;
 import com.google.gerrit.server.account.AccountState;
 import com.google.gerrit.server.mail.EmailHeader.AddressList;
+import com.google.gwtorm.server.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/RebasedPatchSetSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RebasedPatchSetSender.java
new file mode 100644
index 0000000..8fc8238
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RebasedPatchSetSender.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.mail;
+
+import com.google.gerrit.reviewdb.client.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;
+
+/** Send notice to reviewers that a change has been rebased. */
+public class RebasedPatchSetSender extends ReplacePatchSetSender {
+  public static interface Factory {
+    RebasedPatchSetSender create(Change change);
+  }
+
+  @Inject
+  public RebasedPatchSetSender(EmailArguments ea,
+      @AnonymousCowardName String anonymousCowardName, SshInfo si,
+      @Assisted Change c) {
+    super(ea, anonymousCowardName, si, c);
+  }
+
+  @Override
+  protected void formatChange() throws EmailException {
+    appendText(velocifyFile("RebasedPatchSet.vm"));
+  }
+}
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..9705b8a 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
@@ -14,8 +14,9 @@
 
 package com.google.gerrit.server.mail;
 
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.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;
   }
 
@@ -64,6 +67,7 @@
     add(RecipientType.TO, reviewers);
     add(RecipientType.CC, extraCC);
     rcptToAuthors(RecipientType.CC);
+    bccStarredBy();
   }
 
   @Override
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 4c3ed76..2a77d39 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
@@ -14,12 +14,17 @@
 
 package com.google.gerrit.server.mail;
 
-import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.reviewdb.client.Change;
 
 /** Alert a user to a reply to a change, usually commentary made during review. */
 public abstract class ReplyToChangeSender extends ChangeEmail {
-  protected ReplyToChangeSender(EmailArguments ea, Change c, String mc) {
-    super(ea, c, mc);
+  public static interface Factory<T extends ReplyToChangeSender> {
+    public T create(Change change);
+  }
+
+  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
new file mode 100644
index 0000000..c9afdde
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RestoredSender.java
@@ -0,0 +1,48 @@
+// 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.reviewdb.client.Change;
+import com.google.gerrit.server.config.AnonymousCowardName;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+/** Send notice about a change being restored by its owner. */
+public class RestoredSender extends ReplyToChangeSender {
+  public static interface Factory extends
+      ReplyToChangeSender.Factory<RestoredSender> {
+    RestoredSender create(Change change);
+  }
+
+  @Inject
+  public RestoredSender(EmailArguments ea,
+      @AnonymousCowardName String anonymousCowardName, @Assisted Change c) {
+    super(ea, anonymousCowardName, c, "restore");
+  }
+
+  @Override
+  protected void init() throws EmailException {
+    super.init();
+
+    ccAllApprovals();
+    bccStarredBy();
+    bccWatchesNotifyAllComments();
+  }
+
+  @Override
+  protected void formatChange() throws EmailException {
+    appendText(velocifyFile("Restored.vm"));
+  }
+}
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..964bfed 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
@@ -14,7 +14,8 @@
 
 package com.google.gerrit.server.mail;
 
-import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.reviewdb.client.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..4d6eb8e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/SignedTokenEmailTokenVerifier.java
@@ -0,0 +1,93 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.mail;
+
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.config.AuthConfig;
+import com.google.gwtjsonrpc.server.SignedToken;
+import com.google.gwtjsonrpc.server.ValidToken;
+import com.google.gwtjsonrpc.server.XsrfException;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.util.Base64;
+
+import java.io.UnsupportedEncodingException;
+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
new file mode 100644
index 0000000..df93852
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/AddReviewer.java
@@ -0,0 +1,256 @@
+// 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.patch;
+
+import com.google.gerrit.common.data.ApprovalType;
+import com.google.gerrit.common.data.ApprovalTypes;
+import com.google.gerrit.common.data.ReviewerResult;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.ApprovalCategory;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountResolver;
+import com.google.gerrit.server.account.GroupCache;
+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;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import org.eclipse.jgit.lib.Config;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Callable;
+
+public class AddReviewer implements Callable<ReviewerResult> {
+  public final static int DEFAULT_MAX_REVIEWERS_WITHOUT_CHECK = 10;
+  public final static int DEFAULT_MAX_REVIEWERS = 20;
+
+  public interface Factory {
+    AddReviewer create(Change.Id changeId,
+        Collection<String> userNameOrEmailOrGroupNames, boolean confirmed);
+  }
+
+  private final AddReviewerSender.Factory addReviewerSenderFactory;
+  private final AccountResolver accountResolver;
+  private final GroupCache groupCache;
+  private final GroupMembers.Factory groupMembersFactory;
+  private final ChangeControl.Factory changeControlFactory;
+  private final ReviewDb db;
+  private final IdentifiedUser currentUser;
+  private final IdentifiedUser.GenericFactory identifiedUserFactory;
+  private final ApprovalCategory.Id addReviewerCategoryId;
+  private final Config cfg;
+
+  private final Change.Id changeId;
+  private final Collection<String> reviewers;
+  private final boolean confirmed;
+
+  @Inject
+  AddReviewer(final AddReviewerSender.Factory addReviewerSenderFactory,
+      final AccountResolver accountResolver, final GroupCache groupCache,
+      final GroupMembers.Factory groupMembersFactory,
+      final ChangeControl.Factory changeControlFactory, final ReviewDb db,
+      final IdentifiedUser.GenericFactory identifiedUserFactory,
+      final IdentifiedUser currentUser, final ApprovalTypes approvalTypes,
+      final @GerritServerConfig Config cfg, @Assisted final Change.Id changeId,
+      @Assisted final Collection<String> reviewers,
+      @Assisted final boolean confirmed) {
+    this.addReviewerSenderFactory = addReviewerSenderFactory;
+    this.accountResolver = accountResolver;
+    this.groupCache = groupCache;
+    this.groupMembersFactory = groupMembersFactory;
+    this.db = db;
+    this.changeControlFactory = changeControlFactory;
+    this.identifiedUserFactory = identifiedUserFactory;
+    this.currentUser = currentUser;
+    this.cfg = cfg;
+
+    final List<ApprovalType> allTypes = approvalTypes.getApprovalTypes();
+    addReviewerCategoryId =
+        allTypes.get(allTypes.size() - 1).getCategory().getId();
+
+    this.changeId = changeId;
+    this.reviewers = reviewers;
+    this.confirmed = confirmed;
+  }
+
+  @Override
+  public ReviewerResult call() throws Exception {
+    final Set<Account.Id> reviewerIds = new HashSet<Account.Id>();
+    final ChangeControl control = changeControlFactory.validateFor(changeId);
+
+    final ReviewerResult result = new ReviewerResult();
+    for (final String reviewer : reviewers) {
+      final Account account = accountResolver.find(reviewer);
+      if (account == null) {
+        AccountGroup group = groupCache.get(new AccountGroup.NameKey(reviewer));
+
+        if (group == null) {
+          result.addError(new ReviewerResult.Error(
+              ReviewerResult.Error.Type.REVIEWER_NOT_FOUND, reviewer));
+          continue;
+        }
+
+        if (!isLegalReviewerGroup(group.getGroupUUID())) {
+          result.addError(new ReviewerResult.Error(
+              ReviewerResult.Error.Type.GROUP_NOT_ALLOWED, reviewer));
+          continue;
+        }
+
+        final Set<Account> members =
+            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));
+          continue;
+        }
+
+        // if maxAllowed is set to 0, it is allowed to add any number of
+        // reviewers
+        final int maxAllowed =
+            cfg.getInt("addreviewer", "maxAllowed", DEFAULT_MAX_REVIEWERS);
+        if (maxAllowed > 0 && members.size() > maxAllowed) {
+          result.setMemberCount(members.size());
+          result.setAskForConfirmation(false);
+          result.addError(new ReviewerResult.Error(
+              ReviewerResult.Error.Type.GROUP_HAS_TOO_MANY_MEMBERS, reviewer));
+          continue;
+        }
+
+        // if maxWithoutCheck is set to 0, we never ask for confirmation
+        final int maxWithoutConfirmation =
+            cfg.getInt("addreviewer", "maxWithoutConfirmation",
+                DEFAULT_MAX_REVIEWERS_WITHOUT_CHECK);
+        if (!confirmed && maxWithoutConfirmation > 0
+            && members.size() > maxWithoutConfirmation) {
+          result.setMemberCount(members.size());
+          result.setAskForConfirmation(true);
+          result.addError(new ReviewerResult.Error(
+              ReviewerResult.Error.Type.GROUP_HAS_TOO_MANY_MEMBERS, reviewer));
+          continue;
+        }
+
+        for (final Account member : members) {
+          if (member.isActive()) {
+            final IdentifiedUser user =
+                identifiedUserFactory.create(member.getId());
+            // 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());
+            }
+          }
+        }
+        continue;
+      }
+
+      if (!account.isActive()) {
+        result.addError(new ReviewerResult.Error(
+            ReviewerResult.Error.Type.ACCOUNT_INACTIVE,
+            formatUser(account, reviewer)));
+        continue;
+      }
+
+      final IdentifiedUser user = identifiedUserFactory.create(account.getId());
+      // 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)));
+        continue;
+      }
+
+      reviewerIds.add(account.getId());
+    }
+
+    if (reviewerIds.isEmpty()) {
+      return result;
+    }
+
+    // Add the reviewers to the database
+    //
+    final Set<Account.Id> added = new HashSet<Account.Id>();
+    final List<PatchSetApproval> toInsert = new ArrayList<PatchSetApproval>();
+    final PatchSet.Id psid = control.getChange().currentPatchSetId();
+    for (final Account.Id reviewer : reviewerIds) {
+      if (!exists(psid, reviewer)) {
+        // This reviewer has not entered an approval for this change yet.
+        //
+        final PatchSetApproval myca =
+            dummyApproval(control.getChange(), psid, reviewer);
+        toInsert.add(myca);
+        added.add(reviewer);
+      }
+    }
+    db.patchSetApprovals().insert(toInsert);
+
+    // Email the reviewers
+    //
+    // The user knows they added themselves, don't bother emailing them.
+    added.remove(currentUser.getAccountId());
+    if (!added.isEmpty()) {
+      final AddReviewerSender cm;
+
+      cm = addReviewerSenderFactory.create(control.getChange());
+      cm.setFrom(currentUser.getAccountId());
+      cm.addReviewers(added);
+      cm.send();
+    }
+
+    return result;
+  }
+
+  private String formatUser(Account account, String nameOrEmail) {
+    if (nameOrEmail.matches("^[1-9][0-9]*$")) {
+      return RemoveReviewer.formatUser(account, nameOrEmail);
+    } else {
+      return nameOrEmail;
+    }
+  }
+
+  private boolean exists(final PatchSet.Id patchSetId,
+      final Account.Id reviewerId) throws OrmException {
+    return db.patchSetApprovals().byPatchSetUser(patchSetId, reviewerId)
+        .iterator().hasNext();
+  }
+
+  private PatchSetApproval dummyApproval(final Change change,
+      final PatchSet.Id patchSetId, final Account.Id reviewerId) {
+    final PatchSetApproval dummyApproval =
+        new PatchSetApproval(new PatchSetApproval.Key(patchSetId, reviewerId,
+            addReviewerCategoryId), (short) 0);
+    dummyApproval.cache(change);
+    return dummyApproval;
+  }
+
+  public static boolean isLegalReviewerGroup(final AccountGroup.UUID groupUUID) {
+    return !(AccountGroup.ANONYMOUS_USERS.equals(groupUUID)
+             || AccountGroup.REGISTERED_USERS.equals(groupUUID));
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiff.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiff.java
index 3805f8f..46def59 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiff.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiff.java
@@ -19,7 +19,7 @@
 import static com.google.gerrit.server.ioutil.BasicSerialization.writeEnum;
 import static com.google.gerrit.server.ioutil.BasicSerialization.writeVarInt32;
 
-import com.google.gerrit.reviewdb.CodedEnum;
+import com.google.gerrit.reviewdb.client.CodedEnum;
 
 import org.eclipse.jgit.diff.Edit;
 import org.eclipse.jgit.diff.ReplaceEdit;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiffKey.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiffKey.java
index a8d62fc..c5c5925 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiffKey.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiffKey.java
@@ -17,7 +17,7 @@
 import static org.eclipse.jgit.lib.ObjectIdSerialization.readNotNull;
 import static org.eclipse.jgit.lib.ObjectIdSerialization.writeNotNull;
 
-import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.reviewdb.client.Project;
 
 import org.eclipse.jgit.diff.Edit;
 import org.eclipse.jgit.lib.ObjectId;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchFile.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchFile.java
index 4feccd0..f120ebf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchFile.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchFile.java
@@ -16,7 +16,7 @@
 
 import com.google.gerrit.common.errors.CorruptEntityException;
 import com.google.gerrit.common.errors.NoSuchEntityException;
-import com.google.gerrit.reviewdb.Patch;
+import com.google.gerrit.reviewdb.client.Patch;
 
 import org.eclipse.jgit.errors.CorruptObjectException;
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchList.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchList.java
index 7194a22..93d7bf7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchList.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchList.java
@@ -24,8 +24,8 @@
 import static org.eclipse.jgit.lib.ObjectIdSerialization.writeCanBeNull;
 import static org.eclipse.jgit.lib.ObjectIdSerialization.writeNotNull;
 
-import com.google.gerrit.reviewdb.Patch;
-import com.google.gerrit.reviewdb.PatchSet;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.PatchSet;
 
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.ObjectId;
@@ -142,8 +142,12 @@
   }
 
   private int search(final String fileName) {
+    if (Patch.COMMIT_MSG.equals(fileName)) {
+      return 0;
+    }
+
     int high = patches.length;
-    int low = 0;
+    int low = 1;
     while (low < high) {
       final int mid = (low + high) >>> 1;
       final int cmp = patches[mid].getNewName().compareTo(fileName);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCache.java
index a7cf10a..8a61d30 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCache.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.server.patch;
 
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.PatchSet;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
 
 /** Provides a cached list of {@link PatchListEntry}. */
 public interface PatchListCache {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
index e1a0a40..26dbe2d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListCacheImpl.java
@@ -16,10 +16,10 @@
 package com.google.gerrit.server.patch;
 
 
-import com.google.gerrit.reviewdb.AccountDiffPreference.Whitespace;
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.PatchSet;
-import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
 import com.google.gerrit.server.cache.Cache;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.cache.EvictionPolicy;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListEntry.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListEntry.java
index 837ec9c..33ed54e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListEntry.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListEntry.java
@@ -23,10 +23,10 @@
 import static com.google.gerrit.server.ioutil.BasicSerialization.writeString;
 import static com.google.gerrit.server.ioutil.BasicSerialization.writeVarInt32;
 
-import com.google.gerrit.reviewdb.Patch;
-import com.google.gerrit.reviewdb.PatchSet;
-import com.google.gerrit.reviewdb.Patch.ChangeType;
-import com.google.gerrit.reviewdb.Patch.PatchType;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Patch.ChangeType;
+import com.google.gerrit.reviewdb.client.Patch.PatchType;
 
 import org.eclipse.jgit.diff.Edit;
 import org.eclipse.jgit.lib.Constants;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListKey.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListKey.java
index 18fa5b0..de12cc6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListKey.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListKey.java
@@ -21,8 +21,8 @@
 import static org.eclipse.jgit.lib.ObjectIdSerialization.writeCanBeNull;
 import static org.eclipse.jgit.lib.ObjectIdSerialization.writeNotNull;
 
-import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.AccountDiffPreference.Whitespace;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
 
 import org.eclipse.jgit.lib.AnyObjectId;
 import org.eclipse.jgit.lib.ObjectId;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java
index 44e2e89..5bba42b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/PatchListLoader.java
@@ -15,8 +15,8 @@
 
 package com.google.gerrit.server.patch;
 
-import com.google.gerrit.reviewdb.Patch;
-import com.google.gerrit.reviewdb.AccountDiffPreference.Whitespace;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.AccountDiffPreference.Whitespace;
 import com.google.gerrit.server.cache.EntryCreator;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.inject.Inject;
@@ -28,12 +28,22 @@
 import org.eclipse.jgit.diff.HistogramDiff;
 import org.eclipse.jgit.diff.RawText;
 import org.eclipse.jgit.diff.RawTextComparator;
+import org.eclipse.jgit.diff.Sequence;
+import org.eclipse.jgit.dircache.DirCache;
+import org.eclipse.jgit.dircache.DirCacheBuilder;
+import org.eclipse.jgit.dircache.DirCacheEntry;
 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.ObjectReader;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.merge.MergeFormatter;
+import org.eclipse.jgit.merge.MergeResult;
+import org.eclipse.jgit.merge.MergeStrategy;
+import org.eclipse.jgit.merge.ResolveMerger;
 import org.eclipse.jgit.patch.FileHeader;
 import org.eclipse.jgit.patch.FileHeader.PatchType;
 import org.eclipse.jgit.revwalk.RevCommit;
@@ -42,11 +52,15 @@
 import org.eclipse.jgit.revwalk.RevWalk;
 import org.eclipse.jgit.treewalk.TreeWalk;
 import org.eclipse.jgit.treewalk.filter.TreeFilter;
+import org.eclipse.jgit.util.TemporaryBuffer;
 import org.eclipse.jgit.util.io.DisabledOutputStream;
 
 import java.io.IOException;
+import java.io.InputStream;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.List;
+import java.util.Map;
 
 class PatchListLoader extends EntryCreator<PatchListKey, PatchList> {
   private final GitRepositoryManager repoManager;
@@ -85,8 +99,6 @@
 
   private PatchList readPatchList(final PatchListKey key,
       final Repository repo) throws IOException {
-    // TODO(jeffschu) correctly handle merge commits
-
     final RawTextComparator cmp = comparatorFor(key.getWhitespace());
     final ObjectReader reader = repo.newObjectReader();
     try {
@@ -95,6 +107,7 @@
       final RevObject a = aFor(key, repo, rw, b);
 
       if (a == null) {
+        // TODO(sop) Remove this case.
         // This is a merge commit, compared to its ancestor.
         //
         final PatchListEntry[] entries = new PatchListEntry[1];
@@ -216,12 +229,135 @@
         rw.parseBody(r);
         return r;
       }
+      case 2:
+        return automerge(repo, rw, b);
       default:
-        // merge commit, return null to force combined diff behavior
+        // TODO(sop) handle an octopus merge.
         return null;
     }
   }
 
+  private static RevObject automerge(Repository repo, RevWalk rw, RevCommit b)
+      throws IOException {
+    String hash = b.name();
+    String refName = GitRepositoryManager.REFS_CACHE_AUTOMERGE
+        + hash.substring(0, 2)
+        + "/"
+        + hash.substring(2);
+    Ref ref = repo.getRef(refName);
+    if (ref != null && ref.getObjectId() != null) {
+      return rw.parseTree(ref.getObjectId());
+    }
+
+    ObjectId treeId;
+    ResolveMerger m = (ResolveMerger) MergeStrategy.RESOLVE.newMerger(repo, true);
+    ObjectInserter ins = m.getObjectInserter();
+    try {
+      DirCache dc = DirCache.newInCore();
+      m.setDirCache(dc);
+
+      boolean couldMerge = false;
+      try {
+        couldMerge = m.merge(b.getParents());
+      } catch (IOException e) {
+        //
+      }
+
+      if (couldMerge) {
+        treeId = m.getResultTreeId();
+
+      } else {
+        RevCommit ours = b.getParent(0);
+        RevCommit theirs = b.getParent(1);
+        rw.parseBody(ours);
+        rw.parseBody(theirs);
+        String oursMsg = ours.getShortMessage();
+        String theirsMsg = theirs.getShortMessage();
+
+        String oursName = String.format("HEAD   (%s %s)",
+            ours.abbreviate(6).name(),
+            oursMsg.substring(0, Math.min(oursMsg.length(), 60)));
+        String theirsName = String.format("BRANCH (%s %s)",
+            theirs.abbreviate(6).name(),
+            theirsMsg.substring(0, Math.min(theirsMsg.length(), 60)));
+
+        MergeFormatter fmt = new MergeFormatter();
+        Map<String, MergeResult<? extends Sequence>> r = m.getMergeResults();
+        Map<String, ObjectId> resolved = new HashMap<String, ObjectId>();
+        for (String path : r.keySet()) {
+          MergeResult<? extends Sequence> p = r.get(path);
+          TemporaryBuffer buf = new TemporaryBuffer.LocalFile(10 * 1024 * 1024);
+          try {
+            fmt.formatMerge(buf, p, "BASE", oursName, theirsName, "UTF-8");
+            buf.close();
+
+            InputStream in = buf.openInputStream();
+            try {
+              resolved.put(path, ins.insert(Constants.OBJ_BLOB, buf.length(), in));
+            } finally {
+              in.close();
+            }
+          } finally {
+            buf.destroy();
+          }
+        }
+
+        DirCacheBuilder builder = dc.builder();
+        int cnt = dc.getEntryCount();
+        for (int i = 0; i < cnt;) {
+          DirCacheEntry entry = dc.getEntry(i);
+          if (entry.getStage() == 0) {
+            builder.add(entry);
+            i++;
+            continue;
+          }
+
+          int next = dc.nextEntry(i);
+          String path = entry.getPathString();
+          DirCacheEntry res = new DirCacheEntry(path);
+          if (resolved.containsKey(path)) {
+            // For a file with content merge conflict that we produced a result
+            // above on, collapse the file down to a single stage 0 with just
+            // the blob content, and a randomly selected mode (the lowest stage,
+            // which should be the merge base, or ours).
+            res.setFileMode(entry.getFileMode());
+            res.setObjectId(resolved.get(path));
+
+          } else if (next == i + 1) {
+            // If there is exactly one stage present, shouldn't be a conflict...
+            res.setFileMode(entry.getFileMode());
+            res.setObjectId(entry.getObjectId());
+
+          } else if (next == i + 2) {
+            // Two stages suggests a delete/modify conflict. Pick the higher
+            // stage as the automatic result.
+            entry = dc.getEntry(i + 1);
+            res.setFileMode(entry.getFileMode());
+            res.setObjectId(entry.getObjectId());
+
+          } else { // 3 stage conflict, no resolve above
+            // Punt on the 3-stage conflict and show the base, for now.
+            res.setFileMode(entry.getFileMode());
+            res.setObjectId(entry.getObjectId());
+          }
+          builder.add(res);
+          i = next;
+        }
+        builder.finish();
+        treeId = dc.writeTree(ins);
+      }
+      ins.flush();
+    } finally {
+      ins.release();
+    }
+
+    RefUpdate update = repo.updateRef(refName);
+    update.setNewObjectId(treeId);
+    update.disableRefLog();
+    update.forceUpdate();
+    return rw.parseTree(treeId);
+  }
+
   private static ObjectId emptyTree(final Repository repo) throws IOException {
     ObjectInserter oi = repo.newObjectInserter();
     try {
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 646d4ce..f59dcc3 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
@@ -14,18 +14,17 @@
 
 package com.google.gerrit.server.patch;
 
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.PatchSet;
-import com.google.gerrit.reviewdb.PatchSetInfo;
-import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.RevId;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gerrit.reviewdb.UserIdentity;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetInfo;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.reviewdb.client.UserIdentity;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 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.gwtorm.server.OrmException;
 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;
   }
 
@@ -67,16 +63,14 @@
     info.setMessage(src.getFullMessage());
     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();
@@ -96,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 f3e2890..fdabcaa 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,26 +14,30 @@
 
 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;
-import com.google.gerrit.reviewdb.ApprovalCategoryValue;
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.ChangeMessage;
-import com.google.gerrit.reviewdb.PatchLineComment;
-import com.google.gerrit.reviewdb.PatchSet;
-import com.google.gerrit.reviewdb.PatchSetApproval;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.client.ApprovalCategory;
+import com.google.gerrit.reviewdb.client.ApprovalCategoryValue;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.PatchSetInfo;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.mail.CommentSender;
-import com.google.gerrit.server.mail.EmailException;
 import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.util.RequestScopePropagator;
 import com.google.gerrit.server.workflow.FunctionState;
-import com.google.gwtjsonrpc.client.VoidResult;
-import com.google.gwtorm.client.OrmException;
+import com.google.gwtjsonrpc.common.VoidResult;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -56,9 +60,10 @@
 
   public interface Factory {
     PublishComments create(PatchSet.Id patchSetId, String messageText,
-        Set<ApprovalCategoryValue.Id> approvals);
+        Set<ApprovalCategoryValue.Id> approvals, boolean forceMessage);
   }
 
+  private final SchemaFactory<ReviewDb> schemaFactory;
   private final ReviewDb db;
   private final IdentifiedUser user;
   private final ApprovalTypes types;
@@ -66,11 +71,14 @@
   private final PatchSetInfoFactory patchSetInfoFactory;
   private final ChangeControl.Factory changeControlFactory;
   private final FunctionState.Factory functionStateFactory;
-  private final ChangeHookRunner hooks;
+  private final ChangeHooks hooks;
+  private final WorkQueue workQueue;
+  private final RequestScopePropagator requestScopePropagator;
 
   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;
@@ -78,17 +86,22 @@
   private List<PatchLineComment> drafts;
 
   @Inject
-  PublishComments(final ReviewDb db, final IdentifiedUser user,
+  PublishComments(final SchemaFactory<ReviewDb> sf, final ReviewDb db,
+      final IdentifiedUser user,
       final ApprovalTypes approvalTypes,
       final CommentSender.Factory commentSenderFactory,
       final PatchSetInfoFactory patchSetInfoFactory,
       final ChangeControl.Factory changeControlFactory,
       final FunctionState.Factory functionStateFactory,
-      final ChangeHookRunner hooks,
+      final ChangeHooks hooks,
+      final WorkQueue workQueue,
+      final RequestScopePropagator requestScopePropagator,
 
       @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.schemaFactory = sf;
     this.db = db;
     this.user = user;
     this.types = approvalTypes;
@@ -97,14 +110,18 @@
     this.changeControlFactory = changeControlFactory;
     this.functionStateFactory = functionStateFactory;
     this.hooks = hooks;
+    this.workQueue = workQueue;
+    this.requestScopePropagator = requestScopePropagator;
 
     this.patchSetId = patchSetId;
     this.messageText = messageText;
     this.approvals = approvals;
+    this.forceMessage = forceMessage;
   }
 
   @Override
-  public VoidResult call() throws NoSuchChangeException, OrmException {
+  public VoidResult call() throws NoSuchChangeException,
+      InvalidChangeOperationException, OrmException {
     final Change.Id changeId = patchSetId.getParentKey();
     final ChangeControl ctl = changeControlFactory.validateFor(changeId);
     change = ctl.getChange();
@@ -114,16 +131,25 @@
     }
     drafts = drafts();
 
-    publishDrafts();
+    db.changes().beginTransaction(changeId);
+    try {
+      publishDrafts();
 
-    final boolean isCurrent = patchSetId.equals(change.currentPatchSetId());
-    if (isCurrent && change.getStatus().isOpen()) {
-      publishApprovals();
-    } 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;
@@ -137,7 +163,8 @@
     db.patchComments().update(drafts);
   }
 
-  private void publishApprovals() throws OrmException {
+  private void publishApprovals(ChangeControl ctl)
+      throws InvalidChangeOperationException, OrmException {
     ChangeUtil.updated(change);
 
     final Set<ApprovalCategory.Id> dirty = new HashSet<ApprovalCategory.Id>();
@@ -165,13 +192,20 @@
     // Normalize all of the items the user is changing.
     //
     final FunctionState functionState =
-        functionStateFactory.create(change, patchSetId, all);
+        functionStateFactory.create(ctl, patchSetId, all);
     for (final ApprovalCategoryValue.Id want : approvals) {
       final PatchSetApproval a = mine.get(want.getParentKey());
       final short o = a.getValue();
       a.setValue(want.get());
       a.cache(change);
-      functionState.normalize(types.getApprovalType(a.getCategoryId()), a);
+      if (!ApprovalCategory.SUBMIT.equals(a.getCategoryId())) {
+        functionState.normalize(types.byId(a.getCategoryId()), a);
+      }
+      if (want.get() != a.getValue()) {
+        throw new InvalidChangeOperationException(
+            types.byId(a.getCategoryId()).getCategory().getLabelName()
+            + "=" + want.get() + " not permitted");
+      }
       if (o != a.getValue()) {
         // Value changed, ensure we update the database.
         //
@@ -249,7 +283,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));
   }
@@ -274,32 +308,61 @@
   }
 
   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() {
-    try {
-      final CommentSender cm = commentSenderFactory.create(change);
-      cm.setFrom(user.getAccountId());
-      cm.setPatchSet(patchSet, patchSetInfoFactory.get(patchSetId));
-      cm.setChangeMessage(message);
-      cm.setPatchLineComments(drafts);
-      cm.send();
-    } catch (EmailException e) {
-      log.error("Cannot send comments by email for patch set " + patchSetId, e);
-    } catch (PatchSetInfoNotAvailableException e) {
-      log.error("Failed to obtain PatchSetInfo for patch set " + patchSetId, e);
+    if (message == null) {
+      return;
     }
+
+    workQueue.getDefaultQueue()
+        .submit(requestScopePropagator.wrap(new Runnable() {
+      @Override
+      public void run() {
+        PatchSetInfo patchSetInfo;
+        try {
+          ReviewDb reviewDb = schemaFactory.open();
+          try {
+            patchSetInfo = patchSetInfoFactory.get(reviewDb, patchSetId);
+          } finally {
+            reviewDb.close();
+          }
+        } catch (PatchSetInfoNotAvailableException e) {
+          log.error("Cannot read PatchSetInfo of " + patchSetId, e);
+          return;
+        } catch (Exception e) {
+          log.error("Cannot email comments for " + patchSetId, e);
+          return;
+        }
+
+        try {
+          final CommentSender cm = commentSenderFactory.create(change);
+          cm.setFrom(user.getAccountId());
+          cm.setPatchSet(patchSet, patchSetInfo);
+          cm.setChangeMessage(message);
+          cm.setPatchLineComments(drafts);
+          cm.send();
+        } catch (Exception e) {
+          log.error("Cannot email comments for " + patchSetId, e);
+        }
+      }
+
+      @Override
+      public String toString() {
+        return "send-email comments";
+      }
+    }));
   }
 
-  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/patch/RemoveReviewer.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/RemoveReviewer.java
new file mode 100644
index 0000000..af9bb8c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/RemoveReviewer.java
@@ -0,0 +1,130 @@
+// 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.patch;
+
+import com.google.gerrit.common.data.ReviewerResult;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.Callable;
+
+public class RemoveReviewer implements Callable<ReviewerResult> {
+  private static final Logger log =
+      LoggerFactory.getLogger(RemoveReviewer.class);
+
+  public interface Factory {
+    RemoveReviewer create(Change.Id changeId, Set<Account.Id> reviewerId);
+  }
+
+  private final ChangeControl.Factory changeControlFactory;
+  private final ReviewDb db;
+  private final AccountCache accountCache;
+  private final Change.Id changeId;
+  private final Set<Account.Id> ids;
+
+  @Inject
+  RemoveReviewer(ReviewDb db, ChangeControl.Factory changeControlFactory,
+      AccountCache accountCache, @Assisted Change.Id changeId,
+      @Assisted Set<Account.Id> ids) {
+    this.db = db;
+    this.changeControlFactory = changeControlFactory;
+    this.accountCache = accountCache;
+    this.changeId = changeId;
+    this.ids = ids;
+  }
+
+  @Override
+  public ReviewerResult call() throws Exception {
+    ReviewerResult result = new ReviewerResult();
+    ChangeControl ctl = changeControlFactory.validateFor(changeId);
+    Set<Account.Id> rejected = new HashSet<Account.Id>();
+
+    List<PatchSetApproval> current = db.patchSetApprovals().byChange(changeId).toList();
+    for (PatchSetApproval psa : current) {
+      Account.Id who = psa.getAccountId();
+      if (ids.contains(who) && !ctl.canRemoveReviewer(psa) && rejected.add(who)) {
+        result.addError(new ReviewerResult.Error(
+            ReviewerResult.Error.Type.REMOVE_NOT_PERMITTED,
+            formatUser(who)));
+      }
+    }
+
+    List<PatchSetApproval> toDelete = new ArrayList<PatchSetApproval>();
+    for (PatchSetApproval psa : current) {
+      Account.Id who = psa.getAccountId();
+      if (ids.contains(who) && !rejected.contains(who)) {
+        toDelete.add(psa);
+      }
+    }
+
+    try {
+      db.patchSetApprovals().delete(toDelete);
+    } catch (OrmException err) {
+      log.warn("Cannot remove reviewers from change "+changeId, err);
+      Set<Account.Id> failed = new HashSet<Account.Id>();
+      for (PatchSetApproval psa : toDelete) {
+        failed.add(psa.getAccountId());
+      }
+      for (Account.Id who : failed) {
+        result.addError(new ReviewerResult.Error(
+            ReviewerResult.Error.Type.COULD_NOT_REMOVE,
+            formatUser(who)));
+      }
+    }
+
+    return result;
+  }
+
+  private String formatUser(Account.Id who) {
+    AccountState state = accountCache.get(who);
+    if (state != null) {
+      return formatUser(state.getAccount(), who);
+    } else {
+      return who.toString();
+    }
+  }
+
+  static String formatUser(Account a, Object fallback) {
+    if (a.getFullName() != null && !a.getFullName().isEmpty()) {
+      return a.getFullName();
+    }
+
+    if (a.getPreferredEmail() != null && !a.getPreferredEmail().isEmpty()) {
+      return a.getPreferredEmail();
+    }
+
+    if (a.getUserName() != null && a.getUserName().isEmpty()) {
+      return a.getUserName();
+    }
+
+    return fallback.toString();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/AccessControlModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/AccessControlModule.java
index 1e2e7f4..3b1191c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/AccessControlModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/AccessControlModule.java
@@ -16,14 +16,12 @@
 
 import static com.google.inject.Scopes.SINGLETON;
 
-import com.google.gerrit.reviewdb.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.config.FactoryModule;
 import com.google.gerrit.server.config.GitReceivePackGroups;
 import com.google.gerrit.server.config.GitReceivePackGroupsProvider;
 import com.google.gerrit.server.config.GitUploadPackGroups;
 import com.google.gerrit.server.config.GitUploadPackGroupsProvider;
-import com.google.gerrit.server.config.ProjectCreatorGroups;
-import com.google.gerrit.server.config.ProjectCreatorGroupsProvider;
 import com.google.gerrit.server.config.ProjectOwnerGroups;
 import com.google.gerrit.server.config.ProjectOwnerGroupsProvider;
 import com.google.inject.TypeLiteral;
@@ -33,19 +31,15 @@
 public class AccessControlModule extends FactoryModule {
   @Override
   protected void configure() {
-    bind(new TypeLiteral<Set<AccountGroup.Id>>() {}) //
-        .annotatedWith(ProjectCreatorGroups.class) //
-        .toProvider(ProjectCreatorGroupsProvider.class).in(SINGLETON);
-
-    bind(new TypeLiteral<Set<AccountGroup.Id>>() {}) //
+    bind(new TypeLiteral<Set<AccountGroup.UUID>>() {}) //
         .annotatedWith(ProjectOwnerGroups.class) //
         .toProvider(ProjectOwnerGroupsProvider.class).in(SINGLETON);
 
-    bind(new TypeLiteral<Set<AccountGroup.Id>>() {}) //
+    bind(new TypeLiteral<Set<AccountGroup.UUID>>() {}) //
         .annotatedWith(GitUploadPackGroups.class) //
         .toProvider(GitUploadPackGroupsProvider.class).in(SINGLETON);
 
-    bind(new TypeLiteral<Set<AccountGroup.Id>>() {}) //
+    bind(new TypeLiteral<Set<AccountGroup.UUID>>() {}) //
         .annotatedWith(GitReceivePackGroups.class) //
         .toProvider(GitReceivePackGroupsProvider.class).in(SINGLETON);
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CanSubmitResult.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CanSubmitResult.java
deleted file mode 100644
index 0c14b80..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CanSubmitResult.java
+++ /dev/null
@@ -1,43 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.project;
-
-/**
- * Result from {@code ChangeControl.canSubmit()}.
- *
- * @see ChangeControl#canSubmit(com.google.gerrit.reviewdb.PatchSet.Id,
- *      com.google.gerrit.reviewdb.ReviewDb,
- *      com.google.gerrit.common.data.ApprovalTypes,
- *      com.google.gerrit.server.workflow.FunctionState.Factory)
- */
-public class CanSubmitResult {
-  /** Magic constant meaning submitting is possible. */
-  public static final CanSubmitResult OK = new CanSubmitResult("OK");
-
-  private final String errorMessage;
-
-  CanSubmitResult(String error) {
-    this.errorMessage = error;
-  }
-
-  public String getMessage() {
-    return errorMessage;
-  }
-
-  @Override
-  public String toString() {
-    return "CanSubmitResult[" + getMessage() + "]";
-  }
-}
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 3291f1a..d17762e 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
@@ -14,29 +14,47 @@
 
 package com.google.gerrit.server.project;
 
-import com.google.gerrit.common.data.ApprovalType;
-import com.google.gerrit.common.data.ApprovalTypes;
-import com.google.gerrit.reviewdb.ApprovalCategory;
-import com.google.gerrit.reviewdb.Change;
-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.server.ChangeUtil;
+import com.google.gerrit.common.data.PermissionRange;
+import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.rules.PrologEnvironment;
+import com.google.gerrit.rules.StoredValues;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.workflow.CategoryFunction;
-import com.google.gerrit.server.workflow.FunctionState;
-import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
+import com.googlecode.prolog_cafe.compiler.CompileException;
+import com.googlecode.prolog_cafe.lang.IntegerTerm;
+import com.googlecode.prolog_cafe.lang.ListTerm;
+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.Term;
+import com.googlecode.prolog_cafe.lang.VariableTerm;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashSet;
 import java.util.List;
+import java.util.Set;
 
 
 /** Access control management for a user accessing a single change. */
 public class ChangeControl {
+  private static final Logger log = LoggerFactory
+      .getLogger(ChangeControl.class);
+
   public static class GenericFactory {
     private final ProjectControl.GenericFactory projectControl;
 
@@ -91,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;
@@ -117,10 +135,6 @@
     this.change = c;
   }
 
-  public ChangeControl forAnonymousUser() {
-    return new ChangeControl(getRefControl().forAnonymousUser(), getChange());
-  }
-
   public ChangeControl forUser(final CurrentUser who) {
     return new ChangeControl(getRefControl().forUser(who), getChange());
   }
@@ -146,26 +160,64 @@
   }
 
   /** 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
         || getRefControl().isOwner() // branch owner can abandon
         || getProjectControl().isOwner() // project owner can abandon
-        || getCurrentUser().isAdministrator() // site administers are god
+        || getCurrentUser().getCapabilities().canAdministrateServer() // site administers are god
     ;
   }
 
+  /** 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 rebase this change? */
+  public boolean canRebase() {
+    return isOwner() || getRefControl().canSubmit()
+        || getRefControl().canRebase();
+  }
+
   /** Can this user restore this change? */
   public boolean canRestore() {
     return canAbandon(); // Anyone who can abandon the change can restore it back
   }
 
-  public short normalize(ApprovalCategory.Id category, short score) {
-    return getRefControl().normalize(category, score);
+  /** All value ranges of any allowed label permission. */
+  public List<PermissionRange> getLabelRanges() {
+    return getRefControl().getLabelRanges();
+  }
+
+  /** The range of permitted values associated with a label permission. */
+  public PermissionRange getRange(String permission) {
+    return getRefControl().getRange(permission);
   }
 
   /** Can this user add a patch set to this change? */
@@ -182,6 +234,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()) {
@@ -204,7 +271,7 @@
       //
       if (getRefControl().isOwner() // branch owner
           || getProjectControl().isOwner() // project owner
-          || getCurrentUser().isAdministrator()) {
+          || getCurrentUser().getCapabilities().canAdministrateServer()) {
         return true;
       }
     }
@@ -212,62 +279,267 @@
     return false;
   }
 
-  /** @return {@link CanSubmitResult#OK}, or a result with an error message. */
-  public CanSubmitResult canSubmit(final PatchSet.Id patchSetId) {
+  public List<SubmitRecord> canSubmit(ReviewDb db, PatchSet.Id patchSetId) {
     if (change.getStatus().isClosed()) {
-      return new CanSubmitResult("Change " + change.getId() + " is closed");
+      SubmitRecord rec = new SubmitRecord();
+      rec.status = SubmitRecord.Status.CLOSED;
+      return Collections.singletonList(rec);
     }
+
     if (!patchSetId.equals(change.currentPatchSetId())) {
-      return new CanSubmitResult("Patch set " + patchSetId + " is not current");
+      return ruleError("Patch set " + patchSetId + " is not current");
     }
-    if (!getRefControl().canSubmit()) {
-      return new CanSubmitResult("User does not have permission to submit");
+
+    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);
     }
-    if (!(getCurrentUser() instanceof IdentifiedUser)) {
-      return new CanSubmitResult("User is not signed-in");
+
+    List<Term> results = new ArrayList<Term>();
+    Term submitRule;
+    ProjectState projectState = getProjectControl().getProjectState();
+    PrologEnvironment env;
+
+    try {
+      env = projectState.newPrologEnvironment();
+    } catch (CompileException err) {
+      return logRuleError("Cannot consult rules.pl for "
+          + getProject().getName(), err);
     }
-    return CanSubmitResult.OK;
+
+    try {
+      env.set(StoredValues.REVIEW_DB, db);
+      env.set(StoredValues.CHANGE, change);
+      env.set(StoredValues.PATCH_SET_ID, patchSetId);
+      env.set(StoredValues.CHANGE_CONTROL, this);
+
+      submitRule = env.once(
+        "gerrit", "locate_submit_rule",
+        new VariableTerm());
+      if (submitRule == null) {
+        return logRuleError("No user:submit_rule found for "
+            + getProject().getName());
+      }
+
+      try {
+        for (Term[] template : env.all(
+            "gerrit", "can_submit",
+            submitRule,
+            new VariableTerm())) {
+          results.add(template[1]);
+        }
+      } catch (PrologException err) {
+        return logRuleError("Exception calling " + submitRule + " on change "
+            + change.getId() + " of " + getProject().getName(), err);
+      } catch (RuntimeException err) {
+        return logRuleError("Exception calling " + submitRule + " on change "
+            + change.getId() + " of " + getProject().getName(), err);
+      }
+
+      ProjectState parentState = projectState.getParentState();
+      PrologEnvironment childEnv = env;
+      Set<Project.NameKey> projectsSeen = new HashSet<Project.NameKey>();
+      projectsSeen.add(getProject().getNameKey());
+
+      while (parentState != null) {
+        if (!projectsSeen.add(parentState.getProject().getNameKey())) {
+          //parent has been seen before, stop walk up inheritance tree
+          break;
+        }
+        PrologEnvironment parentEnv;
+        try {
+          parentEnv = parentState.newPrologEnvironment();
+        } catch (CompileException err) {
+          return logRuleError("Cannot consult rules.pl for "
+              + parentState.getProject().getName(), err);
+        }
+
+        parentEnv.copyStoredValues(childEnv);
+        Term filterRule =
+            parentEnv.once("gerrit", "locate_submit_filter", new VariableTerm());
+        if (filterRule != null) {
+          try {
+            Term resultsTerm = toListTerm(results);
+            results.clear();
+            Term[] template = parentEnv.once(
+                "gerrit", "filter_submit_results",
+                filterRule,
+                resultsTerm,
+                new VariableTerm());
+            @SuppressWarnings("unchecked")
+            final List<? extends Term> termList = ((ListTerm) template[2]).toJava();
+            results.addAll(termList);
+          } catch (PrologException err) {
+            return logRuleError("Exception calling " + filterRule + " on change "
+                + change.getId() + " of " + parentState.getProject().getName(), err);
+          } catch (RuntimeException err) {
+            return logRuleError("Exception calling " + filterRule + " on change "
+                + change.getId() + " of " + parentState.getProject().getName(), err);
+          }
+        }
+
+        parentState = parentState.getParentState();
+        childEnv = parentEnv;
+      }
+    } finally {
+      env.close();
+    }
+
+    if (results.isEmpty()) {
+      // This should never occur. A well written submit rule will always produce
+      // at least one result informing the caller of the labels that are
+      // required for this change to be submittable. Each label will indicate
+      // whether or not that is actually possible given the permissions.
+      log.error("Submit rule " + submitRule + " for change " + change.getId()
+          + " of " + getProject().getName() + " has no solution.");
+      return ruleError("Project submit rule has no solution");
+    }
+
+    // Convert the results from Prolog Cafe's format to Gerrit's common format.
+    // can_submit/1 terminates when an ok(P) record is found. Therefore walk
+    // the results backwards, using only that ok(P) record if it exists. This
+    // skips partial results that occur early in the output. Later after the loop
+    // the out collection is reversed to restore it to the original ordering.
+    //
+    List<SubmitRecord> out = new ArrayList<SubmitRecord>(results.size());
+    for (int resultIdx = results.size() - 1; 0 <= resultIdx; resultIdx--) {
+      Term submitRecord = results.get(resultIdx);
+      SubmitRecord rec = new SubmitRecord();
+      out.add(rec);
+
+      if (!submitRecord.isStructure() || 1 != submitRecord.arity()) {
+        return logInvalidResult(submitRule, submitRecord);
+      }
+
+      if ("ok".equals(submitRecord.name())) {
+        rec.status = SubmitRecord.Status.OK;
+
+      } else if ("not_ready".equals(submitRecord.name())) {
+        rec.status = SubmitRecord.Status.NOT_READY;
+
+      } else {
+        return logInvalidResult(submitRule, submitRecord);
+      }
+
+      // Unpack the one argument. This should also be a structure with one
+      // argument per label that needs to be reported on to the caller.
+      //
+      submitRecord = submitRecord.arg(0);
+
+      if (!submitRecord.isStructure()) {
+        return logInvalidResult(submitRule, submitRecord);
+      }
+
+      rec.labels = new ArrayList<SubmitRecord.Label> (submitRecord.arity());
+
+      for (Term state : ((StructureTerm) submitRecord).args()) {
+        if (!state.isStructure() || 2 != state.arity() || !"label".equals(state.name())) {
+          return logInvalidResult(submitRule, submitRecord);
+        }
+
+        SubmitRecord.Label lbl = new SubmitRecord.Label();
+        rec.labels.add(lbl);
+
+        lbl.label = state.arg(0).name();
+        Term status = state.arg(1);
+
+        if ("ok".equals(status.name())) {
+          lbl.status = SubmitRecord.Label.Status.OK;
+          appliedBy(lbl, status);
+
+        } else if ("reject".equals(status.name())) {
+          lbl.status = SubmitRecord.Label.Status.REJECT;
+          appliedBy(lbl, status);
+
+        } else if ("need".equals(status.name())) {
+          lbl.status = SubmitRecord.Label.Status.NEED;
+
+        } else if ("impossible".equals(status.name())) {
+          lbl.status = SubmitRecord.Label.Status.IMPOSSIBLE;
+
+        } else {
+          return logInvalidResult(submitRule, submitRecord);
+        }
+      }
+
+      if (rec.status == SubmitRecord.Status.OK) {
+        break;
+      }
+    }
+    Collections.reverse(out);
+
+    return out;
   }
 
-  /** @return {@link CanSubmitResult#OK}, or a result with an error message. */
-  public CanSubmitResult canSubmit(final PatchSet.Id patchSetId, final ReviewDb db,
-        final ApprovalTypes approvalTypes,
-        FunctionState.Factory functionStateFactory)
-         throws OrmException {
+  private List<SubmitRecord> logInvalidResult(Term rule, Term record) {
+    return logRuleError("Submit rule " + rule + " for change " + change.getId()
+        + " of " + getProject().getName() + " output invalid result: " + record);
+  }
 
-    CanSubmitResult result = canSubmit(patchSetId);
-    if (result != CanSubmitResult.OK) {
-      return result;
-    }
+  private List<SubmitRecord> logRuleError(String err, Exception e) {
+    log.error(err, e);
+    return ruleError("Error evaluating project rules, check server log");
+  }
 
-    final List<PatchSetApproval> allApprovals =
-        new ArrayList<PatchSetApproval>(db.patchSetApprovals().byPatchSet(
-            patchSetId).toList());
-    final PatchSetApproval myAction =
-        ChangeUtil.createSubmitApproval(patchSetId,
-            (IdentifiedUser) getCurrentUser(), db);
+  private List<SubmitRecord> logRuleError(String err) {
+    log.error(err);
+    return ruleError("Error evaluating project rules, check server log");
+  }
 
-    final ApprovalType actionType =
-        approvalTypes.getApprovalType(myAction.getCategoryId());
-    if (actionType == null || !actionType.getCategory().isAction()) {
-      return new CanSubmitResult("Invalid action " + myAction.getCategoryId());
-    }
+  private List<SubmitRecord> ruleError(String err) {
+    SubmitRecord rec = new SubmitRecord();
+    rec.status = SubmitRecord.Status.RULE_ERROR;
+    rec.errorMessage = err;
+    return Collections.singletonList(rec);
+  }
 
-    final FunctionState fs =
-        functionStateFactory.create(change, patchSetId, allApprovals);
-    for (ApprovalType c : approvalTypes.getApprovalTypes()) {
-      CategoryFunction.forCategory(c.getCategory()).run(c, fs);
+  private void appliedBy(SubmitRecord.Label label, Term status) {
+    if (status.isStructure() && status.arity() == 1) {
+      Term who = status.arg(0);
+      if (isUser(who)) {
+        label.appliedBy = new Account.Id(((IntegerTerm) who.arg(0)).intValue());
+      }
     }
-    if (!CategoryFunction.forCategory(actionType.getCategory()).isValid(
-        getCurrentUser(), actionType, fs)) {
-      return new CanSubmitResult(actionType.getCategory().getName()
-          + " not permitted");
+  }
+
+  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");
     }
-    fs.normalize(actionType, myAction);
-    if (myAction.getValue() <= 0) {
-      return new CanSubmitResult(actionType.getCategory().getName()
-          + " not permitted");
+    return ps.isDraft();
+  }
+
+  private static boolean isUser(Term who) {
+    return who.isStructure()
+        && who.arity() == 1
+        && who.name().equals("user")
+        && who.arg(0).isInteger();
+  }
+
+  private static Term toListTerm(List<Term> terms) {
+    Term list = Prolog.Nil;
+    for (int i = terms.size() - 1; i >= 0; i--) {
+      list = new ListTerm(terms.get(i), list);
     }
-    return CanSubmitResult.OK;
+    return list;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
new file mode 100644
index 0000000..65bdc1e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
@@ -0,0 +1,264 @@
+// 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.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.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.git.RepositoryCaseMismatchException;
+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.RefUpdate.Result;
+import org.eclipse.jgit.lib.Repository;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Set;
+
+
+/** 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();
+    final Project.NameKey nameKey = createProjectArgs.getProject();
+    try {
+      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 (RepositoryCaseMismatchException e) {
+      throw new ProjectCreationFailedException("Cannot create " + nameKey.get()
+          + " because the name is already occupied by another project."
+          + " The other project has the same name, only spelled in a"
+          + " different case.", e);
+    } catch (RepositoryNotFoundException badName) {
+      throw new ProjectCreationFailedException("Cannot create " + nameKey, badName);
+    } catch (IllegalStateException err) {
+      try {
+        final Repository repo = repoManager.openRepository(nameKey);
+        try {
+          if (repo.getObjectDatabase().exists()) {
+            throw new ProjectCreationFailedException("project \"" + nameKey + "\" exists");
+          }
+        } finally {
+          repo.close();
+        }
+      } catch (RepositoryNotFoundException doesNotExist) {
+        final String msg = "Cannot create " + nameKey;
+        log.error(msg, err);
+        throw new ProjectCreationFailedException(msg, err);
+      }
+    } catch (Exception e) {
+      final String msg = "Cannot create " + nameKey;
+      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();
+    }
+  }
+}
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..98adf85
--- /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.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.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/schema/Schema_50.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/InvalidChangeOperationException.java
similarity index 67%
copy from gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_50.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/project/InvalidChangeOperationException.java
index 4a90c1f..e5c6ba5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_50.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/InvalidChangeOperationException.java
@@ -12,14 +12,14 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.schema;
+package com.google.gerrit.server.project;
 
-import com.google.inject.Inject;
-import com.google.inject.Provider;
 
-public class Schema_50 extends SchemaVersion {
-  @Inject
-  Schema_50(Provider<Schema_49> prior) {
-    super(prior);
+/** Indicates the change operation is not currently valid. */
+public class InvalidChangeOperationException extends Exception {
+  private static final long serialVersionUID = 1L;
+
+  public InvalidChangeOperationException(String msg) {
+    super(msg);
   }
-}
\ No newline at end of file
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java
new file mode 100644
index 0000000..95a8b77
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java
@@ -0,0 +1,273 @@
+// 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.project;
+
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.util.TreeFormatter;
+import com.google.inject.Inject;
+
+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 org.kohsuke.args4j.Option;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.BufferedWriter;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.io.UnsupportedEncodingException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.SortedSet;
+import java.util.TreeMap;
+import java.util.TreeSet;
+
+/** List projects visible to the calling user. */
+public class ListProjects {
+  private static final Logger log = LoggerFactory.getLogger(ListProjects.class);
+
+  public static enum FilterType {
+    CODE {
+      @Override
+      boolean matches(Repository git) throws IOException {
+        return !PERMISSIONS.matches(git);
+      }
+    },
+    PERMISSIONS {
+      @Override
+      boolean matches(Repository git) throws IOException {
+        Ref head = git.getRef(Constants.HEAD);
+        return head != null
+          && head.isSymbolic()
+          && GitRepositoryManager.REF_CONFIG.equals(head.getLeaf().getName());
+      }
+    },
+    ALL {
+      @Override
+      boolean matches(Repository git) {
+        return true;
+      }
+    };
+
+    abstract boolean matches(Repository git) throws IOException;
+  }
+
+  private final CurrentUser currentUser;
+  private final ProjectCache projectCache;
+  private final GitRepositoryManager repoManager;
+  private final ProjectNode.Factory projectNodeFactory;
+
+  @Option(name = "--show-branch", aliases = {"-b"}, multiValued = true,
+      usage = "displays the sha of each project in the specified branch")
+  private List<String> showBranch;
+
+  @Option(name = "--tree", aliases = {"-t"}, usage =
+      "displays project inheritance in a tree-like format\n"
+      + "this option does not work together with the show-branch option")
+  private boolean showTree;
+
+  @Option(name = "--type", usage = "type of project")
+  private FilterType type = FilterType.CODE;
+
+  @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;
+
+  @Inject
+  protected ListProjects(CurrentUser currentUser, ProjectCache projectCache,
+      GitRepositoryManager repoManager,
+      ProjectNode.Factory projectNodeFactory) {
+    this.currentUser = currentUser;
+    this.projectCache = projectCache;
+    this.repoManager = repoManager;
+    this.projectNodeFactory = projectNodeFactory;
+  }
+
+  public List<String> getShowBranch() {
+    return showBranch;
+  }
+
+  public boolean isShowTree() {
+    return showTree;
+  }
+
+  public boolean isShowDescription() {
+    return showDescription;
+  }
+
+  public void display(OutputStream out) {
+    final PrintWriter stdout;
+    try {
+      stdout = new PrintWriter(new BufferedWriter(new OutputStreamWriter(out, "UTF-8")));
+    } catch (UnsupportedEncodingException e) {
+      // Our encoding is required by the specifications for the runtime.
+      throw new RuntimeException("JVM lacks UTF-8 encoding", e);
+    }
+
+    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);
+        if (e == null) {
+          // If we can't get it from the cache, pretend its not present.
+          //
+          continue;
+        }
+
+        final ProjectControl pctl = e.controlFor(currentUser);
+        final boolean isVisible = pctl.isVisible() || (all && pctl.isOwner());
+        if (showTree) {
+          treeMap.put(projectName,
+              projectNodeFactory.create(pctl.getProject(), isVisible));
+          continue;
+        }
+
+        if (!isVisible) {
+          // Require the project itself to be visible to the user.
+          //
+          continue;
+        }
+
+        try {
+          if (showBranch != null) {
+            Repository git = repoManager.openRepository(projectName);
+            try {
+              if (!type.matches(git)) {
+                continue;
+              }
+
+              List<Ref> refs = getBranchRefs(projectName, pctl);
+              if (!hasValidRef(refs)) {
+               continue;
+              }
+
+              for (Ref ref : refs) {
+                if (ref == null) {
+                  // Print stub (forty '-' symbols)
+                  stdout.print("----------------------------------------");
+                } else {
+                  stdout.print(ref.getObjectId().name());
+                }
+                stdout.print(' ');
+              }
+            } finally {
+              git.close();
+            }
+
+          } else if (type != FilterType.ALL) {
+            Repository git = repoManager.openRepository(projectName);
+            try {
+              if (!type.matches(git)) {
+                continue;
+              }
+            } finally {
+              git.close();
+            }
+          }
+
+        } catch (RepositoryNotFoundException err) {
+          // If the Git repository is gone, the project doesn't actually exist anymore.
+          continue;
+        } catch (IOException err) {
+          log.warn("Unexpected error reading " + projectName, err);
+          continue;
+        }
+
+        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) {
+        printProjectTree(stdout, treeMap);
+      }
+    } finally {
+      stdout.flush();
+    }
+  }
+
+  private void printProjectTree(final PrintWriter stdout,
+      final TreeMap<Project.NameKey, ProjectNode> treeMap) {
+    final SortedSet<ProjectNode> sortedNodes = new TreeSet<ProjectNode>();
+
+    // Builds the inheritance tree using a list.
+    //
+    for (final ProjectNode key : treeMap.values()) {
+      if (key.isAllProjects()) {
+        sortedNodes.add(key);
+        continue;
+      }
+
+      ProjectNode node = treeMap.get(key.getParentName());
+      if (node != null) {
+        node.addChild(key);
+      } else {
+        sortedNodes.add(key);
+      }
+    }
+
+    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 {
+      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())
+                || (all && projectControl.isOwner())) {
+            result[i] = ref;
+          }
+        }
+      } finally {
+        git.close();
+      }
+    } catch (IOException ioe) {
+      // Fall through and return what is available.
+    }
+    return Arrays.asList(result);
+  }
+
+  private static boolean hasValidRef(List<Ref> refs) {
+    for (int i = 0; i < refs.size(); i++) {
+      if (refs.get(i) != null) {
+        return true;
+      }
+    }
+    return false;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/NoSuchChangeException.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/NoSuchChangeException.java
index d5a22fd..f766c7f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/NoSuchChangeException.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/NoSuchChangeException.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.project;
 
-import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.reviewdb.client.Change;
 
 /** Indicates the change does not exist. */
 public class NoSuchChangeException extends Exception {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/NoSuchProjectException.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/NoSuchProjectException.java
index a1448d5..2ac494d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/NoSuchProjectException.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/NoSuchProjectException.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.project;
 
-import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.reviewdb.client.Project;
 
 /** Indicates the project does not exist. */
 public class NoSuchProjectException extends Exception {
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
new file mode 100644
index 0000000..19a42ae
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/PerRequestProjectControlCache.java
@@ -0,0 +1,57 @@
+// 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.client.Project;
+import com.google.gerrit.server.CurrentUser;
+import com.google.inject.Inject;
+import com.google.inject.servlet.RequestScoped;
+
+import java.util.HashMap;
+import java.util.Map;
+
+/** Caches {@link ProjectControl} objects for the current user of the request. */
+@RequestScoped
+public class PerRequestProjectControlCache {
+  private final ProjectCache projectCache;
+  private final CurrentUser user;
+  private final Map<Project.NameKey, ProjectControl> controls;
+
+  @Inject
+  PerRequestProjectControlCache(ProjectCache projectCache,
+      CurrentUser userProvider) {
+    this.projectCache = projectCache;
+    this.user = userProvider;
+    this.controls = new HashMap<Project.NameKey, ProjectControl>();
+  }
+
+  ProjectControl get(Project.NameKey nameKey) throws NoSuchProjectException {
+    ProjectControl ctl = controls.get(nameKey);
+    if (ctl == null) {
+      ProjectState p = projectCache.get(nameKey);
+      if (p == null) {
+        throw new NoSuchProjectException(nameKey);
+      }
+      ctl = p.controlFor(user);
+      controls.put(nameKey, ctl);
+    }
+    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/PermissionCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/PermissionCollection.java
new file mode 100644
index 0000000..e416ed7
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/PermissionCollection.java
@@ -0,0 +1,222 @@
+// 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 static com.google.gerrit.server.project.RefControl.isRE;
+
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+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;
+
+/**
+ * Effective permissions applied to a reference in a project.
+ * <p>
+ * A collection may be user specific if a matching {@link AccessSection} uses
+ * "${username}" in its name. The permissions granted in that section may only
+ * be granted to the username that appears in the reference name, and also only
+ * if the user is a member of the relevant group.
+ */
+public class PermissionCollection {
+  @Singleton
+  public static class Factory {
+    private final SectionSortCache sorter;
+
+    @Inject
+    Factory(SectionSortCache sorter) {
+      this.sorter = sorter;
+    }
+
+    /**
+     * Get all permissions that apply to a reference.
+     *
+     * @param matcherList collection of sections that should be considered, in
+     *        priority order (project specific definitions must appear before
+     *        inherited ones).
+     * @param ref reference being accessed.
+     * @param username if the reference is a per-user reference, access sections
+     *        using the parameter variable "${username}" will first have {@code
+     *        username} inserted into them before seeing if they apply to the
+     *        reference named by {@code ref}. If null, per-user references are
+     *        ignored.
+     * @return map of permissions that apply to this reference, keyed by
+     *         permission name.
+     */
+    PermissionCollection filter(Iterable<SectionMatcher> matcherList,
+        String ref, String username) {
+      if (isRE(ref)) {
+        ref = RefControl.shortestExample(ref);
+      } else if (ref.endsWith("/*")) {
+        ref = ref.substring(0, ref.length() - 1);
+      }
+
+      boolean perUser = false;
+      List<AccessSection> sections = new ArrayList<AccessSection>();
+      for (SectionMatcher matcher : matcherList) {
+        // If the matcher has to expand parameters and its prefix matches the
+        // reference there is a very good chance the reference is actually user
+        // specific, even if the matcher does not match the reference. Since its
+        // difficult to prove this is true all of the time, use an approximation
+        // to prevent reuse of collections across users accessing the same
+        // reference at the same time.
+        //
+        // This check usually gets caching right, as most per-user references
+        // use a common prefix like "refs/sandbox/" or "refs/heads/users/"
+        // that will never be shared with non-user references, and the per-user
+        // references are usually less frequent than the non-user references.
+        //
+        if (username != null && !perUser
+            && matcher instanceof SectionMatcher.ExpandParameters) {
+          perUser = ((SectionMatcher.ExpandParameters) matcher).matchPrefix(ref);
+        }
+
+        if (matcher.match(ref, username)) {
+          sections.add(matcher.section);
+        }
+      }
+      sorter.sort(ref, sections);
+
+      Set<SeenRule> seen = new HashSet<SeenRule>();
+      Set<SeenRule> seenBlockingRules = new HashSet<SeenRule>();
+      Set<String> exclusiveGroupPermissions = new HashSet<String>();
+
+      HashMap<String, List<PermissionRule>> permissions =
+          new HashMap<String, List<PermissionRule>>();
+      for (AccessSection section : sections) {
+        for (Permission permission : section.getPermissions()) {
+          boolean exclusivePermissionExists =
+              exclusiveGroupPermissions.contains(permission.getName());
+
+          for (PermissionRule rule : permission.getRules()) {
+            SeenRule s = new SeenRule(section, permission, rule);
+            boolean addRule = false;
+            if (rule.isBlock()) {
+              addRule = seenBlockingRules.add(s);
+            } else {
+              addRule = seen.add(s) && !rule.isDeny() && !exclusivePermissionExists;
+            }
+            if (addRule) {
+              List<PermissionRule> r = permissions.get(permission.getName());
+              if (r == null) {
+                r = new ArrayList<PermissionRule>(2);
+                permissions.put(permission.getName(), r);
+              }
+              r.add(rule);
+            }
+          }
+
+          if (permission.getExclusiveGroup()) {
+            exclusiveGroupPermissions.add(permission.getName());
+          }
+        }
+      }
+
+      return new PermissionCollection(permissions, perUser ? username : null);
+    }
+  }
+
+  private final Map<String, List<PermissionRule>> rules;
+  private final String username;
+
+  private PermissionCollection(Map<String, List<PermissionRule>> rules,
+      String username) {
+    this.rules = rules;
+    this.username = username;
+  }
+
+  /**
+   * @return true if a "${username}" pattern might need to be expanded to build
+   *         this collection, making the results user specific.
+   */
+  public boolean isUserSpecific() {
+    return username != null;
+  }
+
+  /**
+   * Obtain all permission rules for a given type of permission.
+   *
+   * @param permissionName type of permission.
+   * @return all rules that apply to this reference, for any group. Never null;
+   *         the empty list is returned when there are no rules for the requested
+   *         permission name.
+   */
+  public List<PermissionRule> getPermission(String permissionName) {
+    List<PermissionRule> r = rules.get(permissionName);
+    return r != null ? r : Collections.<PermissionRule> emptyList();
+  }
+
+  /**
+   * Obtain all declared permission rules that match the reference.
+   *
+   * @return all rules. The collection will iterate a permission if it was
+   *         declared in the project configuration, either directly or
+   *         inherited. If the project owner did not use a known permission (for
+   *         example {@link Permission#FORGE_SERVER}, then it will not be
+   *         represented in the result even if {@link #getPermission(String)}
+   *         returns an empty list for the same permission.
+   */
+  public Iterable<Map.Entry<String, List<PermissionRule>>> getDeclaredPermissions() {
+    return rules.entrySet();
+  }
+
+  /** Tracks whether or not a permission has been overridden. */
+  private static class SeenRule {
+    final String refPattern;
+    final String permissionName;
+    final AccountGroup.UUID group;
+
+    SeenRule(AccessSection section, Permission permission, PermissionRule rule) {
+      refPattern = section.getName();
+      permissionName = permission.getName();
+      group = rule.getGroup().getUUID();
+    }
+
+    @Override
+    public int hashCode() {
+      int hc = refPattern.hashCode();
+      hc = hc * 31 + permissionName.hashCode();
+      if (group != null) {
+        hc = hc * 31 + group.hashCode();
+      }
+      return hc;
+    }
+
+    @Override
+    public boolean equals(Object other) {
+      if (other instanceof SeenRule) {
+        SeenRule a = this;
+        SeenRule b = (SeenRule) other;
+        return a.refPattern.equals(b.refPattern) //
+            && a.permissionName.equals(b.permissionName) //
+            && eq(a.group, b.group);
+      }
+      return false;
+    }
+
+    private boolean eq(AccountGroup.UUID a, AccountGroup.UUID b) {
+      return a != null && b != null && a.equals(b);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCache.java
index 35b5ee5..70f4013 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCache.java
@@ -14,10 +14,13 @@
 
 package com.google.gerrit.server.project;
 
-import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.reviewdb.client.Project;
 
 /** Cache of project information, including access rights. */
 public interface ProjectCache {
+  /** @return the parent state for all projects on this server. */
+  public ProjectState getAllProjects();
+
   /**
    * Get the cached data for a project by its unique name.
    *
@@ -29,6 +32,17 @@
   /** Invalidate the cached information about the given project. */
   public void evict(Project p);
 
-  /** Invalidate the cached information about all projects. */
-  public void evictAll();
+  /** @return sorted iteration of projects. */
+  public abstract Iterable<Project.NameKey> all();
+
+  /**
+   * Filter the set of registered project names by common prefix.
+   *
+   * @param prefix common prefix.
+   * @return sorted iteration of projects sharing the same prefix.
+   */
+  public abstract Iterable<Project.NameKey> byName(String prefix);
+
+  /** Notify the cache that a new project was constructed. */
+  public void onCreateProject(Project.NameKey newProjectName);
 }
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 48eef87..51d8fb2 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectCacheImpl.java
@@ -14,46 +14,82 @@
 
 package com.google.gerrit.server.project;
 
-import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.RefRight;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.cache.Cache;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.cache.EntryCreator;
-import com.google.gwtorm.client.SchemaFactory;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.ProjectConfig;
 import com.google.inject.Inject;
 import com.google.inject.Module;
 import com.google.inject.Singleton;
 import com.google.inject.TypeLiteral;
 import com.google.inject.name.Named;
 
-import java.util.Collection;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Repository;
+
 import java.util.Collections;
+import java.util.Iterator;
+import java.util.NoSuchElementException;
+import java.util.SortedSet;
+import java.util.TreeSet;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
 
 /** Cache of project information, including access rights. */
 @Singleton
 public class ProjectCacheImpl implements ProjectCache {
   private static final String CACHE_NAME = "projects";
+  private static final String CACHE_LIST = "project_list";
 
   public static Module module() {
     return new CacheModule() {
       @Override
       protected void configure() {
-        final TypeLiteral<Cache<Project.NameKey, ProjectState>> type =
+        final TypeLiteral<Cache<Project.NameKey, ProjectState>> nameType =
             new TypeLiteral<Cache<Project.NameKey, ProjectState>>() {};
-        core(type, CACHE_NAME).populateWith(Loader.class);
+        core(nameType, CACHE_NAME).populateWith(Loader.class);
+
+        final TypeLiteral<Cache<ListKey, SortedSet<Project.NameKey>>> listType =
+            new TypeLiteral<Cache<ListKey, SortedSet<Project.NameKey>>>() {};
+        core(listType, CACHE_LIST).populateWith(Lister.class);
+
         bind(ProjectCacheImpl.class);
         bind(ProjectCache.class).to(ProjectCacheImpl.class);
       }
     };
   }
 
+  private final AllProjectsName allProjectsName;
   private final Cache<Project.NameKey, ProjectState> byName;
+  private final Cache<ListKey,SortedSet<Project.NameKey>> list;
+  private final Lock listLock;
+  private final ProjectCacheClock clock;
 
   @Inject
   ProjectCacheImpl(
-      @Named(CACHE_NAME) final Cache<Project.NameKey, ProjectState> byName) {
+      final AllProjectsName allProjectsName,
+      @Named(CACHE_NAME) final Cache<Project.NameKey, ProjectState> byName,
+      @Named(CACHE_LIST) final Cache<ListKey, SortedSet<Project.NameKey>> list,
+      ProjectCacheClock clock) {
+    this.allProjectsName = allProjectsName;
     this.byName = byName;
+    this.list = list;
+    this.listLock = new ReentrantLock(true /* fair */);
+    this.clock = clock;
+  }
+
+  @Override
+  public ProjectState getAllProjects() {
+    ProjectState state = get(allProjectsName);
+    if (state == null) {
+      // This should never occur, the server must have this
+      // project to process anything.
+      throw new IllegalStateException("Missing project " + allProjectsName);
+    }
+    return state;
   }
 
   /**
@@ -63,7 +99,12 @@
    * @return the cached data; null if no such project exists.
    */
   public ProjectState get(final Project.NameKey projectName) {
-    return byName.get(projectName);
+    ProjectState state = byName.get(projectName);
+    if (state != null && state.needsRefresh(clock.read())) {
+      byName.remove(projectName);
+      state = byName.get(projectName);
+    }
+    return state;
   }
 
   /** Invalidate the cached information about the given project. */
@@ -73,38 +114,120 @@
     }
   }
 
-  /** Invalidate the cached information about all projects. */
-  public void evictAll() {
-    byName.removeAll();
+  @Override
+  public void onCreateProject(Project.NameKey newProjectName) {
+    listLock.lock();
+    try {
+      SortedSet<Project.NameKey> n = list.get(ListKey.ALL);
+      n = new TreeSet<Project.NameKey>(n);
+      n.add(newProjectName);
+      list.put(ListKey.ALL, Collections.unmodifiableSortedSet(n));
+    } finally {
+      listLock.unlock();
+    }
+  }
+
+  @Override
+  public Iterable<Project.NameKey> all() {
+    return list.get(ListKey.ALL);
+  }
+
+  @Override
+  public Iterable<Project.NameKey> byName(final String pfx) {
+    return new Iterable<Project.NameKey>() {
+      @Override
+      public Iterator<Project.NameKey> iterator() {
+        return new Iterator<Project.NameKey>() {
+          private Project.NameKey next;
+          private Iterator<Project.NameKey> itr =
+              list.get(ListKey.ALL).tailSet(new Project.NameKey(pfx)).iterator();
+
+          @Override
+          public boolean hasNext() {
+            if (next != null) {
+              return true;
+            }
+
+            if (!itr.hasNext()) {
+              return false;
+            }
+
+            Project.NameKey r = itr.next();
+            if (r.get().startsWith(pfx)) {
+              next = r;
+              return true;
+            } else {
+              itr = Collections.<Project.NameKey> emptyList().iterator();
+              return false;
+            }
+          }
+
+          @Override
+          public Project.NameKey next() {
+            if (!hasNext()) {
+              throw new NoSuchElementException();
+            }
+
+            Project.NameKey r = next;
+            next = null;
+            return r;
+          }
+
+          @Override
+          public void remove() {
+            throw new UnsupportedOperationException();
+          }
+        };
+      }
+    };
   }
 
   static class Loader extends EntryCreator<Project.NameKey, ProjectState> {
     private final ProjectState.Factory projectStateFactory;
-    private final SchemaFactory<ReviewDb> schema;
+    private final GitRepositoryManager mgr;
 
     @Inject
-    Loader(ProjectState.Factory psf, SchemaFactory<ReviewDb> sf) {
+    Loader(ProjectState.Factory psf, GitRepositoryManager g) {
       projectStateFactory = psf;
-      schema = sf;
+      mgr = g;
     }
 
     @Override
     public ProjectState createEntry(Project.NameKey key) throws Exception {
-      final ReviewDb db = schema.open();
       try {
-        final Project p = db.projects().get(key);
-        if (p == null) {
-          return null;
+        Repository git = mgr.openRepository(key);
+        try {
+          final ProjectConfig cfg = new ProjectConfig(key);
+          cfg.load(git);
+          return projectStateFactory.create(cfg);
+        } finally {
+          git.close();
         }
 
-        final Collection<RefRight> rights =
-            Collections.unmodifiableCollection(db.refRights().byProject(
-                p.getNameKey()).toList());
-
-        return projectStateFactory.create(p, rights);
-      } finally {
-        db.close();
+      } catch (RepositoryNotFoundException notFound) {
+        return null;
       }
     }
   }
+
+  static class ListKey {
+    static final ListKey ALL = new ListKey();
+
+    private ListKey() {
+    }
+  }
+
+  static class Lister extends EntryCreator<ListKey, SortedSet<Project.NameKey>> {
+    private final GitRepositoryManager mgr;
+
+    @Inject
+    Lister(GitRepositoryManager mgr) {
+      this.mgr = mgr;
+    }
+
+    @Override
+    public SortedSet<Project.NameKey> createEntry(ListKey key) throws Exception {
+      return mgr.list();
+    }
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
index 2a55019..29a6432 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectControl.java
@@ -14,28 +14,51 @@
 
 package com.google.gerrit.server.project;
 
-import static com.google.gerrit.common.CollectionsUtil.*;
-import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.ApprovalCategory;
-import com.google.gerrit.reviewdb.Branch;
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.RefRight;
-import com.google.gerrit.reviewdb.SystemConfig;
+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.reviewdb.client.AbstractAgreement;
+import com.google.gerrit.reviewdb.client.AccountAgreement;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupAgreement;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.ContributorAgreement;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.ReplicationUser;
+import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.GitReceivePackGroups;
 import com.google.gerrit.server.config.GitUploadPackGroups;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
+import javax.annotation.Nullable;
+
 /** Access control management for a user accessing a project's data. */
 public class ProjectControl {
+  private static final Logger log =
+      LoggerFactory.getLogger(ProjectControl.class);
+
   public static final int VISIBLE = 1 << 0;
   public static final int OWNER = 1 << 1;
 
@@ -58,22 +81,16 @@
   }
 
   public static class Factory {
-    private final ProjectCache projectCache;
-    private final Provider<CurrentUser> user;
+    private final Provider<PerRequestProjectControlCache> userCache;
 
     @Inject
-    Factory(final ProjectCache pc, final Provider<CurrentUser> cu) {
-      projectCache = pc;
-      user = cu;
+    Factory(Provider<PerRequestProjectControlCache> uc) {
+      userCache = uc;
     }
 
     public ProjectControl controlFor(final Project.NameKey nameKey)
         throws NoSuchProjectException {
-      final ProjectState p = projectCache.get(nameKey);
-      if (p == null) {
-        throw new NoSuchProjectException(nameKey);
-      }
-      return p.controlFor(user.get());
+      return userCache.get().get(nameKey);
     }
 
     public ProjectControl validateFor(final Project.NameKey nameKey)
@@ -103,34 +120,42 @@
     ProjectControl create(CurrentUser who, ProjectState ps);
   }
 
-  private final SystemConfig systemConfig;
-  private final Set<AccountGroup.Id> uploadGroups;
-  private final Set<AccountGroup.Id> receiveGroups;
+  private final Set<AccountGroup.UUID> uploadGroups;
+  private final Set<AccountGroup.UUID> receiveGroups;
 
-  private final RefControl.Factory refControlFactory;
+  private final String canonicalWebUrl;
+  private final SchemaFactory<ReviewDb> schema;
   private final CurrentUser user;
   private final ProjectState state;
+  private final GroupCache groupCache;
+  private final PermissionCollection.Factory permissionFilter;
+
+  private List<SectionMatcher> allSections;
+  private Map<String, RefControl> refControls;
+  private Boolean declaredOwner;
 
   @Inject
-  ProjectControl(final SystemConfig systemConfig,
-      @GitUploadPackGroups Set<AccountGroup.Id> uploadGroups,
-      @GitReceivePackGroups Set<AccountGroup.Id> receiveGroups,
-      final RefControl.Factory refControlFactory,
+  ProjectControl(@GitUploadPackGroups Set<AccountGroup.UUID> uploadGroups,
+      @GitReceivePackGroups Set<AccountGroup.UUID> receiveGroups,
+      final SchemaFactory<ReviewDb> schema, final GroupCache groupCache,
+      final PermissionCollection.Factory permissionFilter,
+      @CanonicalWebUrl @Nullable final String canonicalWebUrl,
       @Assisted CurrentUser who, @Assisted ProjectState ps) {
-    this.systemConfig = systemConfig;
     this.uploadGroups = uploadGroups;
     this.receiveGroups = receiveGroups;
-    this.refControlFactory = refControlFactory;
+    this.schema = schema;
+    this.groupCache = groupCache;
+    this.permissionFilter = permissionFilter;
+    this.canonicalWebUrl = canonicalWebUrl;
     user = who;
     state = ps;
   }
 
-  public ProjectControl forAnonymousUser() {
-    return state.controlForAnonymousUser();
-  }
-
-  public ProjectControl forUser(final CurrentUser who) {
-    return state.controlFor(who);
+  public ProjectControl forUser(CurrentUser who) {
+    ProjectControl r = state.controlFor(who);
+    // Not per-user, and reusing saves lookup time.
+    r.allSections = allSections;
+    return r;
   }
 
   public ChangeControl controlFor(final Change change) {
@@ -142,7 +167,17 @@
   }
 
   public RefControl controlForRef(String refName) {
-    return refControlFactory.create(this, refName);
+    if (refControls == null) {
+      refControls = new HashMap<String, RefControl>();
+    }
+    RefControl ctl = refControls.get(refName);
+    if (ctl == null) {
+      PermissionCollection relevant =
+          permissionFilter.filter(access(), refName, user.getUserName());
+      ctl = new RefControl(this, refName, relevant);
+      refControls.put(refName, ctl);
+    }
+    return ctl;
   }
 
   public CurrentUser getCurrentUser() {
@@ -154,93 +189,257 @@
   }
 
   public Project getProject() {
-    return getProjectState().getProject();
+    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(ApprovalCategory.READ, (short) 1);
+    return (visibleForReplication()
+        || canPerformOnAnyRef(Permission.READ)) && !isHidden();
   }
 
   public boolean canAddRefs() {
-    return (canPerformOnAnyRef(ApprovalCategory.PUSH_HEAD, ApprovalCategory.PUSH_HEAD_CREATE)
+    return (canPerformOnAnyRef(Permission.CREATE)
         || isOwnerAnyRef());
   }
 
   /** Can this user see all the refs in this projects? */
   public boolean allRefsAreVisible() {
     return visibleForReplication()
-        || canPerformOnAllRefs(ApprovalCategory.READ, (short) 1);
+        || canPerformOnAllRefs(Permission.READ);
   }
 
   /** Is this project completely visible for replication? */
   boolean visibleForReplication() {
-    return getCurrentUser() instanceof ReplicationUser
-        && ((ReplicationUser) getCurrentUser()).isEverythingVisible();
+    return user instanceof ReplicationUser
+        && ((ReplicationUser) user).isEverythingVisible();
   }
 
   /** Is this user a project owner? Ownership does not imply {@link #isVisible()} */
   public boolean isOwner() {
-    return controlForRef(RefRight.ALL).isOwner()
-        || getCurrentUser().isAdministrator();
+    return isDeclaredOwner()
+      || user.getCapabilities().canAdministrateServer();
+  }
+
+  private boolean isDeclaredOwner() {
+    if (declaredOwner == null) {
+      declaredOwner = state.isOwner(user.getEffectiveGroups());
+    }
+    return declaredOwner;
   }
 
   /** Does this user have ownership on at least one reference name? */
   public boolean isOwnerAnyRef() {
-    return canPerformOnAnyRef(ApprovalCategory.OWN, (short) 1)
-        || getCurrentUser().isAdministrator();
+    return canPerformOnAnyRef(Permission.OWNER)
+        || user.getCapabilities().canAdministrateServer();
   }
 
   /** @return true if the user can upload to at least one reference */
-  public boolean canPushToAtLeastOneRef() {
-    return canPerformOnAnyRef(ApprovalCategory.READ, (short) 2)
-        || canPerformOnAnyRef(ApprovalCategory.PUSH_HEAD, (short) 1)
-        || canPerformOnAnyRef(ApprovalCategory.PUSH_TAG, (short) 1);
+  public Capable canPushToAtLeastOneRef() {
+    if (! canPerformOnAnyRef(Permission.PUSH) &&
+        ! canPerformOnAnyRef(Permission.PUSH_TAG)) {
+      String pName = state.getProject().getName();
+      return new Capable("Upload denied for project '" + pName + "'");
+    }
+    Project project = state.getProject();
+    if (project.isUseContributorAgreements()) {
+      try {
+        return verifyActiveContributorAgreement();
+      } catch (OrmException e) {
+        log.error("Cannot query database for agreements", e);
+        return new Capable("Cannot verify contribution agreement");
+      }
+    }
+    return Capable.OK;
   }
 
-  // TODO (anatol.pomazau): Try to merge this method with similar RefRightsForPattern#canPerform
-  private boolean canPerformOnAnyRef(ApprovalCategory.Id actionId,
-      short requireValue) {
-    final Set<AccountGroup.Id> groups = getEffectiveUserGroups();
+  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;
+  }
 
-    for (final RefRight pr : state.getAllRights(actionId, true)) {
-      if (groups.contains(pr.getAccountGroupId())
-          && pr.getMaxValue() >= requireValue) {
-        return true;
+  private Capable verifyActiveContributorAgreement() throws OrmException {
+    if (! (user instanceof IdentifiedUser)) {
+      return new Capable("Must be logged in to verify Contributor Agreement");
+    }
+    final IdentifiedUser iUser = (IdentifiedUser) user;
+    final ReviewDb db = schema.open();
+
+    AbstractAgreement bestAgreement = null;
+    ContributorAgreement bestCla = null;
+    try {
+
+      OUTER: for (AccountGroup.UUID groupUUID : iUser.getEffectiveGroups().getKnownGroups()) {
+        AccountGroup group = groupCache.get(groupUUID);
+        if (group == null) {
+          continue;
+        }
+
+        final List<AccountGroupAgreement> temp =
+            db.accountGroupAgreements().byGroup(group.getId()).toList();
+
+        Collections.reverse(temp);
+
+        for (final AccountGroupAgreement a : temp) {
+          final ContributorAgreement cla =
+              db.contributorAgreements().get(a.getAgreementId());
+          if (cla == null) {
+            continue;
+          }
+
+          bestAgreement = a;
+          bestCla = cla;
+          break OUTER;
+        }
+      }
+
+      if (bestAgreement == null) {
+        final List<AccountAgreement> temp =
+            db.accountAgreements().byAccount(iUser.getAccountId()).toList();
+
+        Collections.reverse(temp);
+
+        for (final AccountAgreement a : temp) {
+          final ContributorAgreement cla =
+              db.contributorAgreements().get(a.getAgreementId());
+          if (cla == null) {
+            continue;
+          }
+
+          bestAgreement = a;
+          bestCla = cla;
+          break;
+        }
+      }
+    } finally {
+      db.close();
+    }
+
+
+    if (bestCla != null && !bestCla.isActive()) {
+      final StringBuilder msg = new StringBuilder();
+      msg.append(bestCla.getShortName());
+      msg.append(" contributor agreement is expired.\n");
+      if (canonicalWebUrl != null) {
+        msg.append("\nPlease complete a new agreement");
+        msg.append(":\n\n  ");
+        msg.append(canonicalWebUrl);
+        msg.append("#");
+        msg.append(PageLinks.SETTINGS_AGREEMENTS);
+        msg.append("\n");
+      }
+      msg.append("\n");
+      return new Capable(msg.toString());
+    }
+
+    if (bestCla != null && bestCla.isRequireContactInformation()) {
+      boolean fail = false;
+      fail |= missing(iUser.getAccount().getFullName());
+      fail |= missing(iUser.getAccount().getPreferredEmail());
+      fail |= !iUser.getAccount().isContactFiled();
+
+      if (fail) {
+        final StringBuilder msg = new StringBuilder();
+        msg.append(bestCla.getShortName());
+        msg.append(" contributor agreement requires");
+        msg.append(" current contact information.\n");
+        if (canonicalWebUrl != null) {
+          msg.append("\nPlease review your contact information");
+          msg.append(":\n\n  ");
+          msg.append(canonicalWebUrl);
+          msg.append("#");
+          msg.append(PageLinks.SETTINGS_CONTACT);
+          msg.append("\n");
+        }
+        msg.append("\n");
+        return new Capable(msg.toString());
+      }
+    }
+
+    if (bestAgreement != null) {
+      switch (bestAgreement.getStatus()) {
+        case VERIFIED:
+          return Capable.OK;
+        case REJECTED:
+          return new Capable(bestCla.getShortName()
+              + " contributor agreement was rejected."
+              + "\n       (rejected on " + bestAgreement.getReviewedOn()
+              + ")\n");
+        case NEW:
+          return new Capable(bestCla.getShortName()
+              + " contributor agreement is still pending review.\n");
+      }
+    }
+
+    final StringBuilder msg = new StringBuilder();
+    msg.append(" A Contributor Agreement must be completed before uploading");
+    if (canonicalWebUrl != null) {
+      msg.append(":\n\n  ");
+      msg.append(canonicalWebUrl);
+      msg.append("#");
+      msg.append(PageLinks.SETTINGS_AGREEMENTS);
+      msg.append("\n");
+    } else {
+      msg.append(".");
+    }
+    msg.append("\n");
+    return new Capable(msg.toString());
+  }
+
+  private static boolean missing(final String value) {
+    return value == null || value.trim().equals("");
+  }
+
+  private boolean canPerformOnAnyRef(String permissionName) {
+    for (SectionMatcher matcher : access()) {
+      AccessSection section = matcher.section;
+      Permission permission = section.getPermission(permissionName);
+      if (permission == null) {
+        continue;
+      }
+
+      for (PermissionRule rule : permission.getRules()) {
+        if (rule.isBlock() || rule.isDeny() || !match(rule)) {
+          continue;
+        }
+
+        // Being in a group that was granted this permission is only an
+        // approximation.  There might be overrides and doNotInherit
+        // that would render this to be false.
+        //
+        if (controlForRef(section.getName()).canPerform(permissionName)) {
+          return true;
+        } else {
+          break;
+        }
       }
     }
 
     return false;
   }
 
-  /**
-   * @return the effective groups of the current user for this project
-   */
-  private Set<AccountGroup.Id> getEffectiveUserGroups() {
-    final Set<AccountGroup.Id> userGroups = user.getEffectiveGroups();
-    if (isOwner()) {
-      final Set<AccountGroup.Id> userGroupsOnProject =
-          new HashSet<AccountGroup.Id>(userGroups.size() + 1);
-      userGroupsOnProject.addAll(userGroups);
-      userGroupsOnProject.add(systemConfig.ownerGroupId);
-      return Collections.unmodifiableSet(userGroupsOnProject);
-    } else {
-      return userGroups;
-    }
-  }
-
-  private boolean canPerformOnAllRefs(ApprovalCategory.Id actionId,
-      short requireValue) {
+  private boolean canPerformOnAllRefs(String permission) {
     boolean canPerform = false;
-    final Set<String> patterns = allRefPatterns(actionId);
-    if (patterns.contains(RefRight.ALL)) {
+    Set<String> patterns = allRefPatterns(permission);
+    if (patterns.contains(AccessSection.ALL)) {
       // Only possible if granted on the pattern that
       // matches every possible reference.  Check all
       // patterns also have the permission.
       //
       for (final String pattern : patterns) {
-        if (controlForRef(pattern).canPerform(actionId, requireValue)) {
+        if (controlForRef(pattern).canPerform(permission)) {
           canPerform = true;
         } else {
           return false;
@@ -250,19 +449,52 @@
     return canPerform;
   }
 
-  private Set<String> allRefPatterns(ApprovalCategory.Id actionId) {
-    final Set<String> all = new HashSet<String>();
-    for (final RefRight pr : state.getAllRights(actionId, true)) {
-      all.add(pr.getRefPattern());
+  private Set<String> allRefPatterns(String permissionName) {
+    Set<String> all = new HashSet<String>();
+    for (SectionMatcher matcher : access()) {
+      AccessSection section = matcher.section;
+      Permission permission = section.getPermission(permissionName);
+      if (permission != null) {
+        all.add(section.getName());
+      }
     }
     return all;
   }
 
+  private List<SectionMatcher> access() {
+    if (allSections == null) {
+      allSections = state.getAllSections();
+    }
+    return allSections;
+  }
+
+  boolean match(PermissionRule rule) {
+    return match(rule.getGroup().getUUID());
+  }
+
+  boolean match(AccountGroup.UUID uuid) {
+    if (AccountGroup.PROJECT_OWNERS.equals(uuid)) {
+      return isDeclaredOwner();
+    } else {
+      return user.getEffectiveGroups().contains(uuid);
+    }
+  }
+
   public boolean canRunUploadPack() {
-    return isAnyIncludedIn(uploadGroups, getEffectiveUserGroups());
+    for (AccountGroup.UUID group : uploadGroups) {
+      if (match(group)) {
+        return true;
+      }
+    }
+    return false;
   }
 
   public boolean canRunReceivePack() {
-    return isAnyIncludedIn(receiveGroups, getEffectiveUserGroups());
+    for (AccountGroup.UUID group : receiveGroups) {
+      if (match(group)) {
+        return true;
+      }
+    }
+    return false;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectNode.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectNode.java
new file mode 100644
index 0000000..1c4d7c4
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectNode.java
@@ -0,0 +1,83 @@
+// 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.client.Project;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.util.TreeFormatter.TreeNode;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import java.util.SortedSet;
+import java.util.TreeSet;
+
+/** Node of a Project in a tree formatted by {@link ListProjects}. */
+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
+  protected 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() {
+    return project.getParent(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-server/src/main/java/com/google/gerrit/server/project/ProjectState.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
index 0b8e83a..6eac998 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectState.java
@@ -14,200 +14,207 @@
 
 package com.google.gerrit.server.project;
 
-import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.ApprovalCategory;
-import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.RefRight;
-import com.google.gerrit.server.AnonymousUser;
+import com.google.gerrit.common.CollectionsUtil;
+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.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.rules.PrologEnvironment;
+import com.google.gerrit.rules.RulesCache;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.config.WildProjectName;
+import com.google.gerrit.server.account.CapabilityCollection;
+import com.google.gerrit.server.account.GroupMembership;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.ProjectConfig;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
+import com.googlecode.prolog_cafe.compiler.CompileException;
+import com.googlecode.prolog_cafe.lang.PrologMachineCopy;
+
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+
+import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
-import java.util.Iterator;
-import java.util.LinkedList;
 import java.util.List;
 import java.util.Set;
 
 /** Cached information on a project. */
 public class ProjectState {
   public interface Factory {
-    ProjectState create(Project project, Collection<RefRight> localRights);
+    ProjectState create(ProjectConfig config);
   }
 
-  private final AnonymousUser anonymousUser;
-  private final Project.NameKey wildProject;
+  private final boolean isAllProjects;
+  private final AllProjectsName allProjectsName;
   private final ProjectCache projectCache;
   private final ProjectControl.AssistedFactory projectControlFactory;
+  private final PrologEnvironment.Factory envFactory;
+  private final GitRepositoryManager gitMgr;
+  private final RulesCache rulesCache;
 
-  private final Project project;
-  private final Collection<RefRight> localRights;
-  private final Set<AccountGroup.Id> localOwners;
+  private final ProjectConfig config;
+  private final Set<AccountGroup.UUID> localOwners;
 
-  private volatile Collection<RefRight> inheritedRights;
+  /** Prolog rule state. */
+  private volatile PrologMachineCopy rulesMachine;
+
+  /** Last system time the configuration's revision was examined. */
+  private volatile long lastCheckTime;
+
+  /** Local access sections, wrapped in SectionMatchers for faster evaluation. */
+  private volatile List<SectionMatcher> localAccessSections;
+
+  /** If this is all projects, the capabilities used by the server. */
+  private final CapabilityCollection capabilities;
 
   @Inject
-  protected ProjectState(final AnonymousUser anonymousUser,
+  protected ProjectState(
       final ProjectCache projectCache,
-      @WildProjectName final Project.NameKey wildProject,
+      final AllProjectsName allProjectsName,
       final ProjectControl.AssistedFactory projectControlFactory,
-      @Assisted final Project project,
-      @Assisted Collection<RefRight> rights) {
-    this.anonymousUser = anonymousUser;
+      final PrologEnvironment.Factory envFactory,
+      final GitRepositoryManager gitMgr,
+      final RulesCache rulesCache,
+      @Assisted final ProjectConfig config) {
     this.projectCache = projectCache;
-    this.wildProject = wildProject;
+    this.isAllProjects = config.getProject().getNameKey().equals(allProjectsName);
+    this.allProjectsName = allProjectsName;
     this.projectControlFactory = projectControlFactory;
+    this.envFactory = envFactory;
+    this.gitMgr = gitMgr;
+    this.rulesCache = rulesCache;
+    this.config = config;
+    this.capabilities = isAllProjects
+      ? new CapabilityCollection(config.getAccessSection(AccessSection.GLOBAL_CAPABILITIES))
+      : null;
 
-    if (wildProject.equals(project.getNameKey())) {
-      rights = new ArrayList<RefRight>(rights);
-      for (Iterator<RefRight> itr = rights.iterator(); itr.hasNext();) {
-        if (!itr.next().getApprovalCategoryId().canBeOnWildProject()) {
-          itr.remove();
+    HashSet<AccountGroup.UUID> groups = new HashSet<AccountGroup.UUID>();
+    AccessSection all = config.getAccessSection(AccessSection.ALL);
+    if (all != null) {
+      Permission owner = all.getPermission(Permission.OWNER);
+      if (owner != null) {
+        for (PermissionRule rule : owner.getRules()) {
+          GroupReference ref = rule.getGroup();
+          if (ref.getUUID() != null) {
+            groups.add(ref.getUUID());
+          }
         }
       }
-      rights = Collections.unmodifiableCollection(rights);
-    }
-
-    this.project = project;
-    this.localRights = rights;
-
-    final HashSet<AccountGroup.Id> groups = new HashSet<AccountGroup.Id>();
-    for (final RefRight right : rights) {
-      if (ApprovalCategory.OWN.equals(right.getApprovalCategoryId())
-          && right.getMaxValue() > 0
-          && right.getRefPattern().equals(RefRight.ALL)) {
-        groups.add(right.getAccountGroupId());
-      }
     }
     localOwners = Collections.unmodifiableSet(groups);
   }
 
-  public Project getProject() {
-    return project;
-  }
-
-  /** Get the rights that pertain only to this project. */
-  public Collection<RefRight> getLocalRights() {
-    return localRights;
-  }
-
-  /**
-   * Get the rights that pertain only to this project.
-   *
-   * @param action the category requested.
-   * @return immutable collection of rights for the requested category.
-   */
-  public Collection<RefRight> getLocalRights(ApprovalCategory.Id action) {
-    return filter(getLocalRights(), action);
-  }
-
-  /** Get the rights this project inherits from the wild project. */
-  public Collection<RefRight> getInheritedRights() {
-    if (inheritedRights == null) {
-      inheritedRights = computeInheritedRights();
+  boolean needsRefresh(long generation) {
+    if (generation <= 0) {
+      return isRevisionOutOfDate();
     }
-    return inheritedRights;
-  }
-
-  void setInheritedRights(Collection<RefRight> all) {
-    inheritedRights = all;
-  }
-
-  private Collection<RefRight> computeInheritedRights() {
-    if (isSpecialWildProject()) {
-      return Collections.emptyList();
+    if (lastCheckTime != generation) {
+      lastCheckTime = generation;
+      return isRevisionOutOfDate();
     }
+    return false;
+  }
 
-    List<RefRight> inherited = new ArrayList<RefRight>();
-    Set<Project.NameKey> seen = new HashSet<Project.NameKey>();
-    Project.NameKey parent = project.getParent();
-
-    while (parent != null && seen.add(parent)) {
-      ProjectState s = projectCache.get(parent);
-      if (s != null) {
-        inherited.addAll(s.getLocalRights());
-        parent = s.getProject().getParent();
-      } else {
-        break;
+  private boolean isRevisionOutOfDate() {
+    try {
+      Repository git = gitMgr.openRepository(getProject().getNameKey());
+      try {
+        Ref ref = git.getRef(GitRepositoryManager.REF_CONFIG);
+        if (ref == null || ref.getObjectId() == null) {
+          return true;
+        }
+        return !ref.getObjectId().equals(config.getRevision());
+      } finally {
+        git.close();
       }
-    }
-
-    // Wild project is the parent, or the root of the tree
-    if (parent == null) {
-      inherited.addAll(getWildProjectRights());
-    }
-
-    return Collections.unmodifiableCollection(inherited);
-  }
-
-  private Collection<RefRight> getWildProjectRights() {
-    final ProjectState s = projectCache.get(wildProject);
-    return s != null ? s.getLocalRights() : Collections.<RefRight> emptyList();
-  }
-
-  /**
-   * Utility class that is needed to filter overridden refrights
-   */
-  private static class Grant {
-    final AccountGroup.Id group;
-    final String pattern;
-
-    private Grant(AccountGroup.Id group, String pattern) {
-      this.group = group;
-      this.pattern = pattern;
-    }
-
-    @Override
-    public boolean equals(Object o) {
-      if (o == null)
-        return false;
-      Grant grant = (Grant) o;
-      return group.equals(grant.group) && pattern.equals(grant.pattern);
-    }
-
-    @Override
-    public int hashCode() {
-      int result = group.hashCode();
-      result = 31 * result + pattern.hashCode();
-      return result;
+    } catch (IOException gone) {
+      return true;
     }
   }
 
   /**
-   * Get the rights this project has and inherits from the wild project.
-   *
-   * @param action the category requested.
-   * @param dropOverridden whether to remove inherited permissions in case if we have a
-   *     local one that matches (action,group,ref)
-   * @return immutable collection of rights for the requested category.
+   * @return cached computation of all global capabilities. This should only be
+   *         invoked on the state from {@link ProjectCache#getAllProjects()}.
+   *         Null on any other project.
    */
-  public Collection<RefRight> getAllRights(ApprovalCategory.Id action, boolean dropOverridden) {
-    Collection<RefRight> rights = new LinkedList<RefRight>(getLocalRights(action));
-    rights.addAll(filter(getInheritedRights(), action));
-    if (dropOverridden) {
-      Set<Grant> grants = new HashSet<Grant>();
-      Iterator<RefRight> iter = rights.iterator();
-      while (iter.hasNext()) {
-        RefRight right = iter.next();
+  public CapabilityCollection getCapabilityCollection() {
+    return capabilities;
+  }
 
-        Grant grant = new Grant(right.getAccountGroupId(), right.getRefPattern());
-        if (grants.contains(grant)) {
-          iter.remove();
-        } else {
-          grants.add(grant);
+  /** @return Construct a new PrologEnvironment for the calling thread. */
+  public PrologEnvironment newPrologEnvironment() throws CompileException {
+    PrologMachineCopy pmc = rulesMachine;
+    if (pmc == null) {
+      pmc = rulesCache.loadMachine(
+          getProject().getNameKey(),
+          config.getRulesId());
+      rulesMachine = pmc;
+    }
+    return envFactory.create(pmc);
+  }
+
+  public Project getProject() {
+    return config.getProject();
+  }
+
+  public ProjectConfig getConfig() {
+    return config;
+  }
+
+  /** Get the sections that pertain only to this project. */
+  private List<SectionMatcher> getLocalAccessSections() {
+    List<SectionMatcher> sm = localAccessSections;
+    if (sm == null) {
+      Collection<AccessSection> fromConfig = config.getAccessSections();
+      sm = new ArrayList<SectionMatcher>(fromConfig.size());
+      for (AccessSection section : fromConfig) {
+        SectionMatcher matcher = SectionMatcher.wrap(section);
+        if (matcher != null) {
+          sm.add(matcher);
         }
       }
+      localAccessSections = sm;
     }
-    return Collections.unmodifiableCollection(rights);
+    return sm;
   }
 
-  /** Is this the special wild project which manages inherited rights? */
-  public boolean isSpecialWildProject() {
-    return project.getNameKey().equals(wildProject);
+  /**
+   * Obtain all local and inherited sections. This collection is looked up
+   * dynamically and is not cached. Callers should try to cache this result
+   * per-request as much as possible.
+   */
+  List<SectionMatcher> getAllSections() {
+    if (isAllProjects) {
+      return getLocalAccessSections();
+    }
+
+    List<SectionMatcher> all = new ArrayList<SectionMatcher>();
+    Set<Project.NameKey> seen = new HashSet<Project.NameKey>();
+    ProjectState allProjects = projectCache.getAllProjects();
+    seen.add(getProject().getNameKey());
+
+    ProjectState s = this;
+    do {
+      all.addAll(s.getLocalAccessSections());
+
+      Project.NameKey parent = s.getProject().getParent();
+      if (parent == null || !seen.add(parent)) {
+        break;
+      }
+      s = projectCache.get(parent);
+    } while (s != null);
+    if (seen.add(allProjects.getProject().getNameKey())) {
+      all.addAll(allProjects.getLocalAccessSections());
+    }
+    return all;
   }
 
   /**
@@ -216,13 +223,13 @@
    *         are no local owners the local owners of the nearest parent project
    *         that has local owners are returned
    */
-  public Set<AccountGroup.Id> getOwners() {
-    if (!localOwners.isEmpty() || isSpecialWildProject()
-        || project.getParent() == null) {
+  public Set<AccountGroup.UUID> getOwners() {
+    Project.NameKey parentName = getProject().getParent();
+    if (!localOwners.isEmpty() || parentName == null || isAllProjects) {
       return localOwners;
     }
 
-    final ProjectState parent = projectCache.get(project.getParent());
+    ProjectState parent = projectCache.get(parentName);
     if (parent != null) {
       return parent.getOwners();
     }
@@ -231,44 +238,41 @@
   }
 
   /**
-   * @return all {@link AccountGroup}'s that are allowed to administrate the
-   *         complete project. This includes all groups to which the owner
-   *         privilege for 'refs/*' is assigned for this project (the local
-   *         owners) and all groups to which the owner privilege for 'refs/*' is
-   *         assigned for one of the parent projects (the inherited owners).
+   * @return true if any of the groups listed in {@code groups} was declared to
+   *         be an owner of this project, or one of its parent projects..
    */
-  public Set<AccountGroup.Id> getAllOwners() {
-    final HashSet<AccountGroup.Id> owners = new HashSet<AccountGroup.Id>();
-    for (final RefRight right : getAllRights(ApprovalCategory.OWN, true)) {
-      if (right.getMaxValue() > 0 && right.getRefPattern().equals(RefRight.ALL)) {
-        owners.add(right.getAccountGroupId());
-      }
-    }
-    return Collections.unmodifiableSet(owners);
-  }
+  boolean isOwner(GroupMembership groups) {
+    Set<Project.NameKey> seen = new HashSet<Project.NameKey>();
+    seen.add(getProject().getNameKey());
 
-  public ProjectControl controlForAnonymousUser() {
-    return controlFor(anonymousUser);
+    ProjectState s = this;
+    do {
+      if (groups.containsAnyOf(s.localOwners)) {
+        return true;
+      }
+
+      Project.NameKey parent = s.getProject().getParent();
+      if (parent == null || !seen.add(parent)) {
+        break;
+      }
+      s = projectCache.get(parent);
+    } while (s != null);
+    return false;
   }
 
   public ProjectControl controlFor(final CurrentUser user) {
     return projectControlFactory.create(user, this);
   }
 
-  private static Collection<RefRight> filter(Collection<RefRight> all,
-      ApprovalCategory.Id actionId) {
-    if (all.isEmpty()) {
-      return Collections.emptyList();
+  /**
+   * @return ProjectState of project's parent. If the project does not have a
+   *         parent, return state of the top level project, All-Projects. If
+   *         this project is All-Projects, return null.
+   */
+  public ProjectState getParentState() {
+    if (isAllProjects) {
+      return null;
     }
-    final Collection<RefRight> mine = new ArrayList<RefRight>(all.size());
-    for (final RefRight right : all) {
-      if (right.getApprovalCategoryId().equals(actionId)) {
-        mine.add(right);
-      }
-    }
-    if (mine.isEmpty()) {
-      return Collections.emptyList();
-    }
-    return Collections.unmodifiableCollection(mine);
+    return projectCache.get(getProject().getParent(allProjectsName));
   }
-}
+}
\ No newline at end of file
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java
index 8ddf585..db370e0 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
@@ -14,34 +14,21 @@
 
 package com.google.gerrit.server.project;
 
-import static com.google.gerrit.reviewdb.ApprovalCategory.FORGE_AUTHOR;
-import static com.google.gerrit.reviewdb.ApprovalCategory.FORGE_COMMITTER;
-import static com.google.gerrit.reviewdb.ApprovalCategory.FORGE_IDENTITY;
-import static com.google.gerrit.reviewdb.ApprovalCategory.FORGE_SERVER;
-import static com.google.gerrit.reviewdb.ApprovalCategory.OWN;
-import static com.google.gerrit.reviewdb.ApprovalCategory.PUSH_HEAD;
-import static com.google.gerrit.reviewdb.ApprovalCategory.PUSH_HEAD_CREATE;
-import static com.google.gerrit.reviewdb.ApprovalCategory.PUSH_HEAD_REPLACE;
-import static com.google.gerrit.reviewdb.ApprovalCategory.PUSH_HEAD_UPDATE;
-import static com.google.gerrit.reviewdb.ApprovalCategory.PUSH_TAG;
-import static com.google.gerrit.reviewdb.ApprovalCategory.PUSH_TAG_ANNOTATED;
-import static com.google.gerrit.reviewdb.ApprovalCategory.PUSH_TAG_SIGNED;
-import static com.google.gerrit.reviewdb.ApprovalCategory.READ;
-
-import com.google.gerrit.common.data.ParamertizedString;
-import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.ApprovalCategory;
-import com.google.gerrit.reviewdb.RefRight;
-import com.google.gerrit.reviewdb.SystemConfig;
+import com.google.gerrit.common.data.AccessSection;
+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.common.data.RefConfigSection;
+import com.google.gerrit.common.errors.InvalidNameException;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
+import com.google.gerrit.server.git.GitRepositoryManager;
 
 import dk.brics.automaton.RegExp;
 
-import org.apache.commons.lang.StringUtils;
 import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevObject;
 import org.eclipse.jgit.revwalk.RevTag;
@@ -49,47 +36,33 @@
 
 import java.io.IOException;
 import java.util.ArrayList;
-import java.util.Collection;
 import java.util.Collections;
-import java.util.Comparator;
 import java.util.HashMap;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
-import java.util.SortedMap;
-import java.util.TreeMap;
-import java.util.regex.Pattern;
 
 
 /** Manages access control for Git references (aka branches, tags). */
 public class RefControl {
-  public interface Factory {
-    RefControl create(ProjectControl projectControl, String ref);
-  }
-
-  private final SystemConfig systemConfig;
   private final ProjectControl projectControl;
   private final String refName;
 
+  /** All permissions that apply to this reference. */
+  private final PermissionCollection relevant;
+
+  /** Cached set of permissions matching this user. */
+  private final Map<String, List<PermissionRule>> effective;
+
+  private Boolean owner;
   private Boolean canForgeAuthor;
   private Boolean canForgeCommitter;
 
-  @Inject
-  protected RefControl(final SystemConfig systemConfig,
-      @Assisted final ProjectControl projectControl,
-      @Assisted String ref) {
-    this.systemConfig = systemConfig;
-    if (isRE(ref)) {
-      ref = shortestExample(ref);
-
-    } else if (ref.endsWith("/*")) {
-      ref = ref.substring(0, ref.length() - 1);
-
-    }
-
+  RefControl(ProjectControl projectControl, String ref,
+      PermissionCollection relevant) {
     this.projectControl = projectControl;
     this.refName = ref;
+    this.relevant = relevant;
+    this.effective = new HashMap<String, List<PermissionRule>>();
   }
 
   public String getRefName() {
@@ -101,39 +74,35 @@
   }
 
   public CurrentUser getCurrentUser() {
-    return getProjectControl().getCurrentUser();
+    return projectControl.getCurrentUser();
   }
 
-  public RefControl forAnonymousUser() {
-    return getProjectControl().forAnonymousUser().controlForRef(getRefName());
-  }
-
-  public RefControl forUser(final CurrentUser who) {
-    return getProjectControl().forUser(who).controlForRef(getRefName());
+  public RefControl forUser(CurrentUser who) {
+    ProjectControl newCtl = projectControl.forUser(who);
+    if (relevant.isUserSpecific()) {
+      return newCtl.controlForRef(getRefName());
+    } else {
+      return new RefControl(newCtl, getRefName(), relevant);
+    }
   }
 
   /** Is this user a ref owner? */
   public boolean isOwner() {
-    if (canPerform(OWN, (short) 1)) {
-      return true;
-    }
+    if (owner == null) {
+      if (canPerform(Permission.OWNER)) {
+        owner = true;
 
-    // We have to prevent infinite recursion here, the project control
-    // calls us to find out if there is ownership of all references in
-    // order to determine project level ownership.
-    //
-    if (getRefName().equals(
-        RefRight.ALL.substring(0, RefRight.ALL.length() - 1))) {
-      return getCurrentUser().isAdministrator();
-    } else {
-      return getProjectControl().isOwner();
+      } else {
+        owner = projectControl.isOwner();
+      }
     }
+    return owner;
   }
 
   /** Can this user see this reference exists? */
   public boolean isVisible() {
-    return getProjectControl().visibleForReplication()
-        || canPerform(READ, (short) 1);
+    return (projectControl.visibleForReplication() || canPerform(Permission.READ))
+        && canRead();
   }
 
   /**
@@ -144,27 +113,88 @@
    *         ref
    */
   public boolean canUpload() {
-    return canPerform(READ, (short) 2);
+    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 canPerform(READ, (short) 3);
+    return projectControl.controlForRef("refs/for/" + getRefName())
+        .canPerform(Permission.PUSH_MERGE)
+        && canWrite();
+  }
+
+  /** @return true if this user can rebase changes on this ref */
+  public boolean canRebase() {
+    return canPerform(Permission.REBASE)
+        && canWrite();
   }
 
   /** @return true if this user can submit patch sets to this ref */
   public boolean canSubmit() {
-    return canPerform(ApprovalCategory.SUBMIT, (short) 1);
+    if (GitRepositoryManager.REF_CONFIG.equals(refName)) {
+      // Always allow project owners to submit configuration changes.
+      // Submitting configuration changes modifies the access control
+      // rules. Allowing this to be done by a non-project-owner opens
+      // a security hole enabling editing of access rules, and thus
+      // granting of powers beyond submitting to the configuration.
+      return projectControl.isOwner();
+    }
+    return canPerform(Permission.SUBMIT)
+        && canWrite();
   }
 
   /** @return true if the user can update the reference as a fast-forward. */
   public boolean canUpdate() {
-    return canPerform(PUSH_HEAD, PUSH_HEAD_UPDATE);
+    if (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
+      // a security hole enabling editing of access rules, and thus
+      // granting of powers beyond pushing to the configuration.
+      return false;
+    }
+    return canPerform(Permission.PUSH)
+        && canWrite();
   }
 
   /** @return true if the user can rewind (force push) the reference. */
   public boolean canForceUpdate() {
-    return canPerform(PUSH_HEAD, PUSH_HEAD_REPLACE) || 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 (!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
+      // a security hole enabling editing of access rules, and thus
+      // granting of powers beyond pushing to the configuration.
+      return false;
+    }
+    boolean result = false;
+    for (PermissionRule rule : access(Permission.PUSH)) {
+      if (rule.isBlock()) {
+        return false;
+      }
+      if (rule.getForce()) {
+        result = true;
+      }
+    }
+    return result;
   }
 
   /**
@@ -175,6 +205,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:
@@ -186,7 +219,7 @@
     }
 
     if (object instanceof RevCommit) {
-      return owner || canPerform(PUSH_HEAD, PUSH_HEAD_CREATE);
+      return owner || canPerform(Permission.CREATE);
 
     } else if (object instanceof RevTag) {
       final RevTag tag = (RevTag) object;
@@ -208,7 +241,7 @@
         } else {
           valid = false;
         }
-        if (!valid && !owner && !canPerform(FORGE_IDENTITY, FORGE_COMMITTER)) {
+        if (!valid && !owner && !canForgeCommitter()) {
           return false;
         }
       }
@@ -217,9 +250,9 @@
       // than if it doesn't have a PGP signature.
       //
       if (tag.getFullMessage().contains("-----BEGIN PGP SIGNATURE-----\n")) {
-        return owner || canPerform(PUSH_TAG, PUSH_TAG_SIGNED);
+        return owner || canPerform(Permission.PUSH_TAG);
       } else {
-        return owner || canPerform(PUSH_TAG, PUSH_TAG_ANNOTATED);
+        return owner || canPerform(Permission.PUSH_TAG);
       }
 
     } else {
@@ -234,12 +267,21 @@
    * @return {@code true} if the user specified can delete a Git ref.
    */
   public boolean canDelete() {
+    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.
+      // If a project is to be removed from Gerrit, its repository
+      // should be removed first.
+      return false;
+    }
+
     switch (getCurrentUser().getAccessPath()) {
       case WEB_UI:
-        return isOwner() || canPerform(PUSH_HEAD, PUSH_HEAD_REPLACE);
+        return isOwner() || canPushWithForce();
 
       case GIT:
-        return canPerform(PUSH_HEAD, PUSH_HEAD_REPLACE);
+        return canPushWithForce();
 
       default:
         return false;
@@ -249,7 +291,7 @@
   /** @return true if this user can forge the author line in a commit. */
   public boolean canForgeAuthor() {
     if (canForgeAuthor == null) {
-      canForgeAuthor = canPerform(FORGE_IDENTITY, FORGE_AUTHOR);
+      canForgeAuthor = canPerform(Permission.FORGE_AUTHOR);
     }
     return canForgeAuthor;
   }
@@ -257,361 +299,118 @@
   /** @return true if this user can forge the committer line in a commit. */
   public boolean canForgeCommitter() {
     if (canForgeCommitter == null) {
-      canForgeCommitter = canPerform(FORGE_IDENTITY, FORGE_COMMITTER);
+      canForgeCommitter = canPerform(Permission.FORGE_COMMITTER);
     }
     return canForgeCommitter;
   }
 
   /** @return true if this user can forge the server on the committer line. */
   public boolean canForgeGerritServerIdentity() {
-    return canPerform(FORGE_IDENTITY, FORGE_SERVER);
+    return canPerform(Permission.FORGE_SERVER);
   }
 
-  public short normalize(ApprovalCategory.Id category, short score) {
-    short minAllowed = 0, maxAllowed = 0;
-    for (RefRight r : getApplicableRights(category)) {
-      if (getCurrentUser().getEffectiveGroups().contains(r.getAccountGroupId())) {
-        minAllowed = (short) Math.min(minAllowed, r.getMinValue());
-        maxAllowed = (short) Math.max(maxAllowed, r.getMaxValue());
-      }
-    }
-
-    if (score < minAllowed) {
-      score = minAllowed;
-    }
-    if (score > maxAllowed) {
-      score = maxAllowed;
-    }
-    return score;
-  }
-
-  /**
-   * Convenience holder class used to map a ref pattern to the list of
-   * {@code RefRight}s that use it in the database.
-   */
-  public final static class RefRightsForPattern {
-    private final List<RefRight> rights;
-    private boolean containsExclusive;
-
-    public RefRightsForPattern() {
-      rights = new ArrayList<RefRight>();
-      containsExclusive = false;
-    }
-
-    public void addRight(RefRight right) {
-      rights.add(right);
-      if (right.isExclusive()) {
-        containsExclusive = true;
-      }
-    }
-
-    public List<RefRight> getRights() {
-      return Collections.unmodifiableList(rights);
-    }
-
-    public boolean containsExclusive() {
-      return containsExclusive;
-    }
-
-    /**
-     * Returns The max allowed value for this ref pattern for all specified
-     * groups.
-     *
-     * @param groups The groups of the user
-     * @return The allowed value for this ref for all the specified groups
-     */
-    private boolean allowedValueForRef(Set<AccountGroup.Id> groups, short level) {
-      for (RefRight right : rights) {
-        if (groups.contains(right.getAccountGroupId())
-            && right.getMaxValue() >= level) {
-          return true;
-        }
-      }
-      return false;
-    }
-  }
-
-  boolean canPerform(ApprovalCategory.Id actionId, short level) {
-    final Set<AccountGroup.Id> groups = getCurrentUser().getEffectiveGroups();
-
-    List<RefRight> allRights = new ArrayList<RefRight>();
-    allRights.addAll(getAllRights(actionId));
-
-    SortedMap<String, RefRightsForPattern> perPatternRights =
-      sortedRightsByPattern(allRights);
-
-    for (RefRightsForPattern right : perPatternRights.values()) {
-      if (right.allowedValueForRef(groups, level)) {
-        return true;
-      }
-      if (right.containsExclusive() && !actionId.equals(OWN)) {
-        break;
-      }
-    }
-    return false;
-  }
-
-  /**
-   * Order the Ref Pattern by the most specific. This sort is done by:
-   * <ul>
-   * <li>1 - The minor value of Levenshtein string distance between the branch
-   * name and the regex string shortest example. A shorter distance is a more
-   * specific match.
-   * <li>2 - Finites first, infinities after.
-   * <li>3 - Number of transitions.
-   * <li>4 - Length of the expression text.
-   * </ul>
-   *
-   * Levenshtein distance is a measure of the similarity between two strings.
-   * The distance is the number of deletions, insertions, or substitutions
-   * required to transform one string into another.
-   *
-   * For example, if given refs/heads/m* and refs/heads/*, the distances are 5
-   * and 6. It means that refs/heads/m* is more specific because it's closer to
-   * refs/heads/master than refs/heads/*.
-   *
-   * Another example could be refs/heads/* and refs/heads/[a-zA-Z]*, the
-   * distances are both 6. Both are infinite, but refs/heads/[a-zA-Z]* has more
-   * transitions, which after all turns it more specific.
-   */
-  private final Comparator<String> BY_MOST_SPECIFIC_SORT =
-      new Comparator<String>() {
-        public int compare(final String pattern1, final String pattern2) {
-          int cmp = distance(pattern1) - distance(pattern2);
-          if (cmp == 0) {
-            boolean p1_finite = finite(pattern1);
-            boolean p2_finite = finite(pattern2);
-
-            if (p1_finite && !p2_finite) {
-              cmp = -1;
-            } else if (!p1_finite && p2_finite) {
-              cmp = 1;
-            } else /* if (f1 == f2) */{
-              cmp = 0;
-            }
-          }
-          if (cmp == 0) {
-            cmp = transitions(pattern1) - transitions(pattern2);
-          }
-          if (cmp == 0) {
-            cmp = pattern2.length() - pattern1.length();
-          }
-          return cmp;
-        }
-
-        private int distance(String pattern) {
-          String example;
-          if (isRE(pattern)) {
-            example = shortestExample(pattern);
-
-          } else if (pattern.endsWith("/*")) {
-            example = pattern.substring(0, pattern.length() - 1) + '1';
-
-          } else if (pattern.equals(getRefName())) {
-            return 0;
-
-          } else {
-            return Math.max(pattern.length(), getRefName().length());
-          }
-          return StringUtils.getLevenshteinDistance(example, getRefName());
-        }
-
-        private boolean finite(String pattern) {
-          if (isRE(pattern)) {
-            return toRegExp(pattern).toAutomaton().isFinite();
-
-          } else if (pattern.endsWith("/*")) {
-            return false;
-
-          } else {
-            return true;
+  /** All value ranges of any allowed label permission. */
+  public List<PermissionRange> getLabelRanges() {
+    List<PermissionRange> r = new ArrayList<PermissionRange>();
+    for (Map.Entry<String, List<PermissionRule>> e : relevant.getDeclaredPermissions()) {
+      if (Permission.isLabel(e.getKey())) {
+        int min = 0;
+        int max = 0;
+        for (PermissionRule rule : e.getValue()) {
+          if (projectControl.match(rule)) {
+            min = Math.min(min, rule.getMin());
+            max = Math.max(max, rule.getMax());
           }
         }
-
-        private int transitions(String pattern) {
-          if (isRE(pattern)) {
-            return toRegExp(pattern).toAutomaton().getNumberOfTransitions();
-
-          } else if (pattern.endsWith("/*")) {
-            return pattern.length();
-
-          } else {
-            return pattern.length();
-          }
-        }
-      };
-
-  /**
-   * Sorts all given rights into a map, ordered by descending length of
-   * ref pattern.
-   *
-   * For example, if given the following rights in argument:
-   *
-   * ["refs/heads/master", group1, -1, +1],
-   * ["refs/heads/master", group2, -2, +2],
-   * ["refs/heads/*", group3, -1, +1]
-   * ["refs/heads/stable", group2, -1, +1]
-   *
-   * Then the following map is returned:
-   * "refs/heads/master" => {
-   *      ["refs/heads/master", group1, -1, +1],
-   *      ["refs/heads/master", group2, -2, +2]
-   *  }
-   * "refs/heads/stable" => {["refs/heads/stable", group2, -1, +1]}
-   * "refs/heads/*" => {["refs/heads/*", group3, -1, +1]}
-   *
-   * @param actionRights
-   * @return A sorted map keyed off the ref pattern of all rights.
-   */
-  private SortedMap<String, RefRightsForPattern> sortedRightsByPattern(
-      List<RefRight> actionRights) {
-    SortedMap<String, RefRightsForPattern> rights =
-      new TreeMap<String, RefRightsForPattern>(BY_MOST_SPECIFIC_SORT);
-    for (RefRight actionRight : actionRights) {
-      RefRightsForPattern patternRights =
-        rights.get(actionRight.getRefPattern());
-      if (patternRights == null) {
-        patternRights = new RefRightsForPattern();
-        rights.put(actionRight.getRefPattern(), patternRights);
-      }
-      patternRights.addRight(actionRight);
-    }
-    return rights;
-  }
-
-  private List<RefRight> getAllRights(ApprovalCategory.Id actionId) {
-    final List<RefRight> allRefRights = filter(getProjectState().getAllRights(actionId, true));
-    return resolveOwnerGroups(allRefRights);
-  }
-
-  /**
-   * Returns all applicable rights for a given approval category.
-   *
-   * Applicable rights are defined as the list of {@code RefRight}s which match
-   * the ref for which this object was created, stopping the ref wildcard
-   * matching when an exclusive ref right was encountered, for the given
-   * approval category.
-   * @param id The {@link ApprovalCategory.Id}.
-   * @return All applicable rights.
-   */
-  public List<RefRight> getApplicableRights(final ApprovalCategory.Id id) {
-    List<RefRight> l = new ArrayList<RefRight>();
-    l.addAll(getAllRights(id));
-    SortedMap<String, RefRightsForPattern> perPatternRights =
-      sortedRightsByPattern(l);
-    List<RefRight> applicable = new ArrayList<RefRight>();
-    for (RefRightsForPattern patternRights : perPatternRights.values()) {
-      applicable.addAll(patternRights.getRights());
-      if (patternRights.containsExclusive()) {
-        break;
-      }
-    }
-    return Collections.unmodifiableList(applicable);
-  }
-
-  /**
-   * Resolves all refRights which assign privileges to the 'Project Owners'
-   * group. All other refRights stay unchanged.
-   *
-   * @param refRights refRights to be resolved
-   * @return the resolved refRights
-   */
-  private List<RefRight> resolveOwnerGroups(final List<RefRight> refRights) {
-    final List<RefRight> resolvedRefRights =
-        new ArrayList<RefRight>(refRights.size());
-    for (final RefRight refRight : refRights) {
-      resolvedRefRights.addAll(resolveOwnerGroups(refRight));
-    }
-    return resolvedRefRights;
-  }
-
-  /**
-   * Checks if the given refRight assigns privileges to the 'Project Owners'
-   * group.
-   * If yes, resolves the 'Project Owners' group to the concrete groups that
-   * own the project and creates new refRights for the concrete owner groups
-   * which are returned.
-   * If no, the given refRight is returned unchanged.
-   *
-   * @param refRight refRight
-   * @return the resolved refRights
-   */
-  private Set<RefRight> resolveOwnerGroups(final RefRight refRight) {
-    final Set<RefRight> resolvedRefRights = new HashSet<RefRight>();
-    if (refRight.getAccountGroupId().equals(systemConfig.ownerGroupId)) {
-      for (final AccountGroup.Id ownerGroup : getProjectState().getAllOwners()) {
-        if (!ownerGroup.equals(systemConfig.ownerGroupId)) {
-          resolvedRefRights.add(new RefRight(refRight, ownerGroup));
+        if (min != 0 || max != 0) {
+          r.add(new PermissionRange(e.getKey(), min, max));
         }
       }
-    } else {
-      resolvedRefRights.add(refRight);
     }
-    return resolvedRefRights;
+    return r;
   }
 
-  private List<RefRight> filter(Collection<RefRight> all) {
-    List<RefRight> mine = new ArrayList<RefRight>(all.size());
-    for (RefRight right : all) {
-      if (matches(right.getRefPattern())) {
-        mine.add(right);
+  /** The range of permitted values associated with a label permission. */
+  public PermissionRange getRange(String permission) {
+    if (Permission.isLabel(permission)) {
+      return toRange(permission, access(permission));
+    }
+    return null;
+  }
+
+  private static PermissionRange toRange(String permissionName,
+      List<PermissionRule> ruleList) {
+    int min = 0;
+    int max = 0;
+    int blockMin = Integer.MIN_VALUE;
+    int blockMax = Integer.MAX_VALUE;
+    for (PermissionRule rule : ruleList) {
+      if (rule.isBlock()) {
+        blockMin = Math.max(blockMin, rule.getMin());
+        blockMax = Math.min(blockMax, rule.getMax());
+      } else {
+        min = Math.min(min, rule.getMin());
+        max = Math.max(max, rule.getMax());
       }
     }
+    if (blockMin > Integer.MIN_VALUE) {
+      min = Math.max(min, blockMin + 1);
+    }
+    if (blockMax < Integer.MAX_VALUE) {
+      max = Math.min(max, blockMax - 1);
+    }
+    return new PermissionRange(permissionName, min, max);
+  }
+
+  /** True if the user has this permission. Works only for non labels. */
+  boolean canPerform(String permissionName) {
+    List<PermissionRule> access = access(permissionName);
+    for (PermissionRule rule : access) {
+      if (rule.isBlock() && !rule.getForce()) {
+        return false;
+      }
+    }
+    return !access.isEmpty();
+  }
+
+  /** Rules for the given permission, or the empty list. */
+  private List<PermissionRule> access(String permissionName) {
+    List<PermissionRule> rules = effective.get(permissionName);
+    if (rules != null) {
+      return rules;
+    }
+
+    rules = relevant.getPermission(permissionName);
+
+    if (rules.isEmpty()) {
+      effective.put(permissionName, rules);
+      return rules;
+    }
+
+    if (rules.size() == 1) {
+      if (!projectControl.match(rules.get(0))) {
+        rules = Collections.emptyList();
+      }
+      effective.put(permissionName, rules);
+      return rules;
+    }
+
+    List<PermissionRule> mine = new ArrayList<PermissionRule>(rules.size());
+    for (PermissionRule rule : rules) {
+      if (projectControl.match(rule)) {
+        mine.add(rule);
+      }
+    }
+
+    if (mine.isEmpty()) {
+      mine = Collections.emptyList();
+    }
+    effective.put(permissionName, mine);
     return mine;
   }
 
-  private ProjectState getProjectState() {
-    return projectControl.getProjectState();
-  }
-
-  private boolean matches(String refPattern) {
-    if (isTemplate(refPattern)) {
-      ParamertizedString template = new ParamertizedString(refPattern);
-      HashMap<String, String> p = new HashMap<String, String>();
-
-      if (getCurrentUser() instanceof IdentifiedUser) {
-        p.put("username", ((IdentifiedUser) getCurrentUser()).getUserName());
-      } else {
-        // Right now we only template the username. If not available
-        // this rule cannot be matched at all.
-        //
-        return false;
-      }
-
-      if (isRE(refPattern)) {
-        for (Map.Entry<String, String> ent : p.entrySet()) {
-          ent.setValue(escape(ent.getValue()));
-        }
-      }
-
-      refPattern = template.replace(p);
-    }
-
-    if (isRE(refPattern)) {
-      return Pattern.matches(refPattern, getRefName());
-
-    } else if (refPattern.endsWith("/*")) {
-      String prefix = refPattern.substring(0, refPattern.length() - 1);
-      return getRefName().startsWith(prefix);
-
-    } else {
-      return getRefName().equals(refPattern);
-    }
-  }
-
-  private static boolean isTemplate(String refPattern) {
-    return 0 <= refPattern.indexOf("${");
-  }
-
-  private static String escape(String value) {
-    // Right now the only special character allowed in a
-    // variable value is a . in the username.
-    //
-    return value.replace(".", "\\.");
-  }
-
-  private static boolean isRE(String refPattern) {
-    return refPattern.startsWith(RefRight.REGEX_PREFIX);
+  public static boolean isRE(String refPattern) {
+    return refPattern.startsWith(AccessSection.REGEX_PREFIX);
   }
 
   public static String shortestExample(String pattern) {
@@ -624,10 +423,28 @@
     }
   }
 
-  private static RegExp toRegExp(String refPattern) {
+  public static RegExp toRegExp(String refPattern) {
     if (isRE(refPattern)) {
       refPattern = refPattern.substring(1);
     }
     return new RegExp(refPattern, RegExp.NONE);
   }
+
+  public static void validateRefPattern(String refPattern)
+      throws InvalidNameException {
+    if (refPattern.startsWith(RefConfigSection.REGEX_PREFIX)) {
+      if (!Repository.isValidRefName(RefControl.shortestExample(refPattern))) {
+        throw new InvalidNameException(refPattern);
+      }
+    } else if (refPattern.equals(RefConfigSection.ALL)) {
+      // This is a special case we have to allow, it fails below.
+    } else if (refPattern.endsWith("/*")) {
+      String prefix = refPattern.substring(0, refPattern.length() - 2);
+      if (!Repository.isValidRefName(prefix)) {
+        throw new InvalidNameException(refPattern);
+      }
+    } else if (!Repository.isValidRefName(refPattern)) {
+      throw new InvalidNameException(refPattern);
+    }
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionMatcher.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionMatcher.java
new file mode 100644
index 0000000..1c70b04
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionMatcher.java
@@ -0,0 +1,154 @@
+// 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 static com.google.gerrit.server.project.RefControl.isRE;
+
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.ParameterizedString;
+
+import dk.brics.automaton.Automaton;
+
+import java.util.Collections;
+import java.util.regex.Pattern;
+
+/**
+ * Matches an AccessSection against a reference name.
+ * <p>
+ * These matchers are "compiled" versions of the AccessSection name, supporting
+ * faster selection of which sections are relevant to any given input reference.
+ */
+abstract class SectionMatcher {
+  static SectionMatcher wrap(AccessSection section) {
+    String ref = section.getName();
+    if (AccessSection.isValid(ref)) {
+      return wrap(ref, section);
+    } else {
+      return null;
+    }
+  }
+
+  static SectionMatcher wrap(String pattern, AccessSection section) {
+    if (pattern.contains("${")) {
+      return new ExpandParameters(pattern, section);
+
+    } else if (isRE(pattern)) {
+      return new Regexp(pattern, section);
+
+    } else if (pattern.endsWith("/*")) {
+      return new Prefix(pattern.substring(0, pattern.length() - 1), section);
+
+    } else {
+      return new Exact(pattern, section);
+    }
+  }
+
+  final AccessSection section;
+
+  SectionMatcher(AccessSection section) {
+    this.section = section;
+  }
+
+  abstract boolean match(String ref, String username);
+
+  private static class Exact extends SectionMatcher {
+    private final String expect;
+
+    Exact(String name, AccessSection section) {
+      super(section);
+      expect = name;
+    }
+
+    @Override
+    boolean match(String ref, String username) {
+      return expect.equals(ref);
+    }
+  }
+
+  private static class Prefix extends SectionMatcher {
+    private final String prefix;
+
+    Prefix(String pfx, AccessSection section) {
+      super(section);
+      prefix = pfx;
+    }
+
+    @Override
+    boolean match(String ref, String username) {
+      return ref.startsWith(prefix);
+    }
+  }
+
+  private static class Regexp extends SectionMatcher {
+    private final Pattern pattern;
+
+    Regexp(String re, AccessSection section) {
+      super(section);
+      pattern = Pattern.compile(re);
+    }
+
+    @Override
+    boolean match(String ref, String username) {
+      return pattern.matcher(ref).matches();
+    }
+  }
+
+  static class ExpandParameters extends SectionMatcher {
+    private final ParameterizedString template;
+    private final String prefix;
+
+    ExpandParameters(String pattern, AccessSection section) {
+      super(section);
+      template = new ParameterizedString(pattern);
+
+      if (isRE(pattern)) {
+        // Replace ${username} with ":USERNAME:" as : is not legal
+        // in a reference and the string :USERNAME: is not likely to
+        // be a valid part of the regex. This later allows the pattern
+        // prefix to be clipped, saving time on evaluation.
+        Automaton am = RefControl.toRegExp(
+            template.replace(Collections.singletonMap("username", ":USERNAME:")))
+            .toAutomaton();
+        String rePrefix = am.getCommonPrefix();
+        prefix = rePrefix.substring(0, rePrefix.indexOf(":USERNAME:"));
+      } else {
+        prefix = pattern.substring(0, pattern.indexOf("${"));
+      }
+    }
+
+    @Override
+    boolean match(String ref, String username) {
+      if (!ref.startsWith(prefix) || username == null) {
+        return false;
+      }
+
+      String u;
+      if (isRE(template.getPattern())) {
+        u = username.replace(".", "\\.");
+      } else {
+        u = username;
+      }
+
+      SectionMatcher next = wrap(
+          template.replace(Collections.singletonMap("username", u)),
+          section);
+      return next != null ? next.match(ref, username) : false;
+    }
+
+   boolean matchPrefix(String ref) {
+     return ref.startsWith(prefix);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionSortCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionSortCache.java
new file mode 100644
index 0000000..047997e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SectionSortCache.java
@@ -0,0 +1,170 @@
+// Copyright (C) 2011 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.package com.google.gerrit.server.git;
+
+package com.google.gerrit.server.project;
+
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.server.cache.Cache;
+import com.google.gerrit.server.cache.CacheModule;
+import com.google.gerrit.server.util.MostSpecificComparator;
+import com.google.inject.Inject;
+import com.google.inject.Module;
+import com.google.inject.Singleton;
+import com.google.inject.TypeLiteral;
+import com.google.inject.name.Named;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.IdentityHashMap;
+import java.util.List;
+
+/** Caches the order AccessSections should be sorted for evaluation. */
+@Singleton
+public class SectionSortCache {
+  private static final Logger log =
+      LoggerFactory.getLogger(SectionSortCache.class);
+
+  private static final String CACHE_NAME = "permission_sort";
+
+  public static Module module() {
+    return new CacheModule() {
+      @Override
+      protected void configure() {
+        final TypeLiteral<Cache<EntryKey, EntryVal>> type =
+            new TypeLiteral<Cache<EntryKey, EntryVal>>() {};
+        core(type, CACHE_NAME);
+        bind(SectionSortCache.class);
+      }
+    };
+  }
+
+  private final Cache<EntryKey, EntryVal> cache;
+
+  @Inject
+  SectionSortCache(@Named(CACHE_NAME) Cache<EntryKey, EntryVal> cache) {
+    this.cache = cache;
+  }
+
+  void sort(String ref, List<AccessSection> sections) {
+    final int cnt = sections.size();
+    if (cnt <= 1) {
+      return;
+    }
+
+    EntryKey key = new EntryKey(ref, sections);
+    EntryVal val = cache.get(key);
+    if (val != null) {
+      int[] srcIdx = val.order;
+      if (srcIdx != null) {
+        AccessSection[] srcList = copy(sections);
+        for (int i = 0; i < cnt; i++) {
+          sections.set(i, srcList[srcIdx[i]]);
+        }
+      } else {
+        // Identity transform. No sorting is required.
+      }
+
+    } else {
+      boolean poison = false;
+      IdentityHashMap<AccessSection, Integer> srcMap =
+          new IdentityHashMap<AccessSection, Integer>();
+      for (int i = 0; i < cnt; i++) {
+        poison |= srcMap.put(sections.get(i), i) != null;
+      }
+
+      Collections.sort(sections, new MostSpecificComparator(ref));
+
+      int srcIdx[];
+      if (isIdentityTransform(sections, srcMap)) {
+        srcIdx = null;
+      } else {
+        srcIdx = new int[cnt];
+        for (int i = 0; i < cnt; i++) {
+          srcIdx[i] = srcMap.get(sections.get(i));
+        }
+      }
+
+      if (poison) {
+        log.error("Received duplicate AccessSection instances, not caching sort");
+      } else {
+        cache.put(key, new EntryVal(srcIdx));
+      }
+    }
+  }
+
+  private static AccessSection[] copy(List<AccessSection> sections) {
+    return sections.toArray(new AccessSection[sections.size()]);
+  }
+
+  private static boolean isIdentityTransform(List<AccessSection> sections,
+      IdentityHashMap<AccessSection, Integer> srcMap) {
+    for (int i = 0; i < sections.size(); i++) {
+      if (i != srcMap.get(sections.get(i))) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  static final class EntryKey {
+    private final String ref;
+    private final String[] patterns;
+    private final int hashCode;
+
+    EntryKey(String refName, List<AccessSection> sections) {
+      int hc = refName.hashCode();
+      ref = refName;
+      patterns = new String[sections.size()];
+      for (int i = 0; i < patterns.length; i++) {
+        String n = sections.get(i).getName();
+        patterns[i] = n;
+        hc = hc * 31 + n.hashCode();
+      }
+      hashCode = hc;
+    }
+
+    @Override
+    public int hashCode() {
+      return hashCode;
+    }
+
+    @Override
+    public boolean equals(Object other) {
+      if (other instanceof EntryKey) {
+        EntryKey b = (EntryKey) other;
+        return ref.equals(b.ref) && Arrays.equals(patterns, b.patterns);
+      }
+      return false;
+    }
+  }
+
+  static final class EntryVal {
+    /**
+     * Maps the input index to the output index.
+     * <p>
+     * For {@code x == order[y]} the expression means move the item at
+     * source position {@code x} to the output position {@code y}.
+     */
+    final int[] order;
+
+    EntryVal(int[] order) {
+      this.order = order;
+    }
+  }
+
+
+}
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..c73de60
--- /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.client.Project;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gwtorm.server.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/AndPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/AndPredicate.java
index f79fbac..096e12e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/AndPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/AndPredicate.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.query;
 
-import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.server.OrmException;
 
 import java.util.ArrayList;
 import java.util.Arrays;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/NotPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/NotPredicate.java
index f039e19..f94e1f6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/NotPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/NotPredicate.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.query;
 
-import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.server.OrmException;
 
 import java.util.Collection;
 import java.util.Collections;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/ObjectIdPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/ObjectIdPredicate.java
index 0d68fea..bea4da12 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/ObjectIdPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/ObjectIdPredicate.java
@@ -14,8 +14,6 @@
 
 package com.google.gerrit.server.query;
 
-import com.google.gerrit.server.query.OperatorPredicate;
-
 import org.eclipse.jgit.lib.AbbreviatedObjectId;
 import org.eclipse.jgit.lib.ObjectId;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/OrPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/OrPredicate.java
index a198745..57d21b1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/OrPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/OrPredicate.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.query;
 
-import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.server.OrmException;
 
 import java.util.ArrayList;
 import java.util.Arrays;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/Predicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/Predicate.java
index 485087f..ce47d2d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/Predicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/Predicate.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.query;
 
-import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.server.OrmException;
 
 import java.util.Collection;
 import java.util.Collections;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/VariablePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/VariablePredicate.java
index 5de116f..45b27e5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/VariablePredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/VariablePredicate.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.query;
 
-import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.server.OrmException;
 
 import java.util.Collection;
 import java.util.Collections;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AbstractResultSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AbstractResultSet.java
index 2b8d37d..e072760 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AbstractResultSet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AbstractResultSet.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gwtorm.client.ResultSet;
+import com.google.gwtorm.server.ResultSet;
 
 import java.util.ArrayList;
 import java.util.List;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AgePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AgePredicate.java
index 18a0c82..d0c9e4c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AgePredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AgePredicate.java
@@ -17,11 +17,11 @@
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static java.util.concurrent.TimeUnit.SECONDS;
 
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.query.OperatorPredicate;
-import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Provider;
 
 class AgePredicate extends OperatorPredicate<ChangeData> {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AndSource.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AndSource.java
index e74b390..3a0bfa3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AndSource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/AndSource.java
@@ -16,9 +16,9 @@
 
 import com.google.gerrit.server.query.AndPredicate;
 import com.google.gerrit.server.query.Predicate;
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.client.ResultSet;
-import com.google.gwtorm.client.impl.ListResultSet;
+import com.google.gwtorm.server.ListResultSet;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
 
 import java.util.ArrayList;
 import java.util.Collection;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BranchPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BranchPredicate.java
index 1083d6a..e5b4143 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BranchPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/BranchPredicate.java
@@ -14,21 +14,20 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.reviewdb.Branch;
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.query.OperatorPredicate;
-import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Provider;
 
 class BranchPredicate extends OperatorPredicate<ChangeData> {
   private final Provider<ReviewDb> dbProvider;
-  private final String shortName;
 
   BranchPredicate(Provider<ReviewDb> dbProvider, String branch) {
-    super(ChangeQueryBuilder.FIELD_BRANCH, branch);
+    super(ChangeQueryBuilder.FIELD_BRANCH, branch.startsWith(Branch.R_HEADS)
+        ? branch : Branch.R_HEADS + branch);
     this.dbProvider = dbProvider;
-    this.shortName = branch;
   }
 
   @Override
@@ -38,7 +37,7 @@
       return false;
     }
     return change.getDest().get().startsWith(Branch.R_HEADS)
-        && shortName.equals(change.getDest().getShortName());
+        && getValue().equals(change.getDest().get());
   }
 
   @Override
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 b4965f0..7a85b6f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeData.java
@@ -14,20 +14,29 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.Patch;
-import com.google.gerrit.reviewdb.PatchLineComment;
-import com.google.gerrit.reviewdb.PatchSet;
-import com.google.gerrit.reviewdb.PatchSetApproval;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gerrit.reviewdb.TrackingId;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.TrackingId;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 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.gwtorm.server.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;
@@ -39,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;
@@ -47,6 +57,7 @@
   private Collection<PatchLineComment> comments;
   private Collection<TrackingId> trackingIds;
   private CurrentUser visibleTo;
+  private List<ChangeMessage> messages;
 
   public ChangeData(final Change.Id id) {
     legacyId = id;
@@ -162,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) {
@@ -210,4 +243,12 @@
     }
     return trackingIds;
   }
+
+  public List<ChangeMessage> messages(Provider<ReviewDb> db)
+      throws OrmException {
+    if (messages == null) {
+      messages = db.get().changeMessages().byChange(legacyId).toList();
+    }
+    return messages;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeDataResultSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeDataResultSet.java
index fc7ba59..c2eb9b9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeDataResultSet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeDataResultSet.java
@@ -14,10 +14,10 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.PatchSet;
-import com.google.gerrit.reviewdb.PatchSetApproval;
-import com.google.gwtorm.client.ResultSet;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gwtorm.server.ResultSet;
 
 import java.util.HashSet;
 import java.util.Iterator;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeDataSource.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeDataSource.java
index a770b54..ecfd48c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeDataSource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeDataSource.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.client.ResultSet;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
 
 public interface ChangeDataSource {
   /** @return an estimate of the number of results from {@link #read()}. */
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIdPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIdPredicate.java
index 83107bb..7116aa9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIdPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeIdPredicate.java
@@ -14,11 +14,11 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.query.OperatorPredicate;
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.client.ResultSet;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Provider;
 
 class ChangeIdPredicate extends OperatorPredicate<ChangeData> implements
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
index 577c5af..4d8e806 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryBuilder.java
@@ -15,26 +15,27 @@
 package com.google.gerrit.server.query.change;
 
 import com.google.gerrit.common.data.ApprovalTypes;
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.RevId;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountResolver;
+import com.google.gerrit.server.account.CapabilityControl;
 import com.google.gerrit.server.account.GroupCache;
-import com.google.gerrit.server.config.AuthConfig;
-import com.google.gerrit.server.config.WildProjectName;
+import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.query.IntPredicate;
 import com.google.gerrit.server.query.Predicate;
 import com.google.gerrit.server.query.QueryBuilder;
 import com.google.gerrit.server.query.QueryParseException;
-import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
@@ -98,39 +99,43 @@
     final Provider<ReviewDb> dbProvider;
     final Provider<ChangeQueryRewriter> rewriter;
     final IdentifiedUser.GenericFactory userFactory;
+    final CapabilityControl.Factory capabilityControlFactory;
     final ChangeControl.Factory changeControlFactory;
     final ChangeControl.GenericFactory changeControlGenericFactory;
     final AccountResolver accountResolver;
     final GroupCache groupCache;
-    final AuthConfig authConfig;
     final ApprovalTypes approvalTypes;
-    final Project.NameKey wildProjectName;
+    final AllProjectsName allProjectsName;
     final PatchListCache patchListCache;
     final GitRepositoryManager repoManager;
+    final ProjectCache projectCache;
 
     @Inject
     Arguments(Provider<ReviewDb> dbProvider,
         Provider<ChangeQueryRewriter> rewriter,
         IdentifiedUser.GenericFactory userFactory,
+        CapabilityControl.Factory capabilityControlFactory,
         ChangeControl.Factory changeControlFactory,
         ChangeControl.GenericFactory changeControlGenericFactory,
         AccountResolver accountResolver, GroupCache groupCache,
-        AuthConfig authConfig, ApprovalTypes approvalTypes,
-        @WildProjectName Project.NameKey wildProjectName,
+        ApprovalTypes approvalTypes,
+        AllProjectsName allProjectsName,
         PatchListCache patchListCache,
-        GitRepositoryManager repoManager) {
+        GitRepositoryManager repoManager,
+        ProjectCache projectCache) {
       this.dbProvider = dbProvider;
       this.rewriter = rewriter;
       this.userFactory = userFactory;
+      this.capabilityControlFactory = capabilityControlFactory;
       this.changeControlFactory = changeControlFactory;
       this.changeControlGenericFactory = changeControlGenericFactory;
       this.accountResolver = accountResolver;
       this.groupCache = groupCache;
-      this.authConfig = authConfig;
       this.approvalTypes = approvalTypes;
-      this.wildProjectName = wildProjectName;
+      this.allProjectsName = allProjectsName;
       this.patchListCache = patchListCache;
       this.repoManager = repoManager;
+      this.projectCache = projectCache;
     }
   }
 
@@ -340,17 +345,18 @@
     //
     AccountGroup g = args.groupCache.get(new AccountGroup.NameKey(who));
     if (g != null) {
-      return visibleto(new SingleGroupUser(args.authConfig, g.getId()));
+      return visibleto(new SingleGroupUser(args.capabilityControlFactory,
+          g.getGroupUUID()));
     }
 
     Collection<AccountGroup> matches =
         args.groupCache.get(new AccountGroup.ExternalNameKey(who));
     if (matches != null && !matches.isEmpty()) {
-      HashSet<AccountGroup.Id> ids = new HashSet<AccountGroup.Id>();
+      HashSet<AccountGroup.UUID> ids = new HashSet<AccountGroup.UUID>();
       for (AccountGroup group : matches) {
-        ids.add(group.getId());
+        ids.add(group.getGroupUUID());
       }
-      return visibleto(new SingleGroupUser(args.authConfig, ids));
+      return visibleto(new SingleGroupUser(args.capabilityControlFactory, ids));
     }
 
     throw error("No user or group matches \"" + who + "\".");
@@ -391,7 +397,7 @@
     if (g == null) {
       throw error("Group " + group + " not found");
     }
-    return new OwnerinPredicate(args.dbProvider, args.userFactory, g.getId());
+    return new OwnerinPredicate(args.dbProvider, args.userFactory, g.getGroupUUID());
   }
 
   @Operator
@@ -419,7 +425,7 @@
     if (g == null) {
       throw error("Group " + group + " not found");
     }
-    return new ReviewerinPredicate(args.dbProvider, args.userFactory, g.getId());
+    return new ReviewerinPredicate(args.dbProvider, args.userFactory, g.getGroupUUID());
   }
 
   @Operator
@@ -508,23 +514,19 @@
       // Try to match a project name by substring query.
       final List<ProjectPredicate> predicate =
           new ArrayList<ProjectPredicate>();
-      try {
-        for (final Project p : args.dbProvider.get().projects().all()) {
-          if (p.getName().toLowerCase().contains(query.toLowerCase())) {
-            predicate.add(new ProjectPredicate(args.dbProvider, p.getName()));
-          }
+      for (Project.NameKey name : args.projectCache.all()) {
+        if (name.get().toLowerCase().contains(query.toLowerCase())) {
+          predicate.add(new ProjectPredicate(args.dbProvider, name.get()));
         }
+      }
 
-        // If two or more projects contains "query" as substring create an
-        // OrPredicate holding predicates for all these projects, otherwise if
-        // only one contains that, return only that one predicate by itself.
-        if (predicate.size() == 1) {
-          return predicate.get(0);
-        } else if (predicate.size() > 1) {
-          return Predicate.or(predicate);
-        }
-      } catch (OrmException e) {
-        throw error("Cannot lookup project.", e);
+      // If two or more projects contains "query" as substring create an
+      // OrPredicate holding predicates for all these projects, otherwise if
+      // only one contains that, return only that one predicate by itself.
+      if (predicate.size() == 1) {
+        return predicate.get(0);
+      } else if (predicate.size() > 1) {
+        return Predicate.or(predicate);
       }
 
       throw error("Unsupported query:" + query);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java
index 717b487..34ec2f4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java
@@ -14,16 +14,17 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.ChangeAccess;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.server.ChangeAccess;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.query.IntPredicate;
 import com.google.gerrit.server.query.Predicate;
 import com.google.gerrit.server.query.QueryRewriter;
 import com.google.gerrit.server.query.RewritePredicate;
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.client.ResultSet;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Inject;
 import com.google.inject.OutOfScopeException;
 import com.google.inject.Provider;
@@ -38,7 +39,8 @@
               new ChangeQueryBuilder.Arguments( //
                   new InvalidProvider<ReviewDb>(), //
                   new InvalidProvider<ChangeQueryRewriter>(), //
-                  null, null, null, null, null, null, null, null, null, null), null));
+                  null, null, null, null, null, null, null, //
+                  null, null, null, null), null));
 
   private final Provider<ReviewDb> dbProvider;
 
@@ -117,6 +119,75 @@
     return a.getValue().compareTo(b.getValue()) >= 0 ? a : b;
   }
 
+  @Rewrite("status:open P=(project:*) B=(branch:*)")
+  public Predicate<ChangeData> r05_byBranchOpen(
+      @Named("P") final ProjectPredicate p,
+      @Named("B") final BranchPredicate b) {
+    return new ChangeSource(500) {
+      @Override
+      ResultSet<Change> scan(ChangeAccess a)
+          throws OrmException {
+        return a.byBranchOpenAll(
+            new Branch.NameKey(p.getValueKey(), b.getValue()));
+      }
+
+      @Override
+      public boolean match(ChangeData cd) throws OrmException {
+        return cd.change(dbProvider).getStatus().isOpen()
+            && p.match(cd)
+            && b.match(cd);
+      }
+    };
+  }
+
+  @Rewrite("status:merged P=(project:*) B=(branch:*) S=(sortkey_after:*) L=(limit:*)")
+  public Predicate<ChangeData> r05_byBranchMergedPrev(
+      @Named("P") final ProjectPredicate p,
+      @Named("B") final BranchPredicate b,
+      @Named("S") final SortKeyPredicate.After s,
+      @Named("L") final IntPredicate<ChangeData> l) {
+    return new PaginatedSource(40000, s.getValue(), l.intValue()) {
+      @Override
+      ResultSet<Change> scan(ChangeAccess a, String key, int limit)
+          throws OrmException {
+        return a.byBranchClosedPrev(Change.Status.MERGED.getCode(), //
+            new Branch.NameKey(p.getValueKey(), b.getValue()), key, limit);
+      }
+
+      @Override
+      public boolean match(ChangeData cd) throws OrmException {
+        return cd.change(dbProvider).getStatus() == Change.Status.MERGED
+            && p.match(cd) //
+            && b.match(cd) //
+            && s.match(cd);
+      }
+    };
+  }
+
+  @Rewrite("status:merged P=(project:*) B=(branch:*) S=(sortkey_before:*) L=(limit:*)")
+  public Predicate<ChangeData> r05_byBranchMergedNext(
+      @Named("P") final ProjectPredicate p,
+      @Named("B") final BranchPredicate b,
+      @Named("S") final SortKeyPredicate.Before s,
+      @Named("L") final IntPredicate<ChangeData> l) {
+    return new PaginatedSource(40000, s.getValue(), l.intValue()) {
+      @Override
+      ResultSet<Change> scan(ChangeAccess a, String key, int limit)
+          throws OrmException {
+        return a.byBranchClosedNext(Change.Status.MERGED.getCode(), //
+            new Branch.NameKey(p.getValueKey(), b.getValue()), key, limit);
+      }
+
+      @Override
+      public boolean match(ChangeData cd) throws OrmException {
+        return cd.change(dbProvider).getStatus() == Change.Status.MERGED
+            && p.match(cd) //
+            && b.match(cd) //
+            && s.match(cd);
+      }
+    };
+  }
+
   @Rewrite("status:open P=(project:*) S=(sortkey_after:*) L=(limit:*)")
   public Predicate<ChangeData> r10_byProjectOpenPrev(
       @Named("P") final ProjectPredicate p,
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java
index 4ae2278..6e9e79c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeStatusPredicate.java
@@ -14,11 +14,11 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.query.OperatorPredicate;
 import com.google.gerrit.server.query.Predicate;
-import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Provider;
 
 import java.util.ArrayList;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitPredicate.java
index c03cddc..274b40c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/CommitPredicate.java
@@ -14,12 +14,12 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.reviewdb.PatchSet;
-import com.google.gerrit.reviewdb.RevId;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.query.ObjectIdPredicate;
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.client.ResultSet;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Provider;
 
 import org.eclipse.jgit.lib.AbbreviatedObjectId;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HasDraftByPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HasDraftByPredicate.java
index 07d4dd2..1d9a9a4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HasDraftByPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/HasDraftByPredicate.java
@@ -14,14 +14,14 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.PatchLineComment;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.query.OperatorPredicate;
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.client.ResultSet;
-import com.google.gwtorm.client.impl.ListResultSet;
+import com.google.gwtorm.server.ListResultSet;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Provider;
 
 import java.util.ArrayList;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsReviewedPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsReviewedPredicate.java
index 46c7741..639be517c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsReviewedPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsReviewedPredicate.java
@@ -14,12 +14,12 @@
 
 package com.google.gerrit.server.query.change;
 
-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.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.query.OperatorPredicate;
-import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Provider;
 
 class IsReviewedPredicate extends OperatorPredicate<ChangeData> {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsStarredByPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsStarredByPredicate.java
index aaf8478..3fd9feb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsStarredByPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsStarredByPredicate.java
@@ -14,12 +14,12 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.query.OperatorPredicate;
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.client.ResultSet;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Provider;
 
 class IsStarredByPredicate extends OperatorPredicate<ChangeData> implements
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..413e6c4 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
@@ -14,14 +14,14 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.query.OperatorPredicate;
-import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Provider;
 
 class IsVisibleToPredicate extends OperatorPredicate<ChangeData> {
@@ -30,7 +30,7 @@
       return ((IdentifiedUser) user).getAccountId().toString();
     }
     if (user instanceof SingleGroupUser) {
-      return "group:" + ((SingleGroupUser) user).getEffectiveGroups() //
+      return "group:" + ((SingleGroupUser) user).getEffectiveGroups().getKnownGroups() //
           .iterator().next().toString();
     }
     return user.toString();
@@ -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/IsWatchedByPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java
index 870be73..25cfae7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/IsWatchedByPredicate.java
@@ -14,15 +14,15 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.reviewdb.AccountProjectWatch;
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.query.OperatorPredicate;
 import com.google.gerrit.server.query.Predicate;
 import com.google.gerrit.server.query.QueryParseException;
-import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.server.OrmException;
 
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -79,7 +79,7 @@
     Project.NameKey project = change.getDest().getParentKey();
     List<Predicate<ChangeData>> list = rules.get(project);
     if (list == null) {
-      list = rules.get(args.wildProjectName);
+      list = rules.get(args.allProjectsName);
     }
     if (list != null) {
       for (Predicate<ChangeData> p : list) {
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 e76c278..bf21261 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
@@ -16,14 +16,15 @@
 
 import com.google.gerrit.common.data.ApprovalType;
 import com.google.gerrit.common.data.ApprovalTypes;
-import com.google.gerrit.reviewdb.ApprovalCategory;
-import com.google.gerrit.reviewdb.PatchSetApproval;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.reviewdb.client.ApprovalCategory;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.query.OperatorPredicate;
-import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Provider;
 
 import java.util.regex.Matcher;
@@ -33,48 +34,54 @@
   private static enum Test {
     EQ {
       @Override
-      public boolean match(short psValue, short expValue) {
+      public boolean match(int psValue, int expValue) {
         return psValue == expValue;
       }
     },
     GT_EQ {
       @Override
-      public boolean match(short psValue, short expValue) {
+      public boolean match(int psValue, int expValue) {
         return psValue >= expValue;
       }
     },
     LT_EQ {
       @Override
-      public boolean match(short psValue, short expValue) {
+      public boolean match(int psValue, int expValue) {
         return psValue <= expValue;
       }
     };
 
-    abstract boolean match(short psValue, short expValue);
+    abstract boolean match(int psValue, int expValue);
   }
 
-  private static ApprovalCategory.Id category(ApprovalTypes types, String toFind) {
-    if (types.getApprovalType(new ApprovalCategory.Id(toFind)) != null) {
-      return new ApprovalCategory.Id(toFind);
+  private static ApprovalCategory category(ApprovalTypes types, String toFind) {
+    if (types.byLabel(toFind) != null) {
+      return types.byLabel(toFind).getCategory();
+    }
+
+    if (types.byId(new ApprovalCategory.Id(toFind)) != null) {
+      return types.byId(new ApprovalCategory.Id(toFind)).getCategory();
     }
 
     for (ApprovalType at : types.getApprovalTypes()) {
-      String name = at.getCategory().getName();
-      if (toFind.equalsIgnoreCase(name)) {
-        return at.getCategory().getId();
+      ApprovalCategory category = at.getCategory();
 
-      } else if (toFind.equalsIgnoreCase(name.replace(" ", ""))) {
-        return at.getCategory().getId();
+      if (toFind.equalsIgnoreCase(category.getName())) {
+        return category;
+
+      } else if (toFind.equalsIgnoreCase(category.getName().replace(" ", ""))) {
+        return category;
       }
     }
 
     for (ApprovalType at : types.getApprovalTypes()) {
-      if (toFind.equalsIgnoreCase(at.getCategory().getAbbreviatedName())) {
-        return at.getCategory().getId();
+      ApprovalCategory category = at.getCategory();
+      if (toFind.equalsIgnoreCase(category.getAbbreviatedName())) {
+        return category;
       }
     }
 
-    return new ApprovalCategory.Id(toFind);
+    return new ApprovalCategory(new ApprovalCategory.Id(toFind), toFind);
   }
 
   private static Test op(String op) {
@@ -92,19 +99,20 @@
     }
   }
 
-  private static short value(String value) {
+  private static int value(String value) {
     if (value.startsWith("+")) {
       value = value.substring(1);
     }
-    return Short.parseShort(value);
+    return Integer.parseInt(value);
   }
 
   private final ChangeControl.GenericFactory ccFactory;
   private final IdentifiedUser.GenericFactory userFactory;
   private final Provider<ReviewDb> dbProvider;
   private final Test test;
-  private final ApprovalCategory.Id category;
-  private final short expVal;
+  private final ApprovalCategory category;
+  private final String permissionName;
+  private final int expVal;
 
   LabelPredicate(ChangeControl.GenericFactory ccFactory,
       IdentifiedUser.GenericFactory userFactory, Provider<ReviewDb> dbProvider,
@@ -131,25 +139,27 @@
       test = Test.EQ;
       expVal = 1;
     }
+
+    this.permissionName = Permission.forLabel(category.getLabelName());
   }
 
   @Override
   public boolean match(final ChangeData object) throws OrmException {
     for (PatchSetApproval p : object.currentApprovals(dbProvider)) {
-      if (p.getCategoryId().equals(category)) {
-        short psVal = p.getValue();
+      if (p.getCategoryId().equals(category.getId())) {
+        int psVal = p.getValue();
         if (test.match(psVal, expVal)) {
           // Double check the value is still permitted for the user.
           //
           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;
             }
-            psVal = cc.normalize(category, psVal);
+            psVal = cc.getRange(permissionName).squash(psVal);
           } catch (NoSuchChangeException e) {
             // The project has disappeared.
             //
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyChangeIdPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyChangeIdPredicate.java
index ac544a3..4c47e73 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyChangeIdPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/LegacyChangeIdPredicate.java
@@ -14,12 +14,12 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.query.OperatorPredicate;
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.client.ResultSet;
-import com.google.gwtorm.client.impl.ListResultSet;
+import com.google.gwtorm.server.ListResultSet;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Provider;
 
 import java.util.Collections;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/MessagePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/MessagePredicate.java
index 20b777d..f016c37 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/MessagePredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/MessagePredicate.java
@@ -14,14 +14,14 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.PatchSet;
-import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.RevId;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.query.OperatorPredicate;
-import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Provider;
 
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OrSource.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OrSource.java
index 617a14a..ec5195b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OrSource.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OrSource.java
@@ -14,12 +14,12 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.query.OrPredicate;
 import com.google.gerrit.server.query.Predicate;
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.client.ResultSet;
-import com.google.gwtorm.client.impl.ListResultSet;
+import com.google.gwtorm.server.ListResultSet;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
 
 import java.util.ArrayList;
 import java.util.Collection;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OwnerPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OwnerPredicate.java
index 224dce9..7a85ef6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OwnerPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OwnerPredicate.java
@@ -14,11 +14,11 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.query.OperatorPredicate;
-import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Provider;
 
 class OwnerPredicate extends OperatorPredicate<ChangeData> {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OwnerinPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OwnerinPredicate.java
index 4f6fe5a5..05c4cbf 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OwnerinPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/OwnerinPredicate.java
@@ -14,29 +14,29 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.query.OperatorPredicate;
-import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Provider;
 
 class OwnerinPredicate extends OperatorPredicate<ChangeData> {
   private final Provider<ReviewDb> dbProvider;
   private final IdentifiedUser.GenericFactory userFactory;
-  private final AccountGroup.Id id;
+  private final AccountGroup.UUID uuid;
 
   OwnerinPredicate(Provider<ReviewDb> dbProvider,
-    IdentifiedUser.GenericFactory userFactory, AccountGroup.Id id) {
-    super(ChangeQueryBuilder.FIELD_OWNERIN, id.toString());
+    IdentifiedUser.GenericFactory userFactory, AccountGroup.UUID uuid) {
+    super(ChangeQueryBuilder.FIELD_OWNERIN, uuid.toString());
     this.dbProvider = dbProvider;
     this.userFactory = userFactory;
-    this.id = id;
+    this.uuid = uuid;
   }
 
-  AccountGroup.Id getAccountGroupId() {
-    return id;
+  AccountGroup.UUID getAccountGroupUUID() {
+    return uuid;
   }
 
   @Override
@@ -47,7 +47,7 @@
     }
     final IdentifiedUser owner = userFactory.create(dbProvider,
       change.getOwner());
-    return owner.getEffectiveGroups().contains(id);
+    return owner.getEffectiveGroups().contains(uuid);
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/Paginated.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/Paginated.java
index b046db6..2b3edf7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/Paginated.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/Paginated.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.client.ResultSet;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
 
 interface Paginated {
   int limit();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ProjectPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ProjectPredicate.java
index 91203d6..cce2f2a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ProjectPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ProjectPredicate.java
@@ -14,11 +14,11 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.query.OperatorPredicate;
-import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Provider;
 
 class ProjectPredicate extends OperatorPredicate<ChangeData> {
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 a975f1b..a2fa7fe 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java
@@ -14,17 +14,20 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.PatchSet;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.events.ChangeAttribute;
 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;
-import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
@@ -64,12 +67,17 @@
   private final ChangeQueryBuilder queryBuilder;
   private final ChangeQueryRewriter queryRewriter;
   private final Provider<ReviewDb> db;
+  private final GitRepositoryManager repoManager;
+  private final int maxLimit;
 
-  private int defaultLimit = 500;
   private OutputFormat outputFormat = OutputFormat.TEXT;
   private boolean includePatchSets;
   private boolean includeCurrentPatchSet;
   private boolean includeApprovals;
+  private boolean includeComments;
+  private boolean includeFiles;
+  private boolean includeCommitMessage;
+  private boolean includeDependencies;
 
   private OutputStream outputStream = DisabledOutputStream.INSTANCE;
   private PrintWriter out;
@@ -77,87 +85,147 @@
   @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();
   }
 
   public void setIncludePatchSets(boolean on) {
     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;
   }
 
+  public void setIncludeComments(boolean on) {
+    includeComments = on;
+  }
+
+  public void setIncludeFiles(boolean on) {
+    includeFiles = on;
+  }
+
+  public boolean getIncludeFiles() {
+    return includeFiles;
+  }
+
+  public void setIncludeDependencies(boolean on) {
+    includeDependencies = on;
+  }
+
+  public boolean getIncludeDependencies() {
+    return includeDependencies;
+  }
+
+  public void setIncludeCommitMessage(boolean on) {
+    includeCommitMessage = on;
+  }
+
   public void setOutput(OutputStream out, OutputFormat fmt) {
     this.outputStream = out;
     this.outputFormat = fmt;
   }
 
+  public List<ChangeData> queryChanges(final String queryString)
+      throws OrmException, QueryParseException {
+    final Predicate<ChangeData> visibleToMe = queryBuilder.is_visible();
+    Predicate<ChangeData> s = compileQuery(queryString, visibleToMe);
+    List<ChangeData> results = new ArrayList<ChangeData>();
+    HashSet<Change.Id> want = new HashSet<Change.Id>();
+    for (ChangeData d : ((ChangeDataSource) s).read()) {
+      if (d.hasChange()) {
+        // Checking visibleToMe here should be unnecessary, the
+        // query should have already performed it. But we don't
+        // want to trust the query rewriter that much yet.
+        //
+        if (visibleToMe.match(d)) {
+          results.add(d);
+        }
+      } else {
+        want.add(d.getId());
+      }
+    }
+
+    if (!want.isEmpty()) {
+      for (Change c : db.get().changes().get(want)) {
+        ChangeData d = new ChangeData(c);
+        if (visibleToMe.match(d)) {
+          results.add(d);
+        }
+      }
+    }
+
+    Collections.sort(results, new Comparator<ChangeData>() {
+      @Override
+      public int compare(ChangeData a, ChangeData b) {
+        return b.getChange().getSortKey().compareTo(
+            a.getChange().getSortKey());
+      }
+    });
+
+    int limit = limit(s);
+    if (limit < results.size()) {
+      results = results.subList(0, limit);
+    }
+
+    return results;
+  }
+
   public void query(String queryString) throws IOException {
     out = new PrintWriter( //
         new BufferedWriter( //
             new OutputStreamWriter(outputStream, "UTF-8")));
     try {
+      if (maxLimit <= 0) {
+        ErrorMessage m = new ErrorMessage();
+        m.message = "query disabled";
+        show(m);
+        return;
+      }
+
       try {
         final QueryStats stats = new QueryStats();
         stats.runTimeMilliseconds = System.currentTimeMillis();
 
-        final Predicate<ChangeData> visibleToMe = queryBuilder.is_visible();
-        Predicate<ChangeData> s = compileQuery(queryString, visibleToMe);
-        List<ChangeData> results = new ArrayList<ChangeData>();
-        HashSet<Change.Id> want = new HashSet<Change.Id>();
-        for (ChangeData d : ((ChangeDataSource) s).read()) {
-          if (d.hasChange()) {
-            // Checking visibleToMe here should be unnecessary, the
-            // query should have already performed it. But we don't
-            // want to trust the query rewriter that much yet.
-            //
-            if (visibleToMe.match(d)) {
-              results.add(d);
-            }
-          } else {
-            want.add(d.getId());
-          }
-        }
-
-        if (!want.isEmpty()) {
-          for (Change c : db.get().changes().get(want)) {
-            ChangeData d = new ChangeData(c);
-            if (visibleToMe.match(d)) {
-              results.add(d);
-            }
-          }
-        }
-
-        Collections.sort(results, new Comparator<ChangeData>() {
-          @Override
-          public int compare(ChangeData a, ChangeData b) {
-            return b.getChange().getSortKey().compareTo(
-                a.getChange().getSortKey());
-          }
-        });
-
-        int limit = limit(s);
-        if (limit < results.size()) {
-          results = results.subList(0, limit);
-        }
-
+        List<ChangeData> results = queryChanges(queryString);
         for (ChangeData d : results) {
           ChangeAttribute c = eventFactory.asChangeAttribute(d.getChange());
           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) {
@@ -166,9 +234,27 @@
               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));
+              }
             }
           }
 
+          if (includeComments) {
+            eventFactory.addComments(c, d.messages(db));
+            if (includePatchSets) {
+              for (PatchSetAttribute attribute : c.patchSets) {
+                eventFactory.addPatchSetComments(attribute,  d.comments(db));
+              }
+            }
+          }
+
+          if (includeDependencies) {
+            eventFactory.addDependencies(c, d.getChange());
+          }
+
           show(c);
         }
 
@@ -198,7 +284,7 @@
   }
 
   private int limit(Predicate<ChangeData> s) {
-    return queryBuilder.hasLimit(s) ? queryBuilder.getLimit(s) : defaultLimit;
+    return queryBuilder.hasLimit(s) ? queryBuilder.getLimit(s) : maxLimit;
   }
 
   @SuppressWarnings("unchecked")
@@ -206,13 +292,10 @@
       final Predicate<ChangeData> visibleToMe) throws QueryParseException {
 
     Predicate<ChangeData> q = queryBuilder.parse(queryString);
-    if (!queryBuilder.hasLimit(q)) {
-      q = Predicate.and(q, queryBuilder.limit(defaultLimit));
-    }
     if (!queryBuilder.hasSortKey(q)) {
       q = Predicate.and(q, queryBuilder.sortkey_before("z"));
     }
-    q = Predicate.and(q, visibleToMe);
+    q = Predicate.and(q, queryBuilder.limit(maxLimit), visibleToMe);
 
     Predicate<ChangeData> s = queryRewriter.rewrite(q);
     if (!(s instanceof ChangeDataSource)) {
@@ -262,42 +345,61 @@
         continue;
       }
 
-      indent(depth);
-      out.print(f.getName());
-      out.print(":");
-
-      if (val instanceof Long && isDateField(f.getName())) {
-        out.print(' ');
-        out.print(sdf.format(new Date(((Long) val) * 1000L)));
-        out.print('\n');
-      } else {
-        showTextValue(val, depth);
-      }
+      showField(f.getName(), val, depth);
     }
   }
 
-  private void indent(int depth) {
-    for (int i = 0; i < depth; i++) {
-      out.print("  ");
+  private String indent(int spaces) {
+    if (spaces == 0) {
+      return "";
+    } else {
+      return String.format("%" + spaces + "s", " ");
     }
   }
 
-  private void showTextValue(Object value, int depth) {
-    if (isPrimitive(value)) {
+  private void showField(String field, Object value, int depth) {
+    final int spacesDepthRatio = 2;
+    String indent = indent(depth * spacesDepthRatio);
+    out.print(indent);
+    out.print(field);
+    out.print(':');
+    if (value instanceof String && ((String) value).contains("\n")) {
+      out.print(' ');
+      // Idention for multi-line text is
+      // current depth indetion + length of field + length of ": "
+      indent = indent(indent.length() + field.length() + spacesDepthRatio);
+      out.print(((String) value).replaceAll("\n", "\n" + indent).trim());
+      out.print('\n');
+    } else if (value instanceof Long && isDateField(field)) {
+      out.print(' ');
+      out.print(sdf.format(new Date(((Long) value) * 1000L)));
+      out.print('\n');
+    } else if (isPrimitive(value)) {
       out.print(' ');
       out.print(value);
       out.print('\n');
-
     } else if (value instanceof Collection) {
       out.print('\n');
+      boolean firstElement = true;
       for (Object thing : ((Collection<?>) value)) {
+        // The name of the collection was initially printed at the beginning
+        // of this routine.  Beginning at the second sub-element, reprint
+        // the collection name so humans can separate individual elements
+        // with less strain and error.
+        //
+        if (firstElement) {
+          firstElement = false;
+        } else {
+          out.print(indent);
+          out.print(field);
+          out.print(":\n");
+        }
         if (isPrimitive(thing)) {
           out.print(' ');
           out.print(value);
           out.print('\n');
         } else {
           showText(thing, depth + 1);
-          out.print('\n');
         }
       }
     } else {
@@ -315,7 +417,9 @@
 
   private static boolean isDateField(String name) {
     return "lastUpdated".equals(name) //
-        || "grantedOn".equals(name);
+        || "grantedOn".equals(name) //
+        || "timestamp".equals(name) //
+        || "createdOn".equals(name);
   }
 
   private List<Field> fieldsOf(Class<?> type) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RefPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RefPredicate.java
index f5f83e2..4811f9a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RefPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RefPredicate.java
@@ -14,10 +14,10 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.query.OperatorPredicate;
-import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Provider;
 
 class RefPredicate extends OperatorPredicate<ChangeData> {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexBranchPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexBranchPredicate.java
index a18d43a..6704a10 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexBranchPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexBranchPredicate.java
@@ -14,11 +14,11 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.reviewdb.Branch;
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.query.OperatorPredicate;
-import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Provider;
 
 import dk.brics.automaton.RegExp;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexFilePredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexFilePredicate.java
index 6bccec1..11856e4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexFilePredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexFilePredicate.java
@@ -14,10 +14,10 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.query.OperatorPredicate;
-import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Provider;
 
 import dk.brics.automaton.Automaton;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexProjectPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexProjectPredicate.java
index c35b66e..b8911a4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexProjectPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexProjectPredicate.java
@@ -14,11 +14,11 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.query.OperatorPredicate;
-import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Provider;
 
 import dk.brics.automaton.RegExp;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexRefPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexRefPredicate.java
index e9e9958..1480de6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexRefPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexRefPredicate.java
@@ -14,10 +14,10 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.query.OperatorPredicate;
-import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Provider;
 
 import dk.brics.automaton.RegExp;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexTopicPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexTopicPredicate.java
index e16088c..03814f8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexTopicPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/RegexTopicPredicate.java
@@ -14,10 +14,10 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.query.OperatorPredicate;
-import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Provider;
 
 import dk.brics.automaton.RegExp;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerPredicate.java
index bcece94..8e910df 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerPredicate.java
@@ -14,11 +14,11 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.PatchSetApproval;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.query.OperatorPredicate;
-import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Provider;
 
 class ReviewerPredicate extends OperatorPredicate<ChangeData> {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerinPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerinPredicate.java
index 1784d9a..421c6c6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerinPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ReviewerinPredicate.java
@@ -14,29 +14,29 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.PatchSetApproval;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.query.OperatorPredicate;
-import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Provider;
 
 class ReviewerinPredicate extends OperatorPredicate<ChangeData> {
   private final Provider<ReviewDb> dbProvider;
   private final IdentifiedUser.GenericFactory userFactory;
-  private final AccountGroup.Id id;
+  private final AccountGroup.UUID uuid;
 
   ReviewerinPredicate(Provider<ReviewDb> dbProvider,
-    IdentifiedUser.GenericFactory userFactory, AccountGroup.Id id) {
-    super(ChangeQueryBuilder.FIELD_REVIEWERIN, id.toString());
+    IdentifiedUser.GenericFactory userFactory, AccountGroup.UUID uuid) {
+    super(ChangeQueryBuilder.FIELD_REVIEWERIN, uuid.toString());
     this.dbProvider = dbProvider;
     this.userFactory = userFactory;
-    this.id = id;
+    this.uuid = uuid;
   }
 
-  AccountGroup.Id getAccountGroupId() {
-    return id;
+  AccountGroup.UUID getAccountGroupUUID() {
+    return uuid;
   }
 
   @Override
@@ -44,7 +44,7 @@
     for (PatchSetApproval p : object.approvals(dbProvider)) {
       final IdentifiedUser reviewer = userFactory.create(dbProvider,
         p.getAccountId());
-      if (reviewer.getEffectiveGroups().contains(id)) {
+      if (reviewer.getEffectiveGroups().contains(uuid)) {
         return true;
       }
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SingleGroupUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SingleGroupUser.java
index 2fb6694..cdce217 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SingleGroupUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SingleGroupUser.java
@@ -14,31 +14,35 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.AccountProjectWatch;
-import com.google.gerrit.reviewdb.Change;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch;
+import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.server.AccessPath;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.config.AuthConfig;
+import com.google.gerrit.server.account.CapabilityControl;
+import com.google.gerrit.server.account.GroupMembership;
+import com.google.gerrit.server.account.ListGroupMembership;
 
 import java.util.Collection;
 import java.util.Collections;
 import java.util.Set;
 
 final class SingleGroupUser extends CurrentUser {
-  private final Set<AccountGroup.Id> groups;
+  private final GroupMembership groups;
 
-  SingleGroupUser(AuthConfig authConfig, AccountGroup.Id groupId) {
-    this(authConfig, Collections.singleton(groupId));
+  SingleGroupUser(CapabilityControl.Factory capabilityControlFactory,
+      AccountGroup.UUID groupId) {
+    this(capabilityControlFactory, Collections.singleton(groupId));
   }
 
-  SingleGroupUser(AuthConfig authConfig, Set<AccountGroup.Id> groups) {
-    super(AccessPath.UNKNOWN, authConfig);
-    this.groups = groups;
+  SingleGroupUser(CapabilityControl.Factory capabilityControlFactory,
+      Set<AccountGroup.UUID> groups) {
+    super(capabilityControlFactory, AccessPath.UNKNOWN);
+    this.groups = new ListGroupMembership(groups);
   }
 
   @Override
-  public Set<AccountGroup.Id> getEffectiveGroups() {
+  public GroupMembership getEffectiveGroups() {
     return groups;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SortKeyPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SortKeyPredicate.java
index f67bee1..e7668c6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SortKeyPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/SortKeyPredicate.java
@@ -14,10 +14,10 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.query.OperatorPredicate;
-import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Provider;
 
 abstract class SortKeyPredicate extends OperatorPredicate<ChangeData> {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TopicPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TopicPredicate.java
index 7bc972d..8d58376 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TopicPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TopicPredicate.java
@@ -14,10 +14,10 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.query.OperatorPredicate;
-import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Provider;
 
 class TopicPredicate extends OperatorPredicate<ChangeData> {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TrackingIdPredicate.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TrackingIdPredicate.java
index eef568d..e022f86 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TrackingIdPredicate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/TrackingIdPredicate.java
@@ -14,13 +14,13 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gerrit.reviewdb.TrackingId;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.TrackingId;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.query.OperatorPredicate;
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.client.ResultSet;
-import com.google.gwtorm.client.impl.ListResultSet;
+import com.google.gwtorm.server.ListResultSet;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Provider;
 
 import java.util.ArrayList;
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..2a98e96 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
@@ -15,7 +15,8 @@
 package com.google.gerrit.server.schema;
 
 import static com.google.gerrit.server.config.ConfigUtil.getEnum;
-import static java.util.concurrent.TimeUnit.*;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.SECONDS;
 
 import com.google.gerrit.lifecycle.LifecycleListener;
 import com.google.gerrit.server.config.ConfigUtil;
@@ -87,11 +88,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/DatabaseModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/DatabaseModule.java
index 55a36d6..c7832d4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/DatabaseModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/DatabaseModule.java
@@ -16,18 +16,16 @@
 
 import static com.google.inject.Scopes.SINGLETON;
 
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.config.FactoryModule;
-import com.google.gwtorm.client.SchemaFactory;
 import com.google.gwtorm.jdbc.Database;
+import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.TypeLiteral;
 
 /** Loads the database with standard dependencies. */
 public class DatabaseModule extends FactoryModule {
   @Override
   protected void configure() {
-    install(new SchemaVersion.Module());
-
     bind(new TypeLiteral<SchemaFactory<ReviewDb>>() {}).to(
         new TypeLiteral<Database<ReviewDb>>() {}).in(SINGLETON);
     bind(new TypeLiteral<Database<ReviewDb>>() {}).toProvider(
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/ReviewDbDatabaseProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/ReviewDbDatabaseProvider.java
index 56b0379..ed77fff 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/ReviewDbDatabaseProvider.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/ReviewDbDatabaseProvider.java
@@ -14,9 +14,9 @@
 
 package com.google.gerrit.server.schema;
 
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gwtorm.client.OrmException;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gwtorm.jdbc.Database;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.ProvisionException;
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 a24471a..d8809da 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
@@ -14,29 +14,44 @@
 
 package com.google.gerrit.server.schema;
 
-import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.AccountGroupName;
-import com.google.gerrit.reviewdb.ApprovalCategory;
-import com.google.gerrit.reviewdb.ApprovalCategoryValue;
-import com.google.gerrit.reviewdb.CurrentSchemaVersion;
-import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.RefRight;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gerrit.reviewdb.SystemConfig;
+import com.google.gerrit.common.Version;
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupName;
+import com.google.gerrit.reviewdb.client.ApprovalCategory;
+import com.google.gerrit.reviewdb.client.ApprovalCategoryValue;
+import com.google.gerrit.reviewdb.client.CurrentSchemaVersion;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.SystemConfig;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.account.GroupUUID;
+import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.SitePath;
 import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.workflow.NoOpFunction;
-import com.google.gerrit.server.workflow.SubmitFunction;
-import com.google.gwtjsonrpc.server.SignedToken;
-import com.google.gwtorm.client.OrmException;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.NoReplication;
+import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gwtorm.jdbc.JdbcExecutor;
 import com.google.gwtorm.jdbc.JdbcSchema;
 import com.google.gwtorm.schema.sql.DialectH2;
 import com.google.gwtorm.schema.sql.DialectMySQL;
 import com.google.gwtorm.schema.sql.DialectPostgreSQL;
 import com.google.gwtorm.schema.sql.SqlDialect;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 
+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;
 import java.io.IOException;
 import java.util.ArrayList;
@@ -44,33 +59,49 @@
 
 /** Creates the current database schema and populates initial code rows. */
 public class SchemaCreator {
-  private static final Project.NameKey DEFAULT_WILD_NAME =
-      new Project.NameKey("-- All Projects --");
-
   private final @SitePath
   File site_path;
 
+  private final GitRepositoryManager mgr;
+  private final AllProjectsName allProjectsName;
+  private final PersonIdent serverUser;
+
   private final int versionNbr;
   private final ScriptRunner index_generic;
   private final ScriptRunner index_postgres;
   private final ScriptRunner mysql_nextval;
 
+  private AccountGroup admin;
+  private AccountGroup anonymous;
+  private AccountGroup registered;
+  private AccountGroup owners;
+
   @Inject
-  public SchemaCreator(final SitePaths site,
-      @Current final SchemaVersion version) {
-    this(site.site_path, version);
+  public SchemaCreator(SitePaths site,
+      @Current SchemaVersion version,
+      GitRepositoryManager mgr,
+      AllProjectsName allProjectsName,
+      @GerritPersonIdent PersonIdent au) {
+    this(site.site_path, version, mgr, allProjectsName, au);
   }
 
-  public SchemaCreator(final @SitePath File site,
-      @Current final SchemaVersion version) {
+  public SchemaCreator(@SitePath File site,
+      @Current SchemaVersion version,
+      GitRepositoryManager gitMgr,
+      AllProjectsName ap,
+      @GerritPersonIdent PersonIdent au) {
     site_path = site;
+    mgr = gitMgr;
+    allProjectsName = ap;
+    serverUser = au;
     versionNbr = version.getVersionNbr();
     index_generic = new ScriptRunner("index_generic.sql");
     index_postgres = new ScriptRunner("index_postgres.sql");
     mysql_nextval = new ScriptRunner("mysql_nextval.sql");
   }
 
-  public void create(final ReviewDb db) throws OrmException {
+  public void create(final ReviewDb db) throws OrmException, IOException,
+      ConfigInvalidException {
     final JdbcSchema jdbc = (JdbcSchema) db;
     final JdbcExecutor e = new JdbcExecutor(jdbc);
     try {
@@ -84,15 +115,13 @@
     db.schemaVersion().insert(Collections.singleton(sVer));
 
     final SystemConfig sConfig = initSystemConfig(db);
-    initOwnerCategory(db);
-    initReadCategory(db, sConfig);
     initVerifiedCategory(db);
     initCodeReviewCategory(db, sConfig);
-    initSubmitCategory(db);
-    initPushTagCategory(db);
-    initPushUpdateBranchCategory(db);
-    initForgeIdentityCategory(db, sConfig);
-    initWildCardProject(db);
+
+    if (mgr != null) {
+      // TODO This should never be null when initializing a site.
+      initWildCardProject();
+    }
 
     final SqlDialect d = jdbc.getDialect();
     if (d instanceof DialectH2) {
@@ -110,19 +139,27 @@
     }
   }
 
+  private AccountGroup newGroup(ReviewDb c, String name, AccountGroup.UUID uuid)
+      throws OrmException {
+    if (uuid == null) {
+      uuid = GroupUUID.make(name, serverUser);
+    }
+    return new AccountGroup( //
+        new AccountGroup.NameKey(name), //
+        new AccountGroup.Id(c.nextAccountGroupId()), //
+        uuid);
+  }
+
   private SystemConfig initSystemConfig(final ReviewDb c) throws OrmException {
-    final AccountGroup admin =
-        new AccountGroup(new AccountGroup.NameKey("Administrators"),
-            new AccountGroup.Id(c.nextAccountGroupId()));
+    admin = newGroup(c, "Administrators", null);
     admin.setDescription("Gerrit Site Administrators");
     admin.setType(AccountGroup.Type.INTERNAL);
     c.accountGroups().insert(Collections.singleton(admin));
     c.accountGroupNames().insert(
         Collections.singleton(new AccountGroupName(admin)));
 
-    final AccountGroup anonymous =
-        new AccountGroup(new AccountGroup.NameKey("Anonymous Users"),
-            new AccountGroup.Id(c.nextAccountGroupId()));
+    anonymous =
+        newGroup(c, "Anonymous Users", AccountGroup.ANONYMOUS_USERS);
     anonymous.setDescription("Any user, signed-in or not");
     anonymous.setOwnerGroupId(admin.getId());
     anonymous.setType(AccountGroup.Type.SYSTEM);
@@ -130,9 +167,8 @@
     c.accountGroupNames().insert(
         Collections.singleton(new AccountGroupName(anonymous)));
 
-    final AccountGroup registered =
-        new AccountGroup(new AccountGroup.NameKey("Registered Users"),
-            new AccountGroup.Id(c.nextAccountGroupId()));
+    registered =
+        newGroup(c, "Registered Users", AccountGroup.REGISTERED_USERS);
     registered.setDescription("Any signed-in user");
     registered.setOwnerGroupId(admin.getId());
     registered.setType(AccountGroup.Type.SYSTEM);
@@ -140,9 +176,7 @@
     c.accountGroupNames().insert(
         Collections.singleton(new AccountGroupName(registered)));
 
-    final AccountGroup batchUsers =
-      new AccountGroup(new AccountGroup.NameKey("Non-Interactive Users"),
-          new AccountGroup.Id(c.nextAccountGroupId()));
+    final AccountGroup batchUsers = newGroup(c, "Non-Interactive Users", null);
     batchUsers.setDescription("Users who perform batch actions on Gerrit");
     batchUsers.setOwnerGroupId(admin.getId());
     batchUsers.setType(AccountGroup.Type.INTERNAL);
@@ -150,9 +184,7 @@
     c.accountGroupNames().insert(
         Collections.singleton(new AccountGroupName(batchUsers)));
 
-    final AccountGroup owners =
-        new AccountGroup(new AccountGroup.NameKey("Project Owners"),
-            new AccountGroup.Id(c.nextAccountGroupId()));
+    owners = newGroup(c, "Project Owners", AccountGroup.PROJECT_OWNERS);
     owners.setDescription("Any owner of the project");
     owners.setOwnerGroupId(admin.getId());
     owners.setType(AccountGroup.Type.SYSTEM);
@@ -161,13 +193,6 @@
         Collections.singleton(new AccountGroupName(owners)));
 
     final SystemConfig s = SystemConfig.create();
-    s.registerEmailPrivateKey = SignedToken.generateRandomKey();
-    s.adminGroupId = admin.getId();
-    s.anonymousGroupId = anonymous.getId();
-    s.registeredGroupId = registered.getId();
-    s.batchUsersGroupId = batchUsers.getId();
-    s.ownerGroupId = owners.getId();
-    s.wildProjectName = DEFAULT_WILD_NAME;
     try {
       s.sitePath = site_path.getCanonicalPath();
     } catch (IOException e) {
@@ -177,13 +202,69 @@
     return s;
   }
 
-  private void initWildCardProject(final ReviewDb c) throws OrmException {
-    final Project p;
+  private void initWildCardProject() throws IOException, ConfigInvalidException {
+    Repository git;
+    try {
+      git = mgr.openRepository(allProjectsName);
+    } catch (RepositoryNotFoundException notFound) {
+      // A repository may be missing if this project existed only to store
+      // 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);
+      }
+    }
+    try {
+      MetaDataUpdate md = new MetaDataUpdate(new NoReplication(), allProjectsName, git);
+      md.getCommitBuilder().setAuthor(serverUser);
+      md.getCommitBuilder().setCommitter(serverUser);
 
-    p = new Project(DEFAULT_WILD_NAME);
-    p.setDescription("Rights inherited by all other projects");
-    p.setUseContributorAgreements(false);
-    c.projects().insert(Collections.singleton(p));
+      ProjectConfig config = ProjectConfig.read(md);
+      Project p = config.getProject();
+      p.setDescription("Rights inherited by all other projects");
+      p.setUseContributorAgreements(false);
+
+      AccessSection cap = config.getAccessSection(AccessSection.GLOBAL_CAPABILITIES, true);
+      AccessSection all = config.getAccessSection(AccessSection.ALL, true);
+      AccessSection heads = config.getAccessSection(AccessSection.HEADS, true);
+      AccessSection meta = config.getAccessSection(GitRepositoryManager.REF_CONFIG, true);
+
+      cap.getPermission(GlobalCapability.ADMINISTRATE_SERVER, true)
+        .add(rule(config, admin));
+
+      PermissionRule review = rule(config, registered);
+      review.setRange(-1, 1);
+      heads.getPermission(Permission.LABEL + "Code-Review", true).add(review);
+
+      all.getPermission(Permission.READ, true) //
+          .add(rule(config, admin));
+      all.getPermission(Permission.READ, true) //
+          .add(rule(config, anonymous));
+
+      config.getAccessSection("refs/for/" + AccessSection.ALL, true) //
+          .getPermission(Permission.PUSH, true) //
+          .add(rule(config, registered));
+      all.getPermission(Permission.FORGE_AUTHOR, true) //
+          .add(rule(config, registered));
+
+      meta.getPermission(Permission.READ, true) //
+          .add(rule(config, owners));
+
+      md.setMessage("Initialized Gerrit Code Review " + Version.getVersion());
+      if (!config.commit(md)) {
+        throw new IOException("Cannot create " + allProjectsName.get());
+      }
+    } finally {
+      git.close();
+    }
+  }
+
+  private PermissionRule rule(ProjectConfig config, AccountGroup group) {
+    return new PermissionRule(config.resolve(group));
   }
 
   private void initVerifiedCategory(final ReviewDb c) throws OrmException {
@@ -218,143 +299,6 @@
     vals.add(value(cat, -2, "Do not submit"));
     c.approvalCategories().insert(Collections.singleton(cat));
     c.approvalCategoryValues().insert(vals);
-
-    final RefRight approve =
-        new RefRight(new RefRight.Key(DEFAULT_WILD_NAME,
-            new RefRight.RefPattern("refs/heads/*"), cat.getId(),
-            sConfig.registeredGroupId));
-    approve.setMaxValue((short) 1);
-    approve.setMinValue((short) -1);
-    c.refRights().insert(Collections.singleton(approve));
-  }
-
-  private void initOwnerCategory(final ReviewDb c) throws OrmException {
-    final ApprovalCategory cat;
-    final ArrayList<ApprovalCategoryValue> vals;
-
-    cat = new ApprovalCategory(ApprovalCategory.OWN, "Owner");
-    cat.setPosition((short) -1);
-    cat.setFunctionName(NoOpFunction.NAME);
-    vals = new ArrayList<ApprovalCategoryValue>();
-    vals.add(value(cat, 1, "Administer All Settings"));
-    c.approvalCategories().insert(Collections.singleton(cat));
-    c.approvalCategoryValues().insert(vals);
-  }
-
-  private void initReadCategory(final ReviewDb c, final SystemConfig sConfig)
-      throws OrmException {
-    final ApprovalCategory cat;
-    final ArrayList<ApprovalCategoryValue> vals;
-
-    cat = new ApprovalCategory(ApprovalCategory.READ, "Read Access");
-    cat.setPosition((short) -1);
-    cat.setFunctionName(NoOpFunction.NAME);
-    vals = new ArrayList<ApprovalCategoryValue>();
-    vals.add(value(cat, 3, "Upload merges permission"));
-    vals.add(value(cat, 2, "Upload permission"));
-    vals.add(value(cat, 1, "Read access"));
-    vals.add(value(cat, -1, "No access"));
-    c.approvalCategories().insert(Collections.singleton(cat));
-    c.approvalCategoryValues().insert(vals);
-
-    final RefRight.RefPattern pattern = new RefRight.RefPattern(RefRight.ALL);
-    {
-      final RefRight read =
-          new RefRight(new RefRight.Key(DEFAULT_WILD_NAME, pattern,
-              cat.getId(), sConfig.anonymousGroupId));
-      read.setMaxValue((short) 1);
-      read.setMinValue((short) 1);
-      c.refRights().insert(Collections.singleton(read));
-    }
-    {
-      final RefRight read =
-          new RefRight(new RefRight.Key(DEFAULT_WILD_NAME, pattern,
-              cat.getId(), sConfig.registeredGroupId));
-      read.setMaxValue((short) 2);
-      read.setMinValue((short) 1);
-      c.refRights().insert(Collections.singleton(read));
-    }
-    {
-      final RefRight read =
-          new RefRight(new RefRight.Key(DEFAULT_WILD_NAME, pattern,
-              cat.getId(), sConfig.adminGroupId));
-      read.setMaxValue((short) 1);
-      read.setMinValue((short) 1);
-      c.refRights().insert(Collections.singleton(read));
-    }
-  }
-
-  private void initSubmitCategory(final ReviewDb c) throws OrmException {
-    final ApprovalCategory cat;
-    final ArrayList<ApprovalCategoryValue> vals;
-
-    cat = new ApprovalCategory(ApprovalCategory.SUBMIT, "Submit");
-    cat.setPosition((short) -1);
-    cat.setFunctionName(SubmitFunction.NAME);
-    vals = new ArrayList<ApprovalCategoryValue>();
-    vals.add(value(cat, 1, "Submit"));
-    c.approvalCategories().insert(Collections.singleton(cat));
-    c.approvalCategoryValues().insert(vals);
-  }
-
-  private void initPushTagCategory(final ReviewDb c) throws OrmException {
-    final ApprovalCategory cat;
-    final ArrayList<ApprovalCategoryValue> vals;
-
-    cat = new ApprovalCategory(ApprovalCategory.PUSH_TAG, "Push Tag");
-    cat.setPosition((short) -1);
-    cat.setFunctionName(NoOpFunction.NAME);
-    vals = new ArrayList<ApprovalCategoryValue>();
-    vals.add(value(cat, ApprovalCategory.PUSH_TAG_SIGNED, "Create Signed Tag"));
-    vals.add(value(cat, ApprovalCategory.PUSH_TAG_ANNOTATED,
-        "Create Annotated Tag"));
-    c.approvalCategories().insert(Collections.singleton(cat));
-    c.approvalCategoryValues().insert(vals);
-  }
-
-  private void initPushUpdateBranchCategory(final ReviewDb c)
-      throws OrmException {
-    final ApprovalCategory cat;
-    final ArrayList<ApprovalCategoryValue> vals;
-
-    cat = new ApprovalCategory(ApprovalCategory.PUSH_HEAD, "Push Branch");
-    cat.setPosition((short) -1);
-    cat.setFunctionName(NoOpFunction.NAME);
-    vals = new ArrayList<ApprovalCategoryValue>();
-    vals.add(value(cat, ApprovalCategory.PUSH_HEAD_UPDATE, "Update Branch"));
-    vals.add(value(cat, ApprovalCategory.PUSH_HEAD_CREATE, "Create Branch"));
-    vals.add(value(cat, ApprovalCategory.PUSH_HEAD_REPLACE,
-        "Force Push Branch; Delete Branch"));
-    c.approvalCategories().insert(Collections.singleton(cat));
-    c.approvalCategoryValues().insert(vals);
-  }
-
-  private void initForgeIdentityCategory(final ReviewDb c,
-      final SystemConfig sConfig) throws OrmException {
-    final ApprovalCategory cat;
-    final ArrayList<ApprovalCategoryValue> values;
-
-    cat =
-        new ApprovalCategory(ApprovalCategory.FORGE_IDENTITY, "Forge Identity");
-    cat.setPosition((short) -1);
-    cat.setFunctionName(NoOpFunction.NAME);
-    values = new ArrayList<ApprovalCategoryValue>();
-    values.add(value(cat, ApprovalCategory.FORGE_AUTHOR,
-        "Forge Author Identity"));
-    values.add(value(cat, ApprovalCategory.FORGE_COMMITTER,
-        "Forge Committer or Tagger Identity"));
-    values.add(value(cat, ApprovalCategory.FORGE_SERVER,
-        "Forge Gerrit Code Review Server Identity"));
-    c.approvalCategories().insert(Collections.singleton(cat));
-    c.approvalCategoryValues().insert(values);
-
-    RefRight right =
-        new RefRight(new RefRight.Key(sConfig.wildProjectName,
-            new RefRight.RefPattern(RefRight.ALL),
-            ApprovalCategory.FORGE_IDENTITY, sConfig.registeredGroupId));
-    right.setMinValue(ApprovalCategory.FORGE_AUTHOR);
-    right.setMaxValue(ApprovalCategory.FORGE_AUTHOR);
-    c.refRights().insert(Collections.singleton(right));
   }
 
   private static ApprovalCategoryValue value(final ApprovalCategory cat,
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
new file mode 100644
index 0000000..5ba7d4c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaModule.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.server.schema;
+
+import static com.google.inject.Scopes.SINGLETON;
+
+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 org.eclipse.jgit.lib.PersonIdent;
+
+/** Validate the schema and connect to Git. */
+public class SchemaModule extends FactoryModule {
+  @Override
+  protected void configure() {
+    install(new SchemaVersion.Module());
+
+    bind(PersonIdent.class).annotatedWith(GerritPersonIdent.class)
+      .toProvider(GerritPersonIdentProvider.class);
+
+    bind(AllProjectsName.class)
+      .toProvider(AllProjectsNameProvider.class)
+      .in(SINGLETON);
+
+    bind(String.class).annotatedWith(AnonymousCowardName.class).toProvider(
+        AnonymousCowardNameProvider.class);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaUpdater.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaUpdater.java
index f44eff5..305fb84 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaUpdater.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaUpdater.java
@@ -14,15 +14,17 @@
 
 package com.google.gerrit.server.schema;
 
-import com.google.gerrit.reviewdb.CurrentSchemaVersion;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gerrit.reviewdb.SystemConfig;
+import com.google.gerrit.reviewdb.client.CurrentSchemaVersion;
+import com.google.gerrit.reviewdb.client.SystemConfig;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.config.SitePaths;
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.client.SchemaFactory;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
+import org.eclipse.jgit.errors.ConfigInvalidException;
+
 import java.io.IOException;
 import java.sql.SQLException;
 import java.util.Collections;
@@ -49,7 +51,13 @@
       final SchemaVersion u = updater.get();
       final CurrentSchemaVersion version = getSchemaVersion(db);
       if (version == null) {
-        creator.create(db);
+        try {
+          creator.create(db);
+        } catch (IOException e) {
+          throw new OrmException("Cannot initialize schema", e);
+        } catch (ConfigInvalidException e) {
+          throw new OrmException("Cannot initialize schema", e);
+        }
 
       } else {
         try {
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 978a152..f789300 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
@@ -14,12 +14,12 @@
 
 package com.google.gerrit.server.schema;
 
-import com.google.gerrit.reviewdb.CurrentSchemaVersion;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.client.StatementExecutor;
+import com.google.gerrit.reviewdb.client.CurrentSchemaVersion;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gwtorm.jdbc.JdbcExecutor;
 import com.google.gwtorm.jdbc.JdbcSchema;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.StatementExecutor;
 import com.google.inject.AbstractModule;
 import com.google.inject.Provider;
 
@@ -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_52.class;
+  public static final Class<Schema_64> C = Schema_64.class;
 
   public static class Module extends AbstractModule {
     @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersionCheck.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersionCheck.java
new file mode 100644
index 0000000..75d8a39
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersionCheck.java
@@ -0,0 +1,85 @@
+// 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.schema;
+
+import com.google.gerrit.lifecycle.LifecycleListener;
+import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.reviewdb.client.CurrentSchemaVersion;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Inject;
+import com.google.inject.Module;
+import com.google.inject.Provider;
+import com.google.inject.ProvisionException;
+
+/** Validates the current schema version. */
+public class SchemaVersionCheck implements LifecycleListener {
+  public static Module module () {
+    return new LifecycleModule() {
+      @Override
+      protected void configure() {
+        listener().to(SchemaVersionCheck.class);
+      }
+    };
+  }
+
+  private final SchemaFactory<ReviewDb> schema;
+
+  @Current
+  private final Provider<SchemaVersion> version;
+
+  @Inject
+  public SchemaVersionCheck(SchemaFactory<ReviewDb> schemaFactory,
+      @Current Provider<SchemaVersion> version) {
+    this.schema = schemaFactory;
+    this.version = version;
+  }
+
+  public void start() {
+    try {
+      final ReviewDb db = schema.open();
+      try {
+        final CurrentSchemaVersion sVer = getSchemaVersion(db);
+        final int eVer = version.get().getVersionNbr();
+
+        if (sVer == null) {
+          throw new ProvisionException("Schema not yet initialized."
+              + "  Run init to initialize the schema.");
+        }
+        if (sVer.versionNbr != eVer) {
+          throw new ProvisionException("Unsupported schema version "
+              + sVer.versionNbr + "; expected schema version " + eVer
+              + ".  Run init to upgrade.");
+        }
+      } finally {
+        db.close();
+      }
+    } catch (OrmException e) {
+      throw new ProvisionException("Cannot read schema_version", e);
+    }
+  }
+
+  public void stop() {
+  }
+
+  private CurrentSchemaVersion getSchemaVersion(final ReviewDb db) {
+    try {
+      return db.schemaVersion().get(new CurrentSchemaVersion.Key());
+    } catch (OrmException e) {
+      return null;
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_19.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_19.java
deleted file mode 100644
index 7ad91ff..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_19.java
+++ /dev/null
@@ -1,42 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.gerrit.reviewdb.CurrentSchemaVersion;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gwtorm.client.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.ProvisionException;
-
-class Schema_19 extends SchemaVersion {
-  @Inject
-  Schema_19() {
-    super(new Provider<SchemaVersion>() {
-      public SchemaVersion get() {
-        throw new ProvisionException("Cannot upgrade from 18");
-      }
-    });
-  }
-
-  @Override
-  protected void upgradeFrom(UpdateUI ui, CurrentSchemaVersion curr,
-      ReviewDb db, boolean toTargetVersion) throws OrmException {
-    throw new OrmException("Cannot upgrade from " + curr.versionNbr
-        + "; manually run scripts from"
-        + " http://gerrit.googlecode.com/files/schema-upgrades003_019.zip"
-        + " and restart.");
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_21.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_21.java
deleted file mode 100644
index ed50f7f..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_21.java
+++ /dev/null
@@ -1,74 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gerrit.reviewdb.SystemConfig;
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.gwtorm.schema.sql.DialectH2;
-import com.google.gwtorm.schema.sql.DialectMySQL;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.sql.Statement;
-import java.util.Collections;
-
-class Schema_21 extends SchemaVersion {
-  @Inject
-  Schema_21(Provider<Schema_20> prior) {
-    super(prior);
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException {
-    JdbcSchema jdbc = (JdbcSchema) db;
-    SystemConfig sc = db.systemConfig().get(new SystemConfig.Key());
-
-    Statement s = jdbc.getConnection().createStatement();
-    try {
-      ResultSet r;
-
-      r = s.executeQuery("SELECT name FROM projects WHERE project_id = 0");
-      try {
-        if (!r.next()) {
-          throw new OrmException("Cannot read old wild project");
-        }
-        sc.wildProjectName = new Project.NameKey(r.getString(1));
-      } finally {
-        r.close();
-      }
-
-      if (jdbc.getDialect() instanceof DialectMySQL) {
-        try {
-          s.execute("DROP FUNCTION nextval_project_id");
-        } catch (SQLException se) {
-          ui.message("warning: could not delete function nextval_project_id");
-        }
-
-      } else if (jdbc.getDialect() instanceof DialectH2) {
-        s.execute("ALTER TABLE projects DROP CONSTRAINT"
-            + " IF EXISTS CONSTRAINT_F3");
-      }
-    } finally {
-      s.close();
-    }
-
-    db.systemConfig().update(Collections.singleton(sc));
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_22.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_22.java
deleted file mode 100644
index 8e24aa2..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_22.java
+++ /dev/null
@@ -1,74 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import static com.google.gerrit.reviewdb.AccountExternalId.SCHEME_USERNAME;
-
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.AccountExternalId;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gerrit.reviewdb.AccountExternalId.Key;
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.gwtorm.schema.sql.DialectH2;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.sql.Statement;
-import java.util.ArrayList;
-import java.util.Collection;
-
-class Schema_22 extends SchemaVersion {
-  @Inject
-  Schema_22(Provider<Schema_21> prior) {
-    super(prior);
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException {
-    Statement s = ((JdbcSchema) db).getConnection().createStatement();
-    try {
-      ResultSet results =
-          s.executeQuery(//
-              "SELECT account_id, ssh_user_name"
-                  + " FROM accounts" //
-                  + " WHERE ssh_user_name IS NOT NULL"
-                  + " AND ssh_user_name <> ''");
-      Collection<AccountExternalId> ids = new ArrayList<AccountExternalId>();
-      while (results.next()) {
-        final int accountId = results.getInt(1);
-        final String userName = results.getString(2);
-
-        final Account.Id account = new Account.Id(accountId);
-        final AccountExternalId.Key key = toKey(userName);
-        ids.add(new AccountExternalId(account, key));
-      }
-      db.accountExternalIds().insert(ids);
-
-      if (((JdbcSchema) db).getDialect() instanceof DialectH2) {
-        s.execute("ALTER TABLE accounts DROP CONSTRAINT"
-            + " IF EXISTS CONSTRAINT_AF");
-      }
-    } finally {
-      s.close();
-    }
-  }
-
-  private Key toKey(final String userName) {
-    return new AccountExternalId.Key(SCHEME_USERNAME, userName);
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_23.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_23.java
deleted file mode 100644
index 413fa4e..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_23.java
+++ /dev/null
@@ -1,61 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.AccountGroupName;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.sql.Statement;
-import java.util.ArrayList;
-import java.util.Collection;
-
-class Schema_23 extends SchemaVersion {
-  @Inject
-  Schema_23(Provider<Schema_22> prior) {
-    super(prior);
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException {
-    Collection<AccountGroupName> names = new ArrayList<AccountGroupName>();
-    Statement queryStmt = ((JdbcSchema) db).getConnection().createStatement();
-    try {
-      ResultSet results =
-          queryStmt.executeQuery("SELECT group_id, name FROM account_groups");
-      while (results.next()) {
-        final int id = results.getInt(1);
-        final String name = results.getString(2);
-
-        final AccountGroup.Id group = new AccountGroup.Id(id);
-        final AccountGroup.NameKey key = toKey(name);
-        names.add(new AccountGroupName(key, group));
-      }
-    } finally {
-      queryStmt.close();
-    }
-    db.accountGroupNames().insert(names);
-  }
-
-  private AccountGroup.NameKey toKey(final String name) {
-    return new AccountGroup.NameKey(name);
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_24.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_24.java
deleted file mode 100644
index 0172921..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_24.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-class Schema_24 extends SchemaVersion {
-  @Inject
-  Schema_24(Provider<Schema_23> prior) {
-    super(prior);
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_25.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_25.java
deleted file mode 100644
index fdfff70..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_25.java
+++ /dev/null
@@ -1,112 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.ApprovalCategory;
-import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.RefRight;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-import org.eclipse.jgit.lib.Constants;
-
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.sql.Statement;
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.List;
-import java.util.Set;
-
-class Schema_25 extends SchemaVersion {
-  private Set<ApprovalCategory.Id> nonActions;
-
-  @Inject
-  Schema_25(Provider<Schema_24> prior) {
-    super(prior);
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException {
-    nonActions = new HashSet<ApprovalCategory.Id>();
-    for (ApprovalCategory c : db.approvalCategories().all()) {
-      if (!c.isAction()) {
-        nonActions.add(c.getId());
-      }
-    }
-
-    List<RefRight> rights = new ArrayList<RefRight>();
-    Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
-    try {
-      ResultSet rs = stmt.executeQuery("SELECT * FROM project_rights");
-      try {
-        while (rs.next()) {
-          rights.add(toRefRight(rs));
-        }
-      } finally {
-        rs.close();
-      }
-
-      db.refRights().insert(rights);
-      stmt.execute("CREATE INDEX ref_rights_byCatGroup"
-          + " ON ref_rights (category_id, group_id)");
-    } finally {
-      stmt.close();
-    }
-  }
-
-  private RefRight toRefRight(ResultSet rs) throws SQLException {
-    short min_value = rs.getShort("min_value");
-    short max_value = rs.getShort("max_value");
-    String category_id = rs.getString("category_id");
-    int group_id = rs.getInt("group_id");
-    String project_name = rs.getString("project_name");
-
-    ApprovalCategory.Id category = new ApprovalCategory.Id(category_id);
-    Project.NameKey project = new Project.NameKey(project_name);
-    AccountGroup.Id group = new AccountGroup.Id(group_id);
-
-    RefRight.RefPattern ref;
-    if (category.equals(ApprovalCategory.SUBMIT)
-        || category.equals(ApprovalCategory.PUSH_HEAD)
-        || nonActions.contains(category)) {
-      // Explicitly related to a branch head.
-      ref = new RefRight.RefPattern(Constants.R_HEADS + "*");
-
-    } else if (category.equals(ApprovalCategory.PUSH_TAG)) {
-      // Explicitly related to the tag namespace.
-      ref = new RefRight.RefPattern(Constants.R_TAGS + "/*");
-
-    } else if (category.equals(ApprovalCategory.READ)
-        || category.equals(ApprovalCategory.OWN)) {
-      // Currently these are project-wide rights, so apply that way.
-      ref = new RefRight.RefPattern(RefRight.ALL);
-
-    } else {
-      // Assume project wide for the default.
-      ref = new RefRight.RefPattern(RefRight.ALL);
-    }
-
-    RefRight.Key key = new RefRight.Key(project, ref, category, group);
-    RefRight r = new RefRight(key);
-    r.setMinValue(min_value);
-    r.setMaxValue(max_value);
-    return r;
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_26.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_26.java
deleted file mode 100644
index 9c76af2..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_26.java
+++ /dev/null
@@ -1,47 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.gwtorm.schema.sql.DialectMySQL;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-import java.sql.SQLException;
-import java.sql.Statement;
-
-class Schema_26 extends SchemaVersion {
-  @Inject
-  Schema_26(Provider<Schema_25> prior) {
-    super(prior);
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws SQLException {
-    if (((JdbcSchema) db).getDialect() instanceof DialectMySQL) {
-      Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
-      try {
-        stmt.execute("ALTER TABLE account_group_members_audit" //
-            + " MODIFY removed_on TIMESTAMP NULL DEFAULT NULL");
-        stmt.execute("UPDATE account_group_members_audit" //
-            + " SET removed_on = NULL" //
-            + " WHERE removed_by IS NULL;");
-      } finally {
-        stmt.close();
-      }
-    }
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_27.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_27.java
deleted file mode 100644
index f8febec..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_27.java
+++ /dev/null
@@ -1,93 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.AccountGroupName;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.gwtorm.schema.sql.DialectH2;
-import com.google.gwtorm.schema.sql.DialectMySQL;
-import com.google.gwtorm.schema.sql.DialectPostgreSQL;
-import com.google.gwtorm.schema.sql.SqlDialect;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-import java.sql.SQLException;
-import java.sql.Statement;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.Map;
-
-class Schema_27 extends SchemaVersion {
-  @Inject
-  Schema_27(Provider<Schema_26> prior) {
-    super(prior);
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws SQLException, OrmException {
-    Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
-    try {
-      final SqlDialect dialect = ((JdbcSchema) db).getDialect();
-      if (dialect instanceof DialectPostgreSQL) {
-        stmt.execute("ALTER TABLE account_groups"
-            + " ALTER COLUMN name TYPE VARCHAR(255)");
-        stmt.execute("ALTER TABLE account_group_names"
-            + " ALTER COLUMN name TYPE VARCHAR(255)");
-
-      } else if (dialect instanceof DialectH2) {
-        stmt.execute("ALTER TABLE account_groups"
-            + " ALTER COLUMN name VARCHAR(255)");
-        stmt.execute("ALTER TABLE account_group_names"
-            + " ALTER COLUMN name VARCHAR(255) NOT NULL");
-
-      } else if (dialect instanceof DialectMySQL) {
-        stmt.execute("ALTER TABLE account_groups MODIFY name VARCHAR(255)");
-        stmt.execute("ALTER TABLE account_group_names"
-            + " MODIFY name VARCHAR(255)");
-
-      } else {
-        throw new OrmException("Unsupported dialect " + dialect);
-      }
-    } finally {
-      stmt.close();
-    }
-
-    // Some groups might be missing their names, our older schema
-    // creation logic failed to create the name objects. Do it now.
-    //
-    Map<AccountGroup.NameKey, AccountGroupName> names =
-        db.accountGroupNames().toMap(db.accountGroupNames().all());
-
-    List<AccountGroupName> insert = new ArrayList<AccountGroupName>();
-    List<AccountGroupName> update = new ArrayList<AccountGroupName>();
-
-    for (AccountGroup g : db.accountGroups().all()) {
-      AccountGroupName n = names.get(g.getNameKey());
-      if (n == null) {
-        insert.add(new AccountGroupName(g));
-
-      } else if (!g.getId().equals(n.getId())) {
-        n.setId(g.getId());
-        update.add(n);
-      }
-    }
-
-    db.accountGroupNames().insert(insert);
-    db.accountGroupNames().update(update);
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_28.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_28.java
deleted file mode 100644
index cddbe4e..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_28.java
+++ /dev/null
@@ -1,101 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.gerrit.reviewdb.ApprovalCategory;
-import com.google.gerrit.reviewdb.ApprovalCategoryValue;
-import com.google.gerrit.reviewdb.RefRight;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gerrit.reviewdb.SystemConfig;
-import com.google.gerrit.server.workflow.NoOpFunction;
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-import java.sql.SQLException;
-import java.sql.Statement;
-import java.util.ArrayList;
-import java.util.Collections;
-
-class Schema_28 extends SchemaVersion {
-  @Inject
-  Schema_28(Provider<Schema_27> prior) {
-    super(prior);
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException {
-    final SystemConfig cfg = db.systemConfig().get(new SystemConfig.Key());
-    ApprovalCategory cat;
-
-    initForgeIdentityCategory(db, cfg);
-
-    // Don't grant FORGE_COMMITTER to existing PUSH_HEAD rights. That
-    // is considered a bug that we are fixing with this schema upgrade.
-    // Administrators might need to relax permissions manually after the
-    // upgrade if that forgery is critical to their workflow.
-
-    cat = db.approvalCategories().get(ApprovalCategory.PUSH_TAG);
-    if (cat != null && "Push Annotated Tag".equals(cat.getName())) {
-      cat.setName("Push Tag");
-      db.approvalCategories().update(Collections.singleton(cat));
-    }
-
-    // Since we deleted Push Tags +3, drop anything using +3 down to +2.
-    //
-    Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
-    try {
-      stmt.execute("UPDATE ref_rights SET max_value = "
-          + ApprovalCategory.PUSH_TAG_ANNOTATED + " WHERE max_value >= 3");
-      stmt.execute("UPDATE ref_rights SET min_value = "
-          + ApprovalCategory.PUSH_TAG_ANNOTATED + " WHERE min_value >= 3");
-    } finally {
-      stmt.close();
-    }
-  }
-
-  private void initForgeIdentityCategory(final ReviewDb c,
-      final SystemConfig sConfig) throws OrmException {
-    final ApprovalCategory cat;
-    final ArrayList<ApprovalCategoryValue> values;
-
-    cat =
-        new ApprovalCategory(ApprovalCategory.FORGE_IDENTITY, "Forge Identity");
-    cat.setPosition((short) -1);
-    cat.setFunctionName(NoOpFunction.NAME);
-    values = new ArrayList<ApprovalCategoryValue>();
-    values.add(value(cat, ApprovalCategory.FORGE_AUTHOR,
-        "Forge Author Identity"));
-    values.add(value(cat, ApprovalCategory.FORGE_COMMITTER,
-        "Forge Committer or Tagger Identity"));
-    c.approvalCategories().insert(Collections.singleton(cat));
-    c.approvalCategoryValues().insert(values);
-
-    RefRight right =
-        new RefRight(new RefRight.Key(sConfig.wildProjectName,
-            new RefRight.RefPattern(RefRight.ALL),
-            ApprovalCategory.FORGE_IDENTITY, sConfig.registeredGroupId));
-    right.setMinValue(ApprovalCategory.FORGE_AUTHOR);
-    right.setMaxValue(ApprovalCategory.FORGE_AUTHOR);
-    c.refRights().insert(Collections.singleton(right));
-  }
-
-  private static ApprovalCategoryValue value(final ApprovalCategory cat,
-      final int value, final String name) {
-    return new ApprovalCategoryValue(new ApprovalCategoryValue.Id(cat.getId(),
-        (short) value), name);
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_29.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_29.java
deleted file mode 100644
index 37920bf..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_29.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-class Schema_29 extends SchemaVersion {
-  @Inject
-  Schema_29(Provider<Schema_28> prior) {
-    super(prior);
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_30.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_30.java
deleted file mode 100644
index 7285e32..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_30.java
+++ /dev/null
@@ -1,40 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.gerrit.reviewdb.ApprovalCategory;
-import com.google.gerrit.reviewdb.ApprovalCategoryValue;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gwtorm.client.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-import java.util.Collections;
-
-class Schema_30 extends SchemaVersion {
-  @Inject
-  Schema_30(Provider<Schema_29> prior) {
-    super(prior);
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException {
-    db.approvalCategoryValues().insert(
-        Collections.singleton(new ApprovalCategoryValue(
-            new ApprovalCategoryValue.Id(ApprovalCategory.FORGE_IDENTITY,
-                ApprovalCategory.FORGE_SERVER),
-            "Forge Gerrit Code Review Server Identity")));
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_31.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_31.java
deleted file mode 100644
index 62f57e2..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_31.java
+++ /dev/null
@@ -1,41 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-import java.sql.SQLException;
-import java.sql.Statement;
-
-class Schema_31 extends SchemaVersion {
-  @Inject
-  Schema_31(Provider<Schema_30> prior) {
-    super(prior);
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws SQLException {
-    Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
-    try {
-      stmt.execute("CREATE INDEX changes_byProject"
-          + " ON changes (dest_project_name)");
-    } finally {
-      stmt.close();
-    }
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_32.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_32.java
deleted file mode 100644
index 2af5596..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_32.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-public class Schema_32 extends SchemaVersion {
-  @Inject
-  Schema_32(Provider<Schema_31> prior) {
-    super(prior);
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_33.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_33.java
deleted file mode 100644
index 0c733d7..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_33.java
+++ /dev/null
@@ -1,49 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.AccountGroupName;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gerrit.reviewdb.SystemConfig;
-import com.google.gwtorm.client.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-import java.util.Collections;
-
-public class Schema_33 extends SchemaVersion {
-  @Inject
-  Schema_33(Provider<Schema_32> prior) {
-    super(prior);
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException {
-    SystemConfig config = db.systemConfig().all().toList().get(0);
-    final AccountGroup batchUsers =
-      new AccountGroup(new AccountGroup.NameKey("Non-Interactive Users"),
-          new AccountGroup.Id(db.nextAccountGroupId()));
-    batchUsers.setDescription("Users who perform batch actions on Gerrit");
-    batchUsers.setOwnerGroupId(config.adminGroupId);
-    batchUsers.setType(AccountGroup.Type.INTERNAL);
-    db.accountGroups().insert(Collections.singleton(batchUsers));
-    db.accountGroupNames().insert(
-        Collections.singleton(new AccountGroupName(batchUsers)));
-
-    config.batchUsersGroupId = batchUsers.getId();
-    db.systemConfig().update(Collections.singleton(config));
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_34.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_34.java
deleted file mode 100644
index fa94146..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_34.java
+++ /dev/null
@@ -1,123 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.gerrit.reviewdb.ApprovalCategory;
-import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.RefRight;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gerrit.reviewdb.RefRight.RefPattern;
-import com.google.gerrit.server.project.RefControl.RefRightsForPattern;
-import com.google.gwtorm.client.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-import java.util.ArrayList;
-import java.util.Comparator;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.TreeMap;
-
-public class Schema_34 extends SchemaVersion {
-  private static final Comparator<String> DESCENDING_SORT =
-      new Comparator<String>() {
-
-        @Override
-        public int compare(String a, String b) {
-          int aLength = a.length();
-          int bLength = b.length();
-          if (bLength == aLength) {
-            return a.compareTo(b);
-          }
-          return bLength - aLength;
-        }
-      };
-
-  @Inject
-  Schema_34(Provider<Schema_33> prior) {
-    super(prior);
-  }
-
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException {
-    Iterable<Project> projects = db.projects().all();
-    boolean showedBanner = false;
-
-    List<RefRight> toUpdate = new ArrayList<RefRight>();
-    List<RefRight> toDelete = new ArrayList<RefRight>();
-    for (Project p : projects) {
-      boolean showedProject = false;
-      List<RefRight> pr = db.refRights().byProject(p.getNameKey()).toList();
-      Map<ApprovalCategory.Id, Map<String, RefRightsForPattern>> r =
-        new HashMap<ApprovalCategory.Id, Map<String, RefRightsForPattern>>();
-      for (RefRight right : pr) {
-        ApprovalCategory.Id cat = right.getApprovalCategoryId();
-        if (r.get(cat) == null) {
-          Map<String, RefRightsForPattern> m =
-            new TreeMap<String, RefRightsForPattern>(DESCENDING_SORT);
-          r.put(cat, m);
-        }
-        if (r.get(cat).get(right.getRefPattern()) == null) {
-          RefRightsForPattern s = new RefRightsForPattern();
-          r.get(cat).put(right.getRefPattern(), s);
-        }
-        r.get(cat).get(right.getRefPattern()).addRight(right);
-      }
-
-      for (Map<String, RefRightsForPattern> categoryRights : r.values()) {
-        for (RefRightsForPattern rrp : categoryRights.values()) {
-          RefRight oldRight = rrp.getRights().get(0);
-          if (shouldPrompt(oldRight)) {
-            if (!showedBanner) {
-              ui.message("Entering interactive reference rights migration tool...");
-              showedBanner = true;
-            }
-            if (!showedProject) {
-              ui.message("In project " + p.getName());
-              showedProject = true;
-            }
-            ui.message("For category " + oldRight.getApprovalCategoryId());
-            boolean isWildcard = oldRight.getRefPattern().endsWith("/*");
-            boolean shouldUpdate = ui.yesno(!isWildcard,
-                "Should rights for pattern "
-                + oldRight.getRefPattern()
-                + " be considered exclusive?");
-            if (shouldUpdate) {
-              RefRight.Key newKey = new RefRight.Key(oldRight.getProjectNameKey(),
-                  new RefPattern("-" + oldRight.getRefPattern()),
-                  oldRight.getApprovalCategoryId(),
-                  oldRight.getAccountGroupId());
-              RefRight newRight = new RefRight(newKey);
-              newRight.setMaxValue(oldRight.getMaxValue());
-              newRight.setMinValue(oldRight.getMinValue());
-              toUpdate.add(newRight);
-              toDelete.add(oldRight);
-            }
-          }
-        }
-      }
-    }
-    db.refRights().insert(toUpdate);
-    db.refRights().delete(toDelete);
-  }
-
-  private boolean shouldPrompt(RefRight right) {
-    return !right.getRefPattern().equals("refs/*")
-      && !right.getRefPattern().equals("refs/heads/*")
-      && !right.getRefPattern().equals("refs/tags/*");
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_35.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_35.java
deleted file mode 100644
index 12d90c3..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_35.java
+++ /dev/null
@@ -1,41 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-import java.sql.SQLException;
-import java.sql.Statement;
-
-public class Schema_35 extends SchemaVersion {
-  @Inject
-  Schema_35(Provider<Schema_34> prior) {
-    super(prior);
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws SQLException {
-    Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
-    try {
-      stmt.execute("CREATE INDEX tracking_ids_byTrkId"
-          + " ON tracking_ids (tracking_id)");
-    } finally {
-      stmt.close();
-    }
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_36.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_36.java
deleted file mode 100644
index ba6b841..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_36.java
+++ /dev/null
@@ -1,51 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.gwtorm.schema.sql.DialectMySQL;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-import java.sql.SQLException;
-import java.sql.Statement;
-
-public class Schema_36 extends SchemaVersion {
-  @Inject
-  Schema_36(Provider<Schema_35> prior) {
-    super(prior);
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws SQLException {
-    Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
-    try {
-      if (((JdbcSchema) db).getDialect() instanceof DialectMySQL) {
-        stmt.execute("DROP INDEX account_project_watches_ntNew ON account_project_watches");
-        stmt.execute("DROP INDEX account_project_watches_ntCmt ON account_project_watches");
-        stmt.execute("DROP INDEX account_project_watches_ntSub ON account_project_watches");
-      } else {
-        stmt.execute("DROP INDEX account_project_watches_ntNew");
-        stmt.execute("DROP INDEX account_project_watches_ntCmt");
-        stmt.execute("DROP INDEX account_project_watches_ntSub");
-      }
-      stmt.execute("CREATE INDEX account_project_watches_byProject"
-          + " ON account_project_watches (project_name)");
-    } finally {
-      stmt.close();
-    }
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_37.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_37.java
deleted file mode 100644
index 871f2e9..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_37.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-public class Schema_37 extends SchemaVersion {
-  @Inject
-  Schema_37(Provider<Schema_36> prior) {
-    super(prior);
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_38.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_38.java
deleted file mode 100644
index 59d6fa2..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_38.java
+++ /dev/null
@@ -1,67 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.AccountDiffPreference;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.sql.Statement;
-import java.util.ArrayList;
-import java.util.List;
-
-public class Schema_38 extends SchemaVersion {
-  @Inject
-  Schema_38(Provider<Schema_37> prior) {
-    super(prior);
-  }
-
-  /**
-   * Migrate the account.default_context column to account_diff_preferences.context column.
-   * <p>
-   * Other fields in account_diff_preferences will be filled in with their defaults as
-   * defined in the {@link AccountDiffPreference#createDefault(com.google.gerrit.reviewdb.Account.Id)}
-   */
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException,
-      SQLException {
-    List<AccountDiffPreference> newPrefs =
-        new ArrayList<AccountDiffPreference>();
-
-    Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
-    try {
-      ResultSet result =
-          stmt.executeQuery("SELECT account_id, default_context"
-              + " FROM accounts WHERE default_context <> 10");
-      while (result.next()) {
-        int accountId = result.getInt(1);
-        short defaultContext = result.getShort(2);
-        AccountDiffPreference diffPref = AccountDiffPreference.createDefault(new Account.Id(accountId));
-        diffPref.setContext(defaultContext);
-        newPrefs.add(diffPref);
-      }
-    } finally {
-      stmt.close();
-    }
-
-    db.accountDiffPreferences().insert(newPrefs);
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_39.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_39.java
deleted file mode 100644
index 39ae226..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_39.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-public class Schema_39 extends SchemaVersion {
-  @Inject
-  Schema_39(Provider<Schema_38> prior) {
-    super(prior);
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_40.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_40.java
deleted file mode 100644
index 7d3e4f5..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_40.java
+++ /dev/null
@@ -1,70 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.gerrit.reviewdb.AccountProjectWatch;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.gwtorm.schema.sql.DialectH2;
-import com.google.gwtorm.schema.sql.DialectMySQL;
-import com.google.gwtorm.schema.sql.DialectPostgreSQL;
-import com.google.gwtorm.schema.sql.SqlDialect;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-import java.sql.SQLException;
-import java.sql.Statement;
-
-public class Schema_40 extends SchemaVersion {
-  @Inject
-  Schema_40(Provider<Schema_39> prior) {
-    super(prior);
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws SQLException,
-      OrmException {
-    // Set to "*" the filter field of the previously watched projects
-    //
-    Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
-    try {
-      stmt.execute("UPDATE account_project_watches" //
-          + " SET filter = '" + AccountProjectWatch.FILTER_ALL + "'" //
-          + " WHERE filter IS NULL OR filter = ''");
-
-      // Set the new primary key
-      //
-      final SqlDialect dialect = ((JdbcSchema) db).getDialect();
-      if (dialect instanceof DialectPostgreSQL) {
-        stmt.execute("ALTER TABLE account_project_watches "
-            + "DROP CONSTRAINT account_project_watches_pkey");
-        stmt.execute("ALTER TABLE account_project_watches "
-            + "ADD PRIMARY KEY (account_id, project_name, filter)");
-
-      } else if ((dialect instanceof DialectH2)
-          || (dialect instanceof DialectMySQL)) {
-        stmt.execute("ALTER TABLE account_project_watches DROP PRIMARY KEY");
-        stmt.execute("ALTER TABLE account_project_watches "
-            + "ADD PRIMARY KEY (account_id, project_name, filter)");
-
-      } else {
-        throw new OrmException("Unsupported dialect " + dialect);
-      }
-    } finally {
-      stmt.close();
-    }
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_41.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_41.java
deleted file mode 100644
index 508db43..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_41.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-public class Schema_41 extends SchemaVersion {
-  @Inject
-  Schema_41(Provider<Schema_40> prior) {
-    super(prior);
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_43.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_43.java
deleted file mode 100644
index 0edb7e5..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_43.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-public class Schema_43 extends SchemaVersion {
-  @Inject
-  Schema_43(Provider<Schema_42> prior) {
-    super(prior);
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_44.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_44.java
deleted file mode 100644
index 4ab1986..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_44.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-public class Schema_44 extends SchemaVersion {
-  @Inject
-  Schema_44(Provider<Schema_43> prior) {
-    super(prior);
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_45.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_45.java
deleted file mode 100644
index e37e87d..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_45.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-public class Schema_45 extends SchemaVersion {
-  @Inject
-  Schema_45(Provider<Schema_44> prior) {
-    super(prior);
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_46.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_46.java
deleted file mode 100644
index e7b104c..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_46.java
+++ /dev/null
@@ -1,67 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.AccountGroupName;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-import java.sql.Connection;
-import java.sql.ResultSet;
-import java.sql.SQLException;
-import java.sql.Statement;
-import java.util.Collections;
-
-public class Schema_46 extends SchemaVersion {
-
-  @Inject
-  Schema_46(final Provider<Schema_45> prior) {
-    super(prior);
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws SQLException,
-      OrmException {
-    AccountGroup.Id groupId = new AccountGroup.Id(db.nextAccountGroupId());
-
-    // update system_config
-    final Connection connection = ((JdbcSchema) db).getConnection();
-    Statement stmt = null;
-    try {
-      stmt = connection.createStatement();
-      stmt.execute("UPDATE system_config SET OWNER_GROUP_ID = " + groupId.get());
-      final ResultSet resultSet =
-          stmt.executeQuery("SELECT ADMIN_GROUP_ID FROM system_config");
-      resultSet.next();
-      final int adminGroupId = resultSet.getInt(1);
-
-      // create 'Project Owners' group
-      AccountGroup.NameKey nameKey = new AccountGroup.NameKey("Project Owners");
-      AccountGroup group = new AccountGroup(nameKey, groupId);
-      group.setType(AccountGroup.Type.SYSTEM);
-      group.setOwnerGroupId(new AccountGroup.Id(adminGroupId));
-      group.setDescription("Any owner of the project");
-      AccountGroupName gn = new AccountGroupName(group);
-      db.accountGroupNames().insert(Collections.singleton(gn));
-      db.accountGroups().insert(Collections.singleton(group));
-    } finally {
-      if (stmt != null) stmt.close();
-    }
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_47.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_47.java
deleted file mode 100644
index 124cc02..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_47.java
+++ /dev/null
@@ -1,25 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-public class Schema_47 extends SchemaVersion {
-  @Inject
-  Schema_47(Provider<Schema_46> prior) {
-    super(prior);
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_48.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_48.java
deleted file mode 100644
index 4e8b94d..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_48.java
+++ /dev/null
@@ -1,54 +0,0 @@
-// Copyright (C) 2010 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.schema;
-
-import com.google.gerrit.reviewdb.ApprovalCategory;
-import com.google.gerrit.reviewdb.ApprovalCategoryValue;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.jdbc.JdbcSchema;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-import java.sql.SQLException;
-import java.sql.Statement;
-
-import java.util.Collections;
-
-public class Schema_48 extends SchemaVersion {
-  @Inject
-  Schema_48(Provider<Schema_47> prior) {
-    super(prior);
-  }
-
-  @Override
-  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException, SQLException {
-    // Read +3 allows merges to be uploaded
-    db.approvalCategoryValues().insert(
-        Collections.singleton(new ApprovalCategoryValue(
-            new ApprovalCategoryValue.Id(ApprovalCategory.READ, (short) 3),
-            "Upload merges permission")));
-    // Since we added Read +3, elevate any Read +2 to that level to provide
-    // access equivalent to prior schema versions.
-    Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
-    try {
-      stmt.execute("UPDATE ref_rights SET max_value = 3"
-          + " WHERE category_id = '" + ApprovalCategory.READ.get()
-          + "' AND max_value = 2");
-    } finally {
-      stmt.close();
-    }
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_52.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_52.java
index 3fbbbe0..12a22f9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_52.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_52.java
@@ -14,12 +14,28 @@
 
 package com.google.gerrit.server.schema;
 
+import com.google.gerrit.reviewdb.client.CurrentSchemaVersion;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.ProvisionException;
 
 public class Schema_52 extends SchemaVersion {
   @Inject
-  Schema_52(Provider<Schema_51> prior) {
-    super(prior);
+  Schema_52() {
+    super(new Provider<SchemaVersion>() {
+      public SchemaVersion get() {
+        throw new ProvisionException("Cannot upgrade from 51");
+      }
+    });
+  }
+
+  @Override
+  protected void upgradeFrom(UpdateUI ui, CurrentSchemaVersion curr,
+      ReviewDb db, boolean toTargetVersion) throws OrmException {
+    throw new OrmException("Cannot upgrade from schema " + curr.versionNbr
+        + "; manually run init from Gerrit Code Review 2.1.7"
+        + " and restart this version to continue.");
   }
 }
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
new file mode 100644
index 0000000..d49b34e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_53.java
@@ -0,0 +1,481 @@
+// 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.schema;
+
+import static com.google.gerrit.common.data.Permission.CREATE;
+import static com.google.gerrit.common.data.Permission.FORGE_AUTHOR;
+import static com.google.gerrit.common.data.Permission.FORGE_COMMITTER;
+import static com.google.gerrit.common.data.Permission.FORGE_SERVER;
+import static com.google.gerrit.common.data.Permission.LABEL;
+import static com.google.gerrit.common.data.Permission.OWNER;
+import static com.google.gerrit.common.data.Permission.PUSH;
+import static com.google.gerrit.common.data.Permission.PUSH_MERGE;
+import static com.google.gerrit.common.data.Permission.PUSH_TAG;
+import static com.google.gerrit.common.data.Permission.READ;
+import static com.google.gerrit.common.data.Permission.SUBMIT;
+
+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.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.ApprovalCategory;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.SystemConfig;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.account.GroupUUID;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.NoReplication;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gwtorm.jdbc.JdbcSchema;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+
+import java.io.IOException;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+class Schema_53 extends SchemaVersion {
+  private final GitRepositoryManager mgr;
+  private final PersonIdent serverUser;
+
+  private SystemConfig systemConfig;
+  private Map<AccountGroup.Id, GroupReference> groupMap;
+  private Map<ApprovalCategory.Id, ApprovalCategory> categoryMap;
+  private GroupReference projectOwners;
+
+  private Map<Project.NameKey, Project.NameKey> parentsByProject;
+  private Map<Project.NameKey, List<OldRefRight>> rightsByProject;
+
+  private final String OLD_SUBMIT = "SUBM";
+  private final String OLD_READ = "READ";
+  private final String OLD_OWN = "OWN";
+  private final String OLD_PUSH_TAG = "pTAG";
+  private final String OLD_PUSH_HEAD = "pHD";
+  private final String OLD_FORGE_IDENTITY = "FORG";
+
+  @Inject
+  Schema_53(Provider<Schema_52> prior, GitRepositoryManager mgr,
+      @GerritPersonIdent PersonIdent serverUser) {
+    super(prior);
+    this.mgr = mgr;
+    this.serverUser = serverUser;
+  }
+
+  @Override
+  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException,
+      SQLException {
+    systemConfig = db.systemConfig().get(new SystemConfig.Key());
+    categoryMap = db.approvalCategories().toMap(db.approvalCategories().all());
+
+    assignGroupUUIDs(db);
+    readOldRefRights(db);
+    readProjectParents(db);
+    exportProjectConfig(db);
+
+    deleteActionCategories(db);
+  }
+
+  private void deleteActionCategories(ReviewDb db) throws OrmException {
+    List<ApprovalCategory> delete = new ArrayList<ApprovalCategory>();
+    for (ApprovalCategory category : categoryMap.values()) {
+      if (category.getPosition() < 0) {
+        delete.add(category);
+      }
+    }
+    db.approvalCategories().delete(delete);
+  }
+
+  private void assignGroupUUIDs(ReviewDb db) throws OrmException {
+    groupMap = new HashMap<AccountGroup.Id, GroupReference>();
+    List<AccountGroup> groups = db.accountGroups().all().toList();
+    for (AccountGroup g : groups) {
+      if (g.getId().equals(systemConfig.ownerGroupId)) {
+        g.setGroupUUID(AccountGroup.PROJECT_OWNERS);
+        projectOwners = GroupReference.forGroup(g);
+
+      } else if (g.getId().equals(systemConfig.anonymousGroupId)) {
+        g.setGroupUUID(AccountGroup.ANONYMOUS_USERS);
+
+      } else if (g.getId().equals(systemConfig.registeredGroupId)) {
+        g.setGroupUUID(AccountGroup.REGISTERED_USERS);
+
+      } else {
+        g.setGroupUUID(GroupUUID.make(g.getName(), serverUser));
+      }
+      groupMap.put(g.getId(), GroupReference.forGroup(g));
+    }
+    db.accountGroups().update(groups);
+
+    systemConfig.adminGroupUUID = toUUID(systemConfig.adminGroupId);
+    systemConfig.batchUsersGroupUUID = toUUID(systemConfig.batchUsersGroupId);
+    db.systemConfig().update(Collections.singleton(systemConfig));
+  }
+
+  private AccountGroup.UUID toUUID(AccountGroup.Id id) {
+    return groupMap.get(id).getUUID();
+  }
+
+  private void exportProjectConfig(ReviewDb db) throws OrmException,
+      SQLException {
+    Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
+    ResultSet rs = stmt.executeQuery("SELECT * FROM projects ORDER BY name");
+    while (rs.next()) {
+      final String name = rs.getString("name");
+      final Project.NameKey nameKey = new Project.NameKey(name);
+
+      Repository git;
+      try {
+        git = mgr.openRepository(nameKey);
+      } catch (RepositoryNotFoundException notFound) {
+        // A repository may be missing if this project existed only to store
+        // inheritable permissions. For example 'All-Projects'.
+        try {
+          git = mgr.createRepository(nameKey);
+        } catch (RepositoryNotFoundException err) {
+          throw new OrmException("Cannot create repository " + name, err);
+        }
+      }
+      try {
+        MetaDataUpdate md =
+            new MetaDataUpdate(new NoReplication(), nameKey, git);
+        md.getCommitBuilder().setAuthor(serverUser);
+        md.getCommitBuilder().setCommitter(serverUser);
+
+        ProjectConfig config = ProjectConfig.read(md);
+        loadProject(rs, config.getProject());
+        config.getAccessSections().clear();
+        convertRights(config);
+
+        // Grant out read on the config branch by default.
+        //
+        if (config.getProject().getNameKey().equals(systemConfig.wildProjectName)) {
+          AccessSection meta = config.getAccessSection(GitRepositoryManager.REF_CONFIG, true);
+          Permission read = meta.getPermission(READ, true);
+          read.getRule(config.resolve(projectOwners), true);
+        }
+
+        md.setMessage("Import project configuration from SQL\n");
+        if (!config.commit(md)) {
+          throw new OrmException("Cannot export project " + name);
+        }
+      } catch (ConfigInvalidException err) {
+        throw new OrmException("Cannot read project " + name, err);
+      } catch (IOException err) {
+        throw new OrmException("Cannot export project " + name, err);
+      } finally {
+        git.close();
+      }
+    }
+    rs.close();
+    stmt.close();
+  }
+
+  private void loadProject(ResultSet rs, Project project) throws SQLException,
+      OrmException {
+    project.setDescription(rs.getString("description"));
+    project.setUseContributorAgreements("Y".equals(rs
+        .getString("use_contributor_agreements")));
+
+    switch (rs.getString("submit_type").charAt(0)) {
+      case 'F':
+        project.setSubmitType(Project.SubmitType.FAST_FORWARD_ONLY);
+        break;
+      case 'M':
+        project.setSubmitType(Project.SubmitType.MERGE_IF_NECESSARY);
+        break;
+      case 'A':
+        project.setSubmitType(Project.SubmitType.MERGE_ALWAYS);
+        break;
+      case 'C':
+        project.setSubmitType(Project.SubmitType.CHERRY_PICK);
+        break;
+      default:
+        throw new OrmException("Unsupported submit_type="
+            + rs.getString("submit_type") + " on project " + project.getName());
+    }
+
+    project.setUseSignedOffBy("Y".equals(rs.getString("use_signed_off_by")));
+    project.setRequireChangeID("Y".equals(rs.getString("require_change_id")));
+    project.setUseContentMerge("Y".equals(rs.getString("use_content_merge")));
+    project.setParentName(rs.getString("parent_name"));
+  }
+
+  private void readOldRefRights(ReviewDb db) throws SQLException {
+    rightsByProject = new HashMap<Project.NameKey, List<OldRefRight>>();
+
+    Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
+    ResultSet rs = stmt.executeQuery("SELECT * FROM ref_rights");
+    while (rs.next()) {
+      OldRefRight right = new OldRefRight(rs);
+      if (right.group == null || right.category == null) {
+        continue;
+      }
+
+      List<OldRefRight> list;
+
+      list = rightsByProject.get(right.project);
+      if (list == null) {
+        list = new ArrayList<OldRefRight>();
+        rightsByProject.put(right.project, list);
+      }
+      list.add(right);
+    }
+    rs.close();
+    stmt.close();
+  }
+
+  private void readProjectParents(ReviewDb db) throws SQLException {
+    parentsByProject = new HashMap<Project.NameKey, Project.NameKey>();
+    Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
+    ResultSet rs = stmt.executeQuery("SELECT * FROM projects");
+    while (rs.next()) {
+      String name = rs.getString("name");
+      String parent_name = rs.getString("parent_name");
+      if (parent_name == null) {
+        parent_name = systemConfig.wildProjectName.get();
+      }
+      parentsByProject.put(new Project.NameKey(name), //
+          new Project.NameKey(parent_name));
+    }
+    rs.close();
+    stmt.close();
+  }
+
+  private void convertRights(ProjectConfig config) {
+    List<OldRefRight> myRights =
+        rightsByProject.get(config.getProject().getNameKey());
+    if (myRights == null) {
+      return;
+    }
+
+    for (OldRefRight old : myRights) {
+      AccessSection section = config.getAccessSection(old.ref_pattern, true);
+      GroupReference group = config.resolve(old.group);
+
+      if (OLD_SUBMIT.equals(old.category)) {
+        PermissionRule submit = rule(group);
+        if (old.max_value <= 0) {
+          submit.setDeny();
+        }
+        add(section, SUBMIT, old.exclusive, submit);
+
+      } else if (OLD_READ.equals(old.category)) {
+        if (old.exclusive) {
+          section.getPermission(READ, true).setExclusiveGroup(true);
+          newChangePermission(config, old.ref_pattern).setExclusiveGroup(true);
+        }
+
+        PermissionRule read = rule(group);
+        if (old.max_value <= 0) {
+          read.setDeny();
+        }
+        add(section, READ, old.exclusive, read);
+
+        if (3 <= old.max_value) {
+          newMergePermission(config, old.ref_pattern).add(rule(group));
+        } else if (3 <= inheritedMax(config, old)) {
+          newMergePermission(config, old.ref_pattern).add(deny(group));
+        }
+
+        if (2 <= old.max_value) {
+          newChangePermission(config, old.ref_pattern).add(rule(group));
+        } else if (2 <= inheritedMax(config, old)) {
+          newChangePermission(config, old.ref_pattern).add(deny(group));
+        }
+
+      } else if (OLD_OWN.equals(old.category)) {
+        add(section, OWNER, false, rule(group));
+
+      } else if (OLD_PUSH_TAG.equals(old.category)) {
+        PermissionRule push = rule(group);
+        if (old.max_value <= 0) {
+          push.setDeny();
+        }
+        add(section, PUSH_TAG, old.exclusive, push);
+
+      } else if (OLD_PUSH_HEAD.equals(old.category)) {
+        if (old.exclusive) {
+          section.getPermission(PUSH, true).setExclusiveGroup(true);
+          section.getPermission(CREATE, true).setExclusiveGroup(true);
+        }
+
+        PermissionRule push = rule(group);
+        if (old.max_value <= 0) {
+          push.setDeny();
+        }
+        push.setForce(3 <= old.max_value);
+        add(section, PUSH, old.exclusive, push);
+
+        if (2 <= old.max_value) {
+          add(section, CREATE, old.exclusive, rule(group));
+        } else if (2 <= inheritedMax(config, old)) {
+          add(section, CREATE, old.exclusive, deny(group));
+        }
+
+      } else if (OLD_FORGE_IDENTITY.equals(old.category)) {
+        if (old.exclusive) {
+          section.getPermission(FORGE_AUTHOR, true).setExclusiveGroup(true);
+          section.getPermission(FORGE_COMMITTER, true).setExclusiveGroup(true);
+          section.getPermission(FORGE_SERVER, true).setExclusiveGroup(true);
+        }
+
+        if (1 <= old.max_value) {
+          add(section, FORGE_AUTHOR, old.exclusive, rule(group));
+        }
+
+        if (2 <= old.max_value) {
+          add(section, FORGE_COMMITTER, old.exclusive, rule(group));
+        } else if (2 <= inheritedMax(config, old)) {
+          add(section, FORGE_COMMITTER, old.exclusive, deny(group));
+        }
+
+        if (3 <= old.max_value) {
+          add(section, FORGE_SERVER, old.exclusive, rule(group));
+        } else if (3 <= inheritedMax(config, old)) {
+          add(section, FORGE_SERVER, old.exclusive, deny(group));
+        }
+
+      } else {
+        PermissionRule rule = rule(group);
+        rule.setRange(old.min_value, old.max_value);
+        if (old.min_value == 0 && old.max_value == 0) {
+          rule.setDeny();
+        }
+        add(section, LABEL + varNameOf(old.category), old.exclusive, rule);
+      }
+    }
+  }
+
+  private static Permission newChangePermission(ProjectConfig config,
+      String name) {
+    if (name.startsWith(AccessSection.REGEX_PREFIX)) {
+      name = AccessSection.REGEX_PREFIX
+          + "refs/for/"
+          + name.substring(AccessSection.REGEX_PREFIX.length());
+    } else {
+      name = "refs/for/" + name;
+    }
+    return config.getAccessSection(name, true).getPermission(PUSH, true);
+  }
+
+  private static Permission newMergePermission(ProjectConfig config,
+      String name) {
+    if (name.startsWith(AccessSection.REGEX_PREFIX)) {
+      name = AccessSection.REGEX_PREFIX
+          + "refs/for/"
+          + name.substring(AccessSection.REGEX_PREFIX.length());
+    } else {
+      name = "refs/for/" + name;
+    }
+    return config.getAccessSection(name, true).getPermission(PUSH_MERGE, true);
+  }
+
+  private static PermissionRule rule(GroupReference group) {
+    return new PermissionRule(group);
+  }
+
+  private static PermissionRule deny(GroupReference group) {
+    PermissionRule rule = rule(group);
+    rule.setDeny();
+    return rule;
+  }
+
+  private int inheritedMax(ProjectConfig config, OldRefRight old) {
+    int max = 0;
+
+    String ref = old.ref_pattern;
+    String category = old.category;
+    AccountGroup.UUID group = old.group.getUUID();
+
+    Project.NameKey project = config.getProject().getParent();
+    if (project == null) {
+      project = systemConfig.wildProjectName;
+    }
+    do {
+      List<OldRefRight> rights = rightsByProject.get(project);
+      if (rights != null) {
+        for (OldRefRight r : rights) {
+          if (r.ref_pattern.equals(ref) //
+              && r.group.getUUID().equals(group) //
+              && r.category.equals(category)) {
+            max = Math.max(max, r.max_value);
+            break;
+          }
+        }
+      }
+      project = parentsByProject.get(project);
+    } while (!project.equals(systemConfig.wildProjectName));
+
+    return max;
+  }
+
+  private String varNameOf(String id) {
+    ApprovalCategory category = categoryMap.get(new ApprovalCategory.Id(id));
+    if (category == null) {
+      category = new ApprovalCategory(new ApprovalCategory.Id(id), id);
+    }
+    return category.getLabelName();
+  }
+
+  private static void add(AccessSection section, String name,
+      boolean exclusive, PermissionRule rule) {
+    Permission p = section.getPermission(name, true);
+    if (exclusive) {
+      p.setExclusiveGroup(true);
+    }
+    p.add(rule);
+  }
+
+  private class OldRefRight {
+    final int min_value;
+    final int max_value;
+    final String ref_pattern;
+    final boolean exclusive;
+    final GroupReference group;
+    final String category;
+    final Project.NameKey project;
+
+    OldRefRight(ResultSet rs) throws SQLException {
+      min_value = rs.getInt("min_value");
+      max_value = rs.getInt("max_value");
+      project = new Project.NameKey(rs.getString("project_name"));
+
+      String r = rs.getString("ref_pattern");
+      exclusive = r.startsWith("-");
+      if (exclusive) {
+        r = r.substring(1);
+      }
+      ref_pattern = r;
+
+      category = rs.getString("category_id");
+      group = groupMap.get(new AccountGroup.Id(rs.getInt("group_id")));
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_50.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_54.java
similarity index 89%
copy from gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_50.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_54.java
index 4a90c1f..77c6775 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_50.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_54.java
@@ -17,9 +17,9 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-public class Schema_50 extends SchemaVersion {
+public class Schema_54 extends SchemaVersion {
   @Inject
-  Schema_50(Provider<Schema_49> prior) {
+  Schema_54(Provider<Schema_53> prior) {
     super(prior);
   }
-}
\ No newline at end of file
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_55.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_55.java
new file mode 100644
index 0000000..9032bb0
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_55.java
@@ -0,0 +1,61 @@
+// 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.client.Project;
+import com.google.gerrit.reviewdb.client.SystemConfig;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.git.LocalDiskRepositoryManager;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.RepositoryCache.FileKey;
+import org.eclipse.jgit.util.FS;
+
+import java.io.File;
+import java.util.Collections;
+
+public class Schema_55 extends SchemaVersion {
+  private final LocalDiskRepositoryManager mgr;
+
+  @Inject
+  Schema_55(Provider<Schema_54> prior, LocalDiskRepositoryManager mgr) {
+    super(prior);
+    this.mgr = mgr;
+  }
+
+  @Override
+  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException {
+    SystemConfig sc = db.systemConfig().get(new SystemConfig.Key());
+    String oldName = sc.wildProjectName.get();
+    String newName = "All-Projects";
+    if ("-- All Projects --".equals(oldName)) {
+      ui.message("Renaming \"" + oldName + "\" to \"" + newName + "\"");
+
+      File base = mgr.getBasePath();
+      File oldDir = FileKey.resolve(new File(base, oldName), FS.DETECTED);
+      File newDir = new File(base, newName + Constants.DOT_GIT_EXT);
+      if (!oldDir.renameTo(newDir)) {
+        throw new OrmException("Cannot rename " + oldDir.getAbsolutePath()
+            + " to " + newDir.getAbsolutePath());
+      }
+
+      sc.wildProjectName = new Project.NameKey(newName);
+      db.systemConfig().update(Collections.singleton(sc));
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_56.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_56.java
new file mode 100644
index 0000000..2409b12e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_56.java
@@ -0,0 +1,80 @@
+// 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.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.LocalDiskRepositoryManager;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+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 java.io.IOException;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+
+public class Schema_56 extends SchemaVersion {
+  private final LocalDiskRepositoryManager mgr;
+  private final Set<String> keysOne;
+  private final Set<String> keysTwo;
+
+  @Inject
+  Schema_56(Provider<Schema_55> prior, LocalDiskRepositoryManager mgr) {
+    super(prior);
+    this.mgr = mgr;
+
+    keysOne = new HashSet<String>();
+    keysTwo = new HashSet<String>();
+
+    keysOne.add(GitRepositoryManager.REF_CONFIG);
+    keysTwo.add(GitRepositoryManager.REF_CONFIG);
+    keysTwo.add(Constants.HEAD);
+  }
+
+  @Override
+  protected void migrateData(ReviewDb db, UpdateUI ui) {
+    for (Project.NameKey name : mgr.list()) {
+      Repository git;
+      try {
+        git = mgr.openRepository(name);
+      } catch (RepositoryNotFoundException e) {
+        ui.message("warning: Cannot open " + name.get());
+        continue;
+      }
+      try {
+        Map<String, Ref> all = git.getAllRefs();
+        if (all.keySet().equals(keysOne) || all.keySet().equals(keysTwo)) {
+          try {
+            RefUpdate update = git.updateRef(Constants.HEAD);
+            update.disableRefLog();
+            update.link(GitRepositoryManager.REF_CONFIG);
+          } catch (IOException err) {
+            ui.message("warning: " + name.get() + ": Cannot update HEAD to "
+                + GitRepositoryManager.REF_CONFIG + ": " + err.getMessage());
+          }
+        }
+      } finally {
+        git.close();
+      }
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_57.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_57.java
new file mode 100644
index 0000000..2247fdb
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_57.java
@@ -0,0 +1,170 @@
+// 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.common.data.AccessSection;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.common.data.PermissionRule.Action;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupName;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.SystemConfig;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.config.AllProjectsNameProvider;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.git.LocalDiskRepositoryManager;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.NoReplication;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.storage.file.FileBasedConfig;
+import org.eclipse.jgit.util.FS;
+
+import java.io.IOException;
+import java.util.Collections;
+
+public class Schema_57 extends SchemaVersion {
+  private final SitePaths site;
+  private final LocalDiskRepositoryManager mgr;
+  private final PersonIdent serverUser;
+
+  @Inject
+  Schema_57(Provider<Schema_56> prior, SitePaths site,
+      LocalDiskRepositoryManager mgr, @GerritPersonIdent PersonIdent serverUser) {
+    super(prior);
+    this.site = site;
+    this.mgr = mgr;
+    this.serverUser = serverUser;
+  }
+
+  @Override
+  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException {
+    SystemConfig sc = db.systemConfig().get(new SystemConfig.Key());
+    Project.NameKey allProjects = sc.wildProjectName;
+
+    FileBasedConfig cfg = new FileBasedConfig(site.gerrit_config, FS.DETECTED);
+    boolean cfgDirty = false;
+    try {
+      cfg.load();
+    } catch (ConfigInvalidException err) {
+      throw new OrmException("Cannot read " + site.gerrit_config, err);
+    } catch (IOException err) {
+      throw new OrmException("Cannot read " + site.gerrit_config, err);
+    }
+
+    if (!allProjects.get().equals(AllProjectsNameProvider.DEFAULT)) {
+      ui.message("Setting gerrit.allProjects = " + allProjects.get());
+      cfg.setString("gerrit", null, "allProjects", allProjects.get());
+      cfgDirty = true;
+    }
+
+    try {
+      Repository git = mgr.openRepository(allProjects);
+      try {
+        MetaDataUpdate md = new MetaDataUpdate(new NoReplication(), allProjects, git);
+        md.getCommitBuilder().setAuthor(serverUser);
+        md.getCommitBuilder().setCommitter(serverUser);
+
+        ProjectConfig config = ProjectConfig.read(md);
+        AccessSection cap = config.getAccessSection(AccessSection.GLOBAL_CAPABILITIES, true);
+
+        // Move the Administrators group reference to All-Projects.
+        cap.getPermission(GlobalCapability.ADMINISTRATE_SERVER, true)
+            .add(new PermissionRule(config.resolve(db.accountGroups().get(sc.adminGroupId))));
+
+        // Move the repository.*.createGroup to Create Project.
+        String[] createGroupList = cfg.getStringList("repository", "*", "createGroup");
+        for (String name : createGroupList) {
+          AccountGroup.NameKey key = new AccountGroup.NameKey(name);
+          AccountGroupName groupName = db.accountGroupNames().get(key);
+          if (groupName == null) {
+            continue;
+          }
+
+          AccountGroup group = db.accountGroups().get(groupName.getId());
+          if (group == null) {
+            continue;
+          }
+
+          cap.getPermission(GlobalCapability.CREATE_PROJECT, true)
+              .add(new PermissionRule(config.resolve(group)));
+        }
+        if (createGroupList.length != 0) {
+          ui.message("Moved repository.*.createGroup to 'Create Project' capability");
+          cfg.unset("repository", "*", "createGroup");
+          cfgDirty = true;
+        }
+
+        AccountGroup batch = db.accountGroups().get(sc.batchUsersGroupId);
+        if (batch != null
+            && db.accountGroupMembers().byGroup(sc.batchUsersGroupId).toList().isEmpty()
+            &&  db.accountGroupIncludes().byGroup(sc.batchUsersGroupId).toList().isEmpty()) {
+          // If the batch user group is not used, delete it.
+          //
+          db.accountGroups().delete(Collections.singleton(batch));
+
+          AccountGroupName name = db.accountGroupNames().get(batch.getNameKey());
+          if (name != null) {
+            db.accountGroupNames().delete(Collections.singleton(name));
+          }
+        } else if (batch != null) {
+          cap.getPermission(GlobalCapability.PRIORITY, true)
+              .getRule(config.resolve(batch), true)
+              .setAction(Action.BATCH);
+        }
+
+        md.setMessage("Upgrade to Gerrit Code Review schema 57\n");
+        if (!config.commit(md)) {
+          throw new OrmException("Cannot update " + allProjects);
+        }
+      } finally {
+        git.close();
+      }
+    } catch (ConfigInvalidException err) {
+      throw new OrmException("Cannot read " + allProjects, err);
+    } catch (IOException err) {
+      throw new OrmException("Cannot update " + allProjects, err);
+    }
+
+    if (cfgDirty) {
+      try {
+        cfg.save();
+      } catch (IOException err) {
+        throw new OrmException("Cannot update " + site.gerrit_config, err);
+      }
+    }
+
+    // We cannot set the columns to NULL, so use 0 and a DELETED tag.
+    sc.adminGroupId = new AccountGroup.Id(0);
+    sc.adminGroupUUID = new AccountGroup.UUID("DELETED");
+    sc.anonymousGroupId = new AccountGroup.Id(0);
+    sc.registeredGroupId = new AccountGroup.Id(0);
+    sc.wildProjectName = new Project.NameKey("DELETED");
+    sc.ownerGroupId = new AccountGroup.Id(0);
+    sc.batchUsersGroupId = new AccountGroup.Id(0);
+    sc.batchUsersGroupUUID = new AccountGroup.UUID("DELETED");
+    sc.registerEmailPrivateKey = "DELETED";
+
+    db.systemConfig().update(Collections.singleton(sc));
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_50.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_58.java
similarity index 89%
rename from gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_50.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_58.java
index 4a90c1f..72b0c8b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_50.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_58.java
@@ -17,9 +17,9 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-public class Schema_50 extends SchemaVersion {
+public class Schema_58 extends SchemaVersion {
   @Inject
-  Schema_50(Provider<Schema_49> prior) {
+  Schema_58(Provider<Schema_57> prior) {
     super(prior);
   }
-}
\ No newline at end of file
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_59.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_59.java
new file mode 100644
index 0000000..d90ecc1
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_59.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.schema;
+
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.sql.SQLException;
+import java.util.List;
+
+public class Schema_59 extends SchemaVersion {
+  @Inject
+  Schema_59(Provider<Schema_58> prior) {
+    super(prior);
+  }
+
+  @Override
+  protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException,
+      SQLException {
+    List<Change> allChanges = db.changes().all().toList();
+    for (Change change : allChanges) {
+      change.setMergeable(true);
+      change.setLastSha1MergeTested(null);
+    }
+    db.changes().update(allChanges);
+  }
+}
\ No newline at end of file
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..3f49b0c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_60.java
@@ -0,0 +1,70 @@
+// 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.client.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.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_50.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_61.java
similarity index 89%
copy from gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_50.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_61.java
index 4a90c1f..890c824 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_50.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_61.java
@@ -17,9 +17,9 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-public class Schema_50 extends SchemaVersion {
+public class Schema_61 extends SchemaVersion {
   @Inject
-  Schema_50(Provider<Schema_49> prior) {
+  Schema_61(Provider<Schema_60> prior) {
     super(prior);
   }
-}
\ No newline at end of file
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_50.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_62.java
similarity index 89%
copy from gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_50.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_62.java
index 4a90c1f..4de8e55 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_50.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_62.java
@@ -17,9 +17,9 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-public class Schema_50 extends SchemaVersion {
+public class Schema_62 extends SchemaVersion {
   @Inject
-  Schema_50(Provider<Schema_49> prior) {
+  Schema_62(Provider<Schema_61> prior) {
     super(prior);
   }
-}
\ No newline at end of file
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_51.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_63.java
similarity index 60%
rename from gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_51.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_63.java
index d111160..761c36a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_51.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_63.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2011 The Android Open Source Project
+// Copyright (C) 2012 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -14,17 +14,18 @@
 
 package com.google.gerrit.server.schema;
 
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gwtorm.jdbc.JdbcSchema;
+import com.google.gwtorm.schema.sql.DialectPostgreSQL;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
 import java.sql.SQLException;
 import java.sql.Statement;
 
-public class Schema_51 extends SchemaVersion {
+public class Schema_63 extends SchemaVersion {
   @Inject
-  Schema_51(Provider<Schema_50> prior) {
+  Schema_63(Provider<Schema_62> prior) {
     super(prior);
   }
 
@@ -32,8 +33,14 @@
   protected void migrateData(ReviewDb db, UpdateUI ui) throws SQLException {
     Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
     try {
-      stmt.execute("CREATE INDEX account_group_includes_byInclude"
-          + " ON account_group_includes (include_id)");
+      if (((JdbcSchema) db).getDialect() instanceof DialectPostgreSQL) {
+        stmt.execute("CREATE INDEX changes_byBranchClosed"
+            + " ON changes (status, dest_project_name, dest_branch_name, sort_key)"
+            + " WHERE open = 'N'");
+      } else {
+        stmt.execute("CREATE INDEX changes_byBranchClosed"
+            + " ON changes (status, dest_project_name, dest_branch_name, sort_key)");
+      }
     } finally {
       stmt.close();
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_64.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_64.java
new file mode 100644
index 0000000..26890a3
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_64.java
@@ -0,0 +1,121 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.schema;
+
+import com.google.common.collect.Lists;
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.NoReplication;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gwtorm.jdbc.JdbcSchema;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+
+import java.io.IOException;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.List;
+
+public class Schema_64 extends SchemaVersion {
+  private final AllProjectsName allProjects;
+  private final GitRepositoryManager mgr;
+  private final PersonIdent serverUser;
+
+  @Inject
+  Schema_64(Provider<Schema_63> prior,
+      AllProjectsName allProjects,
+      GitRepositoryManager mgr,
+      @GerritPersonIdent PersonIdent serverUser) {
+    super(prior);
+    this.allProjects = allProjects;
+    this.mgr = mgr;
+    this.serverUser = serverUser;
+  }
+
+  @Override
+  protected void migrateData(ReviewDb db, UpdateUI ui)
+      throws OrmException, SQLException {
+    List<GroupReference> groups = Lists.newArrayList();
+    Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
+    try {
+      ResultSet rs = stmt.executeQuery(
+          "SELECT group_uuid, name FROM account_groups WHERE email_only_authors = 'Y'");
+      try {
+        while (rs.next()) {
+          AccountGroup.UUID uuid = new AccountGroup.UUID(rs.getString(1));
+          GroupReference group = new GroupReference(uuid, rs.getString(2));
+          groups.add(group);
+        }
+      } finally {
+        rs.close();
+      }
+    } finally {
+      stmt.close();
+    }
+
+    if (groups.isEmpty()) {
+      return;
+    }
+    ui.message("Moved account_groups.email_only_authors to 'Email Reviewers' capability");
+
+    Repository git;
+    try {
+      git = mgr.openRepository(allProjects);
+    } catch (RepositoryNotFoundException e) {
+      throw new OrmException(e);
+    }
+    try {
+      MetaDataUpdate md =
+          new MetaDataUpdate(new NoReplication(), allProjects, git);
+      md.getCommitBuilder().setAuthor(serverUser);
+      md.getCommitBuilder().setCommitter(serverUser);
+
+      ProjectConfig config = ProjectConfig.read(md);
+      AccessSection section =
+          config.getAccessSection(AccessSection.GLOBAL_CAPABILITIES, true);
+      Permission capability =
+          section.getPermission(GlobalCapability.EMAIL_REVIEWERS, true);
+      for (GroupReference group : groups) {
+        capability.getRule(config.resolve(group), true).setDeny();
+      }
+
+      md.setMessage("Upgrade to Gerrit Code Review schema 64\n");
+      if (!config.commit(md)) {
+        throw new OrmException("Cannot update " + allProjects);
+      }
+    } catch (IOException e) {
+      throw new OrmException(e);
+    } catch (ConfigInvalidException e) {
+      throw new OrmException(e);
+    } finally {
+      git.close();
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/ScriptRunner.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/ScriptRunner.java
index 34c47a8..8cf2f26 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/ScriptRunner.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/ScriptRunner.java
@@ -14,9 +14,9 @@
 
 package com.google.gerrit.server.schema;
 
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gwtorm.client.OrmException;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gwtorm.jdbc.JdbcSchema;
+import com.google.gwtorm.server.OrmException;
 
 import java.io.BufferedReader;
 import java.io.FileNotFoundException;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/UpdateUI.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/UpdateUI.java
index ac07efd..64b3afa 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/UpdateUI.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/UpdateUI.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.server.schema;
 
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.client.StatementExecutor;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.StatementExecutor;
 
 import java.util.List;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_42.java b/gerrit-server/src/main/java/com/google/gerrit/server/ssh/NoSshInfo.java
similarity index 71%
rename from gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_42.java
rename to gerrit-server/src/main/java/com/google/gerrit/server/ssh/NoSshInfo.java
index 83bca7b..519b922 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_42.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ssh/NoSshInfo.java
@@ -12,14 +12,16 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.schema;
+package com.google.gerrit.server.ssh;
 
-import com.google.inject.Inject;
-import com.google.inject.Provider;
+import com.jcraft.jsch.HostKey;
 
-public class Schema_42 extends SchemaVersion {
-  @Inject
-  Schema_42(Provider<Schema_41> prior) {
-    super(prior);
+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..6a07ca7
--- /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.client.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/schema/Schema_42.java b/gerrit-server/src/main/java/com/google/gerrit/server/ssh/NoSshModule.java
similarity index 64%
copy from gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_42.java
copy to gerrit-server/src/main/java/com/google/gerrit/server/ssh/NoSshModule.java
index 83bca7b..21b1a54 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_42.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ssh/NoSshModule.java
@@ -12,14 +12,17 @@
 // See the License for the specific language governing permissions and
 // limitations under the License.
 
-package com.google.gerrit.server.schema;
+package com.google.gerrit.server.ssh;
 
-import com.google.inject.Inject;
-import com.google.inject.Provider;
+import com.google.inject.AbstractModule;
 
-public class Schema_42 extends SchemaVersion {
-  @Inject
-  Schema_42(Provider<Schema_41> prior) {
-    super(prior);
+/**
+ * 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/ssh/SshKeyCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/ssh/SshKeyCache.java
index b56405a..13c5703 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ssh/SshKeyCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ssh/SshKeyCache.java
@@ -15,7 +15,7 @@
 package com.google.gerrit.server.ssh;
 
 import com.google.gerrit.common.errors.InvalidSshKeyException;
-import com.google.gerrit.reviewdb.AccountSshKey;
+import com.google.gerrit.reviewdb.client.AccountSshKey;
 
 /** Permits controlling the contents of the SSH key cache area. */
 public interface SshKeyCache {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/GuiceRequestScopePropagator.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/GuiceRequestScopePropagator.java
new file mode 100644
index 0000000..9befc7d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/GuiceRequestScopePropagator.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.server.util;
+
+import com.google.common.collect.Maps;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.RemotePeer;
+import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.inject.Inject;
+import com.google.inject.Key;
+import com.google.inject.Provider;
+import com.google.inject.servlet.ServletScopes;
+import com.google.inject.util.Providers;
+import com.google.inject.util.Types;
+
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.net.SocketAddress;
+import java.util.Map;
+import java.util.concurrent.Callable;
+
+import javax.annotation.Nullable;
+
+/** Propagator for Guice's built-in servlet scope. */
+public class GuiceRequestScopePropagator extends RequestScopePropagator {
+
+  private final String url;
+  private final SocketAddress peer;
+  private final CurrentUser user;
+
+  @Inject
+  GuiceRequestScopePropagator(
+      @CanonicalWebUrl @Nullable Provider<String> urlProvider,
+      @RemotePeer Provider<SocketAddress> remotePeerProvider,
+      Provider<CurrentUser> currentUserProvider) {
+    super(ServletScopes.REQUEST);
+    this.url = urlProvider != null ? urlProvider.get() : null;
+    this.peer = remotePeerProvider.get();
+    this.user = currentUserProvider.get();
+  }
+
+  /**
+   * @see RequestScopePropagator#wrap(Callable)
+   */
+  @Override
+  protected <T> Callable<T> wrapImpl(Callable<T> callable) {
+    Map<Key<?>, Object> seedMap = Maps.newHashMap();
+
+    // Request scopes appear to use specific keys in their map, instead of only
+    // providers. Add bindings for both the key to the instance directly and the
+    // provider to the instance to be safe.
+    seedMap.put(Key.get(typeOfProvider(String.class), CanonicalWebUrl.class),
+        Providers.of(url));
+    seedMap.put(Key.get(String.class, CanonicalWebUrl.class), url);
+
+    seedMap.put(Key.get(typeOfProvider(SocketAddress.class), RemotePeer.class),
+        Providers.of(peer));
+    seedMap.put(Key.get(SocketAddress.class, RemotePeer.class), peer);
+
+    seedMap.put(Key.get(typeOfProvider(CurrentUser.class)), Providers.of(user));
+    seedMap.put(Key.get(CurrentUser.class), user);
+
+    return ServletScopes.continueRequest(callable, seedMap);
+  }
+
+  private ParameterizedType typeOfProvider(Type type) {
+    return Types.newParameterizedType(Provider.class, type);
+  }
+}
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..510deaa02
--- /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.client.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() {
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/MostSpecificComparator.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/MostSpecificComparator.java
new file mode 100644
index 0000000..804a7ec
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/MostSpecificComparator.java
@@ -0,0 +1,124 @@
+// 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.RefConfigSection;
+import com.google.gerrit.server.project.RefControl;
+
+import org.apache.commons.lang.StringUtils;
+
+import java.util.Comparator;
+
+/**
+ * Order the Ref Pattern by the most specific. This sort is done by:
+ * <ul>
+ * <li>1 - The minor value of Levenshtein string distance between the branch
+ * name and the regex string shortest example. A shorter distance is a more
+ * specific match.
+ * <li>2 - Finites first, infinities after.
+ * <li>3 - Number of transitions.
+ * <li>4 - Length of the expression text.
+ * </ul>
+ *
+ * Levenshtein distance is a measure of the similarity between two strings.
+ * The distance is the number of deletions, insertions, or substitutions
+ * required to transform one string into another.
+ *
+ * For example, if given refs/heads/m* and refs/heads/*, the distances are 5
+ * and 6. It means that refs/heads/m* is more specific because it's closer to
+ * refs/heads/master than refs/heads/*.
+ *
+ * Another example could be refs/heads/* and refs/heads/[a-zA-Z]*, the
+ * distances are both 6. Both are infinite, but refs/heads/[a-zA-Z]* has more
+ * transitions, which after all turns it more specific.
+ */
+public final class MostSpecificComparator implements
+    Comparator<RefConfigSection> {
+  private final String refName;
+
+  public MostSpecificComparator(String refName) {
+    this.refName = refName;
+  }
+
+  @Override
+  public int compare(RefConfigSection a, RefConfigSection b) {
+    return compare(a.getName(), b.getName());
+  }
+
+  public int compare(final String pattern1, final String pattern2) {
+    int cmp = distance(pattern1) - distance(pattern2);
+    if (cmp == 0) {
+      boolean p1_finite = finite(pattern1);
+      boolean p2_finite = finite(pattern2);
+
+      if (p1_finite && !p2_finite) {
+        cmp = -1;
+      } else if (!p1_finite && p2_finite) {
+        cmp = 1;
+      } else /* if (f1 == f2) */{
+        cmp = 0;
+      }
+    }
+    if (cmp == 0) {
+      cmp = transitions(pattern1) - transitions(pattern2);
+    }
+    if (cmp == 0) {
+      cmp = pattern2.length() - pattern1.length();
+    }
+    return cmp;
+  }
+
+  private int distance(String pattern) {
+    String example;
+    if (RefControl.isRE(pattern)) {
+      example = RefControl.shortestExample(pattern);
+
+    } else if (pattern.endsWith("/*")) {
+      example = pattern.substring(0, pattern.length() - 1) + '1';
+
+    } else if (pattern.equals(refName)) {
+      return 0;
+
+    } else {
+      return Math.max(pattern.length(), refName.length());
+    }
+    return StringUtils.getLevenshteinDistance(example, refName);
+  }
+
+  private boolean finite(String pattern) {
+    if (RefControl.isRE(pattern)) {
+      return RefControl.toRegExp(pattern).toAutomaton().isFinite();
+
+    } else if (pattern.endsWith("/*")) {
+      return false;
+
+    } else {
+      return true;
+    }
+  }
+
+  private int transitions(String pattern) {
+    if (RefControl.isRE(pattern)) {
+      return RefControl.toRegExp(pattern).toAutomaton()
+          .getNumberOfTransitions();
+
+    } else if (pattern.endsWith("/*")) {
+      return pattern.length();
+
+    } else {
+      return pattern.length();
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestScopePropagator.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestScopePropagator.java
new file mode 100644
index 0000000..3661aa2
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestScopePropagator.java
@@ -0,0 +1,181 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.client.Project.NameKey;
+import com.google.gerrit.server.RequestCleanup;
+import com.google.gerrit.server.git.ProjectRunnable;
+import com.google.inject.Key;
+import com.google.inject.Provider;
+import com.google.inject.Scope;
+import com.google.inject.servlet.ServletScopes;
+
+import java.util.concurrent.Callable;
+import java.util.concurrent.Executors;
+
+/**
+ * Base class for propagating request-scoped data between threads.
+ * <p>
+ * Request scopes are typically linked to a {@link ThreadLocal}, which is only
+ * available to the current thread.  In order to allow background work involving
+ * RequestScoped data, the ThreadLocal data must be copied from the request thread to
+ * the new background thread.
+ * <p>
+ * Every type of RequestScope must provide an implementation of
+ * RequestScopePropagator. See {@link #wrap(Callable)} for details on the
+ * implementation, usage, and restrictions.
+ *
+ * @see ThreadLocalRequestScopePropagator
+ */
+public abstract class RequestScopePropagator {
+
+  private final Scope scope;
+
+  protected RequestScopePropagator(Scope scope) {
+    this.scope = scope;
+  }
+
+  /**
+   * Wraps callable in a new {@link Callable} that propagates the current
+   * request state when the callable is invoked. The method must be called in a
+   * request scope and the returned Callable may only be invoked in a thread
+   * that is not already in a request scope. The returned Callable will inherit
+   * toString() from the passed in Callable. A
+   * {@link com.google.gerrit.server.git.WorkQueue.Executor} does not accept a
+   * Callable, so there is no ProjectCallable implementation. Implementations of
+   * this method must be consistent with Guice's
+   * {@link ServletScopes#continueRequest(Callable, java.util.Map)}.
+   * <p>
+   * There are some limitations:
+   * <ul>
+   * <li>Derived objects (i.e. anything marked created in a request scope) will
+   * not be transported.</li>
+   * <li>State changes to the request scoped context after this method is called
+   * will not be seen in the continued thread.</li>
+   * </ul>
+   *
+   * @param callable the Callable to wrap.
+   * @return a new Callable which will execute in the current request scope.
+   */
+  public final <T> Callable<T> wrap(final Callable<T> callable) {
+    final Callable<T> wrapped = wrapImpl(new Callable<T>() {
+      @Override
+      public T call() throws Exception {
+        RequestCleanup cleanup = scope.scope(
+            Key.get(RequestCleanup.class),
+            new Provider<RequestCleanup>() {
+              @Override
+              public RequestCleanup get() {
+                return new RequestCleanup();
+              }
+            }).get();
+
+        try {
+          return callable.call();
+        } finally {
+          cleanup.run();
+        }
+      }
+    });
+
+    return new Callable<T>() {
+      @Override
+      public T call() throws Exception {
+        return wrapped.call();
+      }
+
+      @Override
+      public String toString() {
+        return callable.toString();
+      }
+    };
+  }
+
+  /**
+   * Wraps runnable in a new {@link Runnable} that propagates the current
+   * request state when the runnable is invoked. The method must be called in a
+   * request scope and the returned Runnable may only be invoked in a thread
+   * that is not already in a request scope. The returned Runnable will inherit
+   * toString() from the passed in Runnable. Furthermore, if the passed runnable
+   * is of type {@link ProjectRunnable}, the returned runnable will be of the
+   * same type with the methods delegated.
+   *
+   * See {@link #wrap(Callable)} for details on implementation and usage.
+   *
+   * @param runnable the Runnable to wrap.
+   * @return a new Runnable which will execute in the current request scope.
+   */
+  public final Runnable wrap(final Runnable runnable) {
+    final Callable<Object> wrapped = wrap(Executors.callable(runnable));
+
+    if (runnable instanceof ProjectRunnable) {
+      return new ProjectRunnable() {
+        @Override
+        public void run() {
+          try {
+            wrapped.call();
+          } catch (RuntimeException e) {
+            throw e;
+          } catch (Exception e) {
+            throw new RuntimeException(e); // Not possible.
+          }
+        }
+
+        @Override
+        public NameKey getProjectNameKey() {
+          return ((ProjectRunnable) runnable).getProjectNameKey();
+        }
+
+        @Override
+        public String getRemoteName() {
+          return ((ProjectRunnable) runnable).getRemoteName();
+        }
+
+        @Override
+        public boolean hasCustomizedPrint() {
+          return ((ProjectRunnable) runnable).hasCustomizedPrint();
+        }
+
+        @Override
+        public String toString() {
+          return runnable.toString();
+        }
+      };
+    } else {
+      return new Runnable() {
+        @Override
+        public void run() {
+          try {
+            wrapped.call();
+          } catch (RuntimeException e) {
+            throw e;
+          } catch (Exception e) {
+            throw new RuntimeException(e); // Not possible.
+          }
+        }
+
+        @Override
+        public String toString() {
+          return runnable.toString();
+        }
+      };
+    }
+  }
+
+  /**
+   * @see #wrap(Callable)
+   */
+  protected abstract <T> Callable<T> wrapImpl(final Callable<T> callable);
+}
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..7310703
--- /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.client.Branch;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.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/com/google/gerrit/server/util/ThreadLocalRequestScopePropagator.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestScopePropagator.java
new file mode 100644
index 0000000..581ccc1
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestScopePropagator.java
@@ -0,0 +1,89 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.util;
+
+import com.google.inject.OutOfScopeException;
+import com.google.inject.Scope;
+
+import java.util.concurrent.Callable;
+
+/**
+ * {@link RequestScopePropagator} implementation for request scopes based on
+ * a {@link ThreadLocal} context.
+ *
+ * @param <C> "context" type stored in the {@link ThreadLocal}.
+ */
+public abstract class ThreadLocalRequestScopePropagator<C>
+    extends RequestScopePropagator {
+
+  private final ThreadLocal<C> threadLocal;
+
+  protected ThreadLocalRequestScopePropagator(Scope scope,
+      ThreadLocal<C> threadLocal) {
+    super(scope);
+    this.threadLocal = threadLocal;
+  }
+
+  /**
+   * @see RequestScopePropagator#wrap(Callable)
+   */
+  @Override
+  protected final <T> Callable<T> wrapImpl(final Callable<T> callable) {
+    final C ctx = continuingContext(requireContext());
+    return new Callable<T>() {
+      @Override
+      public T call() throws Exception {
+        if (threadLocal.get() != null) {
+          // This is consistent with the Guice ServletScopes.continueRequest()
+          // behavior.
+          throw new IllegalStateException("Cannot continue request, "
+              + "thread already has request in progress. A new thread must "
+              + "be used to propagate the request scope context.");
+        }
+
+        threadLocal.set(ctx);
+        try {
+          return callable.call();
+        } finally {
+          threadLocal.remove();
+        }
+      }
+    };
+  }
+
+  private C requireContext() {
+    C context = threadLocal.get();
+    if (context == null) {
+      throw new OutOfScopeException("Cannot access scoped object");
+    }
+    return context;
+  }
+
+  /**
+   * Returns a new context object based on the passed in context that has no
+   * request scoped objects initialized.
+   * <p>
+   * Note that some code paths expect request-scoped objects like
+   * {@code CurrentUser} to be constructible starting from just the context
+   * object returned by this method. For example, in the SSH scope, the context
+   * includes the {@code SshSession}, which is used by
+   * {@code SshCurrentUserProvider} to construct a new {@code CurrentUser} in
+   * the new thread.
+   *
+   * @param ctx the context to continue.
+   * @return a new context.
+   */
+  protected abstract C continuingContext(C ctx);
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/TreeFormatter.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/TreeFormatter.java
new file mode 100644
index 0000000..1c68a75
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/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.server.util;
+
+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-server/src/main/java/com/google/gerrit/server/workflow/CategoryFunction.java b/gerrit-server/src/main/java/com/google/gerrit/server/workflow/CategoryFunction.java
index 6700393..f6f1bfb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/workflow/CategoryFunction.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/workflow/CategoryFunction.java
@@ -15,11 +15,8 @@
 package com.google.gerrit.server.workflow;
 
 import com.google.gerrit.common.data.ApprovalType;
-import com.google.gerrit.reviewdb.ApprovalCategory;
-import com.google.gerrit.reviewdb.PatchSetApproval;
-import com.google.gerrit.reviewdb.RefRight;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.project.RefControl;
+import com.google.gerrit.reviewdb.client.ApprovalCategory;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
 
 import java.util.HashMap;
 import java.util.Map;
@@ -29,7 +26,6 @@
   private static Map<String, CategoryFunction> all =
       new HashMap<String, CategoryFunction>();
   static {
-    all.put(SubmitFunction.NAME, new SubmitFunction());
     all.put(MaxWithBlock.NAME, new MaxWithBlock());
     all.put(MaxNoBlock.NAME, new MaxNoBlock());
     all.put(NoOpFunction.NAME, new NoOpFunction());
@@ -44,22 +40,11 @@
    *         is not known to Gerrit and thus cannot be executed.
    */
   public static CategoryFunction forCategory(final ApprovalCategory category) {
-    final CategoryFunction r = forName(category.getFunctionName());
+    final CategoryFunction r = all.get(category.getFunctionName());
     return r != null ? r : new NoOpFunction();
   }
 
   /**
-   * Locate a function by name.
-   *
-   * @param functionName the function's unique name.
-   * @return the function implementation; null if the function is not known to
-   *         Gerrit and thus cannot be executed.
-   */
-  public static CategoryFunction forName(final String functionName) {
-    return all.get(functionName);
-  }
-
-  /**
    * Normalize ChangeApprovals and set the valid flag for this category.
    * <p>
    * Implementors should invoke:
@@ -89,16 +74,4 @@
    *        the valid status into.
    */
   public abstract void run(ApprovalType at, FunctionState state);
-
-  public boolean isValid(final CurrentUser user, final ApprovalType at,
-      final FunctionState state) {
-    RefControl rc = state.controlFor(user);
-    for (final RefRight pr : rc.getApplicableRights(at.getCategory().getId())) {
-      if (user.getEffectiveGroups().contains(pr.getAccountGroupId())
-          && (pr.getMinValue() < 0 || pr.getMaxValue() > 0)) {
-        return true;
-      }
-    }
-    return false;
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/workflow/FunctionState.java b/gerrit-server/src/main/java/com/google/gerrit/server/workflow/FunctionState.java
index 36a52e2..d08bd1f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/workflow/FunctionState.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/workflow/FunctionState.java
@@ -16,21 +16,18 @@
 
 import com.google.gerrit.common.data.ApprovalType;
 import com.google.gerrit.common.data.ApprovalTypes;
-import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.ApprovalCategory;
-import com.google.gerrit.reviewdb.ApprovalCategoryValue;
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.PatchSet;
-import com.google.gerrit.reviewdb.PatchSetApproval;
-import com.google.gerrit.reviewdb.RefRight;
-import com.google.gerrit.reviewdb.ApprovalCategory.Id;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.common.data.PermissionRange;
+import com.google.gerrit.reviewdb.client.ApprovalCategory;
+import com.google.gerrit.reviewdb.client.ApprovalCategoryValue;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.ApprovalCategory.Id;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.GroupCache;
-import com.google.gerrit.server.project.ProjectCache;
-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.project.ChangeControl;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -44,7 +41,7 @@
 /** State passed through to a {@link CategoryFunction}. */
 public class FunctionState {
   public interface Factory {
-    FunctionState create(Change c, PatchSet.Id psId,
+    FunctionState create(ChangeControl c, PatchSet.Id psId,
         Collection<PatchSetApproval> all);
   }
 
@@ -55,20 +52,19 @@
       new HashMap<ApprovalCategory.Id, Collection<PatchSetApproval>>();
   private final Map<ApprovalCategory.Id, Boolean> valid =
       new HashMap<ApprovalCategory.Id, Boolean>();
+  private final ChangeControl callerChangeControl;
   private final Change change;
-  private final ProjectState project;
 
   @Inject
   FunctionState(final ApprovalTypes approvalTypes,
-      final ProjectCache projectCache,
       final IdentifiedUser.GenericFactory userFactory, final GroupCache egc,
-      @Assisted final Change c, @Assisted final PatchSet.Id psId,
+      @Assisted final ChangeControl c, @Assisted final PatchSet.Id psId,
       @Assisted final Collection<PatchSetApproval> all) {
     this.approvalTypes = approvalTypes;
     this.userFactory = userFactory;
 
-    change = c;
-    project = projectCache.get(change.getProject());
+    callerChangeControl = c;
+    change = c.getChange();
 
     for (final PatchSetApproval ca : all) {
       if (psId.equals(ca.getPatchSetId())) {
@@ -139,40 +135,22 @@
    * of them is used.
    * <p>
    */
-  private void applyRightFloor(final PatchSetApproval a) {
+  private void applyRightFloor(final ApprovalType at, final PatchSetApproval a) {
+    final ApprovalCategory category = at.getCategory();
+    final String permission = Permission.forLabel(category.getLabelName());
     final IdentifiedUser user = userFactory.create(a.getAccountId());
-    RefControl rc = controlFor(user);
-
-    // Find the maximal range actually granted to the user.
-    //
-    short minAllowed = 0, maxAllowed = 0;
-    for (final RefRight r : rc.getApplicableRights(a.getCategoryId())) {
-      final AccountGroup.Id grp = r.getAccountGroupId();
-      if (user.getEffectiveGroups().contains(grp)) {
-        minAllowed = (short) Math.min(minAllowed, r.getMinValue());
-        maxAllowed = (short) Math.max(maxAllowed, r.getMaxValue());
-      }
-    }
-
-    // Normalize the value into that range.
-    //
-    if (a.getValue() < minAllowed) {
-      a.setValue(minAllowed);
-    } else if (a.getValue() > maxAllowed) {
-      a.setValue(maxAllowed);
-    }
+    final PermissionRange range = controlFor(user).getRange(permission);
+    a.setValue((short) range.squash(a.getValue()));
   }
 
-  RefControl controlFor(final CurrentUser user) {
-    ProjectControl pc = project.controlFor(user);
-    RefControl rc = pc.controlForRef(change.getDest().get());
-    return rc;
+  private ChangeControl controlFor(CurrentUser user) {
+    return callerChangeControl.forUser(user);
   }
 
   /** Run <code>applyTypeFloor</code>, <code>applyRightFloor</code>. */
   public void normalize(final ApprovalType at, final PatchSetApproval ca) {
     applyTypeFloor(at, ca);
-    applyRightFloor(ca);
+    applyRightFloor(at, ca);
   }
 
   private static Id id(final ApprovalType at) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/workflow/MaxNoBlock.java b/gerrit-server/src/main/java/com/google/gerrit/server/workflow/MaxNoBlock.java
index 5f10c28..3272f8a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/workflow/MaxNoBlock.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/workflow/MaxNoBlock.java
@@ -15,8 +15,8 @@
 package com.google.gerrit.server.workflow;
 
 import com.google.gerrit.common.data.ApprovalType;
-import com.google.gerrit.reviewdb.ApprovalCategory;
-import com.google.gerrit.reviewdb.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.ApprovalCategory;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
 
 /**
  * Computes an {@link ApprovalCategory} by looking at maximum values.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/workflow/MaxWithBlock.java b/gerrit-server/src/main/java/com/google/gerrit/server/workflow/MaxWithBlock.java
index 7bf7e9b..ce84499 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/workflow/MaxWithBlock.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/workflow/MaxWithBlock.java
@@ -15,8 +15,8 @@
 package com.google.gerrit.server.workflow;
 
 import com.google.gerrit.common.data.ApprovalType;
-import com.google.gerrit.reviewdb.ApprovalCategory;
-import com.google.gerrit.reviewdb.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.ApprovalCategory;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
 
 /**
  * Computes an {@link ApprovalCategory} by looking at maximum values.
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/workflow/NoBlock.java b/gerrit-server/src/main/java/com/google/gerrit/server/workflow/NoBlock.java
index a089817..08d0705 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/workflow/NoBlock.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/workflow/NoBlock.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.server.workflow;
 
 import com.google.gerrit.common.data.ApprovalType;
-import com.google.gerrit.server.CurrentUser;
 
 /** A function that does nothing. */
 public class NoBlock extends CategoryFunction {
@@ -25,10 +24,4 @@
   public void run(final ApprovalType at, final FunctionState state) {
     state.valid(at, true);
   }
-
-  @Override
-  public boolean isValid(final CurrentUser user, final ApprovalType at,
-      final FunctionState state) {
-    return true;
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/workflow/NoOpFunction.java b/gerrit-server/src/main/java/com/google/gerrit/server/workflow/NoOpFunction.java
index 6d2c26c..8c76cc5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/workflow/NoOpFunction.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/workflow/NoOpFunction.java
@@ -15,7 +15,6 @@
 package com.google.gerrit.server.workflow;
 
 import com.google.gerrit.common.data.ApprovalType;
-import com.google.gerrit.server.CurrentUser;
 
 /** A function that does nothing. */
 public class NoOpFunction extends CategoryFunction {
@@ -24,10 +23,4 @@
   @Override
   public void run(final ApprovalType at, final FunctionState state) {
   }
-
-  @Override
-  public boolean isValid(final CurrentUser user, final ApprovalType at,
-      final FunctionState state) {
-    return false;
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/workflow/SubmitFunction.java b/gerrit-server/src/main/java/com/google/gerrit/server/workflow/SubmitFunction.java
deleted file mode 100644
index f0a00ff..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/workflow/SubmitFunction.java
+++ /dev/null
@@ -1,68 +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.workflow;
-
-import com.google.gerrit.common.data.ApprovalType;
-import com.google.gerrit.reviewdb.ApprovalCategory;
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.RefRight;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.project.RefControl;
-
-/**
- * Computes if the submit function can be used.
- * <p>
- * In order to be considered "approved" this function requires that all approval
- * categories with a position >= 0 (that is any whose
- * {@link ApprovalCategory#isAction()} method returns false) is valid and that
- * the change state be {@link Change.Status#NEW}.
- * <p>
- * This is mostly useful for actions, like {@link ApprovalCategory#SUBMIT}.
- */
-public class SubmitFunction extends CategoryFunction {
-  public static String NAME = "Submit";
-
-  @Override
-  public void run(final ApprovalType at, final FunctionState state) {
-    state.valid(at, valid(at, state));
-  }
-
-  @Override
-  public boolean isValid(final CurrentUser user, final ApprovalType at,
-      final FunctionState state) {
-    if (valid(at, state)) {
-      RefControl rc = state.controlFor(user);
-      for (final RefRight pr : rc.getApplicableRights(at.getCategory().getId())) {
-        if (user.getEffectiveGroups().contains(pr.getAccountGroupId())
-            && pr.getMaxValue() > 0) {
-          return true;
-        }
-      }
-    }
-    return false;
-  }
-
-  private static boolean valid(final ApprovalType at, final FunctionState state) {
-    if (state.getChange().getStatus() != Change.Status.NEW) {
-      return false;
-    }
-    for (final ApprovalType t : state.getApprovalTypes()) {
-      if (!state.isValid(t)) {
-        return false;
-      }
-    }
-    return true;
-  }
-}
diff --git a/gerrit-server/src/main/java/gerrit/AbstractCommitUserIdentityPredicate.java b/gerrit-server/src/main/java/gerrit/AbstractCommitUserIdentityPredicate.java
new file mode 100644
index 0000000..ac74147
--- /dev/null
+++ b/gerrit-server/src/main/java/gerrit/AbstractCommitUserIdentityPredicate.java
@@ -0,0 +1,79 @@
+// 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 gerrit;
+
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.UserIdentity;
+
+import com.googlecode.prolog_cafe.lang.IntegerTerm;
+import com.googlecode.prolog_cafe.lang.Operation;
+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.StructureTerm;
+import com.googlecode.prolog_cafe.lang.SymbolTerm;
+import com.googlecode.prolog_cafe.lang.Term;
+
+abstract class AbstractCommitUserIdentityPredicate extends Predicate.P3 {
+  private static final long serialVersionUID = 1L;
+  private static final SymbolTerm user = SymbolTerm.intern("user", 1);
+  private static final SymbolTerm anonymous = SymbolTerm.intern("anonymous");
+
+  AbstractCommitUserIdentityPredicate(Term a1, Term a2, Term a3, Operation n) {
+    arg1 = a1;
+    arg2 = a2;
+    arg3 = a3;
+    cont = n;
+  }
+
+  protected Operation exec(Prolog engine, UserIdentity userId) throws PrologException {
+    engine.setB0();
+    Term a1 = arg1.dereference();
+    Term a2 = arg2.dereference();
+    Term a3 = arg3.dereference();
+
+    Term idTerm;
+    Term nameTerm = Prolog.Nil;
+    Term emailTerm = Prolog.Nil;
+
+    Account.Id id = userId.getAccount();
+    if (id == null) {
+      idTerm = anonymous;
+    } else {
+      idTerm = new IntegerTerm(id.get());
+    }
+
+    String name = userId.getName();
+    if (name != null && !name.equals("")) {
+      nameTerm = SymbolTerm.create(name);
+    }
+
+    String email = userId.getEmail();
+    if (email != null && !email.equals("")) {
+      emailTerm = SymbolTerm.create(email);
+    }
+
+    if (!a1.unify(new StructureTerm(user, idTerm), engine.trail)) {
+      return engine.fail();
+    }
+    if (!a2.unify(nameTerm, engine.trail)) {
+      return engine.fail();
+    }
+    if (!a3.unify(emailTerm, engine.trail)) {
+      return engine.fail();
+    }
+    return cont;
+  }
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..c760426
--- /dev/null
+++ b/gerrit-server/src/main/java/gerrit/PRED__load_commit_labels_1.java
@@ -0,0 +1,82 @@
+// Copyright 2011 Google Inc. All Rights Reserved.
+
+package gerrit;
+
+import com.google.gerrit.common.data.ApprovalType;
+import com.google.gerrit.common.data.ApprovalTypes;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.rules.PrologEnvironment;
+import com.google.gerrit.rules.StoredValues;
+import com.google.gwtorm.server.OrmException;
+
+import com.googlecode.prolog_cafe.lang.IntegerTerm;
+import com.googlecode.prolog_cafe.lang.JavaException;
+import com.googlecode.prolog_cafe.lang.ListTerm;
+import com.googlecode.prolog_cafe.lang.Operation;
+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.StructureTerm;
+import com.googlecode.prolog_cafe.lang.SymbolTerm;
+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 {
+  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) {
+    arg1 = a1;
+    cont = n;
+  }
+
+  @Override
+  public Operation exec(Prolog engine) throws PrologException {
+    engine.setB0();
+    Term a1 = arg1.dereference();
+
+    Term listHead = Prolog.Nil;
+    try {
+      PrologEnvironment env = (PrologEnvironment) engine.control;
+      ReviewDb db = StoredValues.REVIEW_DB.get(engine);
+      PatchSet.Id patchSetId = StoredValues.PATCH_SET_ID.get(engine);
+      ApprovalTypes types = env.getInjector().getInstance(ApprovalTypes.class);
+
+      for (PatchSetApproval a : db.patchSetApprovals().byPatchSet(patchSetId)) {
+        if (a.getValue() == 0) {
+          continue;
+        }
+
+        ApprovalType t = types.byId(a.getCategoryId());
+        if (t == null) {
+          continue;
+        }
+
+        StructureTerm labelTerm = new StructureTerm(
+            sym_label,
+            SymbolTerm.intern(t.getCategory().getLabelName()),
+            new IntegerTerm(a.getValue()));
+
+        StructureTerm userTerm = new StructureTerm(
+            sym_user,
+            new IntegerTerm(a.getAccountId().get()));
+
+        listHead = new ListTerm(
+            new StructureTerm(sym_commit_label, labelTerm, userTerm),
+            listHead);
+      }
+    } catch (OrmException err) {
+      throw new JavaException(this, 1, err);
+    }
+
+    if (!a1.unify(listHead, engine.trail)) {
+      return engine.fail();
+    }
+    return cont;
+  }
+}
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
new file mode 100644
index 0000000..68f9bf6
--- /dev/null
+++ b/gerrit-server/src/main/java/gerrit/PRED__user_label_range_4.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 gerrit;
+
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.common.data.PermissionRange;
+import com.google.gerrit.rules.StoredValues;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.project.ChangeControl;
+
+import com.googlecode.prolog_cafe.lang.IllegalTypeException;
+import com.googlecode.prolog_cafe.lang.IntegerTerm;
+import com.googlecode.prolog_cafe.lang.JavaObjectTerm;
+import com.googlecode.prolog_cafe.lang.Operation;
+import com.googlecode.prolog_cafe.lang.PInstantiationException;
+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.Term;
+
+/**
+ * Resolves the valid range for a label on a CurrentUser.
+ *
+ * <pre>
+ *   '$user_label_range'(+Label, +CurrentUser, -Min, -Max)
+ * </pre>
+ */
+class PRED__user_label_range_4 extends Predicate.P4 {
+  private static final long serialVersionUID = 1L;
+
+  PRED__user_label_range_4(Term a1, Term a2, Term a3, Term a4, Operation n) {
+    arg1 = a1;
+    arg2 = a2;
+    arg3 = a3;
+    arg4 = a4;
+    cont = n;
+  }
+
+  @Override
+  public Operation exec(Prolog engine) throws PrologException {
+    engine.setB0();
+    Term a1 = arg1.dereference();
+    Term a2 = arg2.dereference();
+    Term a3 = arg3.dereference();
+    Term a4 = arg4.dereference();
+
+    if (a1.isVariable()) {
+      throw new PInstantiationException(this, 1);
+    }
+    if (!a1.isSymbol()) {
+      throw new IllegalTypeException(this, 1, "atom", a1);
+    }
+    String label = a1.name();
+
+    if (a2.isVariable()) {
+      throw new PInstantiationException(this, 2);
+    }
+    if (!a2.isJavaObject() || !a2.convertible(CurrentUser.class)) {
+      throw new IllegalTypeException(this, 2, "CurrentUser)", a2);
+    }
+    CurrentUser user = (CurrentUser) ((JavaObjectTerm) a2).object();
+
+    ChangeControl ctl = StoredValues.CHANGE_CONTROL.get(engine).forUser(user);
+    PermissionRange range = ctl.getRange(Permission.LABEL + label);
+    if (range == null) {
+      return engine.fail();
+    }
+
+    IntegerTerm min = new IntegerTerm(range.getMin());
+    IntegerTerm max = new IntegerTerm(range.getMax());
+
+    if (!a3.unify(min, engine.trail)) {
+      return engine.fail();
+    }
+
+    if (!a4.unify(max, engine.trail)) {
+      return engine.fail();
+    }
+
+    return cont;
+  }
+}
diff --git a/gerrit-server/src/main/java/gerrit/PRED_change_branch_1.java b/gerrit-server/src/main/java/gerrit/PRED_change_branch_1.java
new file mode 100644
index 0000000..7cc9f35
--- /dev/null
+++ b/gerrit-server/src/main/java/gerrit/PRED_change_branch_1.java
@@ -0,0 +1,49 @@
+// 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 gerrit;
+
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.rules.StoredValues;
+
+import com.googlecode.prolog_cafe.lang.Operation;
+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.SymbolTerm;
+import com.googlecode.prolog_cafe.lang.Term;
+
+public class PRED_change_branch_1 extends Predicate.P1 {
+  private static final long serialVersionUID = 1L;
+
+  public PRED_change_branch_1(Term a1, Operation n) {
+    arg1 = a1;
+    cont = n;
+  }
+
+  @Override
+  public Operation exec(Prolog engine) throws PrologException {
+    engine.setB0();
+    Term a1 = arg1.dereference();
+
+    Change change = StoredValues.CHANGE.get(engine);
+    Branch.NameKey name = change.getDest();
+
+    if (!a1.unify(SymbolTerm.create(name.get()), engine.trail)) {
+      return engine.fail();
+    }
+    return cont;
+  }
+}
\ No newline at end of file
diff --git a/gerrit-server/src/main/java/gerrit/PRED_change_owner_1.java b/gerrit-server/src/main/java/gerrit/PRED_change_owner_1.java
new file mode 100644
index 0000000..09be902
--- /dev/null
+++ b/gerrit-server/src/main/java/gerrit/PRED_change_owner_1.java
@@ -0,0 +1,52 @@
+// 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 gerrit;
+
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.rules.StoredValues;
+
+import com.googlecode.prolog_cafe.lang.IntegerTerm;
+import com.googlecode.prolog_cafe.lang.Operation;
+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.StructureTerm;
+import com.googlecode.prolog_cafe.lang.SymbolTerm;
+import com.googlecode.prolog_cafe.lang.Term;
+
+public class PRED_change_owner_1 extends Predicate.P1 {
+  private static final long serialVersionUID = 1L;
+  private static final SymbolTerm user = SymbolTerm.intern("user", 1);
+
+  public PRED_change_owner_1(Term a1, Operation n) {
+    arg1 = a1;
+    cont = n;
+  }
+
+  @Override
+  public Operation exec(Prolog engine) throws PrologException {
+    engine.setB0();
+    Term a1 = arg1.dereference();
+
+    Change change = StoredValues.CHANGE.get(engine);
+    Account.Id ownerId = change.getOwner();
+
+    if (!a1.unify(new StructureTerm(user, new IntegerTerm(ownerId.get())), engine.trail)) {
+      return engine.fail();
+    }
+    return cont;
+  }
+}
\ No newline at end of file
diff --git a/gerrit-server/src/main/java/gerrit/PRED_change_project_1.java b/gerrit-server/src/main/java/gerrit/PRED_change_project_1.java
new file mode 100644
index 0000000..d1cd20d
--- /dev/null
+++ b/gerrit-server/src/main/java/gerrit/PRED_change_project_1.java
@@ -0,0 +1,49 @@
+// 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 gerrit;
+
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.rules.StoredValues;
+
+import com.googlecode.prolog_cafe.lang.Operation;
+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.SymbolTerm;
+import com.googlecode.prolog_cafe.lang.Term;
+
+public class PRED_change_project_1 extends Predicate.P1 {
+  private static final long serialVersionUID = 1L;
+
+  public PRED_change_project_1(Term a1, Operation n) {
+    arg1 = a1;
+    cont = n;
+  }
+
+  @Override
+  public Operation exec(Prolog engine) throws PrologException {
+    engine.setB0();
+    Term a1 = arg1.dereference();
+
+    Change change = StoredValues.CHANGE.get(engine);
+    Project.NameKey name = change.getProject();
+
+    if (!a1.unify(SymbolTerm.create(name.get()), engine.trail)) {
+      return engine.fail();
+    }
+    return cont;
+  }
+}
\ No newline at end of file
diff --git a/gerrit-server/src/main/java/gerrit/PRED_change_topic_1.java b/gerrit-server/src/main/java/gerrit/PRED_change_topic_1.java
new file mode 100644
index 0000000..d200f7e
--- /dev/null
+++ b/gerrit-server/src/main/java/gerrit/PRED_change_topic_1.java
@@ -0,0 +1,52 @@
+// 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 gerrit;
+
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.rules.StoredValues;
+
+import com.googlecode.prolog_cafe.lang.Operation;
+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.SymbolTerm;
+import com.googlecode.prolog_cafe.lang.Term;
+
+public class PRED_change_topic_1 extends Predicate.P1 {
+  private static final long serialVersionUID = 1L;
+
+  public PRED_change_topic_1(Term a1, Operation n) {
+    arg1 = a1;
+    cont = n;
+  }
+
+  @Override
+  public Operation exec(Prolog engine) throws PrologException {
+    engine.setB0();
+    Term a1 = arg1.dereference();
+
+    Term topicTerm = Prolog.Nil;
+    Change change = StoredValues.CHANGE.get(engine);
+    String topic = change.getTopic();
+    if (topic != null) {
+      topicTerm = SymbolTerm.create(topic);
+    }
+
+    if (!a1.unify(topicTerm, engine.trail)) {
+      return engine.fail();
+    }
+    return cont;
+  }
+}
\ No newline at end of file
diff --git a/gerrit-server/src/main/java/gerrit/PRED_commit_author_3.java b/gerrit-server/src/main/java/gerrit/PRED_commit_author_3.java
new file mode 100644
index 0000000..a0817a1
--- /dev/null
+++ b/gerrit-server/src/main/java/gerrit/PRED_commit_author_3.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 gerrit;
+
+import com.google.gerrit.reviewdb.client.PatchSetInfo;
+import com.google.gerrit.reviewdb.client.UserIdentity;
+import com.google.gerrit.rules.StoredValues;
+
+import com.googlecode.prolog_cafe.lang.Operation;
+import com.googlecode.prolog_cafe.lang.Prolog;
+import com.googlecode.prolog_cafe.lang.PrologException;
+import com.googlecode.prolog_cafe.lang.Term;
+
+public class PRED_commit_author_3 extends AbstractCommitUserIdentityPredicate {
+  private static final long serialVersionUID = 1L;
+
+  public PRED_commit_author_3(Term a1, Term a2, Term a3, Operation n) {
+    super(a1, a2, a3, n);
+  }
+
+  @Override
+  public Operation exec(Prolog engine) throws PrologException {
+    PatchSetInfo psInfo = StoredValues.PATCH_SET_INFO.get(engine);
+    UserIdentity author = psInfo.getAuthor();
+    return exec(engine, author);
+  }
+}
\ No newline at end of file
diff --git a/gerrit-server/src/main/java/gerrit/PRED_commit_committer_3.java b/gerrit-server/src/main/java/gerrit/PRED_commit_committer_3.java
new file mode 100644
index 0000000..78e6cf7
--- /dev/null
+++ b/gerrit-server/src/main/java/gerrit/PRED_commit_committer_3.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 gerrit;
+
+import com.google.gerrit.reviewdb.client.PatchSetInfo;
+import com.google.gerrit.reviewdb.client.UserIdentity;
+import com.google.gerrit.rules.StoredValues;
+
+import com.googlecode.prolog_cafe.lang.Operation;
+import com.googlecode.prolog_cafe.lang.Prolog;
+import com.googlecode.prolog_cafe.lang.PrologException;
+import com.googlecode.prolog_cafe.lang.Term;
+
+public class PRED_commit_committer_3 extends AbstractCommitUserIdentityPredicate {
+  private static final long serialVersionUID = 1L;
+
+  public PRED_commit_committer_3(Term a1, Term a2, Term a3, Operation n) {
+    super(a1, a2, a3, n);
+  }
+
+  @Override
+  public Operation exec(Prolog engine) throws PrologException {
+    PatchSetInfo psInfo = StoredValues.PATCH_SET_INFO.get(engine);
+    UserIdentity committer = psInfo.getCommitter();
+    return exec(engine, committer);
+  }
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..c2c2d1c
--- /dev/null
+++ b/gerrit-server/src/main/java/gerrit/PRED_commit_delta_4.java
@@ -0,0 +1,176 @@
+// 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 gerrit;
+
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.rules.StoredValues;
+import com.google.gerrit.server.patch.PatchList;
+import com.google.gerrit.server.patch.PatchListEntry;
+
+import com.googlecode.prolog_cafe.lang.IllegalTypeException;
+import com.googlecode.prolog_cafe.lang.JavaObjectTerm;
+import com.googlecode.prolog_cafe.lang.Operation;
+import com.googlecode.prolog_cafe.lang.PInstantiationException;
+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.SymbolTerm;
+import com.googlecode.prolog_cafe.lang.Term;
+
+import java.util.Iterator;
+import java.util.regex.Pattern;
+
+/**
+ * Given a regular expression, checks it against the file list in the most
+ * recent patchset of a change. For all files that match the regex, returns the
+ * (new) path of the file, the change type, and the old path of the file if
+ * applicable (if the file was copied or renamed).
+ *
+ * <pre>
+ *   'commit_delta'(+Regex, -ChangeType, -NewPath, -OldPath)
+ * </pre>
+ */
+public class PRED_commit_delta_4 extends Predicate.P4 {
+  private static final long serialVersionUID = 1L;
+  private static final SymbolTerm add = SymbolTerm.intern("add");
+  private static final SymbolTerm modify = SymbolTerm.intern("modify");
+  private static final SymbolTerm delete = SymbolTerm.intern("delete");
+  private static final SymbolTerm rename = SymbolTerm.intern("rename");
+  private static final SymbolTerm copy = SymbolTerm.intern("copy");
+  static final Operation commit_delta_check = new PRED_commit_delta_check();
+  static final Operation commit_delta_next = new PRED_commit_delta_next();
+  static final Operation commit_delta_empty = new PRED_commit_delta_empty();
+
+  public PRED_commit_delta_4(Term a1, Term a2, Term a3, Term a4, Operation n) {
+    arg1 = a1;
+    arg2 = a2;
+    arg3 = a3;
+    arg4 = a4;
+    cont = n;
+  }
+
+  @Override
+  public Operation exec(Prolog engine) throws PrologException {
+    engine.cont = cont;
+    engine.setB0();
+
+    Term a1 = arg1.dereference();
+    if (a1.isVariable()) {
+      throw new PInstantiationException(this, 1);
+    }
+    if (!a1.isSymbol()) {
+      throw new IllegalTypeException(this, 1, "symbol", a1);
+    }
+    Pattern regex = Pattern.compile(a1.name());
+    engine.areg1 = new JavaObjectTerm(regex);
+    engine.areg2 = arg2;
+    engine.areg3 = arg3;
+    engine.areg4 = arg4;
+
+    PatchList pl = StoredValues.PATCH_LIST.get(engine);
+    Iterator<PatchListEntry> iter = pl.getPatches().iterator();
+
+    engine.areg5 = new JavaObjectTerm(iter);
+
+    return engine.jtry5(commit_delta_check, commit_delta_next);
+  }
+
+  private static final class PRED_commit_delta_check extends Operation {
+    @Override
+    public Operation exec(Prolog engine) {
+      Term a1 = engine.areg1;
+      Term a2 = engine.areg2;
+      Term a3 = engine.areg3;
+      Term a4 = engine.areg4;
+      Term a5 = engine.areg5;
+
+      Pattern regex = (Pattern)((JavaObjectTerm)a1).object();
+      @SuppressWarnings("unchecked")
+      Iterator<PatchListEntry> iter =
+        (Iterator<PatchListEntry>)((JavaObjectTerm)a5).object();
+      while (iter.hasNext()) {
+        PatchListEntry patch = iter.next();
+        String newName = patch.getNewName();
+        String oldName = patch.getOldName();
+        Patch.ChangeType changeType = patch.getChangeType();
+
+        if (newName.equals("/COMMIT_MSG")) {
+          continue;
+        }
+
+        if (regex.matcher(newName).matches() ||
+            (oldName != null && regex.matcher(oldName).matches())) {
+          SymbolTerm changeSym = getTypeSymbol(changeType);
+          SymbolTerm newSym = SymbolTerm.create(newName);
+          SymbolTerm oldSym = Prolog.Nil;
+          if (oldName != null) {
+            oldSym = SymbolTerm.create(oldName);
+          }
+
+          if (!a2.unify(changeSym, engine.trail)) {
+            continue;
+          }
+          if (!a3.unify(newSym, engine.trail)) {
+            continue;
+          }
+          if (!a4.unify(oldSym, engine.trail)) {
+            continue;
+          }
+          return engine.cont;
+        }
+      }
+      return engine.fail();
+    }
+  }
+
+  private static final class PRED_commit_delta_next extends Operation {
+    @Override
+    public Operation exec(Prolog engine) {
+      return engine.trust(commit_delta_empty);
+    }
+  }
+
+  private static final class PRED_commit_delta_empty extends Operation {
+    @Override
+    public Operation exec(Prolog engine) {
+      Term a5 = engine.areg5;
+
+      @SuppressWarnings("unchecked")
+      Iterator<PatchListEntry> iter =
+        (Iterator<PatchListEntry>)((JavaObjectTerm)a5).object();
+      if (!iter.hasNext()) {
+        return engine.fail();
+      }
+
+      return engine.jtry5(commit_delta_check, commit_delta_next);
+    }
+  }
+
+  private static SymbolTerm getTypeSymbol(Patch.ChangeType type) {
+    switch (type) {
+      case ADDED:
+        return add;
+      case MODIFIED:
+        return modify;
+      case DELETED:
+        return delete;
+      case RENAMED:
+        return rename;
+      case COPIED:
+        return copy;
+    }
+    throw new IllegalArgumentException("ChangeType not recognized");
+  }
+}
\ No newline at end of file
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
new file mode 100644
index 0000000..f0accf0
--- /dev/null
+++ b/gerrit-server/src/main/java/gerrit/PRED_commit_edits_2.java
@@ -0,0 +1,166 @@
+// 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 gerrit;
+
+import com.google.gerrit.rules.StoredValues;
+import com.google.gerrit.server.patch.PatchList;
+import com.google.gerrit.server.patch.PatchListEntry;
+import com.google.gerrit.server.patch.Text;
+
+import com.googlecode.prolog_cafe.lang.IllegalTypeException;
+import com.googlecode.prolog_cafe.lang.JavaException;
+import com.googlecode.prolog_cafe.lang.Operation;
+import com.googlecode.prolog_cafe.lang.PInstantiationException;
+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.Term;
+
+import org.eclipse.jgit.diff.Edit;
+import org.eclipse.jgit.errors.CorruptObjectException;
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectReader;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevTree;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.treewalk.TreeWalk;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.regex.Pattern;
+
+/**
+ * Returns true if any of the files that match FileNameRegex have edited lines
+ * that match EditRegex
+ *
+ * <pre>
+ *   'commit_edits'(+FileNameRegex, +EditRegex)
+ * </pre>
+ */
+public class PRED_commit_edits_2 extends Predicate.P2 {
+  private static final long serialVersionUID = 1L;
+
+  public PRED_commit_edits_2(Term a1, Term a2, Operation n) {
+    arg1 = a1;
+    arg2 = a2;
+    cont = n;
+  }
+
+  @Override
+  public Operation exec(Prolog engine) throws PrologException {
+    engine.setB0();
+
+    Term a1 = arg1.dereference();
+    Term a2 = arg2.dereference();
+
+    Pattern fileRegex = getRegexParameter(a1);
+    Pattern editRegex = getRegexParameter(a2);
+
+    PatchList pl = StoredValues.PATCH_LIST.get(engine);
+    Repository repo = StoredValues.REPOSITORY.get(engine);
+
+    final ObjectReader reader = repo.newObjectReader();
+    final RevTree aTree;
+    final RevTree bTree;
+    try {
+      final RevWalk rw = new RevWalk(reader);
+      final RevCommit bCommit = rw.parseCommit(pl.getNewId());
+
+      if (pl.getOldId() != null) {
+        aTree = rw.parseTree(pl.getOldId());
+      } else {
+        // Octopus merge with unknown automatic merge result, since the
+        // web UI returns no files to match against, just fail.
+        return engine.fail();
+      }
+      bTree = bCommit.getTree();
+
+      for (PatchListEntry entry : pl.getPatches()) {
+        String newName = entry.getNewName();
+        String oldName = entry.getOldName();
+
+        if (newName.equals("/COMMIT_MSG")) {
+          continue;
+        }
+
+        if (fileRegex.matcher(newName).find() ||
+            (oldName != null && fileRegex.matcher(oldName).find())) {
+          List<Edit> edits = entry.getEdits();
+
+          if (edits.isEmpty()) {
+            continue;
+          }
+          Text tA;
+          if (oldName != null) {
+            tA = load(aTree, oldName, reader);
+          } else {
+            tA = load(aTree, newName, reader);
+          }
+          Text tB = load(bTree, newName, reader);
+          for (Edit edit : edits) {
+            if (tA != Text.EMPTY) {
+              String aDiff = tA.getString(edit.getBeginA(), edit.getEndA(), true);
+              if (editRegex.matcher(aDiff).find()) {
+                return cont;
+              }
+            }
+            if (tB != Text.EMPTY) {
+              String bDiff = tB.getString(edit.getBeginB(), edit.getEndB(), true);
+              if (editRegex.matcher(bDiff).find()) {
+                return cont;
+              }
+            }
+          }
+        }
+      }
+    } catch (IOException err) {
+      throw new JavaException(this, 1, err);
+    } finally {
+      reader.release();
+    }
+
+    return engine.fail();
+  }
+
+  private Pattern getRegexParameter(Term term) {
+    if (term.isVariable()) {
+      throw new PInstantiationException(this, 1);
+    }
+    if (!term.isSymbol()) {
+      throw new IllegalTypeException(this, 1, "symbol", term);
+    }
+    return Pattern.compile(term.name(), Pattern.MULTILINE);
+  }
+
+  private Text load(final ObjectId tree, final String path, final ObjectReader reader)
+      throws MissingObjectException, IncorrectObjectTypeException,
+      CorruptObjectException, IOException {
+    if (path == null) {
+      return Text.EMPTY;
+    }
+    final TreeWalk tw = TreeWalk.forPath(reader, path, tree);
+    if (tw == null) {
+      return Text.EMPTY;
+    }
+    if (tw.getFileMode(0).getObjectType() != Constants.OBJ_BLOB) {
+      return Text.EMPTY;
+    }
+    return new Text(reader.open(tw.getObjectId(0), Constants.OBJ_BLOB));
+  }
+}
\ No newline at end of file
diff --git a/gerrit-server/src/main/java/gerrit/PRED_commit_message_1.java b/gerrit-server/src/main/java/gerrit/PRED_commit_message_1.java
new file mode 100644
index 0000000..0ed7443
--- /dev/null
+++ b/gerrit-server/src/main/java/gerrit/PRED_commit_message_1.java
@@ -0,0 +1,55 @@
+// 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 gerrit;
+
+import com.google.gerrit.reviewdb.client.PatchSetInfo;
+import com.google.gerrit.rules.StoredValues;
+
+import com.googlecode.prolog_cafe.lang.Operation;
+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.SymbolTerm;
+import com.googlecode.prolog_cafe.lang.Term;
+
+/**
+ * Returns the commit message as a symbol
+ *
+ * <pre>
+ *   'commit_message'(-Msg)
+ * </pre>
+ */
+public class PRED_commit_message_1 extends Predicate.P1 {
+  private static final long serialVersionUID = 1L;
+
+  public PRED_commit_message_1(Term a1, Operation n) {
+    arg1 = a1;
+    cont = n;
+  }
+
+  @Override
+  public Operation exec(Prolog engine) throws PrologException {
+    engine.setB0();
+    Term a1 = arg1.dereference();
+
+    PatchSetInfo psInfo = StoredValues.PATCH_SET_INFO.get(engine);
+
+    SymbolTerm msg = SymbolTerm.create(psInfo.getMessage());
+    if (!a1.unify(msg, engine.trail)) {
+      return engine.fail();
+    }
+    return cont;
+  }
+}
\ No newline at end of file
diff --git a/gerrit-server/src/main/java/gerrit/PRED_current_user_1.java b/gerrit-server/src/main/java/gerrit/PRED_current_user_1.java
new file mode 100644
index 0000000..23cedce
--- /dev/null
+++ b/gerrit-server/src/main/java/gerrit/PRED_current_user_1.java
@@ -0,0 +1,75 @@
+// 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 gerrit;
+
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.rules.StoredValues;
+import com.google.gerrit.server.AnonymousUser;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.PeerDaemonUser;
+import com.google.gerrit.server.ReplicationUser;
+import com.google.gerrit.server.project.ChangeControl;
+
+import com.googlecode.prolog_cafe.lang.EvaluationException;
+import com.googlecode.prolog_cafe.lang.IntegerTerm;
+import com.googlecode.prolog_cafe.lang.Operation;
+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.StructureTerm;
+import com.googlecode.prolog_cafe.lang.SymbolTerm;
+import com.googlecode.prolog_cafe.lang.Term;
+
+public class PRED_current_user_1 extends Predicate.P1 {
+  private static final long serialVersionUID = 1L;
+  private static final SymbolTerm user = SymbolTerm.intern("user", 1);
+  private static final SymbolTerm anonymous = SymbolTerm.intern("anonymous");
+  private static final SymbolTerm peerDaemon = SymbolTerm.intern("peer_daemon");
+  private static final SymbolTerm replication = SymbolTerm.intern("replication");
+
+  public PRED_current_user_1(Term a1, Operation n) {
+    arg1 = a1;
+    cont = n;
+  }
+
+  @Override
+  public Operation exec(Prolog engine) throws PrologException {
+    engine.setB0();
+    Term a1 = arg1.dereference();
+
+    ChangeControl cControl = StoredValues.CHANGE_CONTROL.get(engine);
+    CurrentUser curUser = cControl.getCurrentUser();
+    Term resultTerm;
+
+    if (curUser instanceof IdentifiedUser) {
+      Account.Id id = ((IdentifiedUser)curUser).getAccountId();
+      resultTerm = new IntegerTerm(id.get());
+    } else if (curUser instanceof AnonymousUser) {
+      resultTerm = anonymous;
+    } else if (curUser instanceof PeerDaemonUser) {
+      resultTerm = peerDaemon;
+    } else if (curUser instanceof ReplicationUser) {
+      resultTerm = replication;
+    } else {
+      throw new EvaluationException("Unknown user type");
+    }
+
+    if (!a1.unify(new StructureTerm(user, resultTerm), engine.trail)) {
+      return engine.fail();
+    }
+    return cont;
+  }
+}
\ No newline at end of file
diff --git a/gerrit-server/src/main/java/gerrit/PRED_current_user_2.java b/gerrit-server/src/main/java/gerrit/PRED_current_user_2.java
new file mode 100644
index 0000000..0a15608
--- /dev/null
+++ b/gerrit-server/src/main/java/gerrit/PRED_current_user_2.java
@@ -0,0 +1,155 @@
+// 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 gerrit;
+
+import static com.googlecode.prolog_cafe.lang.SymbolTerm.intern;
+
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.rules.PrologEnvironment;
+import com.google.gerrit.rules.StoredValues;
+import com.google.gerrit.server.AnonymousUser;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.inject.Provider;
+
+import com.googlecode.prolog_cafe.lang.HashtableOfTerm;
+import com.googlecode.prolog_cafe.lang.IllegalTypeException;
+import com.googlecode.prolog_cafe.lang.IntegerTerm;
+import com.googlecode.prolog_cafe.lang.InternalException;
+import com.googlecode.prolog_cafe.lang.JavaObjectTerm;
+import com.googlecode.prolog_cafe.lang.Operation;
+import com.googlecode.prolog_cafe.lang.PInstantiationException;
+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.StructureTerm;
+import com.googlecode.prolog_cafe.lang.SymbolTerm;
+import com.googlecode.prolog_cafe.lang.Term;
+
+/**
+ * Loads a CurrentUser object for a user identity.
+ * <p>
+ * Values are cached in the hash {@code current_user}, avoiding recreation
+ * during a single evaluation.
+ *
+ * <pre>
+ *   current_user(user(+AccountId), -CurrentUser).
+ * </pre>
+ */
+class PRED_current_user_2 extends Predicate.P2 {
+  private static final long serialVersionUID = 1L;
+  private static final SymbolTerm user = intern("user", 1);
+  private static final SymbolTerm anonymous = intern("anonymous");
+  private static final SymbolTerm current_user = intern("current_user");
+
+  PRED_current_user_2(Term a1, Term a2, Operation n) {
+    arg1 = a1;
+    arg2 = a2;
+    cont = n;
+  }
+
+  @Override
+  public Operation exec(Prolog engine) throws PrologException {
+    engine.setB0();
+    Term a1 = arg1.dereference();
+    Term a2 = arg2.dereference();
+
+    if (a1.isVariable()) {
+      throw new PInstantiationException(this, 1);
+    }
+
+    HashtableOfTerm userHash = userHash(engine);
+    Term userTerm = userHash.get(a1);
+    if (userTerm != null && userTerm.isJavaObject()) {
+      if (!(((JavaObjectTerm) userTerm).object() instanceof CurrentUser)) {
+        userTerm = createUser(engine, a1, userHash);
+      }
+    } else {
+      userTerm = createUser(engine, a1, userHash);
+    }
+
+    if (!a2.unify(userTerm, engine.trail)) {
+      return engine.fail();
+    }
+
+    return cont;
+  }
+
+  public Term createUser(Prolog engine, Term key, HashtableOfTerm userHash) {
+    if (!key.isStructure()
+        || key.arity() != 1
+        || !((StructureTerm) key).functor().equals(user)) {
+      throw new IllegalTypeException(this, 1, "user(int)", key);
+    }
+
+    Term idTerm = key.arg(0);
+    CurrentUser user;
+    if (idTerm.isInteger()) {
+      Account.Id accountId = new Account.Id(((IntegerTerm) idTerm).intValue());
+
+      final ReviewDb db = StoredValues.REVIEW_DB.getOrNull(engine);
+      IdentifiedUser.GenericFactory userFactory = userFactory(engine);
+      if (db != null) {
+        user = userFactory.create(new Provider<ReviewDb>() {
+          public ReviewDb get() {
+            return db;
+          }
+        }, accountId);
+      } else {
+        user = userFactory.create(accountId);
+      }
+
+
+    } else if (idTerm.equals(anonymous)) {
+      user = anonymousUser(engine);
+
+    } else {
+      throw new IllegalTypeException(this, 1, "user(int)", key);
+    }
+
+    Term userTerm = new JavaObjectTerm(user);
+    userHash.put(key, userTerm);
+    return userTerm;
+  }
+
+  private static HashtableOfTerm userHash(Prolog engine) {
+    Term userHash = engine.getHashManager().get(current_user);
+    if (userHash == null) {
+      HashtableOfTerm users = new HashtableOfTerm();
+      engine.getHashManager().put(current_user, new JavaObjectTerm(userHash));
+      return users;
+    }
+
+    if (userHash.isJavaObject()) {
+      Object obj = ((JavaObjectTerm) userHash).object();
+      if (obj instanceof HashtableOfTerm) {
+        return (HashtableOfTerm) obj;
+      }
+    }
+
+    throw new InternalException(current_user + " is not HashtableOfTerm");
+  }
+
+  private static AnonymousUser anonymousUser(Prolog engine) {
+    PrologEnvironment env = (PrologEnvironment) engine.control;
+    return env.getInjector().getInstance(AnonymousUser.class);
+  }
+
+  private static IdentifiedUser.GenericFactory userFactory(Prolog engine) {
+    PrologEnvironment env = (PrologEnvironment) engine.control;
+    return env.getInjector().getInstance(IdentifiedUser.GenericFactory.class);
+  }
+}
diff --git a/gerrit-server/src/main/java/gerrit/PRED_get_legacy_approval_types_1.java b/gerrit-server/src/main/java/gerrit/PRED_get_legacy_approval_types_1.java
new file mode 100644
index 0000000..9399865
--- /dev/null
+++ b/gerrit-server/src/main/java/gerrit/PRED_get_legacy_approval_types_1.java
@@ -0,0 +1,84 @@
+// 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 gerrit;
+
+import com.google.gerrit.common.data.ApprovalType;
+import com.google.gerrit.common.data.ApprovalTypes;
+import com.google.gerrit.rules.PrologEnvironment;
+
+import com.googlecode.prolog_cafe.lang.IntegerTerm;
+import com.googlecode.prolog_cafe.lang.ListTerm;
+import com.googlecode.prolog_cafe.lang.Operation;
+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.StructureTerm;
+import com.googlecode.prolog_cafe.lang.SymbolTerm;
+import com.googlecode.prolog_cafe.lang.Term;
+
+import java.util.List;
+
+/**
+ * Obtain a list of approval types from the server configuration.
+ * <p>
+ * Unifies to a Prolog list of: {@code approval_type(Label, Id, Fun, Min, Max)}
+ * where:
+ * <ul>
+ * <li>{@code Label} - the newer style label name</li>
+ * <li>{@code Id} - the legacy ApprovalCategory.Id from the database</li>
+ * <li>{@code Fun} - legacy function name</li>
+ * <li>{@code Min, Max} - the smallest and largest configured values.</li>
+ * </ul>
+ */
+class PRED_get_legacy_approval_types_1 extends Predicate.P1 {
+  private static final long serialVersionUID = 1L;
+
+  PRED_get_legacy_approval_types_1(Term a1, Operation n) {
+    arg1 = a1;
+    cont = n;
+  }
+
+  @Override
+  public Operation exec(Prolog engine) throws PrologException {
+    engine.setB0();
+    Term a1 = arg1.dereference();
+
+    PrologEnvironment env = (PrologEnvironment) engine.control;
+    ApprovalTypes types = env.getInjector().getInstance(ApprovalTypes.class);
+
+    List<ApprovalType> list = types.getApprovalTypes();
+    Term head = Prolog.Nil;
+    for (int idx = list.size() - 1; 0 <= idx; idx--) {
+      head = new ListTerm(export(list.get(idx)), head);
+    }
+
+    if (!a1.unify(head, engine.trail)) {
+      return engine.fail();
+    }
+    return cont;
+  }
+
+  static final SymbolTerm symApprovalType = SymbolTerm.intern(
+      "approval_type", 5);
+
+  static Term export(ApprovalType type) {
+    return new StructureTerm(symApprovalType,
+        SymbolTerm.intern(type.getCategory().getLabelName()),
+        SymbolTerm.intern(type.getCategory().getId().get()),
+        SymbolTerm.intern(type.getCategory().getFunctionName()),
+        new IntegerTerm(type.getMin().getValue()),
+        new IntegerTerm(type.getMax().getValue()));
+  }
+}
diff --git a/gerrit-server/src/main/prolog/gerrit_common.pl b/gerrit-server/src/main/prolog/gerrit_common.pl
new file mode 100644
index 0000000..3313162
--- /dev/null
+++ b/gerrit-server/src/main/prolog/gerrit_common.pl
@@ -0,0 +1,405 @@
+%% 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 gerrit.
+'$init' :- init.
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%
+%% init:
+%%
+%%   Initialize the module's private state. These typically take the form of global
+%%   aliased hashes carrying "constant" data about the current change for any
+%%   predicate that needs to obtain it.
+%%
+init :-
+  define_hash(commit_labels),
+  define_hash(current_user).
+
+define_hash(A) :- hash_exists(A), !, hash_clear(A).
+define_hash(A) :- atom(A), !, new_hash(_, [alias(A)]).
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%
+%% commit_label/2:
+%%
+%% During rule evaluation of a change, this predicate is defined to
+%% be a table of labels that pertain to the commit of interest.
+%%
+%%   commit_label( label('Code-Review', 2), user(12345789) ).
+%%   commit_label( label('Verified', -1), user(8181) ).
+%%
+:- public commit_label/2.
+%%
+commit_label(L, User) :- L = label(H, _),
+  atom(H),
+  !,
+  hash_get(commit_labels, H, Cached),
+  ( [] == Cached ->
+    get_commit_labels(_),
+    hash_get(commit_labels, H, Rs), !
+    ;
+    Rs = Cached
+  ),
+  scan_commit_labels(Rs, L, User)
+  .
+commit_label(Label, User) :-
+  get_commit_labels(Rs),
+  scan_commit_labels(Rs, Label, User).
+
+scan_commit_labels([R | Rs], L, U) :- R = commit_label(L, U).
+scan_commit_labels([_ | Rs], L, U) :- scan_commit_labels(Rs, L, U).
+scan_commit_labels([], _, _) :- fail.
+
+get_commit_labels(Rs) :-
+  hash_contains_key(commit_labels, '$all'),
+  !,
+  hash_get(commit_labels, '$all', Rs)
+  .
+get_commit_labels(Rs) :-
+  '_load_commit_labels'(Rs),
+  set_commit_labels(Rs).
+
+set_commit_labels(Rs) :-
+  define_hash(commit_labels),
+  hash_put(commit_labels, '$all', Rs),
+  index_commit_labels(Rs).
+
+index_commit_labels([]).
+index_commit_labels([R | Rs]) :-
+  R = commit_label(label(H, _), _),
+  atom(H),
+  !,
+  hash_get(commit_labels, H, Tmp),
+  hash_put(commit_labels, H, [R | Tmp]),
+  index_commit_labels(Rs)
+  .
+index_commit_labels([_ | Rs]) :-
+  index_commit_labels(Rs).
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%
+%% user_label_range/4:
+%%
+%%   Lookup the range allowed to be used.
+%%
+user_label_range(Label, Who, Min, Max) :-
+  Who = user(_), !,
+  atom(Label),
+  current_user(Who, User),
+  '_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)), _)
+  .
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%
+%% not_same/2:
+%%
+:- public not_same/2.
+%%
+not_same(ok(A), ok(B)) :- !, A \= B.
+not_same(label(_, ok(A)), label(_, ok(B))) :- !, A \= B.
+not_same(_, _).
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%
+%% can_submit/2:
+%%
+%%   Executes the SubmitRule for each solution until one where all of the
+%%   states has the format label(_, ok(_)) is found, then cut away any
+%%   remaining choice points leaving this as the last solution.
+%%
+:- public can_submit/2.
+%%
+can_submit(SubmitRule, S) :-
+  call_submit_rule(SubmitRule, Tmp),
+  Tmp =.. [submit | Ls],
+  ( is_all_ok(Ls) -> S = ok(Tmp), ! ; S = not_ready(Tmp) ).
+
+call_submit_rule(P:X, Arg) :- !, F =.. [X, Arg], P:F.
+call_submit_rule(X, Arg) :- !, F =.. [X, Arg], F.
+
+is_all_ok([]).
+is_all_ok([label(_, ok(__)) | Ls]) :- is_all_ok(Ls).
+is_all_ok(_) :- fail.
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%
+%% locate_submit_rule/1:
+%%
+%%   Finds a submit_rule depending on what rules are available.
+%%   If none are available, use default_submit/1.
+%%
+:- public locate_submit_rule/1.
+%%
+
+locate_submit_rule(RuleName) :-
+  '$compiled_predicate'(user, submit_rule, 1),
+  !,
+  RuleName = user:submit_rule
+  .
+locate_submit_rule(RuleName) :-
+  clause(user:submit_rule(_), _),
+  !,
+  RuleName = user:submit_rule
+  .
+locate_submit_rule(RuleName) :-
+  RuleName = gerrit:default_submit.
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%
+%% default_submit/1:
+%%
+:- public default_submit/1.
+%%
+default_submit(P) :-
+  get_legacy_approval_types(ApprovalTypes),
+  default_submit(ApprovalTypes, P).
+
+% Apply the old "all approval categories must be satisfied"
+% loop by scanning over all of the approval types to build
+% up the submit record.
+%
+default_submit(ApprovalTypes, P) :-
+  default_submit(ApprovalTypes, [], Tmp),
+  reverse(Tmp, Ls),
+  P =.. [ submit | Ls].
+
+default_submit([], Out, Out).
+default_submit([Type | Types], Tmp, Out) :-
+  approval_type(Label, Id, Fun, Min, Max) = Type,
+  legacy_submit_rule(Fun, Label, Id, Min, Max, Status),
+  R = label(Label, Status),
+  default_submit(Types, [R | Tmp], Out).
+
+
+%% legacy_submit_rule:
+%%
+%% Apply the old -2..+2 style logic.
+%%
+legacy_submit_rule('MaxWithBlock', Label, Id, Min, Max, T) :- !, max_with_block(Label, Min, Max, T).
+legacy_submit_rule('MaxNoBlock', Label, Id, Min, Max, T) :- !, max_no_block(Label, Max, T).
+legacy_submit_rule('NoBlock', Label, Id, Min, Max, T) :- !, T = ok(_).
+legacy_submit_rule('NoOp', Label, Id, Min, Max, T) :- !, T = ok(_).
+legacy_submit_rule(Fun, Label, Id, Min, Max, T) :- T = impossible(unsupported(Fun)).
+
+
+%% max_with_block:
+%%
+%% - The minimum is never used.
+%% - At least one maximum is used.
+%%
+:- public max_with_block/4.
+%%
+max_with_block(Label, Min, Max, reject(Who)) :-
+  check_label_range_permission(Label, Min, ok(Who)),
+  !
+  .
+max_with_block(Label, Min, Max, ok(Who)) :-
+  \+ check_label_range_permission(Label, Min, ok(_)),
+  check_label_range_permission(Label, Max, ok(Who)),
+  !
+  .
+max_with_block(Label, Min, Max, need(Max)) :-
+  true
+  .
+%TODO Uncomment this clause when group suggesting is possible.
+%max_with_block(Label, Min, Max, need(Max, Group)) :-
+%  \+ check_label_range_permission(Label, Max, ok(_)),
+%  check_label_range_permission(Label, Max, ask(Group))
+%  .
+%max_with_block(Label, Min, Max, impossible(no_access)) :-
+%  \+ check_label_range_permission(Label, Max, ask(Group))
+%  .
+
+
+%% max_no_block:
+%%
+%% - At least one maximum is used.
+%%
+max_no_block(Label, Max, ok(Who)) :-
+  check_label_range_permission(Label, Max, ok(Who)),
+  !
+  .
+max_no_block(Label, Max, need(Max)) :-
+  true
+  .
+%TODO Uncomment this clause when group suggesting is possible.
+%max_no_block(Label, Max, need(Max, Group)) :-
+%  check_label_range_permission(Label, Max, ask(Group))
+%  .
+%max_no_block(Label, Max, impossible(no_access)) :-
+%  \+ check_label_range_permission(Label, Max, ask(Group))
+%  .
+
+
+%% check_label_range_permission:
+%%
+check_label_range_permission(Label, ExpValue, ok(Who)) :-
+  commit_label(label(Label, ExpValue), Who),
+  user_label_range(Label, Who, Min, Max),
+  Min @=< ExpValue, ExpValue @=< Max
+  .
+%TODO Uncomment this clause when group suggesting is possible.
+%check_label_range_permission(Label, ExpValue, ask(Group)) :-
+%  grant_range(Label, Group, Min, Max),
+%  Min @=< ExpValue, ExpValue @=< Max
+%  .
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%
+%% filter_submit_results/3:
+%%
+%%   Executes the submit_filter against the given list of results,
+%%   returns a list of filtered results.
+%%
+:- public filter_submit_results/3.
+%%
+filter_submit_results(Filter, In, Out) :-
+    filter_submit_results(Filter, In, [], Tmp),
+    reverse(Tmp, Out).
+filter_submit_results(Filter, [I | In], Tmp, Out) :-
+    arg(1, I, R),
+    call_submit_filter(Filter, R, S),
+    !,
+    S =.. [submit | Ls],
+    ( is_all_ok(Ls) -> T = ok(S) ; T = not_ready(S) ),
+    filter_submit_results(Filter, In, [T | Tmp], Out).
+filter_submit_results(Filter, [_ | In], Tmp, Out) :-
+   filter_submit_results(Filter, In, Tmp, Out), 
+   !
+   .
+filter_submit_results(Filter, [], Out, Out).
+
+call_submit_filter(P:X, R, S) :- !, F =.. [X, R, S], P:F.
+call_submit_filter(X, R, S) :- F =.. [X, R, S], F.
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%
+%% locate_submit_filter/1:
+%%
+%%   Finds a submit_filter if available.
+%%
+:- public locate_submit_filter/1.
+%%
+locate_submit_filter(FilterName) :-
+  '$compiled_predicate'(user, submit_filter, 2),
+  !,
+  FilterName = user:submit_filter
+  .
+locate_submit_filter(FilterName) :-
+  clause(user:submit_filter(_,_), _),
+  FilterName = user:submit_filter
+  .
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%
+%% find_label/3:
+%%
+%%   Finds labels successively and fails when there are no more results.
+%%
+:- public find_label/3.
+%%
+find_label([], _, _) :- !, fail.
+find_label(List, Name, Label) :-
+  List = [_ | _],
+  !,
+  find_label2(List, Name, Label).
+find_label(S, Name, Label) :-
+  S =.. [submit | Ls],
+  find_label2(Ls, Name, Label).
+
+find_label2([L | _ ], Name, L) :- L = label(Name, _).
+find_label2([_ | Ls], Name, L) :- find_label2(Ls, Name, L).
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%
+%% remove_label/3:
+%%
+%%   Removes all occurances of label(Name, Status).
+%%
+:- public remove_label/3.
+%%
+remove_label([], _, []) :- !.
+remove_label(List, Label, Out) :-
+  List = [_ | _],
+  !,
+  subtract1(List, Label, Out).
+remove_label(S, Label, Out) :-
+  S =.. [submit | Ls],
+  subtract1(Ls, Label, Tmp),
+  Out =.. [submit | Tmp].
+
+subtract1([], _, []) :- !.
+subtract1([E | L], E, R) :- !, subtract1(L, E, R).
+subtract1([H | L], E, [H | R]) :- subtract1(L, E, R).
+
+
+%% commit_author/1:
+%%
+:- public commit_author/1.
+%%
+commit_author(Author) :-
+  commit_author(Author, _, _).
+
+
+%% commit_committer/1:
+%%
+:- public commit_committer/1.
+%%
+commit_committer(Committer) :-
+  commit_committer(Committer, _, _).
+
+
+%% commit_delta/1:
+%%
+:- public commit_delta/1.
+%%
+commit_delta(Regex) :-
+  once(commit_delta(Regex, _, _, _)).
+
+
+%% commit_delta/3:
+%%
+:- public commit_delta/3.
+%%
+commit_delta(Regex, Type, Path) :-
+  commit_delta(Regex, TmpType, NewPath, OldPath),
+  split_commit_delta(TmpType, NewPath, OldPath, Type, Path).
+
+split_commit_delta(rename, NewPath, OldPath, delete, OldPath).
+split_commit_delta(rename, NewPath, OldPath, add, NewPath) :- !.
+split_commit_delta(copy, NewPath, OldPath, add, NewPath) :- !.
+split_commit_delta(Type, Path, _, Type, Path).
+
+
+%% commit_message_matches/1:
+%%
+:- public commit_message_matches/1.
+%%
+commit_message_matches(Pattern) :-
+  commit_message(Msg),
+  regex_matches(Pattern, Msg).
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/RebasedPatchSet.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/RebasedPatchSet.vm
new file mode 100644
index 0000000..e761627
--- /dev/null
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/RebasedPatchSet.vm
@@ -0,0 +1,54 @@
+## Copyright (C) 2012 The Android Open Source Project
+##
+## Licensed under the Apache License, Version 2.0 (the "License");
+## you may not use this file except in compliance with the License.
+## You may obtain a copy of the License at
+##
+## http://www.apache.org/licenses/LICENSE-2.0
+##
+## Unless required by applicable law or agreed to in writing, software
+## distributed under the License is distributed on an "AS IS" BASIS,
+## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+## See the License for the specific language governing permissions and
+## limitations under the License.
+##
+##
+## Template Type:
+## -------------
+## This is a velocity mail template, see: http://velocity.apache.org and the
+## gerrit-docs:config-mail.txt for more info on modifying gerrit mail templates.
+##
+## Template File Names and extensions:
+## ----------------------------------
+## 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.example file to a .vm
+## file and edit it appropriately.
+##
+## This Template:
+## --------------
+## The RebasedPatchSet.vm template will determine the contents of the email
+## related to a user rebasing a patchset for a change through the Gerrit UI.
+## It is a ChangeEmail: see ChangeSubject.vm and ChangeFooter.vm.
+##
+#if($email.reviewerNames)
+Hello $email.joinStrings($email.reviewerNames, ', '),
+
+I'd like you to reexamine a rebased change.#if($email.changeUrl)  Please visit
+
+    $email.changeUrl
+
+to look at the new rebased patch set (#$patchSet.patchSetId).
+#end
+#else
+$fromName has created a new patch set by issuing a rebase in Gerrit (#$patchSet.patchSetId).
+#end
+
+Change subject: $change.subject
+......................................................................
+
+$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 c1de87e..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
+$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
new file mode 100644
index 0000000..afcbcc5
--- /dev/null
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Restored.vm
@@ -0,0 +1,44 @@
+## 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.
+##
+##
+## Template Type:
+## -------------
+## This is a velocity mail template, see: http://velocity.apache.org and the
+## gerrit-docs:config-mail.txt for more info on modifying gerrit mail templates.
+##
+## Template File Names and extensions:
+## ----------------------------------
+## 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.example file to a .vm
+## file and edit it appropriately.
+##
+## This Template:
+## --------------
+## The Restored.vm template will determine the contents of the email related
+## to a change being restored.   It is a ChangeEmail: see ChangeSubject.vm and
+## ChangeFooter.vm.
+##
+$fromName has restored this change.
+
+Change subject: $change.subject
+......................................................................
+
+
+#if ($coverLetter)
+$coverLetter
+
+#end
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/main/resources/com/google/gerrit/server/tools/root/hooks/commit-msg b/gerrit-server/src/main/resources/com/google/gerrit/server/tools/root/hooks/commit-msg
index a0e3554..212ffb1 100644
--- a/gerrit-server/src/main/resources/com/google/gerrit/server/tools/root/hooks/commit-msg
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/tools/root/hooks/commit-msg
@@ -36,56 +36,126 @@
 		return
 	fi
 
+	# Does Change-Id: already exist? if so, exit (no change).
 	if grep -i '^Change-Id:' "$MSG" >/dev/null
 	then
 		return
 	fi
 
 	id=`_gen_ChangeId`
-	perl -e '
-		$MSG = shift;
-		$id = shift;
-		$CHANGE_ID_AFTER = shift;
+	T="$MSG.tmp.$$"
+	AWK=awk
+	if [ -x /usr/xpg4/bin/awk ]; then
+		# Solaris AWK is just too broken
+		AWK=/usr/xpg4/bin/awk
+	fi
 
-		undef $/;
-		open(I, $MSG); $_ = <I>; close I;
-		s|^diff --git a/.*||ms;
-		s|^#.*$||mg;
-		exit unless $_;
+	# How this works:
+	# - parse the commit message as (textLine+ blankLine*)*
+	# - assume textLine+ to be a footer until proven otherwise
+	# - exception: the first block is not footer (as it is the title)
+	# - read textLine+ into a variable
+	# - then count blankLines
+	# - once the next textLine appears, print textLine+ blankLine* as these
+	#   aren't footer
+	# - in END, the last textLine+ block is available for footer parsing
+	$AWK '
+	BEGIN {
+		# while we start with the assumption that textLine+
+		# is a footer, the first block is not.
+		isFooter = 0
+		footerComment = 0
+		blankLines = 0
+	}
 
-		@message = split /\n/;
-		$haveFooter = 0;
-		$startFooter = @message;
-		for($line = @message - 1; $line >= 0; $line--) {
-			$_ = $message[$line];
+	# Skip lines starting with "#" without any spaces before it.
+	/^#/ { next }
 
-			if (/^[a-zA-Z0-9-]+:/ && !m,^[a-z0-9-]+://,) {
-				$haveFooter++;
-				next;
+	# Skip the line starting with the diff command and everything after it,
+	# up to the end of the file, assuming it is only patch data.
+	# If more than one line before the diff was empty, strip all but one.
+	/^diff --git a/ {
+		blankLines = 0
+		while (getline) { }
+		next
+	}
+
+	# Count blank lines outside footer comments
+	/^$/ && (footerComment == 0) {
+		blankLines++
+		next
+	}
+
+	# Catch footer comment
+	/^\[[a-zA-Z0-9-]+:/ && (isFooter == 1) {
+		footerComment = 1
+	}
+
+	/]$/ && (footerComment == 1) {
+		footerComment = 2
+	}
+
+	# We have a non-blank line after blank lines. Handle this.
+	(blankLines > 0) {
+		print lines
+		for (i = 0; i < blankLines; i++) {
+			print ""
+		}
+
+		lines = ""
+		blankLines = 0
+		isFooter = 1
+		footerComment = 0
+	}
+
+	# Detect that the current block is not the footer
+	(footerComment == 0) && (!/^\[?[a-zA-Z0-9-]+:/ || /^[a-zA-Z0-9-]+:\/\//) {
+		isFooter = 0
+	}
+
+	{
+		# We need this information about the current last comment line
+		if (footerComment == 2) {
+			footerComment = 0
+		}
+		if (lines != "") {
+			lines = lines "\n";
+		}
+		lines = lines $0
+	}
+
+	# Footer handling:
+	# If the last block is considered a footer, splice in the Change-Id at the
+	# right place.
+	# Look for the right place to inject Change-Id by considering
+	# CHANGE_ID_AFTER. Keys listed in it (case insensitive) come first,
+	# then Change-Id, then everything else (eg. Signed-off-by:).
+	#
+	# Otherwise just print the last block, a new line and the Change-Id as a
+	# block of its own.
+	END {
+		unprinted = 1
+		if (isFooter == 0) {
+			print lines "\n"
+			lines = ""
+		}
+		changeIdAfter = "^(" tolower("'"$CHANGE_ID_AFTER"'") "):"
+		numlines = split(lines, footer, "\n")
+		for (line = 1; line <= numlines; line++) {
+			if (unprinted && match(tolower(footer[line]), changeIdAfter) != 1) {
+				unprinted = 0
+				print "Change-Id: I'"$id"'"
 			}
-			next if /^[ []/;
-			$startFooter = $line if ($haveFooter && /^\r?$/);
-			last;
+			print footer[line]
 		}
-
-		@footer = @message[$startFooter+1..@message];
-		@message = @message[0..$startFooter];
-		push(@footer, "") unless @footer;
-
-		for ($line = 0; $line < @footer; $line++) {
-			$_ = $footer[$line];
-			next if /^($CHANGE_ID_AFTER):/i;
-			last;
+		if (unprinted) {
+			print "Change-Id: I'"$id"'"
 		}
-		splice(@footer, $line, 0, "Change-Id: I$id");
-
-		$_ = join("\n", @message, @footer);
-		open(O, ">$MSG"); print O; close O;
-	' "$MSG" "$id" "$CHANGE_ID_AFTER"
+	}' "$MSG" > $T && mv $T "$MSG" || rm -f $T
 }
 _gen_ChangeIdInput() {
 	echo "tree `git write-tree`"
-	if parent=`git rev-parse HEAD^0 2>/dev/null`
+	if parent=`git rev-parse "HEAD^0" 2>/dev/null`
 	then
 		echo "parent $parent"
 	fi
diff --git a/gerrit-server/src/test/java/com/google/gerrit/rules/GerritCommonTest.java b/gerrit-server/src/test/java/com/google/gerrit/rules/GerritCommonTest.java
new file mode 100644
index 0000000..9f2ccbf
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/rules/GerritCommonTest.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.rules;
+
+import com.google.gerrit.common.data.ApprovalType;
+import com.google.gerrit.common.data.ApprovalTypes;
+import com.google.gerrit.reviewdb.client.ApprovalCategory;
+import com.google.gerrit.reviewdb.client.ApprovalCategoryValue;
+import com.google.inject.AbstractModule;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+public class GerritCommonTest extends PrologTestCase {
+  @Override
+  public void setUp() throws Exception {
+    super.setUp();
+
+    final ApprovalTypes types = new ApprovalTypes(Arrays.asList(
+        codeReviewCategory(),
+        verifiedCategory()
+    ));
+
+    load("gerrit", "gerrit_common_test.pl", new AbstractModule() {
+      @Override
+      protected void configure() {
+        bind(ApprovalTypes.class).toInstance(types);
+      }
+    });
+  }
+
+  private static ApprovalType codeReviewCategory() {
+    ApprovalCategory cat = category(0, "CRVW", "Code Review");
+    List<ApprovalCategoryValue> vals = newList();
+    vals.add(value(cat, 2, "Looks good to me, approved"));
+    vals.add(value(cat, 1, "Looks good to me, but someone else must approve"));
+    vals.add(value(cat, 0, "No score"));
+    vals.add(value(cat, -1, "I would prefer that you didn't submit this"));
+    vals.add(value(cat, -2, "Do not submit"));
+    return new ApprovalType(cat, vals);
+  }
+
+  private static ApprovalType verifiedCategory() {
+    ApprovalCategory cat = category(1, "VRIF", "Verified");
+    List<ApprovalCategoryValue> vals = newList();
+    vals.add(value(cat, 1, "Verified"));
+    vals.add(value(cat, 0, "No score"));
+    vals.add(value(cat, -1, "Fails"));
+    return new ApprovalType(cat, vals);
+  }
+
+  private static ApprovalCategory category(int pos, String id, String name) {
+    ApprovalCategory cat;
+    cat = new ApprovalCategory(new ApprovalCategory.Id(id), name);
+    cat.setPosition((short) pos);
+    return cat;
+  }
+
+  private static ArrayList<ApprovalCategoryValue> newList() {
+    return new ArrayList<ApprovalCategoryValue>();
+  }
+
+  private static ApprovalCategoryValue value(ApprovalCategory c, int v, String n) {
+    return new ApprovalCategoryValue(
+        new ApprovalCategoryValue.Id(c.getId(), (short) v),
+        n);
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/rules/PrologTestCase.java b/gerrit-server/src/test/java/com/google/gerrit/rules/PrologTestCase.java
new file mode 100644
index 0000000..cc9d40f
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/rules/PrologTestCase.java
@@ -0,0 +1,181 @@
+// 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.rules;
+
+import com.google.inject.Guice;
+import com.google.inject.Module;
+
+import com.googlecode.prolog_cafe.compiler.CompileException;
+import com.googlecode.prolog_cafe.lang.BufferingPrologControl;
+import com.googlecode.prolog_cafe.lang.JavaObjectTerm;
+import com.googlecode.prolog_cafe.lang.Prolog;
+import com.googlecode.prolog_cafe.lang.PrologClassLoader;
+import com.googlecode.prolog_cafe.lang.PrologMachineCopy;
+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;
+
+import junit.framework.TestCase;
+
+import java.io.BufferedReader;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.PushbackReader;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+
+
+/** Base class for any tests written in Prolog. */
+public abstract class PrologTestCase extends TestCase {
+  private static final SymbolTerm test_1 = SymbolTerm.intern("test", 1);
+
+  private String pkg;
+  private boolean hasSetup;
+  private boolean hasTeardown;
+  private List<Term> tests;
+  private PrologMachineCopy machine;
+  private PrologEnvironment.Factory envFactory;
+
+  protected void load(String pkg, String prologResource, Module... modules)
+      throws CompileException, IOException {
+    ArrayList<Module> moduleList = new ArrayList<Module>();
+    moduleList.add(new PrologModule());
+    moduleList.addAll(Arrays.asList(modules));
+
+    envFactory = Guice.createInjector(moduleList)
+        .getInstance(PrologEnvironment.Factory.class);
+    PrologEnvironment env = envFactory.create(newMachine());
+    consult(env, getClass(), prologResource);
+
+    this.pkg = pkg;
+    hasSetup = has(env, "setup");
+    hasTeardown = has(env, "teardown");
+
+    StructureTerm head = new StructureTerm(":",
+        SymbolTerm.intern(pkg),
+        new StructureTerm(test_1, new VariableTerm()));
+
+    tests = new ArrayList<Term>();
+    for (Term[] pair : env.all(Prolog.BUILTIN, "clause", head, new VariableTerm())) {
+      tests.add(pair[0]);
+    }
+    assertTrue("has tests", tests.size() > 0);
+    machine = PrologMachineCopy.save(env);
+  }
+
+  private PrologMachineCopy newMachine() {
+    BufferingPrologControl ctl = new BufferingPrologControl();
+    ctl.setMaxDatabaseSize(16 * 1024);
+    ctl.setPrologClassLoader(new PrologClassLoader(getClass().getClassLoader()));
+    return PrologMachineCopy.save(ctl);
+  }
+
+  protected void consult(BufferingPrologControl env,
+      Class<?> clazz,
+      String prologResource) throws CompileException, IOException {
+    InputStream in = clazz.getResourceAsStream(prologResource);
+    if (in == null) {
+      throw new FileNotFoundException(prologResource);
+    }
+    try {
+      SymbolTerm pathTerm = SymbolTerm.create(prologResource);
+      JavaObjectTerm inTerm =
+          new JavaObjectTerm(new PushbackReader(new BufferedReader(
+              new InputStreamReader(in, "UTF-8")), Prolog.PUSHBACK_SIZE));
+      if (!env.execute(Prolog.BUILTIN, "consult_stream", pathTerm, inTerm)) {
+        throw new CompileException("Cannot consult " + prologResource);
+      }
+    } finally {
+      in.close();
+    }
+  }
+
+  private boolean has(BufferingPrologControl env, String name) {
+    StructureTerm head = SymbolTerm.create(pkg, name, 0);
+    return env.execute(Prolog.BUILTIN, "clause", head, new VariableTerm());
+  }
+
+  public void testRunPrologTestCases() {
+    int errors = 0;
+    long start = System.currentTimeMillis();
+
+    for (Term test : tests) {
+      PrologEnvironment env = envFactory.create(machine);
+      env.setEnabled(Prolog.Feature.IO, true);
+
+      System.out.format("Prolog %-60s ...", removePackage(test));
+      System.out.flush();
+
+      if (hasSetup) {
+        call(env, "setup");
+      }
+
+      List<Term> all = env.all(Prolog.BUILTIN, "call", test);
+
+      if (hasTeardown) {
+        call(env, "teardown");
+      }
+
+      System.out.println(all.size() == 1 ? "OK" : "FAIL");
+
+      if (all.size() > 0 && !test.equals(all.get(0))) {
+        for (Term t : all) {
+          Term head = ((StructureTerm) removePackage(t)).args()[0];
+          Term[] args = ((StructureTerm) head).args();
+          System.out.print("  Result: ");
+          for (int i = 0; i < args.length; i++) {
+            if (0 < i) {
+              System.out.print(", ");
+            }
+            System.out.print(args[i]);
+          }
+          System.out.println();
+        }
+        System.out.println();
+      }
+
+      if (all.size() != 1) {
+       errors++;
+      }
+    }
+
+    long end = System.currentTimeMillis();
+    System.out.println("-------------------------------");
+    System.out.format("Prolog tests: %d, Failures: %d, Time elapsed %.3f sec",
+        tests.size(), errors, (end - start) / 1000.0);
+    System.out.println();
+
+    assertEquals("No Errors", 0, errors);
+  }
+
+  private void call(BufferingPrologControl env, String name) {
+    StructureTerm head = SymbolTerm.create(pkg, name, 0);
+    if (!env.execute(Prolog.BUILTIN, "call", head)) {
+      fail("Cannot invoke " + pkg + ":" + name);
+    }
+  }
+
+  private Term removePackage(Term test) {
+    Term name = test;
+    if (name.isStructure() && ":".equals(((StructureTerm) name).name())) {
+      name = ((StructureTerm) name).args()[1];
+    }
+    return name;
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/config/ConfigUtilTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/config/ConfigUtilTest.java
index 386a6d1..37197ec 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/config/ConfigUtilTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/config/ConfigUtilTest.java
@@ -15,7 +15,10 @@
 package com.google.gerrit.server.config;
 
 import static java.util.concurrent.TimeUnit.DAYS;
-import static java.util.concurrent.TimeUnit.*;
+import static java.util.concurrent.TimeUnit.HOURS;
+import static java.util.concurrent.TimeUnit.MILLISECONDS;
+import static java.util.concurrent.TimeUnit.MINUTES;
+import static java.util.concurrent.TimeUnit.SECONDS;
 
 import junit.framework.TestCase;
 
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
new file mode 100644
index 0000000..ca2b03a
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/git/ProjectConfigTest.java
@@ -0,0 +1,195 @@
+// 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.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+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.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Project;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase;
+import org.eclipse.jgit.junit.TestRepository;
+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.RevObject;
+import org.eclipse.jgit.util.RawParseUtils;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+
+public class ProjectConfigTest extends LocalDiskRepositoryTestCase {
+  private final GroupReference developers = new GroupReference(
+      new AccountGroup.UUID("X"), "Developers");
+  private final GroupReference staff = new GroupReference(
+      new AccountGroup.UUID("Y"), "Staff");
+
+  private Repository db;
+  private TestRepository<Repository> util;
+
+  @Override
+  @Before
+  public void setUp() throws Exception {
+    super.setUp();
+    db = createBareRepository();
+    util = new TestRepository<Repository>(db);
+  }
+
+  @Test
+  public void testReadConfig() throws Exception {
+    RevCommit rev = util.commit(util.tree( //
+        util.file("groups", util.blob(group(developers))), //
+        util.file("project.config", util.blob(""//
+            + "[access \"refs/heads/*\"]\n" //
+            + "  exclusiveGroupPermissions = read submit create\n" //
+            + "  submit = group Developers\n" //
+            + "  push = group Developers\n" //
+            + "  read = group Developers\n")) //
+        ));
+
+    ProjectConfig cfg = read(rev);
+    AccessSection section = cfg.getAccessSection("refs/heads/*");
+    assertNotNull("has refs/heads/*", section);
+    assertNull("no refs/*", cfg.getAccessSection("refs/*"));
+
+    Permission create = section.getPermission(Permission.CREATE);
+    Permission submit = section.getPermission(Permission.SUBMIT);
+    Permission read = section.getPermission(Permission.READ);
+    Permission push = section.getPermission(Permission.PUSH);
+
+    assertTrue(create.getExclusiveGroup());
+    assertTrue(submit.getExclusiveGroup());
+    assertTrue(read.getExclusiveGroup());
+    assertFalse(push.getExclusiveGroup());
+  }
+
+  @Test
+  public void testEditConfig() throws Exception {
+    RevCommit rev = util.commit(util.tree( //
+        util.file("groups", util.blob(group(developers))), //
+        util.file("project.config", util.blob(""//
+            + "[access \"refs/heads/*\"]\n" //
+            + "  exclusiveGroupPermissions = read submit\n" //
+            + "  submit = group Developers\n" //
+            + "  upload = group Developers\n" //
+            + "  read = group Developers\n")) //
+        ));
+    update(rev);
+
+    ProjectConfig cfg = read(rev);
+    AccessSection section = cfg.getAccessSection("refs/heads/*");
+    Permission submit = section.getPermission(Permission.SUBMIT);
+    submit.add(new PermissionRule(cfg.resolve(staff)));
+    rev = commit(cfg);
+    assertEquals(""//
+        + "[access \"refs/heads/*\"]\n" //
+        + "  exclusiveGroupPermissions = read submit\n" //
+        + "  submit = group Developers\n" //
+        + "\tsubmit = group Staff\n" //
+        + "  upload = group Developers\n" //
+        + "  read = group Developers\n"//
+        + "[project]\n"//
+        + "\tstate = active\n", text(rev, "project.config"));
+  }
+
+  @Test
+  public void testEditConfigMissingGroupTableEntry() throws Exception {
+    RevCommit rev = util.commit(util.tree( //
+        util.file("groups", util.blob(group(developers))), //
+        util.file("project.config", util.blob(""//
+            + "[access \"refs/heads/*\"]\n" //
+            + "  exclusiveGroupPermissions = read submit\n" //
+            + "  submit = group People Who Can Submit\n" //
+            + "  upload = group Developers\n" //
+            + "  read = group Developers\n")) //
+        ));
+    update(rev);
+
+    ProjectConfig cfg = read(rev);
+    AccessSection section = cfg.getAccessSection("refs/heads/*");
+    Permission submit = section.getPermission(Permission.SUBMIT);
+    submit.add(new PermissionRule(cfg.resolve(staff)));
+    rev = commit(cfg);
+    assertEquals(""//
+        + "[access \"refs/heads/*\"]\n" //
+        + "  exclusiveGroupPermissions = read submit\n" //
+        + "  submit = group People Who Can Submit\n" //
+        + "\tsubmit = group Staff\n" //
+        + "  upload = group Developers\n" //
+        + "  read = group Developers\n"//
+        + "[project]\n"//
+        + "\tstate = active\n", text(rev, "project.config"));
+  }
+
+  private ProjectConfig read(RevCommit rev) throws IOException,
+      ConfigInvalidException {
+    ProjectConfig cfg = new ProjectConfig(new Project.NameKey("test"));
+    cfg.load(db, rev);
+    return cfg;
+  }
+
+  private RevCommit commit(ProjectConfig cfg) throws IOException,
+      MissingObjectException, IncorrectObjectTypeException {
+    MetaDataUpdate md = new MetaDataUpdate(new NoReplication(), //
+        cfg.getProject().getNameKey(), //
+        db);
+    util.tick(5);
+    util.setAuthorAndCommitter(md.getCommitBuilder());
+    md.setMessage("Edit\n");
+    assertTrue("commit finished", cfg.commit(md));
+
+    Ref ref = db.getRef(GitRepositoryManager.REF_CONFIG);
+    return util.getRevWalk().parseCommit(ref.getObjectId());
+  }
+
+  private void update(RevCommit rev) throws Exception {
+    RefUpdate u = db.updateRef(GitRepositoryManager.REF_CONFIG);
+    u.disableRefLog();
+    u.setNewObjectId(rev);
+    switch (u.forceUpdate()) {
+      case FAST_FORWARD:
+      case FORCED:
+      case NEW:
+      case NO_CHANGE:
+        break;
+      default:
+        fail("Cannot update ref for test: " + u.getResult());
+    }
+  }
+
+  private String text(RevCommit rev, String path) throws Exception {
+    RevObject blob = util.get(rev.getTree(), path);
+    byte[] data = db.open(blob).getCachedBytes(Integer.MAX_VALUE);
+    return RawParseUtils.decode(data);
+  }
+
+  private static String group(GroupReference g) {
+    return g.getUUID().get() + "\t" + g.getName() + "\n";
+  }
+}
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/git/PushReplicationTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/PushReplicationTest.java
index b95c7da..7ae705f 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/git/PushReplicationTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/git/PushReplicationTest.java
@@ -14,7 +14,9 @@
 
 package com.google.gerrit.server.git;
 
-import static com.google.gerrit.server.git.PushReplication.ReplicationConfig.*;
+import static com.google.gerrit.server.git.PushReplication.ReplicationConfig.encode;
+import static com.google.gerrit.server.git.PushReplication.ReplicationConfig.needsUrlEncoding;
+
 import junit.framework.TestCase;
 
 import org.eclipse.jgit.transport.URIish;
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..b32d54c
--- /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.client.Account;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.SubmoduleSubscription;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.reviewdb.server.SubmoduleSubscriptionAccess;
+import com.google.gwtorm.client.KeyUtil;
+import com.google.gwtorm.server.ListResultSet;
+import com.google.gwtorm.server.ResultSet;
+import com.google.gwtorm.server.SchemaFactory;
+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 f819dac..29e3df0 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
@@ -20,9 +20,9 @@
 import static org.easymock.EasyMock.replay;
 import static org.easymock.EasyMock.verify;
 
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.AccountExternalId;
-import com.google.gerrit.reviewdb.AccountGroup;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountExternalId;
+import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
 
@@ -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) {
@@ -278,7 +279,7 @@
     account.setFullName(name);
     account.setPreferredEmail(email);
     final AccountState s =
-        new AccountState(account, Collections.<AccountGroup.Id> emptySet(),
+        new AccountState(account, Collections.<AccountGroup.UUID> emptySet(),
             Collections.<AccountExternalId> emptySet());
     return s;
   }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/patch/PatchListEntryTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/patch/PatchListEntryTest.java
index 24f6f63..7df0696 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/patch/PatchListEntryTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/patch/PatchListEntryTest.java
@@ -15,7 +15,7 @@
 
 package com.google.gerrit.server.patch;
 
-import com.google.gerrit.reviewdb.Patch;
+import com.google.gerrit.reviewdb.client.Patch;
 
 import junit.framework.TestCase;
 
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java
index 6b0f13a..cd6fa69 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/project/RefControlTest.java
@@ -14,44 +14,58 @@
 
 package com.google.gerrit.server.project;
 
-import static com.google.gerrit.reviewdb.ApprovalCategory.OWN;
-import static com.google.gerrit.reviewdb.ApprovalCategory.READ;
-import static com.google.gerrit.reviewdb.ApprovalCategory.SUBMIT;
+import static com.google.gerrit.common.data.Permission.LABEL;
+import static com.google.gerrit.common.data.Permission.OWNER;
+import static com.google.gerrit.common.data.Permission.PUSH;
+import static com.google.gerrit.common.data.Permission.READ;
+import static com.google.gerrit.common.data.Permission.SUBMIT;
 
-import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.AccountProjectWatch;
-import com.google.gerrit.reviewdb.ApprovalCategory;
-import com.google.gerrit.reviewdb.Change;
-import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.RefRight;
-import com.google.gerrit.reviewdb.SystemConfig;
-import com.google.gerrit.reviewdb.RefRight.RefPattern;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.gerrit.common.data.Capable;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.common.data.PermissionRange;
+import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.rules.PrologEnvironment;
+import com.google.gerrit.rules.RulesCache;
 import com.google.gerrit.server.AccessPath;
-import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.config.AuthConfig;
+import com.google.gerrit.server.account.CapabilityControl;
+import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.account.GroupMembership;
+import com.google.gerrit.server.account.ListGroupMembership;
+import com.google.gerrit.server.cache.ConcurrentHashMapCache;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.FactoryModule;
 import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.inject.AbstractModule;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Guice;
 import com.google.inject.Injector;
 
 import junit.framework.TestCase;
 
-import org.apache.commons.codec.binary.Base64;
 import org.eclipse.jgit.lib.Config;
 
-import java.io.UnsupportedEncodingException;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashMap;
 import java.util.HashSet;
-import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 public class RefControlTest extends TestCase {
   public void testOwnerProject() {
-    grant(local, OWN, admin, "refs/*", 1);
+    grant(local, OWNER, admin, "refs/*");
 
     ProjectControl uBlah = user(devs);
     ProjectControl uAdmin = user(devs, admin);
@@ -61,8 +75,8 @@
   }
 
   public void testBranchDelegation1() {
-    grant(local, OWN, admin, "refs/*", 1);
-    grant(local, OWN, devs, "refs/heads/x/*", 1);
+    grant(local, OWNER, admin, "refs/*");
+    grant(local, OWNER, devs, "refs/heads/x/*");
 
     ProjectControl uDev = user(devs);
     assertFalse("not owner", uDev.isOwner());
@@ -77,9 +91,10 @@
   }
 
   public void testBranchDelegation2() {
-    grant(local, OWN, admin, "refs/*", 1);
-    grant(local, OWN, devs, "refs/heads/x/*", 1);
-    grant(local, OWN, fixers, "-refs/heads/x/y/*", 1);
+    grant(local, OWNER, admin, "refs/*");
+    grant(local, OWNER, devs, "refs/heads/x/*");
+    grant(local, OWNER, fixers, "refs/heads/x/y/*");
+    doNotInherit(local, OWNER, "refs/heads/x/y/*");
 
     ProjectControl uDev = user(devs);
     assertFalse("not owner", uDev.isOwner());
@@ -104,11 +119,14 @@
   }
 
   public void testInheritRead_SingleBranchDeniesUpload() {
-    grant(parent, READ, registered, "refs/*", 1, 2);
-    grant(local, READ, registered, "-refs/heads/foobar", 1);
+    grant(parent, READ, registered, "refs/*");
+    grant(parent, PUSH, registered, "refs/for/refs/*");
+    grant(local, READ, registered, "refs/heads/foobar");
+    doNotInherit(local, READ, "refs/heads/foobar");
+    doNotInherit(local, PUSH, "refs/for/refs/heads/foobar");
 
     ProjectControl u = user();
-    assertTrue("can upload", u.canPushToAtLeastOneRef());
+    assertTrue("can upload", u.canPushToAtLeastOneRef() == Capable.OK);
 
     assertTrue("can upload refs/heads/master", //
         u.controlForRef("refs/heads/master").canUpload());
@@ -118,11 +136,12 @@
   }
 
   public void testInheritRead_SingleBranchDoesNotOverrideInherited() {
-    grant(parent, READ, registered, "refs/*", 1, 2);
-    grant(local, READ, registered, "refs/heads/foobar", 1);
+    grant(parent, READ, registered, "refs/*");
+    grant(parent, PUSH, registered, "refs/for/refs/*");
+    grant(local, READ, registered, "refs/heads/foobar");
 
     ProjectControl u = user();
-    assertTrue("can upload", u.canPushToAtLeastOneRef());
+    assertTrue("can upload", u.canPushToAtLeastOneRef() == Capable.OK);
 
     assertTrue("can upload refs/heads/master", //
         u.controlForRef("refs/heads/master").canUpload());
@@ -131,17 +150,29 @@
         u.controlForRef("refs/heads/foobar").canUpload());
   }
 
+  public void testInheritDuplicateSections() {
+    grant(parent, READ, admin, "refs/*");
+    grant(local, READ, devs, "refs/heads/*");
+    local.getProject().setParentName(parent.getProject().getName());
+    assertTrue("a can read", user("a", admin).isVisible());
+
+    local = new ProjectConfig(new Project.NameKey("local"));
+    local.createInMemory();
+    grant(local, READ, devs, "refs/*");
+    assertTrue("d can read", user("d", devs).isVisible());
+  }
+
   public void testInheritRead_OverrideWithDeny() {
-    grant(parent, READ, registered, "refs/*", 1);
-    grant(local, READ, registered, "refs/*", 0);
+    grant(parent, READ, registered, "refs/*");
+    grant(local, READ, registered, "refs/*").setDeny();
 
     ProjectControl u = user();
     assertFalse("can't read", u.isVisible());
   }
 
   public void testInheritRead_AppendWithDenyOfRef() {
-    grant(parent, READ, registered, "refs/*", 1);
-    grant(local, READ, registered, "refs/heads/*", 0);
+    grant(parent, READ, registered, "refs/*");
+    grant(local, READ, registered, "refs/heads/*").setDeny();
 
     ProjectControl u = user();
     assertTrue("can read", u.isVisible());
@@ -151,9 +182,9 @@
   }
 
   public void testInheritRead_OverridesAndDeniesOfRef() {
-    grant(parent, READ, registered, "refs/*", 1);
-    grant(local, READ, registered, "refs/*", 0);
-    grant(local, READ, registered, "refs/heads/*", -1, 1);
+    grant(parent, READ, registered, "refs/*");
+    grant(local, READ, registered, "refs/*").setDeny();
+    grant(local, READ, registered, "refs/heads/*");
 
     ProjectControl u = user();
     assertTrue("can read", u.isVisible());
@@ -163,9 +194,9 @@
   }
 
   public void testInheritSubmit_OverridesAndDeniesOfRef() {
-    grant(parent, SUBMIT, registered, "refs/*", 1);
-    grant(local, SUBMIT, registered, "refs/*", 0);
-    grant(local, SUBMIT, registered, "refs/heads/*", -1, 1);
+    grant(parent, SUBMIT, registered, "refs/*");
+    grant(local, SUBMIT, registered, "refs/*").setDeny();
+    grant(local, SUBMIT, registered, "refs/heads/*");
 
     ProjectControl u = user();
     assertFalse("can't submit", u.controlForRef("refs/foobar").canSubmit());
@@ -174,70 +205,140 @@
   }
 
   public void testCannotUploadToAnyRef() {
-    grant(parent, READ, registered, "refs/*", 1);
-    grant(local, READ, devs, "refs/heads/*", 1, 2);
+    grant(parent, READ, registered, "refs/*");
+    grant(local, READ, devs, "refs/heads/*");
+    grant(local, PUSH, devs, "refs/for/refs/heads/*");
 
     ProjectControl u = user();
-    assertFalse("cannot upload", u.canPushToAtLeastOneRef());
+    assertFalse("cannot upload", u.canPushToAtLeastOneRef() == Capable.OK);
     assertFalse("cannot upload refs/heads/master", //
         u.controlForRef("refs/heads/master").canUpload());
   }
 
+  public void testUsernamePatternNonRegex() {
+    grant(local, READ, devs, "refs/sb/${username}/heads/*");
 
-  // -----------------------------------------------------------------------
-
-  private final Project.NameKey local = new Project.NameKey("test");
-  private final Project.NameKey parent = new Project.NameKey("parent");
-  private final AccountGroup.Id admin = new AccountGroup.Id(1);
-  private final AccountGroup.Id anonymous = new AccountGroup.Id(2);
-  private final AccountGroup.Id registered = new AccountGroup.Id(3);
-  private final AccountGroup.Id owners = new AccountGroup.Id(4);
-
-  private final AccountGroup.Id devs = new AccountGroup.Id(5);
-  private final AccountGroup.Id fixers = new AccountGroup.Id(6);
-
-  private final SystemConfig systemConfig;
-  private final AuthConfig authConfig;
-  private final AnonymousUser anonymousUser;
-
-  public RefControlTest() {
-    systemConfig = SystemConfig.create();
-    systemConfig.adminGroupId = admin;
-    systemConfig.anonymousGroupId = anonymous;
-    systemConfig.registeredGroupId = registered;
-    systemConfig.ownerGroupId = owners;
-    systemConfig.batchUsersGroupId = anonymous;
-    try {
-      byte[] bin = "abcdefghijklmnopqrstuvwxyz".getBytes("UTF-8");
-      systemConfig.registerEmailPrivateKey = Base64.encodeBase64String(bin);
-    } catch (UnsupportedEncodingException err) {
-      throw new RuntimeException("Cannot encode key", err);
-    }
-
-    Injector injector = Guice.createInjector(new AbstractModule() {
-      @Override
-      protected void configure() {
-        bind(Config.class) //
-            .annotatedWith(GerritServerConfig.class) //
-            .toInstance(new Config());
-
-        bind(SystemConfig.class).toInstance(systemConfig);
-        bind(AuthConfig.class);
-        bind(AnonymousUser.class);
-      }
-    });
-    authConfig = injector.getInstance(AuthConfig.class);
-    anonymousUser = injector.getInstance(AnonymousUser.class);
+    ProjectControl u = user("u", devs), d = user("d", devs);
+    assertFalse("u can't read", u.controlForRef("refs/sb/d/heads/foobar").isVisible());
+    assertTrue("d can read", d.controlForRef("refs/sb/d/heads/foobar").isVisible());
   }
 
-  private List<RefRight> localRights;
-  private List<RefRight> inheritedRights;
+  public void testUsernamePatternWithRegex() {
+    grant(local, READ, devs, "^refs/sb/${username}/heads/.*");
+
+    ProjectControl u = user("d.v", devs), d = user("dev", devs);
+    assertFalse("u can't read", u.controlForRef("refs/sb/dev/heads/foobar").isVisible());
+    assertTrue("d can read", d.controlForRef("refs/sb/dev/heads/foobar").isVisible());
+  }
+
+  public void testSortWithRegex() {
+    grant(local, READ, devs, "^refs/heads/.*");
+    grant(parent, READ, anonymous, "^refs/heads/.*-QA-.*");
+
+    ProjectControl u = user(devs), d = user(devs);
+    assertTrue("u can read", u.controlForRef("refs/heads/foo-QA-bar").isVisible());
+    assertTrue("d can read", d.controlForRef("refs/heads/foo-QA-bar").isVisible());
+  }
+
+  public void testBlockRule_ParentBlocksChild() {
+    grant(local, PUSH, devs, "refs/tags/*");
+    grant(parent, PUSH, anonymous, "refs/tags/*").setBlock();
+
+    ProjectControl u = user(devs);
+    assertFalse("u can't force update tag", u.controlForRef("refs/tags/V10").canForceUpdate());
+  }
+
+  public void testBlockLabelRange_ParentBlocksChild() {
+    grant(local, LABEL + "Code-Review", -2, +2, devs, "refs/heads/*");
+    grant(parent, LABEL + "Code-Review", -2, +2, devs, "refs/heads/*").setBlock();
+
+    ProjectControl u = user(devs);
+
+    PermissionRange range = u.controlForRef("refs/heads/master").getRange(LABEL + "Code-Review");
+    assertTrue("u can vote -1", range.contains(-1));
+    assertTrue("u can vote +1", range.contains(1));
+    assertFalse("u can't vote -2", range.contains(-2));
+    assertFalse("u can't vote 2", range.contains(2));
+  }
+  // -----------------------------------------------------------------------
+
+  private final Map<Project.NameKey, ProjectState> all;
+  private final AllProjectsName allProjectsName = new AllProjectsName("parent");
+  private final ProjectCache projectCache;
+
+  private ProjectConfig local;
+  private ProjectConfig parent;
+  private PermissionCollection.Factory sectionSorter;
+
+  private final AccountGroup.UUID admin = new AccountGroup.UUID("test.admin");
+  private final AccountGroup.UUID anonymous = AccountGroup.ANONYMOUS_USERS;
+  private final AccountGroup.UUID registered = AccountGroup.REGISTERED_USERS;
+
+  private final AccountGroup.UUID devs = new AccountGroup.UUID("test.devs");
+  private final AccountGroup.UUID fixers = new AccountGroup.UUID("test.fixers");
+
+  private final CapabilityControl.Factory capabilityControlFactory;
+
+  public RefControlTest() {
+    all = new HashMap<Project.NameKey, ProjectState>();
+    projectCache = new ProjectCache() {
+      @Override
+      public ProjectState getAllProjects() {
+        return get(allProjectsName);
+      }
+
+      @Override
+      public ProjectState get(Project.NameKey projectName) {
+        return all.get(projectName);
+      }
+
+      @Override
+      public void evict(Project p) {
+      }
+
+      @Override
+      public Iterable<Project.NameKey> all() {
+        return Collections.emptySet();
+      }
+
+      @Override
+      public Iterable<Project.NameKey> byName(String prefix) {
+        return Collections.emptySet();
+      }
+
+      @Override
+      public void onCreateProject(Project.NameKey newProjectName) {
+      }
+    };
+
+    Injector injector = Guice.createInjector(new FactoryModule() {
+      @Override
+      protected void configure() {
+        bind(Config.class)
+            .annotatedWith(GerritServerConfig.class)
+            .toInstance(new Config());
+
+        factory(CapabilityControl.Factory.class);
+        bind(ProjectCache.class).toInstance(projectCache);
+      }
+    });
+    capabilityControlFactory = injector.getInstance(CapabilityControl.Factory.class);
+  }
 
   @Override
-  protected void setUp() throws Exception {
+  public void setUp() throws Exception {
     super.setUp();
-    localRights = new ArrayList<RefRight>();
-    inheritedRights = new ArrayList<RefRight>();
+
+    parent = new ProjectConfig(new Project.NameKey("parent"));
+    parent.createInMemory();
+
+    local = new ProjectConfig(new Project.NameKey("local"));
+    local.createInMemory();
+
+    sectionSorter =
+        new PermissionCollection.Factory(
+            new SectionSortCache(
+                new ConcurrentHashMapCache<SectionSortCache.EntryKey, SectionSortCache.EntryVal>()));
   }
 
   private static void assertOwner(String ref, ProjectControl u) {
@@ -248,68 +349,96 @@
     assertFalse("NOT OWN " + ref, u.controlForRef(ref).isOwner());
   }
 
-  private void grant(Project.NameKey project, ApprovalCategory.Id categoryId,
-      AccountGroup.Id group, String ref, int maxValue) {
-    grant(project, categoryId, group, ref, maxValue, maxValue);
+  private PermissionRule grant(ProjectConfig project, String permissionName,
+      AccountGroup.UUID group, String ref) {
+    return grant(project, permissionName, newRule(project, group), ref);
   }
 
-  private void grant(Project.NameKey project, ApprovalCategory.Id categoryId, AccountGroup.Id group,
-      String ref, int minValue, int maxValue) {
-    RefRight right =
-        new RefRight(new RefRight.Key(project, new RefPattern(ref),
-            categoryId, group));
-    right.setMinValue((short) minValue);
-    right.setMaxValue((short) maxValue);
-
-    if (project == parent) {
-      inheritedRights.add(right);
-    } else if (project == local) {
-      localRights.add(right);
-    } else {
-      fail("Unknown project key: " + project);
-    }
+  private PermissionRule grant(ProjectConfig project, String permissionName,
+      int min, int max, AccountGroup.UUID group, String ref) {
+    PermissionRule rule = newRule(project, group);
+    rule.setMin(min);
+    rule.setMax(max);
+    return grant(project, permissionName, rule, ref);
   }
 
-  private ProjectControl user(AccountGroup.Id... memberOf) {
-    RefControl.Factory refControlFactory = new RefControl.Factory() {
-      @Override
-      public RefControl create(final ProjectControl projectControl, final String ref) {
-        return new RefControl(systemConfig, projectControl, ref);
-      }
-    };
-    return new ProjectControl(systemConfig,
-        Collections.<AccountGroup.Id> emptySet(),
-        Collections.<AccountGroup.Id> emptySet(), refControlFactory,
-        new MockUser(memberOf), newProjectState());
+
+  private PermissionRule grant(ProjectConfig project, String permissionName,
+      PermissionRule rule, String ref) {
+    project.getAccessSection(ref, true) //
+        .getPermission(permissionName, true) //
+        .add(rule);
+    return rule;
+  }
+
+  private void doNotInherit(ProjectConfig project, String permissionName,
+      String ref) {
+    project.getAccessSection(ref, true) //
+        .getPermission(permissionName, true) //
+        .setExclusiveGroup(true);
+  }
+
+  private PermissionRule newRule(ProjectConfig project, AccountGroup.UUID groupUUID) {
+    GroupReference group = new GroupReference(groupUUID, groupUUID.get());
+    group = project.resolve(group);
+
+    return new PermissionRule(group);
+  }
+
+  private ProjectControl user(AccountGroup.UUID... memberOf) {
+    return user(null, memberOf);
+  }
+
+  private ProjectControl user(String name, AccountGroup.UUID... memberOf) {
+    SchemaFactory<ReviewDb> schema = null;
+    GroupCache groupCache = null;
+    String canonicalWebUrl = "http://localhost";
+
+    return new ProjectControl(Collections.<AccountGroup.UUID> emptySet(),
+        Collections.<AccountGroup.UUID> emptySet(), schema, groupCache,
+        sectionSorter,
+        canonicalWebUrl, new MockUser(name, memberOf),
+        newProjectState());
   }
 
   private ProjectState newProjectState() {
-    ProjectCache projectCache = null;
-    Project.NameKey wildProject = new Project.NameKey("-- All Projects --");
+    PrologEnvironment.Factory envFactory = null;
+    GitRepositoryManager mgr = null;
     ProjectControl.AssistedFactory projectControlFactory = null;
-    ProjectState ps =
-        new ProjectState(anonymousUser, projectCache, wildProject,
-            projectControlFactory, new Project(parent), localRights);
-    ps.setInheritedRights(inheritedRights);
-    return ps;
+    RulesCache rulesCache = null;
+    all.put(local.getProject().getNameKey(), new ProjectState(
+        projectCache, allProjectsName, projectControlFactory,
+        envFactory, mgr, rulesCache, local));
+    all.put(parent.getProject().getNameKey(), new ProjectState(
+        projectCache, allProjectsName, projectControlFactory,
+        envFactory, mgr, rulesCache, parent));
+    return all.get(local.getProject().getNameKey());
   }
 
   private class MockUser extends CurrentUser {
-    private final Set<AccountGroup.Id> groups;
+    private final String username;
+    private final GroupMembership groups;
 
-    MockUser(AccountGroup.Id[] groupId) {
-      super(AccessPath.UNKNOWN, RefControlTest.this.authConfig);
-      groups = new HashSet<AccountGroup.Id>(Arrays.asList(groupId));
-      groups.add(registered);
-      groups.add(anonymous);
+    MockUser(String name, AccountGroup.UUID[] groupId) {
+      super(RefControlTest.this.capabilityControlFactory, AccessPath.UNKNOWN);
+      username = name;
+      ArrayList<AccountGroup.UUID> groupIds = Lists.newArrayList(groupId);
+      groupIds.add(registered);
+      groupIds.add(anonymous);
+      groups = new ListGroupMembership(groupIds);
     }
 
     @Override
-    public Set<AccountGroup.Id> getEffectiveGroups() {
+    public GroupMembership getEffectiveGroups() {
       return groups;
     }
 
     @Override
+    public String getUserName() {
+      return username;
+    }
+
+    @Override
     public Set<Change.Id> getStarredChanges() {
       return Collections.emptySet();
     }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/RegexFilePredicateTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/RegexFilePredicateTest.java
index 3a390b5..ce7b25c 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/query/change/RegexFilePredicateTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/query/change/RegexFilePredicateTest.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.reviewdb.Change;
-import com.google.gwtorm.client.OrmException;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gwtorm.server.OrmException;
 
 import junit.framework.TestCase;
 
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaCreatorTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaCreatorTest.java
index ee9af13..8750ee4 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaCreatorTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaCreatorTest.java
@@ -14,22 +14,17 @@
 
 package com.google.gerrit.server.schema;
 
-import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.ApprovalCategory;
-import com.google.gerrit.reviewdb.ApprovalCategoryValue;
-import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.RefRight;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gerrit.reviewdb.SystemConfig;
-import com.google.gerrit.server.workflow.NoOpFunction;
-import com.google.gerrit.server.workflow.SubmitFunction;
+import com.google.gerrit.reviewdb.client.ApprovalCategory;
+import com.google.gerrit.reviewdb.client.ApprovalCategoryValue;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.testutil.InMemoryDatabase;
-import com.google.gwtorm.client.OrmException;
 import com.google.gwtorm.jdbc.JdbcSchema;
+import com.google.gwtorm.server.OrmException;
 
 import junit.framework.TestCase;
 
 import java.io.File;
+import java.io.IOException;
 import java.sql.ResultSet;
 import java.sql.SQLException;
 import java.util.HashSet;
@@ -50,7 +45,8 @@
     super.tearDown();
   }
 
-  public void testGetCauses_CreateSchema() throws OrmException, SQLException {
+  public void testGetCauses_CreateSchema() throws OrmException, SQLException,
+      IOException {
     // Initially the schema should be empty.
     //
     {
@@ -73,11 +69,6 @@
     //
     db.create();
     db.assertSchemaVersion();
-    final SystemConfig config = db.getSystemConfig();
-    assertNotNull(config);
-    assertNotNull(config.adminGroupId);
-    assertNotNull(config.anonymousGroupId);
-    assertNotNull(config.registeredGroupId);
 
     // By default sitePath is set to the current working directory.
     //
@@ -85,88 +76,7 @@
     if (sitePath.getName().equals(".")) {
       sitePath = sitePath.getParentFile();
     }
-    assertEquals(sitePath.getAbsolutePath(), config.sitePath);
-
-    // This is randomly generated and should be at least 20 bytes long.
-    //
-    assertNotNull(config.registerEmailPrivateKey);
-    assertTrue(20 < config.registerEmailPrivateKey.length());
-  }
-
-  public void testSubsequentGetReads() throws OrmException {
-    db.create();
-    final SystemConfig exp = db.getSystemConfig();
-    final SystemConfig act = db.getSystemConfig();
-
-    assertNotSame(exp, act);
-    assertEquals(exp.adminGroupId, act.adminGroupId);
-    assertEquals(exp.anonymousGroupId, act.anonymousGroupId);
-    assertEquals(exp.registeredGroupId, act.registeredGroupId);
-    assertEquals(exp.sitePath, act.sitePath);
-    assertEquals(exp.registerEmailPrivateKey, act.registerEmailPrivateKey);
-  }
-
-  public void testCreateSchema_Group_Administrators() throws OrmException {
-    db.create();
-    final SystemConfig config = db.getSystemConfig();
-    final ReviewDb c = db.open();
-    try {
-      final AccountGroup admin = c.accountGroups().get(config.adminGroupId);
-      assertNotNull(admin);
-      assertEquals(config.adminGroupId, admin.getId());
-      assertEquals("Administrators", admin.getName());
-      assertSame(AccountGroup.Type.INTERNAL, admin.getType());
-    } finally {
-      c.close();
-    }
-  }
-
-  public void testCreateSchema_Group_AnonymousUsers() throws OrmException {
-    db.create();
-    final SystemConfig config = db.getSystemConfig();
-    final ReviewDb c = db.open();
-    try {
-      final AccountGroup anon = c.accountGroups().get(config.anonymousGroupId);
-      assertNotNull(anon);
-      assertEquals(config.anonymousGroupId, anon.getId());
-      assertEquals("Anonymous Users", anon.getName());
-      assertSame(AccountGroup.Type.SYSTEM, anon.getType());
-    } finally {
-      c.close();
-    }
-  }
-
-  public void testCreateSchema_Group_RegisteredUsers() throws OrmException {
-    db.create();
-    final SystemConfig config = db.getSystemConfig();
-    final ReviewDb c = db.open();
-    try {
-      final AccountGroup reg = c.accountGroups().get(config.registeredGroupId);
-      assertNotNull(reg);
-      assertEquals(config.registeredGroupId, reg.getId());
-      assertEquals("Registered Users", reg.getName());
-      assertSame(AccountGroup.Type.SYSTEM, reg.getType());
-    } finally {
-      c.close();
-    }
-  }
-
-  public void testCreateSchema_WildCardProject() throws OrmException {
-    final ReviewDb c = db.create().open();
-    try {
-      final SystemConfig cfg;
-      final Project all;
-
-      cfg = c.systemConfig().get(new SystemConfig.Key());
-      all = c.projects().get(cfg.wildProjectName);
-      assertNotNull(all);
-      assertEquals("-- All Projects --", all.getName());
-      assertFalse(all.isUseContributorAgreements());
-      assertFalse(all.isUseSignedOffBy());
-      assertFalse(all.isRequireChangeID());
-    } finally {
-      c.close();
-    }
+    assertEquals(sitePath.getCanonicalPath(), db.getSystemConfig().sitePath);
   }
 
   public void testCreateSchema_ApprovalCategory_CodeReview()
@@ -182,7 +92,6 @@
       assertEquals("R", cat.getAbbreviatedName());
       assertEquals("MaxWithBlock", cat.getFunctionName());
       assertTrue(cat.isCopyMinScore());
-      assertFalse(cat.isAction());
       assertTrue(0 <= cat.getPosition());
     } finally {
       c.close();
@@ -190,101 +99,6 @@
     assertValueRange(codeReview, -2, -1, 0, 1, 2);
   }
 
-  public void testCreateSchema_ApprovalCategory_Read() throws OrmException {
-    final ReviewDb c = db.create().open();
-    try {
-      final ApprovalCategory cat;
-
-      cat = c.approvalCategories().get(ApprovalCategory.READ);
-      assertNotNull(cat);
-      assertEquals(ApprovalCategory.READ, cat.getId());
-      assertEquals("Read Access", cat.getName());
-      assertNull(cat.getAbbreviatedName());
-      assertEquals(NoOpFunction.NAME, cat.getFunctionName());
-      assertTrue(cat.isAction());
-    } finally {
-      c.close();
-    }
-    assertValueRange(ApprovalCategory.READ, -1, 1, 2, 3);
-  }
-
-  public void testCreateSchema_ApprovalCategory_Submit() throws OrmException {
-    final ReviewDb c = db.create().open();
-    try {
-      final ApprovalCategory cat;
-
-      cat = c.approvalCategories().get(ApprovalCategory.SUBMIT);
-      assertNotNull(cat);
-      assertEquals(ApprovalCategory.SUBMIT, cat.getId());
-      assertEquals("Submit", cat.getName());
-      assertNull(cat.getAbbreviatedName());
-      assertEquals(SubmitFunction.NAME, cat.getFunctionName());
-      assertTrue(cat.isAction());
-    } finally {
-      c.close();
-    }
-    assertValueRange(ApprovalCategory.SUBMIT, 1);
-  }
-
-  public void testCreateSchema_ApprovalCategory_PushTag() throws OrmException {
-    final ReviewDb c = db.create().open();
-    try {
-      final ApprovalCategory cat;
-
-      cat = c.approvalCategories().get(ApprovalCategory.PUSH_TAG);
-      assertNotNull(cat);
-      assertEquals(ApprovalCategory.PUSH_TAG, cat.getId());
-      assertEquals("Push Tag", cat.getName());
-      assertNull(cat.getAbbreviatedName());
-      assertEquals(NoOpFunction.NAME, cat.getFunctionName());
-      assertTrue(cat.isAction());
-    } finally {
-      c.close();
-    }
-    assertValueRange(ApprovalCategory.PUSH_TAG, //
-        ApprovalCategory.PUSH_TAG_SIGNED, //
-        ApprovalCategory.PUSH_TAG_ANNOTATED);
-  }
-
-  public void testCreateSchema_ApprovalCategory_PushHead() throws OrmException {
-    final ReviewDb c = db.create().open();
-    try {
-      final ApprovalCategory cat;
-
-      cat = c.approvalCategories().get(ApprovalCategory.PUSH_HEAD);
-      assertNotNull(cat);
-      assertEquals(ApprovalCategory.PUSH_HEAD, cat.getId());
-      assertEquals("Push Branch", cat.getName());
-      assertNull(cat.getAbbreviatedName());
-      assertEquals(NoOpFunction.NAME, cat.getFunctionName());
-      assertTrue(cat.isAction());
-    } finally {
-      c.close();
-    }
-    assertValueRange(ApprovalCategory.PUSH_HEAD, //
-        ApprovalCategory.PUSH_HEAD_UPDATE, //
-        ApprovalCategory.PUSH_HEAD_CREATE, //
-        ApprovalCategory.PUSH_HEAD_REPLACE);
-  }
-
-  public void testCreateSchema_ApprovalCategory_Owner() throws OrmException {
-    final ReviewDb c = db.create().open();
-    try {
-      final ApprovalCategory cat;
-
-      cat = c.approvalCategories().get(ApprovalCategory.OWN);
-      assertNotNull(cat);
-      assertEquals(ApprovalCategory.OWN, cat.getId());
-      assertEquals("Owner", cat.getName());
-      assertNull(cat.getAbbreviatedName());
-      assertEquals(NoOpFunction.NAME, cat.getFunctionName());
-      assertTrue(cat.isAction());
-    } finally {
-      c.close();
-    }
-    assertValueRange(ApprovalCategory.OWN, 1);
-  }
-
   private void assertValueRange(ApprovalCategory.Id cat, int... range)
       throws OrmException {
     final HashSet<ApprovalCategoryValue.Id> act =
@@ -314,57 +128,4 @@
       fail("Category " + cat + " has additional values: " + act);
     }
   }
-
-  public void testCreateSchema_DefaultAccess_AnonymousUsers()
-      throws OrmException {
-    db.create();
-    final SystemConfig config = db.getSystemConfig();
-    assertDefaultRight("refs/*", config.anonymousGroupId,
-        ApprovalCategory.READ, 1, 1);
-  }
-
-  public void testCreateSchema_DefaultAccess_RegisteredUsers()
-      throws OrmException {
-    db.create();
-    final SystemConfig config = db.getSystemConfig();
-    assertDefaultRight("refs/*", config.registeredGroupId,
-        ApprovalCategory.READ, 1, 2);
-    assertDefaultRight("refs/heads/*", config.registeredGroupId, codeReview,
-        -1, 1);
-  }
-
-  public void testCreateSchema_DefaultAccess_Administrators()
-      throws OrmException {
-    db.create();
-    final SystemConfig config = db.getSystemConfig();
-    assertDefaultRight("refs/*", config.adminGroupId, ApprovalCategory.READ, 1,
-        1);
-  }
-
-  private void assertDefaultRight(final String pattern,
-      final AccountGroup.Id group, final ApprovalCategory.Id category, int min,
-      int max) throws OrmException {
-    final ReviewDb c = db.open();
-    try {
-      final SystemConfig cfg;
-      final Project all;
-      final RefRight right;
-
-      cfg = c.systemConfig().get(new SystemConfig.Key());
-      all = c.projects().get(cfg.wildProjectName);
-      right =
-          c.refRights().get(
-              new RefRight.Key(all.getNameKey(), new RefRight.RefPattern(
-                  pattern), category, group));
-
-      assertNotNull(right);
-      assertEquals(all.getNameKey(), right.getProjectNameKey());
-      assertEquals(group, right.getAccountGroupId());
-      assertEquals(category, right.getApprovalCategoryId());
-      assertEquals(min, right.getMinValue());
-      assertEquals(max, right.getMaxValue());
-    } finally {
-      c.close();
-    }
-  }
 }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaUpdaterTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaUpdaterTest.java
index a009f24..39cbfe4 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaUpdaterTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/schema/SchemaUpdaterTest.java
@@ -14,21 +14,31 @@
 
 package com.google.gerrit.server.schema;
 
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gerrit.reviewdb.SystemConfig;
+import com.google.gerrit.reviewdb.client.SystemConfig;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+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.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.LocalDiskRepositoryManager;
 import com.google.gerrit.testutil.InMemoryDatabase;
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.client.SchemaFactory;
-import com.google.gwtorm.client.StatementExecutor;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.gwtorm.server.StatementExecutor;
 import com.google.inject.AbstractModule;
 import com.google.inject.Guice;
 import com.google.inject.TypeLiteral;
 
 import junit.framework.TestCase;
 
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.PersonIdent;
+
 import java.io.File;
 import java.io.FileNotFoundException;
+import java.io.IOException;
 import java.util.List;
 import java.util.UUID;
 
@@ -47,7 +57,8 @@
     super.tearDown();
   }
 
-  public void testUpdate() throws OrmException, FileNotFoundException {
+  public void testUpdate() throws OrmException, FileNotFoundException,
+      IOException {
     db.create();
 
     final File site = new File(UUID.randomUUID().toString());
@@ -58,6 +69,25 @@
         bind(new TypeLiteral<SchemaFactory<ReviewDb>>() {}).toInstance(db);
         bind(SitePaths.class).toInstance(paths);
         install(new SchemaVersion.Module());
+
+        Config cfg = new Config();
+        cfg.setString("gerrit", null, "basePath", "git");
+        cfg.setString("user", null, "name", "Gerrit Code Review");
+        cfg.setString("user", null, "email", "gerrit@localhost");
+
+        bind(Config.class) //
+            .annotatedWith(GerritServerConfig.class) //
+            .toInstance(cfg);
+
+        bind(PersonIdent.class) //
+            .annotatedWith(GerritPersonIdent.class) //
+            .toProvider(GerritPersonIdentProvider.class);
+
+        bind(AllProjectsName.class)
+            .toInstance(new AllProjectsName("All-Projects"));
+
+        bind(GitRepositoryManager.class) //
+            .to(LocalDiskRepositoryManager.class);
       }
     }).getInstance(SchemaUpdater.class);
 
@@ -82,6 +112,6 @@
 
     db.assertSchemaVersion();
     final SystemConfig sc = db.getSystemConfig();
-    assertEquals(paths.site_path.getAbsolutePath(), sc.sitePath);
+    assertEquals(paths.site_path.getCanonicalPath(), sc.sitePath);
   }
 }
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/CommitMsgHookTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/CommitMsgHookTest.java
index 0d84a93..17fe068 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/CommitMsgHookTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/tools/hooks/CommitMsgHookTest.java
@@ -388,6 +388,25 @@
             "git://example.com/ fixes this\n"));
   }
 
+  @Test
+  public void testWithFalseTags() throws Exception {
+    assertEquals("foo\n" + //
+	"\n" + //
+	"FakeLine:\n" + //
+	"  foo\n" + //
+	"  bar\n" + //
+	"\n" + //
+	"Change-Id: I67632a37fd2e08a35f766f52fc9a47f4ea868c55\n" + //
+	"RealTag: abc\n", //
+	call("foo\n" + //
+	    "\n" + //
+	    "FakeLine:\n" + //
+	    "  foo\n" + //
+	    "  bar\n" + //
+	    "\n" + //
+	    "RealTag: abc\n"));
+  }
+
   private void hookDoesNotModify(final String in) throws Exception {
     assertEquals(in, call(in));
   }
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..c8e684f
--- /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.client.Branch;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.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-server/src/test/java/com/google/gerrit/testutil/InMemoryDatabase.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryDatabase.java
index fe138c6..a44f84f 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryDatabase.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryDatabase.java
@@ -14,24 +14,35 @@
 
 package com.google.gerrit.testutil;
 
-import com.google.gerrit.reviewdb.CurrentSchemaVersion;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gerrit.reviewdb.SystemConfig;
-import com.google.gerrit.server.config.SystemConfigProvider;
+import com.google.gerrit.reviewdb.client.CurrentSchemaVersion;
+import com.google.gerrit.reviewdb.client.SystemConfig;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+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.GerritServerConfig;
+import com.google.gerrit.server.config.SitePath;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.LocalDiskRepositoryManager;
 import com.google.gerrit.server.schema.Current;
 import com.google.gerrit.server.schema.SchemaCreator;
 import com.google.gerrit.server.schema.SchemaVersion;
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.client.SchemaFactory;
 import com.google.gwtorm.jdbc.Database;
 import com.google.gwtorm.jdbc.SimpleDataSource;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.AbstractModule;
 import com.google.inject.Guice;
 import com.google.inject.Key;
-import com.google.inject.Provider;
 
 import junit.framework.TestCase;
 
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.PersonIdent;
+
 import java.io.File;
+import java.io.IOException;
 import java.sql.Connection;
 import java.sql.SQLException;
 import java.util.Properties;
@@ -84,8 +95,36 @@
       database = new Database<ReviewDb>(dataSource, ReviewDb.class);
 
       schemaVersion =
-          Guice.createInjector(new SchemaVersion.Module()).getBinding(
-              Key.get(SchemaVersion.class, Current.class)).getProvider().get();
+          Guice.createInjector(new AbstractModule() {
+            @Override
+            protected void configure() {
+              install(new SchemaVersion.Module());
+
+              bind(File.class) //
+                  .annotatedWith(SitePath.class) //
+                  .toInstance(new File("."));
+
+              Config cfg = new Config();
+              cfg.setString("gerrit", null, "basePath", "git");
+              cfg.setString("user", null, "name", "Gerrit Code Review");
+              cfg.setString("user", null, "email", "gerrit@localhost");
+
+              bind(Config.class) //
+                  .annotatedWith(GerritServerConfig.class) //
+                  .toInstance(cfg);
+
+              bind(PersonIdent.class) //
+                  .annotatedWith(GerritPersonIdent.class) //
+                  .toProvider(GerritPersonIdentProvider.class);
+
+              bind(AllProjectsName.class)
+                  .toInstance(new AllProjectsName("All-Projects"));
+
+              bind(GitRepositoryManager.class) //
+                  .to(LocalDiskRepositoryManager.class);
+            }
+          }).getBinding(Key.get(SchemaVersion.class, Current.class))
+              .getProvider().get();
     } catch (SQLException e) {
       throw new OrmException(e);
     }
@@ -106,7 +145,18 @@
       created = true;
       final ReviewDb c = open();
       try {
-        new SchemaCreator(new File("."), schemaVersion).create(c);
+        try {
+          new SchemaCreator(
+              new File("."),
+              schemaVersion,
+              null,
+              new AllProjectsName("Test-Projects"),
+              new PersonIdent("name", "email@site")).create(c);
+        } catch (IOException e) {
+          throw new OrmException("Cannot create in-memory database", e);
+        } catch (ConfigInvalidException e) {
+          throw new OrmException("Cannot create in-memory database", e);
+        }
       } finally {
         c.close();
       }
@@ -128,12 +178,13 @@
     }
   }
 
-  public SystemConfig getSystemConfig() {
-    return new SystemConfigProvider(this, new Provider<SchemaVersion>() {
-      public SchemaVersion get() {
-        return schemaVersion;
-      }
-    }).get();
+  public SystemConfig getSystemConfig() throws OrmException {
+    final ReviewDb c = open();
+    try {
+      return c.systemConfig().get(new SystemConfig.Key());
+    } finally {
+      c.close();
+    }
   }
 
   public CurrentSchemaVersion getSchemaVersion() throws OrmException {
diff --git a/gerrit-server/src/test/resources/com/google/gerrit/rules/gerrit_common_test.pl b/gerrit-server/src/test/resources/com/google/gerrit/rules/gerrit_common_test.pl
new file mode 100644
index 0000000..db899a7
--- /dev/null
+++ b/gerrit-server/src/test/resources/com/google/gerrit/rules/gerrit_common_test.pl
@@ -0,0 +1,180 @@
+%% 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 gerrit.
+
+
+%% not_same
+%%
+test(not_same_success) :-
+  not_same(ok(a), ok(b)),
+  not_same(label(e, ok(a)), label(e, ok(b))).
+
+
+%% get_legacy_approval_types
+%%
+test(get_legacy_approval_types) :-
+  get_legacy_approval_types(T),
+  T = [C, V],
+  C = approval_type('Code-Review', 'CRVW', 'MaxWithBlock', -2, 2),
+  V = approval_type('Verified', 'VRIF', 'MaxWithBlock', -1, 1).
+
+
+%% commit_label
+%%
+test(commit_label_all) :-
+  findall(commit_label(L, U), commit_label(L, U), Out),
+  all_commit_labels(Ls),
+  Ls = Out.
+
+test(commit_label_CodeReview) :-
+  L = label('Code-Review', _),
+  findall(p(L, U), commit_label(L, U), Out),
+  [ p(label('Code-Review', 2), test_user(bob)),
+    p(label('Code-Review', 2), test_user(alice)) ] == Out.
+
+
+%% max_with_block
+%%
+test(max_with_block_success_accept_max_score) :-
+  max_with_block('Code-Review', -2, 2, ok(test_user(alice))).
+
+test(max_with_block_success_reject_min_score) :-
+  max_with_block('You-Fail', -1, 1, reject(test_user(failer))).
+
+test(max_with_block_success_need_suggest) :-
+  max_with_block('Verified', -1, 1, need(1)).
+
+skip_test(max_with_block_success_impossible) :-
+  max_with_block('Code-Style', 0, 1, impossible(no_access)).
+
+
+%% default_submit
+%%
+test(default_submit_fails) :-
+  findall(P, default_submit(P), All),
+  All = [submit(C, V)],
+  C = label('Code-Review', ok(test_user(alice))),
+  V = label('Verified', need(1)).
+
+
+%% can_submit
+%%
+test(can_submit_ok) :-
+  set_commit_labels([
+    commit_label( label('Code-Review', 2), test_user(alice) ),
+    commit_label( label('Verified', 1), test_user(builder) )
+  ]),
+  can_submit(gerrit:default_submit, S),
+  S = ok(submit(C, V)),
+  C = label('Code-Review', ok(test_user(alice))),
+  V = label('Verified', ok(test_user(builder))).
+
+test(can_submit_not_ready) :-
+  can_submit(gerrit:default_submit, S),
+  S = not_ready(submit(C, V)),
+  C = label('Code-Review', ok(test_user(alice))),
+  V = label('Verified', need(1)).
+
+test(can_submit_only_verified_not_ready) :-
+  can_submit(submit_only_verified, S),
+  S = not_ready(submit(V)),
+  V = label('Verified', need(1)).
+
+
+%% filter_submit_results
+%%
+test(filter_submit_remove_verified) :-
+  can_submit(gerrit:default_submit, R),
+  filter_submit_results(filter_out_v, [R], S),
+  S = [ok(submit(C))],
+  C = label('Code-Review', ok(test_user(alice))).
+
+test(filter_submit_add_code_review) :-
+  set_commit_labels([
+    commit_label( label('Code-Review', 2), test_user(alice) ),
+    commit_label( label('Verified', 1), test_user(builder) )
+  ]),
+  can_submit(submit_only_verified, R),
+  filter_submit_results(filter_in_cr, [R], S),
+  S = [ok(submit(C, V))],
+  C = label('Code-Review', ok(test_user(alice))),
+  V = label('Verified', ok(test_user(builder))).
+
+
+%% find_label
+%%
+test(find_default_code_review) :-
+  can_submit(gerrit:default_submit, R),
+  arg(1, R, S),
+  find_label(S, 'Code-Review', L),
+  L = label('Code-Review', ok(test_user(alice))).
+
+test(find_default_verified) :-
+  can_submit(gerrit:default_submit, R),
+  arg(1, R, S),
+  find_label(S, 'Verified', L),
+  L = label('Verified', need(1)).
+
+
+%% remove_label
+%%
+test(remove_default_code_review) :-
+  can_submit(gerrit:default_submit, R),
+  arg(1, R, S),
+  C = label('Code-Review', ok(test_user(alice))),
+  remove_label(S, C, Out),
+  Out = submit(V),
+  V = label('Verified', need(1)).
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%
+%% Supporting Data
+%%
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+setup :-
+  init,
+  all_commit_labels(Ls),
+  set_commit_labels(Ls).
+
+all_commit_labels(Ls) :-
+  Ls = [
+    commit_label( label('Code-Review', 2), test_user(alice) ),
+    commit_label( label('Code-Review', 2), test_user(bob) ),
+    commit_label( label('You-Fail', -1), test_user(failer) ),
+    commit_label( label('You-Fail', -1), test_user(alice) )
+  ].
+
+submit_only_verified(P) :-
+  max_with_block('Verified', -1, 1, Status),
+  P = submit(label('Verified', Status)).
+
+filter_out_v(R, S) :-
+  find_label(R, 'Verified', Verified), !,
+  remove_label(R, Verified, S).
+filter_out_v(R, S).
+
+filter_in_cr(R, S) :-
+  R =.. [submit | Labels],
+  max_with_block('Code-Review', -2, 2, Status),
+  CR = label('Code-Review', Status),
+  S =.. [submit , CR | Labels].
+
+:- package user.
+test_grant('Code-Review', test_user(alice), range(-2, 2)).
+test_grant('Verified', test_user(builder), range(-1, 1)).
+test_grant('You-Fail', test_user(alice), range(-1, 1)).
+test_grant('You-Fail', test_user(failer), range(-1, 1)).
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/.settings/org.eclipse.core.resources.prefs b/gerrit-sshd/.settings/org.eclipse.core.resources.prefs
index 82eb859..c780f44 100644
--- a/gerrit-sshd/.settings/org.eclipse.core.resources.prefs
+++ b/gerrit-sshd/.settings/org.eclipse.core.resources.prefs
@@ -1,3 +1,4 @@
-#Tue Sep 02 16:59:24 PDT 2008
+#Thu Jul 28 11:02:36 PDT 2011
 eclipse.preferences.version=1
+encoding//src/main/java=UTF-8
 encoding/<project>=UTF-8
diff --git a/gerrit-sshd/.settings/org.eclipse.jdt.core.prefs b/gerrit-sshd/.settings/org.eclipse.jdt.core.prefs
index 04afc7f..470942d 100644
--- a/gerrit-sshd/.settings/org.eclipse.jdt.core.prefs
+++ b/gerrit-sshd/.settings/org.eclipse.jdt.core.prefs
@@ -1,4 +1,4 @@
-#Tue May 12 17:44:13 PDT 2009
+#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
@@ -9,6 +9,7 @@
 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
diff --git a/gerrit-sshd/pom.xml b/gerrit-sshd/pom.xml
index 32bcf57..55e8725 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.1-SNAPSHOT</version>
+    <version>2.4-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/AbstractGitCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AbstractGitCommand.java
index a72b50e..eb8a5c2 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AbstractGitCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AbstractGitCommand.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.sshd;
 
-import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.AccessPath;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.git.GitRepositoryManager;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java
index f7d7226..a926e77 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/BaseCommand.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.sshd;
 
-import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.Project.NameKey;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.Project.NameKey;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.RequestCleanup;
@@ -43,6 +43,7 @@
 import java.io.BufferedWriter;
 import java.io.IOException;
 import java.io.InputStream;
+import java.io.InterruptedIOException;
 import java.io.OutputStream;
 import java.io.OutputStreamWriter;
 import java.io.PrintWriter;
@@ -57,10 +58,7 @@
   private static final int PRIVATE_STATUS = 1 << 30;
   static final int STATUS_CANCEL = PRIVATE_STATUS | 1;
   static final int STATUS_NOT_FOUND = PRIVATE_STATUS | 2;
-  static final int STATUS_NOT_ADMIN = PRIVATE_STATUS | 3;
-
-  @Option(name = "--help", usage = "display this help text", aliases = {"-h"})
-  private boolean help;
+  public static final int STATUS_NOT_ADMIN = PRIVATE_STATUS | 3;
 
   @SuppressWarnings("unused")
   @Option(name = "--", usage = "end of options", handler = EndOfOptionsHandler.class)
@@ -154,28 +152,37 @@
    * @see Argument
    */
   protected void parseCommandLine() throws UnloggedFailure {
-    final CmdLineParser clp = newCmdLineParser();
+    parseCommandLine(this);
+  }
+
+  /**
+   * Parses the command line argument, injecting parsed values into fields.
+   * <p>
+   * This method must be explicitly invoked to cause a parse.
+   *
+   * @param options object whose fields declare Option and Argument annotations
+   *        to describe the parameters of the command. Usually {@code this}.
+   * @throws UnloggedFailure if the command line arguments were invalid.
+   * @see Option
+   * @see Argument
+   */
+  protected void parseCommandLine(Object options) throws UnloggedFailure {
+    final CmdLineParser clp = newCmdLineParser(options);
     try {
       clp.parseArgument(argv);
     } catch (IllegalArgumentException err) {
-      if (!help) {
+      if (!clp.wasHelpRequestedByOption()) {
         throw new UnloggedFailure(1, "fatal: " + err.getMessage());
       }
     } catch (CmdLineException err) {
-      if (!help) {
+      if (!clp.wasHelpRequestedByOption()) {
         throw new UnloggedFailure(1, "fatal: " + err.getMessage());
       }
     }
 
-    if (help) {
-      final StringWriter msg = new StringWriter();
-      msg.write(commandName);
-      clp.printSingleLineUsage(msg, null);
-      msg.write('\n');
-
-      msg.write('\n');
-      clp.printUsage(msg, null);
-      msg.write('\n');
+    if (clp.wasHelpRequestedByOption()) {
+      StringWriter msg = new StringWriter();
+      clp.printDetailedUsage(commandName, msg);
       msg.write(usage());
       throw new UnloggedFailure(1, msg.toString());
     }
@@ -186,8 +193,8 @@
   }
 
   /** Construct a new parser for this command's received command line. */
-  protected CmdLineParser newCmdLineParser() {
-    return cmdLineParserFactory.create(this);
+  protected CmdLineParser newCmdLineParser(Object options) {
+    return cmdLineParserFactory.create(options);
   }
 
   /**
@@ -239,7 +246,8 @@
   protected synchronized void startThread(final CommandRunnable thunk) {
     final TaskThunk tt = new TaskThunk(thunk);
 
-    if (isAdminCommand()||(isAdminHighPriorityCommand() && userProvider.get().isAdministrator())) {
+    if (isAdminCommand() || (isAdminHighPriorityCommand()
+        && userProvider.get().getCapabilities().canAdministrateServer())) {
       // Admin commands should not block the main work threads (there
       // might be an interactive shell there), nor should they wait
       // for the main work threads.
@@ -285,17 +293,13 @@
   }
 
   private int handleError(final Throwable e) {
-    if (e.getClass() == IOException.class
-        && "Pipe closed".equals(e.getMessage())) {
-      // This is sshd telling us the client just dropped off while
-      // we were waiting for a read or a write to complete. Either
-      // way its not really a fatal error. Don't log it.
-      //
-      return 127;
-    }
-
-    if (e.getClass() == SshException.class
-        && "Already closed".equals(e.getMessage())) {
+    if ((e.getClass() == IOException.class
+         && "Pipe closed".equals(e.getMessage()))
+        || //
+        (e.getClass() == SshException.class
+         && "Already closed".equals(e.getMessage()))
+        || //
+        e.getClass() == InterruptedIOException.class) {
       // This is sshd telling us the client just dropped off while
       // we were waiting for a read or a write to complete. Either
       // way its not really a fatal error. Don't log it.
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandExecutorProvider.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandExecutorProvider.java
index a66386f..5c6f80a 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandExecutorProvider.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandExecutorProvider.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.sshd;
 
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.git.QueueProvider;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -33,12 +34,6 @@
 
   @Override
   public WorkQueue.Executor get() {
-    WorkQueue.Executor executor;
-    if (user.isBatchUser()) {
-      executor = queues.getBatchQueue();
-    } else {
-      executor = queues.getInteractiveQueue();
-    }
-    return executor;
+    return queues.getQueue(user.getCapabilities().getQueueType());
   }
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandExecutorQueueProvider.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandExecutorQueueProvider.java
index d5a3373..bafb9ee 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandExecutorQueueProvider.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandExecutorQueueProvider.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.sshd;
 
 import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.git.QueueProvider;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.inject.Inject;
 
@@ -64,13 +65,13 @@
   }
 
   @Override
-  public WorkQueue.Executor getInteractiveQueue() {
-    return interactiveExecutor;
+  public WorkQueue.Executor getQueue(QueueType type) {
+    switch (type) {
+      case INTERACTIVE:
+        return interactiveExecutor;
+      case BATCH:
+      default:
+        return batchExecutor;
+    }
   }
-
-  @Override
-  public WorkQueue.Executor getBatchQueue() {
-    return batchExecutor;
-  }
-
 }
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..66e6add 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandFactoryProvider.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandFactoryProvider.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.sshd;
 
+import com.google.common.util.concurrent.Atomics;
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.sshd.SshScope.Context;
@@ -36,6 +38,11 @@
 import java.util.ArrayList;
 import java.util.List;
 import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
+import java.util.concurrent.Future;
+import java.util.concurrent.ScheduledExecutorService;
+import java.util.concurrent.atomic.AtomicBoolean;
+import java.util.concurrent.atomic.AtomicReference;
 
 /**
  * Creates a CommandFactory using commands registered by {@link CommandModule}.
@@ -46,7 +53,8 @@
 
   private final DispatchCommandProvider dispatcher;
   private final SshLog log;
-  private final Executor startExecutor;
+  private final ScheduledExecutorService startExecutor;
+  private final Executor destroyExecutor;
 
   @Inject
   CommandFactoryProvider(
@@ -58,6 +66,11 @@
 
     int threads = cfg.getInt("sshd","commandStartThreads", 2);
     startExecutor = workQueue.createQueue(threads, "SshCommandStart");
+    destroyExecutor = Executors.newSingleThreadExecutor(
+        new ThreadFactoryBuilder()
+          .setNameFormat("SshCommandDestroy-%s")
+          .setDaemon(true)
+          .build());
   }
 
   @Override
@@ -79,11 +92,14 @@
     private Environment env;
     private Context ctx;
     private DispatchCommand cmd;
-    private boolean logged;
+    private final AtomicBoolean logged;
+    private final AtomicReference<Future<?>> task;
 
     Trampoline(final String cmdLine) {
       commandLine = cmdLine;
       argv = split(cmdLine);
+      logged = new AtomicBoolean();
+      task = Atomics.newReference();
     }
 
     public void setInputStream(final InputStream in) {
@@ -109,7 +125,8 @@
 
     public void start(final Environment env) throws IOException {
       this.env = env;
-      startExecutor.execute(new Runnable() {
+      final Context ctx = this.ctx;
+      task.set(startExecutor.submit(new Runnable() {
         public void run() {
           try {
             onStart();
@@ -123,7 +140,7 @@
         public String toString() {
           return "start (user " + ctx.getSession().getUsername() + ")";
         }
-      });
+      }));
     }
 
     private void onStart() throws IOException {
@@ -172,16 +189,26 @@
     }
 
     private void log(final int rc) {
-      synchronized (this) {
-        if (!logged) {
-          log.onExecute(rc);
-          logged = true;
-        }
+      if (logged.compareAndSet(false, true)) {
+        log.onExecute(rc);
       }
     }
 
     @Override
     public void destroy() {
+      Future<?> future = task.getAndSet(null);
+      if (future != null) {
+        future.cancel(true);
+        destroyExecutor.execute(new Runnable() {
+          @Override
+          public void run() {
+            onDestroy();
+          }
+        });
+      }
+    }
+
+    private void onDestroy() {
       synchronized (this) {
         if (cmd != null) {
           final Context old = SshScope.set(ctx);
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..a96a661 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
@@ -14,11 +14,12 @@
 
 package com.google.gerrit.sshd;
 
-import com.google.gerrit.reviewdb.AccountSshKey;
+import com.google.gerrit.reviewdb.client.AccountSshKey;
 import com.google.gerrit.server.AccessPath;
 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/DispatchCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java
index a421f70..8daa7f4 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java
@@ -71,7 +71,8 @@
 
       final Command cmd = p.get();
 
-      if (isAdminCommand(cmd) && !currentUser.get().isAdministrator()) {
+      if (isAdminCommand(cmd)
+          && !currentUser.get().getCapabilities().canAdministrateServer()) {
         final String msg = "fatal: Not a Gerrit administrator";
         throw new UnloggedFailure(BaseCommand.STATUS_NOT_ADMIN, msg);
       }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/NoShell.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/NoShell.java
index cc9d6ab..be513b3 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/NoShell.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/NoShell.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.sshd;
 
-import com.google.gerrit.reviewdb.Account;
+import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.ssh.SshInfo;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/QueueProvider.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/QueueProvider.java
deleted file mode 100644
index 282472a..0000000
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/QueueProvider.java
+++ /dev/null
@@ -1,11 +0,0 @@
-package com.google.gerrit.sshd;
-
-import com.google.gerrit.server.git.WorkQueue;
-
-public interface QueueProvider {
-
-  public WorkQueue.Executor getInteractiveQueue();
-
-  public WorkQueue.Executor getBatchQueue();
-
-}
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 2052343..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
@@ -120,6 +120,7 @@
   private static final Logger log = LoggerFactory.getLogger(SshDaemon.class);
 
   private final List<SocketAddress> listen;
+  private final List<String> advertisedAddress;
   private final boolean keepAlive;
   private final List<HostKey> hostKeys;
   private volatile IoAcceptor acceptor;
@@ -132,6 +133,7 @@
     setPort(IANA_SSH_PORT /* never used */);
 
     listen = parseListen(cfg);
+    advertisedAddress = parseAdvertisedAddress(cfg);
     reuseAddress = cfg.getBoolean("sshd", "reuseaddress", true);
     keepAlive = cfg.getBoolean("sshd", "tcpkeepalive", true);
 
@@ -214,7 +216,7 @@
 
   @Override
   public synchronized void start() {
-    if (acceptor == null) {
+    if (acceptor == null && !listen.isEmpty()) {
       checkConfig();
 
       acceptor = createAcceptor();
@@ -255,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) {
@@ -262,9 +268,9 @@
       buf.putRawPublicKey(pub);
       final byte[] keyBin = buf.getCompactData();
 
-      for (final InetSocketAddress addr : myAddresses()) {
+      for (final String addr : myAdvertisedAddresses()) {
         try {
-          r.add(new HostKey(SocketUtil.format(addr, IANA_SSH_PORT), keyBin));
+          r.add(new HostKey(addr, keyBin));
         } catch (JSchException e) {
           log.warn("Cannot format SSHD host key", e);
         }
@@ -273,6 +279,19 @@
     return Collections.unmodifiableList(r);
   }
 
+  private List<String> myAdvertisedAddresses() {
+    if (advertisedAddress != null) {
+      return advertisedAddress;
+    } else {
+      List<InetSocketAddress> addrs = myAddresses();
+      List<String> strAddrs = new ArrayList<String>(addrs.size());
+      for (final InetSocketAddress addr : addrs) {
+        strAddrs.add(SocketUtil.format(addr, IANA_SSH_PORT));
+      }
+      return strAddrs;
+    }
+  }
+
   private List<InetSocketAddress> myAddresses() {
     ArrayList<InetSocketAddress> pub = new ArrayList<InetSocketAddress>();
     ArrayList<InetSocketAddress> local = new ArrayList<InetSocketAddress>();
@@ -317,6 +336,14 @@
     return r.toString();
   }
 
+  private List<String> parseAdvertisedAddress(final Config cfg) {
+    final String[] want = cfg.getStringList("sshd", null, "advertisedaddress");
+    if (want.length == 0) {
+      return null;
+    }
+    return Arrays.asList(want);
+  }
+
   private List<SocketAddress> parseListen(final Config cfg) {
     final ArrayList<SocketAddress> bind = new ArrayList<SocketAddress>(2);
     final String[] want = cfg.getStringList("sshd", null, "listenaddress");
@@ -325,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));
@@ -335,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/SshKeyCacheEntry.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheEntry.java
index 81e019e..0d2f1fc 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheEntry.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheEntry.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.sshd;
 
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.AccountSshKey;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountSshKey;
 
 import java.security.PublicKey;
 
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheImpl.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
index c5f64f7..1f5ac28 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshKeyCacheImpl.java
@@ -14,18 +14,18 @@
 
 package com.google.gerrit.sshd;
 
-import static com.google.gerrit.reviewdb.AccountExternalId.SCHEME_USERNAME;
+import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_USERNAME;
 
 import com.google.gerrit.common.errors.InvalidSshKeyException;
-import com.google.gerrit.reviewdb.AccountExternalId;
-import com.google.gerrit.reviewdb.AccountSshKey;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.client.AccountExternalId;
+import com.google.gerrit.reviewdb.client.AccountSshKey;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.cache.Cache;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.cache.EntryCreator;
 import com.google.gerrit.server.ssh.SshKeyCache;
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.client.SchemaFactory;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Module;
 import com.google.inject.Singleton;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLog.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLog.java
index 32d5a07..923ac98 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLog.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLog.java
@@ -18,6 +18,7 @@
 import com.google.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.server.util.IdGenerator;
 import com.google.gerrit.sshd.SshScope.Context;
@@ -33,6 +34,7 @@
 import org.apache.log4j.Logger;
 import org.apache.log4j.spi.ErrorHandler;
 import org.apache.log4j.spi.LoggingEvent;
+import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.util.QuotedString;
 
 import java.io.File;
@@ -59,7 +61,7 @@
 
   @Inject
   SshLog(final Provider<SshSession> session, final Provider<Context> context,
-      final SitePaths site) {
+      final SitePaths site, @GerritServerConfig Config config) {
     this.session = session;
     this.context = context;
 
@@ -77,7 +79,7 @@
 
     async = new AsyncAppender();
     async.setBlocking(true);
-    async.setBufferSize(64);
+    async.setBufferSize(config.getInt("core", "asyncLoggingBufferSize", 64));
     async.setLocationInfo(false);
     async.addAppender(dst);
     async.activateOptions();
@@ -99,7 +101,7 @@
   void onAuthFail(final SshSession sd) {
     final LoggingEvent event = new LoggingEvent( //
         Logger.class.getName(), // fqnOfCategoryClass
-        null, // logger (optional)
+        log, // logger
         System.currentTimeMillis(), // when
         Level.INFO, // level
         "AUTH FAILURE FROM " + sd.getRemoteAddressAsString(), // message text
@@ -168,7 +170,7 @@
 
     final LoggingEvent event = new LoggingEvent( //
         Logger.class.getName(), // fqnOfCategoryClass
-        null, // logger (optional)
+        log, // logger
         System.currentTimeMillis(), // when
         Level.INFO, // level
         msg, // message text
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 5b8edf0..54b0bb5 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
@@ -17,9 +17,9 @@
 import static com.google.inject.Scopes.SINGLETON;
 
 import com.google.gerrit.lifecycle.LifecycleModule;
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.PatchSet;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PeerDaemonUser;
@@ -28,10 +28,13 @@
 import com.google.gerrit.server.account.ChangeUserName;
 import com.google.gerrit.server.config.FactoryModule;
 import com.google.gerrit.server.config.GerritRequestModule;
+import com.google.gerrit.server.git.QueueProvider;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.ssh.SshInfo;
+import com.google.gerrit.server.util.RequestScopePropagator;
 import com.google.gerrit.sshd.args4j.AccountGroupIdHandler;
+import com.google.gerrit.sshd.args4j.AccountGroupUUIDHandler;
 import com.google.gerrit.sshd.args4j.AccountIdHandler;
 import com.google.gerrit.sshd.args4j.PatchSetIdHandler;
 import com.google.gerrit.sshd.args4j.ProjectControlHandler;
@@ -39,11 +42,7 @@
 import com.google.gerrit.sshd.commands.DefaultCommandModule;
 import com.google.gerrit.sshd.commands.QueryShell;
 import com.google.gerrit.util.cli.CmdLineParser;
-import com.google.gerrit.util.cli.OptionHandlerFactory;
 import com.google.gerrit.util.cli.OptionHandlerUtil;
-import com.google.inject.Key;
-import com.google.inject.TypeLiteral;
-import com.google.inject.assistedinject.FactoryProvider;
 import com.google.inject.servlet.RequestScoped;
 
 import org.apache.sshd.common.KeyPairProvider;
@@ -58,6 +57,7 @@
   @Override
   protected void configure() {
     bindScope(RequestScoped.class, SshScope.REQUEST);
+    bind(RequestScopePropagator.class).to(SshScope.Propagator.class);
 
     configureRequestScope();
     configureCmdLineParser();
@@ -117,6 +117,7 @@
 
     registerOptionHandler(Account.Id.class, AccountIdHandler.class);
     registerOptionHandler(AccountGroup.Id.class, AccountGroupIdHandler.class);
+    registerOptionHandler(AccountGroup.UUID.class, AccountGroupUUIDHandler.class);
     registerOptionHandler(PatchSet.Id.class, PatchSetIdHandler.class);
     registerOptionHandler(ProjectControl.class, ProjectControlHandler.class);
     registerOptionHandler(SocketAddress.class, SocketAddressHandler.class);
@@ -124,14 +125,6 @@
 
   private <T> void registerOptionHandler(Class<T> type,
       Class<? extends OptionHandler<T>> impl) {
-    final Key<OptionHandlerFactory<T>> key = OptionHandlerUtil.keyFor(type);
-
-    final TypeLiteral<OptionHandlerFactory<T>> factoryType =
-        new TypeLiteral<OptionHandlerFactory<T>>() {};
-
-    final TypeLiteral<? extends OptionHandler<T>> implType =
-        TypeLiteral.get(impl);
-
-    bind(key).toProvider(FactoryProvider.newFactory(factoryType, implType));
+    install(OptionHandlerUtil.moduleFor(type, impl));
   }
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshScope.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshScope.java
index d32d429..92609b5 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshScope.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshScope.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.sshd;
 
 import com.google.gerrit.server.RequestCleanup;
+import com.google.gerrit.server.util.ThreadLocalRequestScopePropagator;
 import com.google.inject.Key;
 import com.google.inject.OutOfScopeException;
 import com.google.inject.Provider;
@@ -38,7 +39,7 @@
     volatile long started;
     volatile long finished;
 
-    Context(final SshSession s, final String c) {
+    private Context(final SshSession s, final String c, final long at) {
       cleanup = new RequestCleanup();
       session = s;
       commandLine = c;
@@ -46,25 +47,17 @@
       map = new HashMap<Key<?>, Object>();
       map.put(RC_KEY, cleanup);
 
-      final long now = System.currentTimeMillis();
-      created = now;
-      started = now;
-      finished = now;
+      created = started = finished = at;
     }
 
     private Context(Context p, SshSession s, String c) {
-      cleanup = new RequestCleanup();
-      session = s;
-      commandLine = c;
-
-      map = new HashMap<Key<?>, Object>();
-      map.put(RC_KEY, cleanup);
-
-      created = p.created;
+      this(s, c, p.created);
       started = p.started;
       finished = p.finished;
+    }
 
-      p.cleanup.add(cleanup);
+    Context(final SshSession s, final String c) {
+      this(s, c, System.currentTimeMillis());
     }
 
     String getCommandLine() {
@@ -86,28 +79,43 @@
     }
 
     synchronized Context subContext(SshSession newSession, String newCommandLine) {
-      return new Context(this, newSession, newCommandLine);
+      Context ctx = new Context(this, newSession, newCommandLine);
+      cleanup.add(ctx.cleanup);
+      return ctx;
     }
   }
 
   static class ContextProvider implements Provider<Context> {
     @Override
     public Context get() {
-      return getContext();
+      return requireContext();
     }
   }
 
   static class SshSessionProvider implements Provider<SshSession> {
     @Override
     public SshSession get() {
-      return getContext().getSession();
+      return requireContext().getSession();
+    }
+  }
+
+  static class Propagator extends ThreadLocalRequestScopePropagator<Context> {
+    Propagator() {
+      super(REQUEST, current);
+    }
+
+    @Override
+    protected Context continuingContext(Context ctx) {
+      // The cleanup is not chained, since the RequestScopePropagator executors
+      // the Context's cleanup when finished executing.
+      return new Context(ctx, ctx.getSession(), ctx.getCommandLine());
     }
   }
 
   private static final ThreadLocal<Context> current =
       new ThreadLocal<Context>();
 
-  private static Context getContext() {
+  private static Context requireContext() {
     final Context ctx = current.get();
     if (ctx == null) {
       throw new OutOfScopeException("Not in command/request");
@@ -126,7 +134,7 @@
     public <T> Provider<T> scope(final Key<T> key, final Provider<T> creator) {
       return new Provider<T>() {
         public T get() {
-          return getContext().get(key, creator);
+          return requireContext().get(key, creator);
         }
 
         @Override
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshUtil.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshUtil.java
index 65cfcdb..da245a3 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshUtil.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshUtil.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.sshd;
 
-import com.google.gerrit.reviewdb.AccountSshKey;
+import com.google.gerrit.reviewdb.client.AccountSshKey;
 
 import org.apache.commons.codec.binary.Base64;
 import org.apache.sshd.common.KeyPairProvider;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java
index 199d210..492966e 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SuExec.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.sshd;
 
-import com.google.gerrit.reviewdb.Account;
+import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.AccessPath;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/AccountGroupIdHandler.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/AccountGroupIdHandler.java
index f269541..307a10a 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/AccountGroupIdHandler.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/AccountGroupIdHandler.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.sshd.args4j;
 
-import com.google.gerrit.reviewdb.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.account.GroupCache;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
@@ -29,11 +29,10 @@
 public class AccountGroupIdHandler extends OptionHandler<AccountGroup.Id> {
   private final GroupCache groupCache;
 
-  @SuppressWarnings({"unchecked", "rawtypes"})
   @Inject
   public AccountGroupIdHandler(final GroupCache groupCache,
       @Assisted final CmdLineParser parser, @Assisted final OptionDef option,
-      @Assisted final Setter setter) {
+      @Assisted final Setter<AccountGroup.Id> setter) {
     super(parser, option, setter);
     this.groupCache = groupCache;
   }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/AccountGroupUUIDHandler.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/AccountGroupUUIDHandler.java
new file mode 100644
index 0000000..49bf695
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/AccountGroupUUIDHandler.java
@@ -0,0 +1,56 @@
+// 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.args4j;
+
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.account.GroupCache;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import org.kohsuke.args4j.CmdLineException;
+import org.kohsuke.args4j.CmdLineParser;
+import org.kohsuke.args4j.OptionDef;
+import org.kohsuke.args4j.spi.OptionHandler;
+import org.kohsuke.args4j.spi.Parameters;
+import org.kohsuke.args4j.spi.Setter;
+
+public class AccountGroupUUIDHandler extends OptionHandler<AccountGroup.UUID> {
+  private final GroupCache groupCache;
+
+  @Inject
+  public AccountGroupUUIDHandler(final GroupCache groupCache,
+      @Assisted final CmdLineParser parser, @Assisted final OptionDef option,
+      @Assisted final Setter<AccountGroup.UUID> setter) {
+    super(parser, option, setter);
+    this.groupCache = groupCache;
+  }
+
+  @Override
+  public final int parseArguments(final Parameters params)
+      throws CmdLineException {
+    final String n = params.getParameter(0);
+    final AccountGroup group = groupCache.get(new AccountGroup.NameKey(n));
+    if (group == null) {
+      throw new CmdLineException(owner, "Group \"" + n + "\" does not exist");
+    }
+    setter.addValue(group.getGroupUUID());
+    return 1;
+  }
+
+  @Override
+  public final String getDefaultMetaVariable() {
+    return "GROUP";
+  }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/AccountIdHandler.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/AccountIdHandler.java
index b8bf0fd..d54ae34 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/AccountIdHandler.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/AccountIdHandler.java
@@ -14,13 +14,14 @@
 
 package com.google.gerrit.sshd.args4j;
 
-import com.google.gerrit.reviewdb.Account;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AuthType;
 import com.google.gerrit.server.account.AccountException;
 import com.google.gerrit.server.account.AccountManager;
 import com.google.gerrit.server.account.AccountResolver;
 import com.google.gerrit.server.account.AuthRequest;
-import com.google.gerrit.server.account.AuthResult;
-import com.google.gwtorm.client.OrmException;
+import com.google.gerrit.server.config.AuthConfig;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -34,16 +35,18 @@
 public class AccountIdHandler extends OptionHandler<Account.Id> {
   private final AccountResolver accountResolver;
   private final AccountManager accountManager;
+  private final AuthType authType;
 
-  @SuppressWarnings({"unchecked", "rawtypes"})
   @Inject
   public AccountIdHandler(final AccountResolver accountResolver,
       final AccountManager accountManager,
+      final AuthConfig authConfig,
       @Assisted final CmdLineParser parser, @Assisted final OptionDef option,
-      @Assisted final Setter setter) {
+      @Assisted final Setter<Account.Id> setter) {
     super(parser, option, setter);
     this.accountResolver = accountResolver;
     this.accountManager = accountManager;
+    this.authType = authConfig.getAuthType();
   }
 
   @Override
@@ -56,7 +59,15 @@
       if (a != null) {
         accountId = a.getId();
       } else {
-        accountId = createAccountIfUserCanBeAuthenticated(token);
+        switch (authType) {
+          case HTTP_LDAP:
+          case CLIENT_SSL_CERT_LDAP:
+          case LDAP:
+            accountId = createAccountByLdap(token);
+            break;
+          default:
+            throw new CmdLineException(owner, "user \"" + token + "\" not found");
+        }
       }
     } catch (OrmException e) {
       throw new CmdLineException(owner, "database is down");
@@ -65,15 +76,18 @@
     return 1;
   }
 
-  private Account.Id createAccountIfUserCanBeAuthenticated(final String username)
+  private Account.Id createAccountByLdap(String user)
       throws CmdLineException {
+    if (!user.matches(Account.USER_NAME_PATTERN)) {
+      throw new CmdLineException(owner, "user \"" + user + "\" not found");
+    }
+
     try {
-      final AuthRequest areq = AuthRequest.forUser(username);
-      final AuthResult arsp = accountManager.authenticate(areq);
-      return arsp.getAccountId();
+      AuthRequest req = AuthRequest.forUser(user);
+      req.setSkipAuthentication(true);
+      return accountManager.authenticate(req).getAccountId();
     } catch (AccountException e) {
-      throw new CmdLineException(owner, "Unable to authenticate user \""
-          + username + "\"", e);
+      throw new CmdLineException(owner, "user \"" + user + "\" not found");
     }
   }
 
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/PatchSetIdHandler.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/PatchSetIdHandler.java
index 847184d..2d6a4df 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/PatchSetIdHandler.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/PatchSetIdHandler.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.sshd.args4j;
 
-import com.google.gerrit.reviewdb.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -26,10 +26,10 @@
 import org.kohsuke.args4j.spi.Setter;
 
 public class PatchSetIdHandler extends OptionHandler<PatchSet.Id> {
-  @SuppressWarnings({"unchecked", "rawtypes"})
+
   @Inject
   public PatchSetIdHandler(@Assisted final CmdLineParser parser,
-      @Assisted final OptionDef option, @Assisted final Setter setter) {
+      @Assisted final OptionDef option, @Assisted final Setter<PatchSet.Id> setter) {
     super(parser, option, setter);
   }
 
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/ProjectControlHandler.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/ProjectControlHandler.java
index 48733c03b..e0f7c4c 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/ProjectControlHandler.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/ProjectControlHandler.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.sshd.args4j;
 
-import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.inject.Inject;
@@ -30,12 +30,11 @@
 public class ProjectControlHandler extends OptionHandler<ProjectControl> {
   private final ProjectControl.Factory projectControlFactory;
 
-  @SuppressWarnings({"unchecked", "rawtypes"})
   @Inject
   public ProjectControlHandler(
       final ProjectControl.Factory projectControlFactory,
       @Assisted final CmdLineParser parser, @Assisted final OptionDef option,
-      @Assisted final Setter setter) {
+      @Assisted final Setter<ProjectControl> setter) {
     super(parser, option, setter);
     this.projectControlFactory = projectControlFactory;
   }
@@ -46,14 +45,21 @@
     final String token = params.getParameter(0);
     String projectName = token;
 
+    while (projectName.endsWith("/")) {
+      projectName = projectName.substring(0, projectName.length() - 1);
+    }
+
     if (projectName.endsWith(".git")) {
       // Be nice and drop the trailing ".git" suffix, which we never keep
       // in our database, but clients might mistakenly provide anyway.
       //
       projectName = projectName.substring(0, projectName.length() - 4);
+      while (projectName.endsWith("/")) {
+        projectName = projectName.substring(0, projectName.length() - 1);
+      }
     }
 
-    if (projectName.startsWith("/")) {
+    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
@@ -64,8 +70,8 @@
 
     final ProjectControl control;
     try {
-      control =
-          projectControlFactory.validateFor(new Project.NameKey(projectName));
+      Project.NameKey nameKey = new Project.NameKey(projectName);
+      control = projectControlFactory.validateFor(nameKey);
     } catch (NoSuchProjectException e) {
       throw new CmdLineException(owner, "'" + token + "': not a Gerrit project");
     }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/SocketAddressHandler.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/SocketAddressHandler.java
index 50e41ae..454a084 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/SocketAddressHandler.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/SocketAddressHandler.java
@@ -28,10 +28,10 @@
 import java.net.SocketAddress;
 
 public class SocketAddressHandler extends OptionHandler<SocketAddress> {
-  @SuppressWarnings({"unchecked", "rawtypes"})
+
   @Inject
   public SocketAddressHandler(@Assisted final CmdLineParser parser,
-      @Assisted final OptionDef option, @Assisted final Setter setter) {
+      @Assisted final OptionDef option, @Assisted final Setter<SocketAddress> setter) {
     super(parser, option, setter);
   }
 
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/SubcommandHandler.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/SubcommandHandler.java
index 434230f..3df73a8 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/SubcommandHandler.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/args4j/SubcommandHandler.java
@@ -25,10 +25,10 @@
 import org.kohsuke.args4j.spi.Setter;
 
 public class SubcommandHandler extends OptionHandler<String> {
-  @SuppressWarnings({"unchecked", "rawtypes"})
+
   @Inject
   public SubcommandHandler(@Assisted final CmdLineParser parser,
-      @Assisted final OptionDef option, @Assisted final Setter setter) {
+      @Assisted final OptionDef option, @Assisted final Setter<String> setter) {
     super(parser, option, setter);
   }
 
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 07c573d..950bf341 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java
@@ -14,62 +14,93 @@
 
 package com.google.gerrit.sshd.commands;
 
-import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gerrit.server.config.WildProjectName;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.sshd.AdminCommand;
 import com.google.gerrit.sshd.BaseCommand;
-import com.google.gwtorm.client.OrmException;
 import com.google.inject.Inject;
 
 import org.apache.sshd.server.Environment;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.kohsuke.args4j.Argument;
 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.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 
 @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")
-  private List<ProjectControl> children = new ArrayList<ProjectControl>();
+  @Option(name = "--children-of", metaVar = "NAME",
+      usage = "parent project for which the child projects should be reparented")
+  private ProjectControl oldParent;
 
-  @Inject
-  private ReviewDb db;
+  @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
   private ProjectCache projectCache;
 
   @Inject
-  @WildProjectName
-  private Project.NameKey wildProject;
+  private MetaDataUpdate.User metaDataUpdateFactory;
+
+  @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 OrmException, UnloggedFailure {
+  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(wildProject);
+    grandParents.add(allProjectsName);
 
     if (newParent != null) {
       newParentKey = newParent.getProject().getNameKey();
@@ -86,48 +117,64 @@
           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 (wildProject.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 '"
-            + (newParentKey != null ? newParentKey.get() : wildProject.get())
+            + (newParentKey != null ? newParentKey.get() : allProjectsName.get())
             + "'\n");
         continue;
       }
 
-      final Project child = db.projects().get(key);
-      if (child == null) {
-        // Race condition? Its in the cache, but not the database.
-        //
-        err.append("error: Project '" + name + "' not found\n");
-        continue;
+      try {
+        MetaDataUpdate md = metaDataUpdateFactory.create(nameKey);
+        try {
+          ProjectConfig config = ProjectConfig.read(md);
+          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");
+          }
+        } finally {
+          md.close();
+        }
+      } catch (RepositoryNotFoundException notFound) {
+        err.append("error: Project " + name + " not found\n");
+      } catch (IOException e) {
+        final String msg = "Cannot update project " + name;
+        log.error(msg, e);
+        err.append("error: " + msg + "\n");
+      } catch (ConfigInvalidException e) {
+        final String msg = "Cannot update project " + name;
+        log.error(msg, e);
+        err.append("error: " + msg + "\n");
       }
 
-      child.setParent(newParentKey);
-      db.projects().update(Collections.singleton(child));
+      projectCache.evict(project);
     }
 
-    // Invalidate all projects in cache since inherited rights were changed.
-    //
-    projectCache.evictAll();
-
     if (err.length() > 0) {
       while (err.charAt(err.length() - 1) == '\n') {
         err.setLength(err.length() - 1);
@@ -135,4 +182,66 @@
       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 = e.getProject().getParent(allProjectsName);
+    }
+    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(e.getProject().getParent(projectName))) {
+        childProjects.add(e.getProject());
+      }
+    }
+    return childProjects;
+  }
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminShowCaches.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminShowCaches.java
deleted file mode 100644
index c27ad0d..0000000
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminShowCaches.java
+++ /dev/null
@@ -1,208 +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.sshd.AdminCommand;
-
-import net.sf.ehcache.Ehcache;
-import net.sf.ehcache.Statistics;
-import net.sf.ehcache.config.CacheConfiguration;
-
-import org.apache.sshd.server.Environment;
-import org.eclipse.jgit.storage.file.WindowCacheStatAccessor;
-
-import java.io.PrintWriter;
-
-/** Show the current cache states. */
-@AdminCommand
-final class AdminShowCaches extends CacheCommand {
-  private PrintWriter p;
-
-  @Override
-  public void start(final Environment env) {
-    startThread(new CommandRunnable() {
-      @Override
-      public void run() throws Exception {
-        parseCommandLine();
-        display();
-      }
-    });
-  }
-
-  private void display() {
-    p = toPrintWriter(out);
-
-    p.print(String.format(//
-        "%1s %-18s %-4s|%-20s|  %-5s  |%-14s|\n" //
-        , "" //
-        , "Name" //
-        , "Max" //
-        , "Object Count" //
-        , "AvgGet" //
-        , "Hit Ratio" //
-    ));
-    p.print(String.format(//
-        "%1s %-18s %-4s|%6s %6s %6s|  %-5s   |%-4s %-4s %-4s|\n" //
-        , "" //
-        , "" //
-        , "Age" //
-        , "Disk" //
-        , "Mem" //
-        , "Cnt" //
-        , "" //
-        , "Disk" //
-        , "Mem" //
-        , "Agg" //
-    ));
-    p.println("------------------"
-        + "-------+--------------------+----------+--------------+");
-    for (final Ehcache cache : getAllCaches()) {
-      final CacheConfiguration cfg = cache.getCacheConfiguration();
-      final boolean useDisk = cfg.isDiskPersistent() || cfg.isOverflowToDisk();
-      final Statistics stat = cache.getStatistics();
-      final long total = stat.getCacheHits() + stat.getCacheMisses();
-
-      if (useDisk) {
-        p.print(String.format(//
-            "D %-18s %-4s|%6s %6s %6s| %7s  |%4s %4s %4s|\n" //
-            , cache.getName() //
-            , interval(cfg.getTimeToLiveSeconds()) //
-            , count(stat.getDiskStoreObjectCount()) //
-            , count(stat.getMemoryStoreObjectCount()) //
-            , count(stat.getObjectCount()) //
-            , duration(stat.getAverageGetTime()) //
-            , percent(stat.getOnDiskHits(), total) //
-            , percent(stat.getInMemoryHits(), total) //
-            , percent(stat.getCacheHits(), total) //
-            ));
-      } else {
-        p.print(String.format(//
-            "  %-18s %-4s|%6s %6s %6s| %7s  |%4s %4s %4s|\n" //
-            , cache.getName() //
-            , interval(cfg.getTimeToLiveSeconds()) //
-            , "", "" //
-            , count(stat.getObjectCount()) //
-            , duration(stat.getAverageGetTime()) //
-            , "", "" //
-            , percent(stat.getCacheHits(), total) //
-            ));
-      }
-    }
-    p.println();
-
-    final Runtime r = Runtime.getRuntime();
-    final long mMax = r.maxMemory();
-    final long mFree = r.freeMemory();
-    final long mTotal = r.totalMemory();
-    final long mInuse = mTotal - mFree;
-    final long jgitBytes = WindowCacheStatAccessor.getOpenBytes();
-
-    p.println("JGit Buffer Cache:");
-    fItemCount("open files", WindowCacheStatAccessor.getOpenFiles());
-    fByteCount("loaded", jgitBytes);
-    fPercent("mem%", jgitBytes, mTotal);
-    p.println();
-
-    p.println("JVM Heap:");
-    fByteCount("max", mMax);
-    fByteCount("inuse", mInuse);
-    fPercent("mem%", mInuse, mTotal);
-    p.println();
-
-    p.flush();
-  }
-
-  private void fItemCount(final String name, final long value) {
-    p.println(String.format("  %1$-12s: %2$15d", name, value));
-  }
-
-  private void fByteCount(final String name, double value) {
-    String suffix = "bytes";
-    if (value > 1024) {
-      value /= 1024;
-      suffix = "kb";
-    }
-    if (value > 1024) {
-      value /= 1024;
-      suffix = "mb";
-    }
-    if (value > 1024) {
-      value /= 1024;
-      suffix = "gb";
-    }
-    p.println(String.format("  %1$-12s: %2$6.2f %3$s", name, value, suffix));
-  }
-
-  private String count(long cnt) {
-    if (cnt == 0) {
-      return "";
-    }
-    return String.format("%6d", cnt);
-  }
-
-  private String duration(double ms) {
-    if (Math.abs(ms) <= 0.05) {
-      return "";
-    }
-    String suffix = "ms";
-    if (ms >= 1000) {
-      ms /= 1000;
-      suffix = "s ";
-    }
-    return String.format("%4.1f%s", ms, suffix);
-  }
-
-  private String interval(double ttl) {
-    if (ttl == 0) {
-      return "inf";
-    }
-
-    String suffix = "s";
-    if (ttl >= 60) {
-      ttl /= 60;
-      suffix = "m";
-
-      if (ttl >= 60) {
-        ttl /= 60;
-        suffix = "h";
-      }
-
-      if (ttl >= 24) {
-        ttl /= 24;
-        suffix = "d";
-
-        if (ttl >= 365) {
-          ttl /= 365;
-          suffix = "y";
-        }
-      }
-    }
-
-    return Integer.toString((int) ttl) + suffix;
-  }
-
-  private String percent(final long value, final long total) {
-    if (total <= 0) {
-      return "";
-    }
-    final long pcent = (100 * value) / total;
-    return String.format("%3d%%", (int) pcent);
-  }
-
-  private void fPercent(final String name, final long value, final long total) {
-    final long pcent = 0 < total ? (100 * value) / total : 0;
-    p.println(String.format("  %1$-12s: %2$3d%%", name, (int) pcent));
-  }
-}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ApproveOption.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ApproveOption.java
index cc23d1d..a1b8988 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ApproveOption.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ApproveOption.java
@@ -15,8 +15,8 @@
 package com.google.gerrit.sshd.commands;
 
 import com.google.gerrit.common.data.ApprovalType;
-import com.google.gerrit.reviewdb.ApprovalCategory;
-import com.google.gerrit.reviewdb.ApprovalCategoryValue;
+import com.google.gerrit.reviewdb.client.ApprovalCategory;
+import com.google.gerrit.reviewdb.client.ApprovalCategoryValue;
 
 import org.kohsuke.args4j.CmdLineException;
 import org.kohsuke.args4j.CmdLineParser;
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/AdminCreateAccount.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
similarity index 86%
rename from gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminCreateAccount.java
rename to gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
index 06e92ca..38e0bcb 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminCreateAccount.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
@@ -15,21 +15,20 @@
 package com.google.gerrit.sshd.commands;
 
 import com.google.gerrit.common.errors.InvalidSshKeyException;
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.AccountExternalId;
-import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.AccountGroupMember;
-import com.google.gerrit.reviewdb.AccountGroupMemberAudit;
-import com.google.gerrit.reviewdb.AccountSshKey;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountExternalId;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupMember;
+import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
+import com.google.gerrit.reviewdb.client.AccountSshKey;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountByEmailCache;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.ssh.SshKeyCache;
-import com.google.gerrit.sshd.AdminCommand;
 import com.google.gerrit.sshd.BaseCommand;
-import com.google.gwtorm.client.OrmDuplicateKeyException;
-import com.google.gwtorm.client.OrmException;
+import com.google.gwtorm.server.OrmDuplicateKeyException;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 
 import org.apache.sshd.server.Environment;
@@ -46,8 +45,7 @@
 import java.util.List;
 
 /** Create a new user account. **/
-@AdminCommand
-final class AdminCreateAccount extends BaseCommand {
+final class CreateAccountCommand extends BaseCommand {
   @Option(name = "--group", aliases = {"-g"}, metaVar = "GROUP", usage = "groups to add account to")
   private List<AccountGroup.Id> groups = new ArrayList<AccountGroup.Id>();
 
@@ -83,6 +81,13 @@
     startThread(new CommandRunnable() {
       @Override
       public void run() throws Exception {
+        if (!currentUser.getCapabilities().canCreateAccount()) {
+          String msg = String.format(
+            "fatal: %s does not have \"Create Account\" capability.",
+            currentUser.getUserName());
+          throw new UnloggedFailure(BaseCommand.STATUS_NOT_ADMIN, msg);
+        }
+
         parseCommandLine();
         createAccount();
       }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminCreateGroup.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
similarity index 77%
rename from gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminCreateGroup.java
rename to gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
index 8622741..98202e2 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminCreateGroup.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
@@ -15,19 +15,17 @@
 package com.google.gerrit.sshd.commands;
 
 import com.google.gerrit.common.errors.NameAlreadyUsedException;
-import com.google.gerrit.reviewdb.Account;
-import com.google.gerrit.reviewdb.AccountGroup;
+import com.google.gerrit.common.errors.PermissionDeniedException;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.account.PerformCreateGroup;
-import com.google.gerrit.sshd.AdminCommand;
 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 org.kohsuke.args4j.Option;
 
-import java.io.IOException;
 import java.util.HashSet;
 import java.util.Set;
 
@@ -36,8 +34,7 @@
  * <p>
  * Optionally, puts an initial set of user in the newly created group.
  */
-@AdminCommand
-public class AdminCreateGroup extends BaseCommand {
+final class CreateGroupCommand extends BaseCommand {
   @Option(name = "--owner", aliases = {"-o"}, metaVar = "GROUP", usage = "owning group, if not specified the group will be self-owning")
   private AccountGroup.Id ownerGroupId;
 
@@ -68,24 +65,25 @@
   private PerformCreateGroup.Factory performCreateGroupFactory;
 
   @Override
-  public void start(Environment env) throws IOException {
+  public void start(Environment env) {
     startThread(new CommandRunnable() {
       @Override
       public void run() throws Exception {
         parseCommandLine();
-        createGroup();
+        try {
+          performCreateGroupFactory.create().createGroup(groupName,
+              groupDescription,
+              visibleToAll,
+              ownerGroupId,
+              initialMembers,
+              initialGroups);
+        } catch (PermissionDeniedException e) {
+          throw die(e);
+
+        } catch (NameAlreadyUsedException e) {
+          throw die(e);
+        }
       }
     });
   }
-
-  private void createGroup() throws OrmException, UnloggedFailure {
-    final PerformCreateGroup performCreateGroup =
-        performCreateGroupFactory.create();
-    try {
-      performCreateGroup.createGroup(groupName, groupDescription, visibleToAll,
-          ownerGroupId, initialMembers, initialGroups);
-    } catch (NameAlreadyUsedException e) {
-      throw die(e);
-    }
-  }
-}
\ No newline at end of file
+}
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 0be02fd..0000000
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProject.java
+++ /dev/null
@@ -1,264 +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.CollectionsUtil;
-import com.google.gerrit.reviewdb.AccountGroup;
-import com.google.gerrit.reviewdb.ApprovalCategory;
-import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.RefRight;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gerrit.reviewdb.Project.SubmitType;
-import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.config.ProjectCreatorGroups;
-import com.google.gerrit.server.config.ProjectOwnerGroups;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.ReplicationQueue;
-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.eclipse.jgit.lib.CommitBuilder;
-import org.eclipse.jgit.lib.ConfigConstants;
-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.eclipse.jgit.lib.StoredConfig;
-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.Collections;
-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", required = true, aliases = {"-n"}, metaVar = "NAME", usage = "name of project to be created")
-  private String projectName;
-
-  @Option(name = "--owner", aliases = {"-o"}, usage = "owner(s) of project")
-  private List<AccountGroup.Id> 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 = "DESC", 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;
-
-  @Inject
-  private ReviewDb db;
-
-  @Inject
-  private GitRepositoryManager repoManager;
-
-  @Inject
-  @ProjectCreatorGroups
-  private Set<AccountGroup.Id> projectCreatorGroups;
-
-  @Inject
-  @ProjectOwnerGroups
-  private Set<AccountGroup.Id> projectOwnerGroups;
-
-  @Inject
-  private IdentifiedUser currentUser;
-
-  @Inject
-  private ReplicationQueue rq;
-
-  @Inject
-  @GerritPersonIdent
-  private PersonIdent serverIdent;
-
-  private Project.NameKey nameKey;
-
-  @Override
-  public void start(final Environment env) {
-    startThread(new CommandRunnable() {
-      @Override
-      public void run() throws Exception {
-        PrintWriter p = toPrintWriter(out);
-
-        parseCommandLine();
-
-        try {
-          validateParameters();
-          nameKey = new Project.NameKey(projectName);
-
-          if (!permissionsOnly) {
-            final Repository repo = repoManager.createRepository(nameKey);
-            try {
-              repo.create(true);
-
-              StoredConfig config = repo.getConfig();
-              config.setBoolean(ConfigConstants.CONFIG_CORE_SECTION,
-                  null, ConfigConstants.CONFIG_KEY_LOGALLREFUPDATES, true);
-              config.save();
-
-              RefUpdate u = repo.updateRef(Constants.HEAD);
-              u.disableRefLog();
-              u.link(branch);
-
-              repoManager.setProjectDescription(nameKey, projectDescription);
-
-              createProject();
-
-              rq.replicateNewProject(nameKey, branch);
-
-              if (createEmptyCommit) {
-                createEmptyCommit(repo, nameKey, branch);
-              }
-            } finally {
-              repo.close();
-            }
-          } else {
-            createProject();
-          }
-        } catch (Exception e) {
-          p.print("Error when trying to create project: " + e.getMessage()
-              + "\n");
-          p.flush();
-        }
-
-      }
-    });
-  }
-
-  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.setCommitter(serverIdent);
-      cb.setAuthor(cb.getCommitter());
-      cb.setMessage("Initial empty repository");
-
-      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 createProject() throws OrmException {
-    List<RefRight> access = new ArrayList<RefRight>();
-    for (AccountGroup.Id ownerId : ownerIds) {
-      final RefRight.Key prk =
-          new RefRight.Key(nameKey, new RefRight.RefPattern(
-              RefRight.ALL), ApprovalCategory.OWN, ownerId);
-      final RefRight pr = new RefRight(prk);
-      pr.setMaxValue((short) 1);
-      pr.setMinValue((short) 1);
-      access.add(pr);
-    }
-    db.refRights().insert(access);
-
-    final Project newProject = new Project(nameKey);
-    newProject.setDescription(projectDescription);
-    newProject.setSubmitType(submitType);
-    newProject.setUseContributorAgreements(contributorAgreements);
-    newProject.setUseSignedOffBy(signedOffBy);
-    newProject.setUseContentMerge(contentMerge);
-    newProject.setRequireChangeID(requireChangeID);
-    if (newParent != null) {
-      newProject.setParent(newParent.getProject().getNameKey());
-    }
-
-    db.projects().insert(Collections.singleton(newProject));
-  }
-
-  private void validateParameters() throws Failure {
-    if (projectName.endsWith(Constants.DOT_GIT_EXT)) {
-      projectName = projectName.substring(0, //
-          projectName.length() - Constants.DOT_GIT_EXT.length());
-    }
-
-    if (!CollectionsUtil.isAnyIncludedIn(currentUser.getEffectiveGroups(), projectCreatorGroups)) {
-      throw new Failure(1, "fatal: Not permitted to create " + projectName);
-    }
-
-    if (ownerIds != null && !ownerIds.isEmpty()) {
-      ownerIds =
-          new ArrayList<AccountGroup.Id>(new HashSet<AccountGroup.Id>(ownerIds));
-    } else {
-      ownerIds = new ArrayList<AccountGroup.Id>(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..a93cab1
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
@@ -0,0 +1,159 @@
+// 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.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.Project.SubmitType;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.project.CreateProject;
+import com.google.gerrit.server.project.CreateProjectArgs;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.project.SuggestParentCandidates;
+import com.google.gerrit.sshd.BaseCommand;
+import com.google.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 3511c71..16461b6 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
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.sshd.commands;
 
+import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.sshd.CommandModule;
 import com.google.gerrit.sshd.CommandName;
 import com.google.gerrit.sshd.Commands;
@@ -34,11 +35,12 @@
     // SlaveCommandModule.
 
     command(gerrit).toProvider(new DispatchCommandProvider(gerrit));
-    command(gerrit, "flush-caches").to(AdminFlushCaches.class);
-    command(gerrit, "ls-projects").to(ListProjects.class);
+    command(gerrit, "flush-caches").to(FlushCaches.class);
+    command(gerrit, "ls-projects").to(ListProjectsCommand.class);
+    command(gerrit, "ls-groups").to(ListGroupsCommand.class);
     command(gerrit, "query").to(Query.class);
-    command(gerrit, "show-caches").to(AdminShowCaches.class);
-    command(gerrit, "show-connections").to(AdminShowConnections.class);
+    command(gerrit, "show-caches").to(ShowCaches.class);
+    command(gerrit, "show-connections").to(ShowConnections.class);
     command(gerrit, "show-queue").to(ShowQueue.class);
     command(gerrit, "stream-events").to(StreamEvents.class);
     command(gerrit, "version").to(VersionCommand.class);
@@ -48,7 +50,7 @@
     command(git, "upload-pack").to(Upload.class);
 
     command("ps").to(ShowQueue.class);
-    command("kill").to(AdminKill.class);
+    command("kill").to(KillCommand.class);
     command("scp").to(ScpCommand.class);
 
     // Honor the legacy hyphenated forms as aliases for the non-hyphenated forms
@@ -58,5 +60,12 @@
     command("gerrit-receive-pack").to(Commands.key(git, "receive-pack"));
 
     command("suexec").to(SuExec.class);
+
+    install(new LifecycleModule() {
+      @Override
+      protected void configure() {
+        listener().to(ShowCaches.StartupListener.class);
+      }
+    });
   }
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminFlushCaches.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/FlushCaches.java
similarity index 77%
rename from gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminFlushCaches.java
rename to gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/FlushCaches.java
index b3241ff..639cc42 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminFlushCaches.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/FlushCaches.java
@@ -14,7 +14,9 @@
 
 package com.google.gerrit.sshd.commands;
 
-import com.google.gerrit.sshd.AdminCommand;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.sshd.BaseCommand;
+import com.google.inject.Inject;
 
 import net.sf.ehcache.Ehcache;
 
@@ -27,8 +29,9 @@
 import java.util.SortedSet;
 
 /** Causes the caches to purge all entries and reload. */
-@AdminCommand
-final class AdminFlushCaches extends CacheCommand {
+final class FlushCaches extends CacheCommand {
+  private static final String WEB_SESSIONS = "web_sessions";
+
   @Option(name = "--cache", usage = "flush named cache", metaVar = "NAME")
   private List<String> caches = new ArrayList<String>();
 
@@ -38,6 +41,9 @@
   @Option(name = "--list", usage = "list available caches")
   private boolean list;
 
+  @Inject
+  IdentifiedUser currentUser;
+
   private PrintWriter p;
 
   @Override
@@ -45,6 +51,13 @@
     startThread(new CommandRunnable() {
       @Override
       public void run() throws Exception {
+        if (!currentUser.getCapabilities().canFlushCaches()) {
+          String msg = String.format(
+            "fatal: %s does not have \"Flush Caches\" capability.",
+            currentUser.getUserName());
+          throw new UnloggedFailure(BaseCommand.STATUS_NOT_ADMIN, msg);
+        }
+
         parseCommandLine();
         flush();
       }
@@ -52,6 +65,14 @@
   }
 
   private void flush() throws Failure {
+    if (caches.contains(WEB_SESSIONS)
+        && !currentUser.getCapabilities().canAdministrateServer()) {
+      String msg = String.format(
+          "fatal: only site administrators can flush %s",
+          WEB_SESSIONS);
+      throw new UnloggedFailure(BaseCommand.STATUS_NOT_ADMIN, msg);
+    }
+
     p = toPrintWriter(err);
     if (list) {
       if (all || caches.size() > 0) {
@@ -113,7 +134,7 @@
       return true;
 
     } else if (all) {
-      if ("web_sessions".equals(cacheName)) {
+      if (WEB_SESSIONS.equals(cacheName)) {
         return false;
       }
       return true;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminKill.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/KillCommand.java
similarity index 80%
rename from gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminKill.java
rename to gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/KillCommand.java
index 5550964..69018af 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminKill.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/KillCommand.java
@@ -14,10 +14,10 @@
 
 package com.google.gerrit.sshd.commands;
 
+import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.git.WorkQueue.Task;
 import com.google.gerrit.server.util.IdGenerator;
-import com.google.gerrit.sshd.AdminCommand;
 import com.google.gerrit.sshd.BaseCommand;
 import com.google.inject.Inject;
 
@@ -29,8 +29,10 @@
 import java.util.Set;
 
 /** Kill a task in the work queue. */
-@AdminCommand
-final class AdminKill extends BaseCommand {
+final class KillCommand extends BaseCommand {
+  @Inject
+  private IdentifiedUser currentUser;
+
   @Inject
   private WorkQueue workQueue;
 
@@ -50,8 +52,15 @@
     startThread(new CommandRunnable() {
       @Override
       public void run() throws Exception {
+        if (!currentUser.getCapabilities().canKillTask()) {
+          String msg = String.format(
+            "fatal: %s does not have \"Kill Task\" capability.",
+            currentUser.getUserName());
+          throw new UnloggedFailure(BaseCommand.STATUS_NOT_ADMIN, msg);
+        }
+
         parseCommandLine();
-        AdminKill.this.commitMurder();
+        KillCommand.this.commitMurder();
       }
     });
   }
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..e0b988e
--- /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.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.VisibleGroups;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.sshd.BaseCommand;
+import com.google.gwtorm.server.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
deleted file mode 100644
index bb04f7a..0000000
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListProjects.java
+++ /dev/null
@@ -1,318 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.sshd.commands;
-
-import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.reviewdb.ReviewDb;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.config.WildProjectName;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.server.project.ProjectControl;
-import com.google.gerrit.server.project.ProjectState;
-import com.google.gerrit.sshd.BaseCommand;
-import com.google.gwtorm.client.OrmException;
-import com.google.inject.Inject;
-
-import org.apache.sshd.server.Environment;
-import org.eclipse.jgit.lib.Ref;
-import org.eclipse.jgit.lib.Repository;
-import org.kohsuke.args4j.Option;
-
-import java.io.IOException;
-import java.io.PrintWriter;
-import java.util.ArrayList;
-import java.util.List;
-import java.util.TreeMap;
-
-final class ListProjects extends BaseCommand {
-  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)";
-
-  @Inject
-  private ReviewDb db;
-
-  @Inject
-  private IdentifiedUser currentUser;
-
-  @Inject
-  private ProjectCache projectCache;
-
-  @Inject
-  private GitRepositoryManager repoManager;
-
-  @Inject
-  @WildProjectName
-  private Project.NameKey wildProject;
-
-  @Option(name = "--show-branch", aliases = {"-b"}, usage = "displays the sha of each project in the specified branch")
-  private String showBranch;
-
-  @Option(name = "--tree", aliases = {"-t"}, usage = "displays project inheritance in a tree-like format\n" +
-      "this option does not work together with the show-branch option")
-  private boolean showTree;
-
-  private String currentTabSeparator = DEFAULT_TAB_SEPARATOR;
-
-  @Override
-  public void start(final Environment env) {
-    startThread(new CommandRunnable() {
-      @Override
-      public void run() throws Exception {
-        parseCommandLine();
-        ListProjects.this.display();
-      }
-    });
-  }
-
-  private void display() throws Failure {
-    if (showTree && (showBranch != null)) {
-      throw new UnloggedFailure(1, "fatal: --tree and --show-branch options are not compatible.");
-    }
-
-    final PrintWriter stdout = toPrintWriter(out);
-
-    TreeMap<String, TreeNode> treeMap = null;
-
-    if (showTree) {
-      treeMap = new TreeMap<String, TreeNode>();
-    }
-
-    try {
-      for (final Project p : db.projects().all()) {
-        if (p.getNameKey().equals(wildProject)) {
-          // This project "doesn't exist". At least not as a repository.
-          //
-          continue;
-        }
-
-        final ProjectState e = projectCache.get(p.getNameKey());
-        if (e == null) {
-          // If we can't get it from the cache, pretend its not present.
-          //
-          continue;
-        }
-
-        final ProjectControl pctl = e.controlFor(currentUser);
-
-        if (!showTree) {
-
-          if (!pctl.isVisible()) {
-            // Require the project itself to be visible to the user.
-            //
-            continue;
-          }
-
-          if (showBranch != null) {
-            final Ref ref = getBranchRef(p.getNameKey());
-            if (ref == null || ref.getObjectId() == null
-                || !pctl.controlForRef(ref.getLeaf().getName()).isVisible()) {
-              // No branch, or the user can't see this branch, so skip it.
-              //
-              continue;
-            }
-
-            stdout.print(ref.getObjectId().name());
-            stdout.print(' ');
-          }
-
-          stdout.print(p.getName() + "\n");
-        } else {
-          treeMap.put(p.getName(), new TreeNode(p, pctl.isVisible()));
-        }
-      }
-
-      if (showTree && treeMap.size() > 0) {
-        final List<TreeNode> sortedNodes = new ArrayList<TreeNode>();
-
-        // Builds the inheritance tree using a list.
-        //
-        for (final TreeNode key : treeMap.values()) {
-          final String parentName = key.getParentName();
-          if (parentName != null) {
-            final TreeNode node = treeMap.get((String)parentName);
-            if (node != null) {
-              node.addChild(key);
-            } else {
-              sortedNodes.add(key);
-            }
-          } else {
-            sortedNodes.add(key);
-          }
-        }
-
-        // 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));
-        stdout.flush();
-      }
-    } catch (OrmException e) {
-      throw new Failure(1, "fatal: database error", e);
-    } finally {
-      stdout.flush();
-    }
-  }
-
-  private Ref getBranchRef(Project.NameKey projectName) {
-    try {
-      final Repository r = repoManager.openRepository(projectName);
-      try {
-        return r.getRef(showBranch);
-      } finally {
-        r.close();
-      }
-    } catch (IOException ioe) {
-      return null;
-    }
-  }
-
-  /** 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/ListProjectsCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java
new file mode 100644
index 0000000..d0bbc72
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java
@@ -0,0 +1,43 @@
+// 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.server.project.ListProjects;
+import com.google.gerrit.sshd.BaseCommand;
+import com.google.inject.Inject;
+
+import org.apache.sshd.server.Environment;
+
+final class ListProjectsCommand extends BaseCommand {
+  @Inject
+  private ListProjects impl;
+
+  @Override
+  public void start(final Environment env) {
+    startThread(new CommandRunnable() {
+      @Override
+      public void run() throws Exception {
+        parseCommandLine(impl);
+        if (impl.isShowTree() && (impl.getShowBranch() != null)) {
+          throw new UnloggedFailure(1, "fatal: --tree and --show-branch options are not compatible.");
+        }
+        if (impl.isShowTree() && impl.isShowDescription()) {
+          throw new UnloggedFailure(1, "fatal: --tree and --description options are not compatible.");
+        }
+        impl.display(out);
+      }
+    });
+  }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/MasterCommandModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/MasterCommandModule.java
index 466d454..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
@@ -26,12 +26,14 @@
     final CommandName gerrit = Commands.named("gerrit");
 
     command(gerrit, "approve").to(ReviewCommand.class);
-    command(gerrit, "create-account").to(AdminCreateAccount.class);
-    command(gerrit, "create-group").to(AdminCreateGroup.class);
-    command(gerrit, "create-project").to(CreateProject.class);
+    command(gerrit, "create-account").to(CreateAccountCommand.class);
+    command(gerrit, "create-group").to(CreateGroupCommand.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);
-    command(gerrit, "replicate").to(AdminReplicate.class);
+    command(gerrit, "replicate").to(Replicate.class);
     command(gerrit, "set-project-parent").to(AdminSetParent.class);
     command(gerrit, "review").to(ReviewCommand.class);
   }
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 fecdc59..d9a1c3f 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
@@ -51,6 +51,26 @@
     processor.setIncludeApprovals(on);
   }
 
+  @Option(name = "--comments", usage = "Include patch set and inline comments")
+  void setComments(boolean on) {
+    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);
+  }
+
+  @Option(name = "--dependencies", usage = "Include depends-on and needed-by information")
+  void setDependencies(boolean on) {
+    processor.setIncludeDependencies(on);
+  }
+
   @Argument(index = 0, required = true, multiValued = true, metaVar = "QUERY", usage = "Query to execute")
   private List<String> query;
 
@@ -61,11 +81,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/QueryShell.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/QueryShell.java
index e68c747..18ce77f 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/QueryShell.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/QueryShell.java
@@ -15,11 +15,11 @@
 package com.google.gerrit.sshd.commands;
 
 import com.google.gerrit.common.Version;
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gson.JsonObject;
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.client.SchemaFactory;
 import com.google.gwtorm.jdbc.JdbcSchema;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
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 e31925b..85f53bf 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
@@ -14,22 +14,25 @@
 
 package com.google.gerrit.sshd.commands;
 
-import com.google.gerrit.reviewdb.Account;
+import com.google.gerrit.common.data.Capable;
+import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.git.AsyncReceiveCommits;
 import com.google.gerrit.server.git.ReceiveCommits;
 import com.google.gerrit.server.git.TransferConfig;
 import com.google.gerrit.server.git.VisibleRefFilter;
 import com.google.gerrit.sshd.AbstractGitCommand;
 import com.google.inject.Inject;
 
+import org.eclipse.jgit.errors.TooLargeObjectInPackException;
 import org.eclipse.jgit.errors.UnpackException;
 import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.transport.AdvertiseRefsHook;
 import org.eclipse.jgit.transport.ReceivePack;
-import org.eclipse.jgit.transport.RefFilter;
 import org.kohsuke.args4j.Option;
 
 import java.io.IOException;
-import java.io.InterruptedIOException;
+import java.io.PrintWriter;
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
@@ -39,7 +42,7 @@
 /** Receives change upload over SSH using the Git receive-pack protocol. */
 final class Receive extends AbstractGitCommand {
   @Inject
-  private ReceiveCommits.Factory factory;
+  private AsyncReceiveCommits.Factory factory;
 
   @Inject
   private IdentifiedUser currentUser;
@@ -69,10 +72,11 @@
       throw new Failure(1, "fatal: receive-pack not permitted on this server");
     }
 
-    final ReceiveCommits receive = factory.create(projectControl, repo);
+    final ReceiveCommits receive = factory.create(projectControl, repo)
+        .getReceiveCommits();
 
-    ReceiveCommits.Capable r = receive.canUpload();
-    if (r != ReceiveCommits.Capable.OK) {
+    Capable r = receive.canUpload();
+    if (r != Capable.OK) {
       throw new UnloggedFailure(1, "\nfatal: " + r.getMessage());
     }
 
@@ -85,13 +89,20 @@
     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);
-    } catch (InterruptedIOException err) {
-      throw new Failure(128, "fatal: client IO read/write timeout", err);
-
     } catch (UnpackException badStream) {
+      // In case this was caused by the user pushing an object whose size
+      // is larger than the receive.maxObjectSizeLimit gerrit.config parameter
+      // we want to present this error to the user
+      if (badStream.getCause() instanceof TooLargeObjectInPackException) {
+        PrintWriter p = toPrintWriter(err);
+        p.print("error: " + badStream.getCause().getMessage() + "\n");
+        p.flush();
+      }
+
       // This may have been triggered by branch level access controls.
       // Log what the heck is going on, as detailed as we can.
       //
@@ -99,17 +110,17 @@
       msg.append("Unpack error on project \""
           + projectControl.getProject().getName() + "\":\n");
 
-      msg.append("  RefFilter: " + rp.getRefFilter());
-      if (rp.getRefFilter() == RefFilter.DEFAULT) {
+      msg.append("  AdvertiseRefsHook: " + rp.getAdvertiseRefsHook());
+      if (rp.getAdvertiseRefsHook() == AdvertiseRefsHook.DEFAULT) {
         msg.append("DEFAULT");
-      } else if (rp.getRefFilter() instanceof VisibleRefFilter) {
+      } else if (rp.getAdvertiseRefsHook() instanceof VisibleRefFilter) {
         msg.append("VisibleRefFilter");
       } else {
-        msg.append(rp.getRefFilter().getClass());
+        msg.append(rp.getAdvertiseRefsHook().getClass());
       }
       msg.append("\n");
 
-      if (rp.getRefFilter() instanceof VisibleRefFilter) {
+      if (rp.getAdvertiseRefsHook() instanceof VisibleRefFilter) {
         Map<String, Ref> adv = rp.getAdvertisedRefs();
         msg.append("  Visible references (" + adv.size() + "):\n");
         for (Ref ref : adv.values()) {
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..5b6cf39
--- /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.server.OrmException;
+import com.google.inject.Inject;
+
+import org.apache.sshd.server.Environment;
+import org.kohsuke.args4j.Argument;
+
+import java.io.IOException;
+
+public class RenameGroupCommand extends BaseCommand {
+
+  @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/AdminReplicate.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Replicate.java
similarity index 83%
rename from gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminReplicate.java
rename to gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Replicate.java
index 8e2c74f..bc4e0bb 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminReplicate.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Replicate.java
@@ -14,11 +14,11 @@
 
 package com.google.gerrit.sshd.commands;
 
-import com.google.gerrit.reviewdb.Project;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.git.PushAllProjectsOp;
 import com.google.gerrit.server.git.ReplicationQueue;
 import com.google.gerrit.server.project.ProjectCache;
-import com.google.gerrit.sshd.AdminCommand;
 import com.google.gerrit.sshd.BaseCommand;
 import com.google.inject.Inject;
 
@@ -31,8 +31,7 @@
 import java.util.concurrent.TimeUnit;
 
 /** Force a project to replicate, again. */
-@AdminCommand
-final class AdminReplicate extends BaseCommand {
+final class Replicate extends BaseCommand {
   @Option(name = "--all", usage = "push all known projects")
   private boolean all;
 
@@ -43,6 +42,9 @@
   private List<String> projectNames = new ArrayList<String>(2);
 
   @Inject
+  IdentifiedUser currentUser;
+
+  @Inject
   private PushAllProjectsOp.Factory pushAllOpFactory;
 
   @Inject
@@ -56,8 +58,15 @@
     startThread(new CommandRunnable() {
       @Override
       public void run() throws Exception {
+        if (!currentUser.getCapabilities().canStartReplication()) {
+          String msg = String.format(
+            "fatal: %s does not have \"Start Replication\" capability.",
+            currentUser.getUserName());
+          throw new UnloggedFailure(BaseCommand.STATUS_NOT_ADMIN, msg);
+        }
+
         parseCommandLine();
-        AdminReplicate.this.schedule();
+        Replicate.this.schedule();
       }
     });
   }
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 47bae4f..640adbf 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,33 +14,29 @@
 
 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.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.common.data.ReviewResult;
+import com.google.gerrit.reviewdb.client.ApprovalCategory;
+import com.google.gerrit.reviewdb.client.ApprovalCategoryValue;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+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.patch.PublishComments;
-import com.google.gerrit.server.project.CanSubmitResult;
-import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.project.ProjectControl;
-import com.google.gerrit.server.workflow.FunctionState;
 import com.google.gerrit.sshd.BaseCommand;
 import com.google.gerrit.util.cli.CmdLineParser;
-import com.google.gwtorm.client.OrmException;
-import com.google.gwtorm.client.ResultSet;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Inject;
 
 import org.apache.sshd.server.Environment;
@@ -55,15 +51,14 @@
 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 =
       LoggerFactory.getLogger(ReviewCommand.class);
 
   @Override
-  protected final CmdLineParser newCmdLineParser() {
-    final CmdLineParser parser = super.newCmdLineParser();
+  protected final CmdLineParser newCmdLineParser(Object options) {
+    final CmdLineParser parser = super.newCmdLineParser(options);
     for (ApproveOption c : optionList) {
       parser.addOption(c, c);
     }
@@ -98,40 +93,42 @@
   @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;
 
   @Inject
-  private IdentifiedUser currentUser;
-
-  @Inject
-  private MergeQueue merger;
-
-  @Inject
-  private MergeOp.Factory opFactory;
-
-  @Inject
   private ApprovalTypes approvalTypes;
 
   @Inject
-  private ChangeControl.Factory changeControlFactory;
+  private DeleteDraftPatchSet.Factory deleteDraftPatchSetFactory;
 
   @Inject
-  private AbandonedSender.Factory abandonedSenderFactory;
-
-  @Inject
-  private FunctionState.Factory functionStateFactory;
+  private AbandonChange.Factory abandonChangeFactory;
 
   @Inject
   private PublishComments.Factory publishCommentsFactory;
 
   @Inject
-  private ChangeHookRunner hooks;
+  private PublishDraft.Factory publishDraftFactory;
+
+  @Inject
+  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() {
@@ -146,6 +143,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;
@@ -155,6 +169,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 "
@@ -168,46 +185,12 @@
               + " 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);
-          }
-        }
       }
     });
   }
 
-  private void approveOne(final PatchSet.Id patchSetId)
-      throws NoSuchChangeException, UnloggedFailure, OrmException,
-             EmailException {
-
-    final Change.Id changeId = patchSetId.getParentKey();
-    ChangeControl changeControl = changeControlFactory.validateFor(changeId);
+  private void approveOne(final PatchSet.Id patchSetId) throws
+      NoSuchChangeException, OrmException, EmailException, Failure {
 
     if (changeComment == null) {
       changeComment = "";
@@ -217,43 +200,83 @@
     for (ApproveOption ao : optionList) {
       Short v = ao.value();
       if (v != null) {
-        assertScoreIsAllowed(patchSetId, changeControl, ao, v);
         aps.add(new ApprovalCategoryValue.Id(ao.getCategoryId(), v));
       }
     }
 
-    publishCommentsFactory.create(patchSetId, changeComment, aps).call();
+    try {
+      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");
-      }
-    }
-
-    if (restoreChange) {
-      if (changeControl.canRestore()) {
-        ChangeUtil.restore(patchSetId, currentUser, changeComment, db,
-          abandonedSenderFactory, hooks);
-      } else {
-        throw error("Not permitted to restore change");
+      if (abandonChange) {
+        final ReviewResult result = abandonChangeFactory.create(
+            patchSetId, changeComment).call();
+        handleReviewResultErrors(result);
+      } else if (restoreChange) {
+        final ReviewResult result = restoreChangeFactory.create(
+            patchSetId, changeComment).call();
+        handleReviewResultErrors(result);
       }
       if (submitChange) {
-        changeControl = changeControlFactory.validateFor(changeId);
+        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) {
-      CanSubmitResult result =
-          changeControl.canSubmit(patchSetId, db, approvalTypes,
-              functionStateFactory);
-      if (result == CanSubmitResult.OK) {
-        toSubmit.add(patchSetId);
-      } else {
-        throw error(result.getMessage());
+    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 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:
+          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:
+          errMsg += "failure in review";
       }
+      if (resultError.getMessage() != null) {
+        errMsg += ": " + resultError.getMessage();
+      }
+      writeError(errMsg);
     }
   }
 
@@ -321,22 +344,6 @@
     return projectControl.getProject().getNameKey().equals(change.getProject());
   }
 
-  private void assertScoreIsAllowed(final PatchSet.Id patchSetId,
-      final ChangeControl changeControl, ApproveOption ao, Short v)
-      throws UnloggedFailure {
-    final PatchSetApproval psa =
-        new PatchSetApproval(new PatchSetApproval.Key(patchSetId, currentUser
-            .getAccountId(), ao.getCategoryId()), v);
-    final FunctionState fs =
-        functionStateFactory.create(changeControl.getChange(), patchSetId,
-            Collections.<PatchSetApproval> emptyList());
-    psa.setValue(v);
-    fs.normalize(approvalTypes.getApprovalType(psa.getCategoryId()), psa);
-    if (v != psa.getValue()) {
-      throw error(ao.name() + "=" + ao.value() + " not permitted");
-    }
-  }
-
   private void initOptionList() {
     optionList = new ArrayList<ApproveOption>();
 
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
new file mode 100644
index 0000000..6e1a32b
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
@@ -0,0 +1,292 @@
+// 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.ReviewerResult;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.RevId;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.patch.AddReviewer;
+import com.google.gerrit.server.patch.RemoveReviewer;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.sshd.BaseCommand;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
+import com.google.inject.Inject;
+
+import org.apache.sshd.server.Environment;
+import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.Option;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.text.MessageFormat;
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+public class SetReviewersCommand extends BaseCommand {
+  private static final Logger log =
+      LoggerFactory.getLogger(SetReviewersCommand.class);
+
+  @Option(name = "--project", aliases = "-p", usage = "project containing the change")
+  private ProjectControl projectControl;
+
+  @Option(name = "--add", aliases = {"-a"}, metaVar = "REVIEWER", usage = "user or group that should be added as reviewer")
+  private List<String> toAdd = new ArrayList<String>();
+
+  @Option(name = "--remove", aliases = {"-r"}, metaVar = "REVIEWER", usage = "user that should be removed from the reviewer list")
+  void optionRemove(Account.Id who) {
+    toRemove.add(who);
+  }
+
+  @Argument(index = 0, required = true, multiValued = true, metaVar = "COMMIT", usage = "changes to modify")
+  void addChange(String token) {
+    try {
+      changes.addAll(parseChangeId(token));
+    } catch (UnloggedFailure e) {
+      throw new IllegalArgumentException(e.getMessage(), e);
+    } catch (OrmException e) {
+      throw new IllegalArgumentException("database is down", e);
+    }
+  }
+
+  @Inject
+  private ReviewDb db;
+
+  @Inject
+  private AddReviewer.Factory addReviewerFactory;
+
+  @Inject
+  private RemoveReviewer.Factory removeReviewerFactory;
+
+  @Inject
+  private ChangeControl.Factory changeControlFactory;
+
+  private Set<Account.Id> toRemove = new HashSet<Account.Id>();
+  private Set<Change.Id> changes = new HashSet<Change.Id>();
+
+  @Override
+  public final void start(final Environment env) {
+    startThread(new CommandRunnable() {
+      @Override
+      public void run() throws Failure {
+        parseCommandLine();
+
+        boolean ok = true;
+        for (Change.Id changeId : changes) {
+          try {
+            ok &= modifyOne(changeId);
+          } catch (Exception err) {
+            ok = false;
+            log.error("Error updating reviewers on change " + changeId, err);
+            writeError("fatal", "internal error while updating " + changeId);
+          }
+        }
+
+        if (!ok) {
+          throw error("fatal: one or more updates failed; review output above");
+        }
+      }
+    });
+  }
+
+  private boolean modifyOne(Change.Id changeId) throws Exception {
+    changeControlFactory.validateFor(changeId);
+
+    ReviewerResult result;
+    boolean ok = true;
+
+    // Remove reviewers
+    //
+    result = removeReviewerFactory.create(changeId, toRemove).call();
+    ok &= result.getErrors().isEmpty();
+    for (ReviewerResult.Error resultError : result.getErrors()) {
+      String message;
+      switch (resultError.getType()) {
+        case REMOVE_NOT_PERMITTED:
+          message = "not permitted to remove {0} from {1}";
+          break;
+        case COULD_NOT_REMOVE:
+          message = "could not remove {0} from {1}";
+          break;
+        default:
+          message = "could not remove {0}: {2}";
+      }
+      writeError("error", MessageFormat.format(message,
+          resultError.getName(), changeId, resultError.getType()));
+    }
+
+    // Add reviewers
+    //
+    result =
+        addReviewerFactory.create(changeId, toAdd, true).call();
+    ok &= result.getErrors().isEmpty();
+    for (ReviewerResult.Error resultError : result.getErrors()) {
+      String message;
+      switch (resultError.getType()) {
+        case REVIEWER_NOT_FOUND:
+          message = "account or group {0} not found";
+          break;
+        case ACCOUNT_INACTIVE:
+          message = "account {0} inactive";
+          break;
+        case CHANGE_NOT_VISIBLE:
+          message = "change {1} not visible to {0}";
+          break;
+        case GROUP_EMPTY:
+          message = "group {0} is empty";
+          break;
+        case GROUP_HAS_TOO_MANY_MEMBERS:
+          message = "group {0} has too many members";
+          break;
+        case GROUP_NOT_ALLOWED:
+          message = "group {0} is not allowed as reviewer";
+          break;
+        default:
+          message = "could not add {0}: {2}";
+      }
+      writeError("error", MessageFormat.format(message,
+          resultError.getName(), changeId, resultError.getType()));
+    }
+
+    return ok;
+  }
+
+  private Set<Change.Id> parseChangeId(String idstr)
+      throws UnloggedFailure, OrmException {
+    Set<Change.Id> matched = new HashSet<Change.Id>(4);
+    boolean isCommit = idstr.matches("^([0-9a-fA-F]{4," + RevId.LEN + "})$");
+
+    // By newer style changeKey?
+    //
+    boolean changeKeyParses = false;
+    if (idstr.matches("^I[0-9a-fA-F]*$")) {
+      Change.Key key;
+      try {
+        key = Change.Key.parse(idstr);
+        changeKeyParses = true;
+      } catch (IllegalArgumentException e) {
+        key = null;
+        changeKeyParses = false;
+      }
+
+      if (changeKeyParses) {
+        for (Change change : db.changes().byKeyRange(key, key.max())) {
+          matchChange(matched, change);
+        }
+      }
+    }
+
+    // By commit?
+    //
+    if (isCommit) {
+      RevId id = new RevId(idstr);
+      ResultSet<PatchSet> patches;
+      if (id.isComplete()) {
+        patches = db.patchSets().byRevision(id);
+      } else {
+        patches = db.patchSets().byRevisionRange(id, id.max());
+      }
+
+      for (PatchSet ps : patches) {
+        matchChange(matched, ps.getId().getParentKey());
+      }
+    }
+
+    // By older style changeId?
+    //
+    boolean changeIdParses = false;
+    if (idstr.matches("^[1-9][0-9]*$")) {
+      Change.Id id;
+      try {
+        id = Change.Id.parse(idstr);
+        changeIdParses = true;
+      } catch (IllegalArgumentException e) {
+        id = null;
+        changeIdParses = false;
+      }
+
+      if (changeIdParses) {
+        matchChange(matched, id);
+      }
+    }
+
+    if (!changeKeyParses && !isCommit && !changeIdParses) {
+      throw error("\"" + idstr + "\" is not a valid change");
+    }
+
+    switch (matched.size()) {
+      case 0:
+        throw error("\"" + idstr + "\" no such change");
+
+      case 1:
+        return matched;
+
+      default:
+        throw error("\"" + idstr + "\" matches multiple changes");
+    }
+  }
+
+  private void matchChange(Set<Change.Id> matched, Change.Id changeId) {
+    if (changeId != null && !matched.contains(changeId)) {
+      try {
+        matchChange(matched, db.changes().get(changeId));
+      } catch (OrmException e) {
+        log.warn("Error reading change " + changeId, e);
+      }
+    }
+  }
+
+  private void matchChange(Set<Change.Id> matched, Change change) {
+    try {
+      if (change != null
+          && inProject(change)
+          && 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);
+    }
+  }
+
+  private boolean inProject(Change change) {
+    if (projectControl != null) {
+      return projectControl.getProject().getNameKey().equals(change.getProject());
+    } else {
+      // No --project option, so they want every project.
+      return true;
+    }
+  }
+
+  private void writeError(String type, String msg) {
+    try {
+      err.write((type + ": " + msg + "\n").getBytes(ENC));
+    } catch (IOException e) {
+    }
+  }
+
+  private static UnloggedFailure error(String msg) {
+    return new UnloggedFailure(1, msg);
+  }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowCaches.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowCaches.java
new file mode 100644
index 0000000..4de10d6
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowCaches.java
@@ -0,0 +1,376 @@
+// 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.Version;
+import com.google.gerrit.lifecycle.LifecycleListener;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.config.SitePath;
+import com.google.gerrit.server.git.WorkQueue;
+import com.google.gerrit.server.git.WorkQueue.Task;
+import com.google.gerrit.sshd.BaseCommand;
+import com.google.gerrit.sshd.SshDaemon;
+import com.google.inject.Inject;
+
+import net.sf.ehcache.Ehcache;
+import net.sf.ehcache.Statistics;
+import net.sf.ehcache.config.CacheConfiguration;
+
+import org.apache.mina.core.service.IoAcceptor;
+import org.apache.mina.core.session.IoSession;
+import org.apache.sshd.server.Environment;
+import org.eclipse.jgit.storage.file.WindowCacheStatAccessor;
+import org.kohsuke.args4j.Option;
+
+import java.io.File;
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.lang.management.ManagementFactory;
+import java.lang.management.OperatingSystemMXBean;
+import java.lang.management.RuntimeMXBean;
+import java.net.InetAddress;
+import java.net.UnknownHostException;
+import java.text.SimpleDateFormat;
+import java.util.Collection;
+import java.util.Date;
+
+/** Show the current cache states. */
+final class ShowCaches extends CacheCommand {
+  private static volatile long serverStarted;
+
+  static class StartupListener implements LifecycleListener {
+    @Override
+    public void start() {
+      serverStarted = System.currentTimeMillis();
+    }
+
+    @Override
+    public void stop() {
+    }
+  }
+
+  @Option(name = "--gc", usage = "perform Java GC before printing memory stats")
+  private boolean gc;
+
+  @Option(name = "--show-jvm", usage = "show details about the JVM")
+  private boolean showJVM;
+
+  @Inject
+  private IdentifiedUser currentUser;
+
+  @Inject
+  private WorkQueue workQueue;
+
+  @Inject
+  private SshDaemon daemon;
+
+  @Inject
+  @SitePath
+  private File sitePath;
+
+  private PrintWriter p;
+
+  @Override
+  public void start(final Environment env) {
+    startThread(new CommandRunnable() {
+      @Override
+      public void run() throws Exception {
+        if (!currentUser.getCapabilities().canViewCaches()) {
+          String msg = String.format(
+            "fatal: %s does not have \"View Caches\" capability.",
+            currentUser.getUserName());
+          throw new UnloggedFailure(BaseCommand.STATUS_NOT_ADMIN, msg);
+        }
+
+        parseCommandLine();
+        display();
+      }
+    });
+  }
+
+  private void display() {
+    p = toPrintWriter(out);
+
+    Date now = new Date();
+    p.format(
+        "%-25s %-20s      now  %16s\n",
+        "Gerrit Code Review",
+        Version.getVersion() != null ? Version.getVersion() : "",
+        new SimpleDateFormat("HH:mm:ss   zzz").format(now));
+    p.format(
+        "%-25s %-20s   uptime %16s\n",
+        "", "",
+        uptime(now.getTime() - serverStarted));
+    p.print('\n');
+
+    p.print(String.format(//
+        "%1s %-18s %-4s|%-20s|  %-5s  |%-14s|\n" //
+        , "" //
+        , "Name" //
+        , "Max" //
+        , "Object Count" //
+        , "AvgGet" //
+        , "Hit Ratio" //
+    ));
+    p.print(String.format(//
+        "%1s %-18s %-4s|%6s %6s %6s|  %-5s   |%-4s %-4s %-4s|\n" //
+        , "" //
+        , "" //
+        , "Age" //
+        , "Disk" //
+        , "Mem" //
+        , "Cnt" //
+        , "" //
+        , "Disk" //
+        , "Mem" //
+        , "Agg" //
+    ));
+    p.print("------------------"
+        + "-------+--------------------+----------+--------------+\n");
+    for (final Ehcache cache : getAllCaches()) {
+      final CacheConfiguration cfg = cache.getCacheConfiguration();
+      final boolean useDisk = cfg.isDiskPersistent() || cfg.isOverflowToDisk();
+      final Statistics stat = cache.getStatistics();
+      final long total = stat.getCacheHits() + stat.getCacheMisses();
+
+      if (useDisk) {
+        p.print(String.format(//
+            "D %-18s %-4s|%6s %6s %6s| %7s  |%4s %4s %4s|\n" //
+            , cache.getName() //
+            , interval(cfg.getTimeToLiveSeconds()) //
+            , count(stat.getDiskStoreObjectCount()) //
+            , count(stat.getMemoryStoreObjectCount()) //
+            , count(stat.getObjectCount()) //
+            , duration(stat.getAverageGetTime()) //
+            , percent(stat.getOnDiskHits(), total) //
+            , percent(stat.getInMemoryHits(), total) //
+            , percent(stat.getCacheHits(), total) //
+            ));
+      } else {
+        p.print(String.format(//
+            "  %-18s %-4s|%6s %6s %6s| %7s  |%4s %4s %4s|\n" //
+            , cache.getName() //
+            , interval(cfg.getTimeToLiveSeconds()) //
+            , "", "" //
+            , count(stat.getObjectCount()) //
+            , duration(stat.getAverageGetTime()) //
+            , "", "" //
+            , percent(stat.getCacheHits(), total) //
+            ));
+      }
+    }
+    p.print('\n');
+
+    if (gc) {
+      System.gc();
+      System.runFinalization();
+      System.gc();
+    }
+
+    sshSummary();
+    taskSummary();
+    memSummary();
+
+    if (showJVM) {
+      jvmSummary();
+    }
+
+    p.flush();
+  }
+
+  private void memSummary() {
+    final Runtime r = Runtime.getRuntime();
+    final long mMax = r.maxMemory();
+    final long mFree = r.freeMemory();
+    final long mTotal = r.totalMemory();
+    final long mInuse = mTotal - mFree;
+
+    final int jgitOpen = WindowCacheStatAccessor.getOpenFiles();
+    final long jgitBytes = WindowCacheStatAccessor.getOpenBytes();
+
+    p.format("Mem: %s total = %s used + %s free + %s buffers\n",
+        bytes(mTotal),
+        bytes(mInuse - jgitBytes),
+        bytes(mFree),
+        bytes(jgitBytes));
+    p.format("     %s max\n", bytes(mMax));
+    p.format("    %8d open files, %8d cpus available, %8d threads\n",
+        jgitOpen,
+        r.availableProcessors(),
+        ManagementFactory.getThreadMXBean().getThreadCount());
+    p.print('\n');
+  }
+
+  private void taskSummary() {
+    Collection<Task<?>> pending = workQueue.getTasks();
+    int tasksTotal = pending.size();
+    int tasksRunning = 0, tasksReady = 0, tasksSleeping = 0;
+    for (Task<?> task : pending) {
+      switch (task.getState()) {
+        case RUNNING: tasksRunning++; break;
+        case READY: tasksReady++; break;
+        case SLEEPING: tasksSleeping++; break;
+      }
+    }
+    p.format(
+        "Tasks: %4d  total = %4d running +   %4d ready + %4d sleeping\n",
+        tasksTotal,
+        tasksRunning,
+        tasksReady,
+        tasksSleeping);
+  }
+
+  private void sshSummary() {
+    IoAcceptor acceptor = daemon.getIoAcceptor();
+    if (acceptor == null) {
+      return;
+    }
+
+    long now = System.currentTimeMillis();
+    Collection<IoSession> list = acceptor.getManagedSessions().values();
+    long oldest = now;
+    for (IoSession s : list) {
+      oldest = Math.min(oldest, s.getCreationTime());
+    }
+
+    p.format(
+        "SSH:   %4d  users, oldest session started %s ago\n",
+        list.size(),
+        uptime(now - oldest));
+  }
+
+  private void jvmSummary() {
+    OperatingSystemMXBean osBean = ManagementFactory.getOperatingSystemMXBean();
+    RuntimeMXBean runtimeBean = ManagementFactory.getRuntimeMXBean();
+    p.format("JVM: %s %s %s\n",
+        runtimeBean.getVmVendor(),
+        runtimeBean.getVmName(),
+        runtimeBean.getVmVersion());
+    p.format("  on %s %s %s\n", "",
+        osBean.getName(),
+        osBean.getVersion(),
+        osBean.getArch());
+    try {
+      p.format("  running as %s on %s\n",
+          System.getProperty("user.name"),
+          InetAddress.getLocalHost().getHostName());
+    } catch (UnknownHostException e) {
+    }
+    p.format("  cwd  %s\n", path(new File(".").getAbsoluteFile().getParentFile()));
+    p.format("  site %s\n", path(sitePath));
+  }
+
+  private String path(File file) {
+    try {
+      return file.getCanonicalPath();
+    } catch (IOException err) {
+      return file.getAbsolutePath();
+    }
+  }
+
+  private String uptime(long uptimeMillis) {
+    if (uptimeMillis < 1000) {
+      return String.format("%3d ms", uptimeMillis);
+    }
+
+    long uptime = uptimeMillis / 1000L;
+
+    long min = uptime / 60;
+    if (min < 60) {
+      return String.format("%2d min %2d sec", min, uptime - min * 60);
+    }
+
+    long hr = uptime / 3600;
+    if (hr < 24) {
+      min = (uptime - hr * 3600) / 60;
+      return String.format("%2d hrs %2d min", hr, min);
+    }
+
+    long days = uptime / (24 * 3600);
+    hr = (uptime - (days * 24 * 3600)) / 3600;
+    return String.format("%4d days %2d hrs", days, hr);
+  }
+
+  private String bytes(double value) {
+    value /= 1024;
+    String suffix = "k";
+
+    if (value > 1024) {
+      value /= 1024;
+      suffix = "m";
+    }
+    if (value > 1024) {
+      value /= 1024;
+      suffix = "g";
+    }
+    return String.format("%1$6.2f%2$s", value, suffix);
+  }
+
+  private String count(long cnt) {
+    if (cnt == 0) {
+      return "";
+    }
+    return String.format("%6d", cnt);
+  }
+
+  private String duration(double ms) {
+    if (Math.abs(ms) <= 0.05) {
+      return "";
+    }
+    String suffix = "ms";
+    if (ms >= 1000) {
+      ms /= 1000;
+      suffix = "s ";
+    }
+    return String.format("%4.1f%s", ms, suffix);
+  }
+
+  private String interval(double ttl) {
+    if (ttl == 0) {
+      return "inf";
+    }
+
+    String suffix = "s";
+    if (ttl >= 60) {
+      ttl /= 60;
+      suffix = "m";
+
+      if (ttl >= 60) {
+        ttl /= 60;
+        suffix = "h";
+      }
+
+      if (ttl >= 24) {
+        ttl /= 24;
+        suffix = "d";
+
+        if (ttl >= 365) {
+          ttl /= 365;
+          suffix = "y";
+        }
+      }
+    }
+
+    return Integer.toString((int) ttl) + suffix;
+  }
+
+  private String percent(final long value, final long total) {
+    if (total <= 0) {
+      return "";
+    }
+    final long pcent = (100 * value) / total;
+    return String.format("%3d%%", (int) pcent);
+  }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminShowConnections.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowConnections.java
similarity index 92%
rename from gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminShowConnections.java
rename to gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowConnections.java
index b6c6119..a72ce90 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminShowConnections.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowConnections.java
@@ -17,7 +17,6 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.util.IdGenerator;
-import com.google.gerrit.sshd.AdminCommand;
 import com.google.gerrit.sshd.BaseCommand;
 import com.google.gerrit.sshd.SshDaemon;
 import com.google.gerrit.sshd.SshSession;
@@ -41,14 +40,16 @@
 import java.util.List;
 
 /** Show the current SSH connections. */
-@AdminCommand
-final class AdminShowConnections extends BaseCommand {
+final class ShowConnections extends BaseCommand {
   @Option(name = "--numeric", aliases = {"-n"}, usage = "don't resolve names")
   private boolean numeric;
 
   private PrintWriter p;
 
   @Inject
+  IdentifiedUser currentUser;
+
+  @Inject
   private SshDaemon daemon;
 
   @Override
@@ -56,8 +57,15 @@
     startThread(new CommandRunnable() {
       @Override
       public void run() throws Exception {
+        if (!currentUser.getCapabilities().canViewConnections()) {
+          String msg = String.format(
+            "fatal: %s does not have \"View Connections\" capability.",
+            currentUser.getUserName());
+          throw new UnloggedFailure(BaseCommand.STATUS_NOT_ADMIN, msg);
+        }
+
         parseCommandLine();
-        AdminShowConnections.this.display();
+        ShowConnections.this.display();
       }
     });
   }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowQueue.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowQueue.java
index a196a3e..e835ffe 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowQueue.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowQueue.java
@@ -14,8 +14,8 @@
 
 package com.google.gerrit.sshd.commands;
 
-import com.google.gerrit.reviewdb.Project;
-import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.git.WorkQueue.ProjectTask;
 import com.google.gerrit.server.git.WorkQueue.Task;
@@ -50,7 +50,7 @@
   private ProjectCache projectCache;
 
   @Inject
-  private CurrentUser userProvider;
+  private IdentifiedUser currentUser;
 
   private PrintWriter p;
   private int columns = 80;
@@ -110,7 +110,7 @@
 
     int numberOfPendingTasks = 0;
     final long now = System.currentTimeMillis();
-    final boolean isAdministrator = userProvider.isAdministrator();
+    final boolean viewAll = currentUser.getCapabilities().canViewQueue();
 
     for (final Task<?> task : pending) {
       final long delay = task.getDelay(TimeUnit.MILLISECONDS);
@@ -137,7 +137,7 @@
       Project.NameKey projectName = null;
       String remoteName = null;
 
-      if (!isAdministrator) {
+      if (!viewAll) {
         if (task instanceof ProjectTask<?>) {
           projectName = ((ProjectTask<?>)task).getProjectNameKey();
           remoteName = ((ProjectTask<?>)task).getRemoteName();
@@ -149,7 +149,7 @@
           e = projectCache.get(projectName);
         }
 
-        regularUserCanSee = e != null && e.controlFor(userProvider).isVisible();
+        regularUserCanSee = e != null && e.controlFor(currentUser).isVisible();
 
         if (regularUserCanSee) {
           numberOfPendingTasks++;
@@ -157,7 +157,7 @@
       }
 
       // Shows information about tasks depending on the user rights
-      if (isAdministrator || (!hasCustomizedPrint && regularUserCanSee)) {
+      if (viewAll || (!hasCustomizedPrint && regularUserCanSee)) {
         p.print(String.format("%8s %-12s %-8s %s\n", //
             id(task.getTaskId()), start, "", format(task)));
       } else if (regularUserCanSee) {
@@ -174,7 +174,7 @@
     p.print("----------------------------------------------"
         + "--------------------------------\n");
 
-    if (isAdministrator) {
+    if (viewAll) {
       numberOfPendingTasks = pending.size();
     }
 
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/Upload.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Upload.java
index 951568a..46eb788 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Upload.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Upload.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.sshd.commands;
 
-import com.google.gerrit.reviewdb.ReviewDb;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.git.TagCache;
 import com.google.gerrit.server.git.TransferConfig;
 import com.google.gerrit.server.git.VisibleRefFilter;
@@ -25,7 +25,6 @@
 import org.eclipse.jgit.transport.UploadPack;
 
 import java.io.IOException;
-import java.io.InterruptedIOException;
 
 /** Publishes Git repositories over SSH using the Git upload-pack protocol. */
 final class Upload extends AbstractGitCommand {
@@ -46,15 +45,11 @@
 
     final UploadPack up = new UploadPack(repo);
     if (!projectControl.allRefsAreVisible()) {
-      up.setRefFilter(new VisibleRefFilter(tagCache, repo, projectControl,
-          db.get(), true));
+      up.setAdvertiseRefsHook(new VisibleRefFilter(tagCache, repo,
+          projectControl, db.get(), true));
     }
     up.setPackConfig(config.getPackConfig());
     up.setTimeout(config.getTimeout());
-    try {
-      up.upload(in, out, err);
-    } catch (InterruptedIOException err) {
-      throw new Failure(128, "fatal: client IO read/write timeout", err);
-    }
+    up.upload(in, out, err);
   }
 }
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/.settings/org.eclipse.core.resources.prefs b/gerrit-util-cli/.settings/org.eclipse.core.resources.prefs
index 82eb859..c780f44 100644
--- a/gerrit-util-cli/.settings/org.eclipse.core.resources.prefs
+++ b/gerrit-util-cli/.settings/org.eclipse.core.resources.prefs
@@ -1,3 +1,4 @@
-#Tue Sep 02 16:59:24 PDT 2008
+#Thu Jul 28 11:02:36 PDT 2011
 eclipse.preferences.version=1
+encoding//src/main/java=UTF-8
 encoding/<project>=UTF-8
diff --git a/gerrit-util-cli/.settings/org.eclipse.jdt.core.prefs b/gerrit-util-cli/.settings/org.eclipse.jdt.core.prefs
index 04afc7f..470942d 100644
--- a/gerrit-util-cli/.settings/org.eclipse.jdt.core.prefs
+++ b/gerrit-util-cli/.settings/org.eclipse.jdt.core.prefs
@@ -1,4 +1,4 @@
-#Tue May 12 17:44:13 PDT 2009
+#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
@@ -9,6 +9,7 @@
 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
diff --git a/gerrit-util-cli/pom.xml b/gerrit-util-cli/pom.xml
index 6db5241..4ecbda4 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.1-SNAPSHOT</version>
+    <version>2.4-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-util-cli</artifactId>
@@ -39,12 +39,12 @@
     </dependency>
 
     <dependency>
-      <groupId>com.google.code.guice</groupId>
+      <groupId>com.google.inject</groupId>
       <artifactId>guice</artifactId>
     </dependency>
 
     <dependency>
-      <groupId>com.google.code.guice</groupId>
+      <groupId>com.google.inject.extensions</groupId>
       <artifactId>guice-assistedinject</artifactId>
     </dependency>
   </dependencies>
diff --git a/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/CmdLineParser.java b/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/CmdLineParser.java
index d5a2a98..de2f7e9 100644
--- a/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/CmdLineParser.java
+++ b/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/CmdLineParser.java
@@ -42,13 +42,19 @@
 import org.kohsuke.args4j.Argument;
 import org.kohsuke.args4j.CmdLineException;
 import org.kohsuke.args4j.IllegalAnnotationError;
+import org.kohsuke.args4j.NamedOptionDef;
 import org.kohsuke.args4j.Option;
 import org.kohsuke.args4j.OptionDef;
+import org.kohsuke.args4j.spi.BooleanOptionHandler;
 import org.kohsuke.args4j.spi.OptionHandler;
 import org.kohsuke.args4j.spi.Setter;
 
+import java.io.StringWriter;
 import java.io.Writer;
+import java.lang.annotation.Annotation;
 import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
 import java.util.ResourceBundle;
 
 /**
@@ -60,6 +66,7 @@
  * args4j style format prior to invoking args4j for parsing.
  */
 public class CmdLineParser {
+
   public interface Factory {
     CmdLineParser create(Object bean);
   }
@@ -102,6 +109,19 @@
     parser.printUsage(out, rb);
   }
 
+  public void printDetailedUsage(String name, StringWriter out) {
+    out.write(name);
+    printSingleLineUsage(out, null);
+    out.write('\n');
+    out.write('\n');
+    printUsage(out, null);
+    out.write('\n');
+  }
+
+  public boolean wasHelpRequestedByOption() {
+    return parser.help.value;
+  }
+
   public void parseArgument(final String... args) throws CmdLineException {
     final ArrayList<String> tmp = new ArrayList<String>(args.length);
     for (int argi = 0; argi < args.length; argi++) {
@@ -123,13 +143,86 @@
 
       tmp.add(str);
     }
-
     parser.parseArgument(tmp.toArray(new String[tmp.size()]));
   }
 
+  public void parseOptionMap(Map<String, String[]> parameters)
+      throws CmdLineException {
+    ArrayList<String> tmp = new ArrayList<String>();
+    for (Map.Entry<String, String[]> ent : parameters.entrySet()) {
+      String name = ent.getKey();
+      if (!name.startsWith("-")) {
+        if (name.length() == 1) {
+          name = "-" + name;
+        } else {
+          name = "--" + name;
+        }
+      }
+
+      if (findHandler(name) instanceof BooleanOptionHandler) {
+        boolean on = false;
+        for (String value : ent.getValue()) {
+          on = toBoolean(ent.getKey(), value);
+        }
+        if (on) {
+          tmp.add(name);
+        }
+      } else {
+        for (String value : ent.getValue()) {
+          tmp.add(name);
+          tmp.add(value);
+        }
+      }
+    }
+    parser.parseArgument(tmp.toArray(new String[tmp.size()]));
+  }
+
+  @SuppressWarnings("rawtypes")
+  private OptionHandler findHandler(String name) {
+    for (OptionHandler handler : parser.options) {
+      if (handler.option instanceof NamedOptionDef) {
+        NamedOptionDef def = (NamedOptionDef) handler.option;
+        if (name.equals(def.name())) {
+          return handler;
+        }
+        for (String alias : def.aliases()) {
+          if (name.equals(alias)) {
+            return handler;
+          }
+        }
+      }
+    }
+    return null;
+  }
+
+  private boolean toBoolean(String name, String value) throws CmdLineException {
+    if ("true".equals(value) || "t".equals(value)
+        || "yes".equals(value) || "y".equals(value)
+        || "on".equals(value)
+        || "1".equals(value)
+        || value == null || "".equals(value)) {
+      return true;
+    }
+
+    if ("false".equals(value) || "f".equals(value)
+        || "no".equals(value) || "n".equals(value)
+        || "off".equals(value)
+        || "0".equals(value)) {
+      return false;
+    }
+
+    throw new CmdLineException(parser, String.format(
+        "invalid boolean \"%s=%s\"", name, value));
+  }
+
   private class MyParser extends org.kohsuke.args4j.CmdLineParser {
+    @SuppressWarnings("rawtypes")
+    private List<OptionHandler> options;
+    private HelpOption help;
+
     MyParser(final Object bean) {
       super(bean);
+      ensureOptionsInitialized();
     }
 
     @SuppressWarnings({"unchecked", "rawtypes"})
@@ -137,7 +230,7 @@
     protected OptionHandler createOptionHandler(final OptionDef option,
         final Setter setter) {
       if (isHandlerSpecified(option) || isEnum(setter) || isPrimitive(setter)) {
-        return super.createOptionHandler(option, setter);
+        return add(super.createOptionHandler(option, setter));
       }
 
       final Key<OptionHandlerFactory<?>> key =
@@ -145,12 +238,28 @@
       Injector i = injector;
       while (i != null) {
         if (i.getBindings().containsKey(key)) {
-          return i.getInstance(key).create(this, option, setter);
+          return add(i.getInstance(key).create(this, option, setter));
         }
         i = i.getParent();
       }
 
-      return super.createOptionHandler(option, setter);
+      return add(super.createOptionHandler(option, setter));
+    }
+
+    @SuppressWarnings("rawtypes")
+    private OptionHandler add(OptionHandler handler) {
+      ensureOptionsInitialized();
+      options.add(handler);
+      return handler;
+    }
+
+    @SuppressWarnings("rawtypes")
+    private void ensureOptionsInitialized() {
+      if (options == null) {
+        help = new HelpOption();
+        options = new ArrayList<OptionHandler>();
+        addOption(help, help);
+      }
     }
 
     private boolean isHandlerSpecified(final OptionDef option) {
@@ -165,4 +274,63 @@
       return setter.getType().isPrimitive();
     }
   }
+
+  private static class HelpOption implements Option, Setter<Boolean> {
+    private boolean value;
+
+    @Override
+    public String name() {
+      return "--help";
+    }
+
+    @Override
+    public String[] aliases() {
+      return new String[] {"-h"};
+    }
+
+    @Override
+    public String usage() {
+      return "display this help text";
+    }
+
+    @Override
+    public void addValue(Boolean val) {
+      value = val;
+    }
+
+    @Override
+    public Class<? extends OptionHandler<Boolean>> handler() {
+      return BooleanOptionHandler.class;
+    }
+
+    @Override
+    public String metaVar() {
+      return "";
+    }
+
+    @Override
+    public boolean multiValued() {
+      return false;
+    }
+
+    @Override
+    public boolean required() {
+      return false;
+    }
+
+    @Override
+    public Class<? extends Annotation> annotationType() {
+      return Option.class;
+    }
+
+    @Override
+    public Class<Boolean> getType() {
+      return Boolean.class;
+    }
+
+    @Override
+    public boolean isMultiValued() {
+      return multiValued();
+    }
+  }
 }
diff --git a/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/OptionHandlerFactory.java b/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/OptionHandlerFactory.java
index 0009d05..8e99778 100644
--- a/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/OptionHandlerFactory.java
+++ b/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/OptionHandlerFactory.java
@@ -20,7 +20,6 @@
 
 /** Creates an args4j OptionHandler through a Guice Injector. */
 public interface OptionHandlerFactory<T> {
-  @SuppressWarnings("rawtypes")
-  OptionHandler create(org.kohsuke.args4j.CmdLineParser cmdLineParser,
-      OptionDef optionDef, Setter setter);
+  OptionHandler<T> create(org.kohsuke.args4j.CmdLineParser cmdLineParser,
+      OptionDef optionDef, Setter<T> setter);
 }
diff --git a/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/OptionHandlerUtil.java b/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/OptionHandlerUtil.java
index ab67397..7af544c 100644
--- a/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/OptionHandlerUtil.java
+++ b/gerrit-util-cli/src/main/java/com/google/gerrit/util/cli/OptionHandlerUtil.java
@@ -15,8 +15,11 @@
 package com.google.gerrit.util.cli;
 
 import com.google.inject.Key;
-import com.google.inject.TypeLiteral;
-import com.google.inject.internal.MoreTypes.ParameterizedTypeImpl;
+import com.google.inject.Module;
+import com.google.inject.assistedinject.FactoryModuleBuilder;
+import com.google.inject.util.Types;
+
+import org.kohsuke.args4j.spi.OptionHandler;
 
 import java.lang.reflect.Type;
 
@@ -25,10 +28,21 @@
   /** Generate a key for an {@link OptionHandlerFactory} in Guice. */
   @SuppressWarnings("unchecked")
   public static <T> Key<OptionHandlerFactory<T>> keyFor(final Class<T> valueType) {
-    final Type factoryType =
-        new ParameterizedTypeImpl(null, OptionHandlerFactory.class, valueType);
+    final Type factoryType = Types.newParameterizedType(OptionHandlerFactory.class, valueType);
+    return (Key<OptionHandlerFactory<T>>) Key.get(factoryType);
+  }
 
-    return (Key<OptionHandlerFactory<T>>) Key.get(TypeLiteral.get(factoryType));
+  @SuppressWarnings("unchecked")
+  private static <T> Key<OptionHandler<T>> handlerOf(Class<T> type) {
+    final Type handlerType = Types.newParameterizedTypeWithOwner(null, OptionHandler.class, type);
+    return (Key<OptionHandler<T>>) Key.get(handlerType);
+  }
+
+  @SuppressWarnings("unchecked")
+  public static <T> Module moduleFor(final Class<T> type, Class<? extends OptionHandler<T>> impl) {
+    return new FactoryModuleBuilder()
+        .implement(handlerOf(type), impl)
+        .build(keyFor(type));
   }
 
   private OptionHandlerUtil() {
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/.settings/org.eclipse.core.resources.prefs b/gerrit-util-ssl/.settings/org.eclipse.core.resources.prefs
index 82eb859..589908f 100644
--- a/gerrit-util-ssl/.settings/org.eclipse.core.resources.prefs
+++ b/gerrit-util-ssl/.settings/org.eclipse.core.resources.prefs
@@ -1,3 +1,4 @@
-#Tue Sep 02 16:59:24 PDT 2008
+#Thu Jul 28 11:02:35 PDT 2011
 eclipse.preferences.version=1
+encoding//src/main/java=UTF-8
 encoding/<project>=UTF-8
diff --git a/gerrit-util-ssl/.settings/org.eclipse.jdt.core.prefs b/gerrit-util-ssl/.settings/org.eclipse.jdt.core.prefs
index 04afc7f..5f73a7f 100644
--- a/gerrit-util-ssl/.settings/org.eclipse.jdt.core.prefs
+++ b/gerrit-util-ssl/.settings/org.eclipse.jdt.core.prefs
@@ -1,4 +1,4 @@
-#Tue May 12 17:44:13 PDT 2009
+#Thu Jul 28 11:02:35 PDT 2011
 eclipse.preferences.version=1
 org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
 org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
@@ -9,6 +9,7 @@
 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
diff --git a/gerrit-util-ssl/pom.xml b/gerrit-util-ssl/pom.xml
index 39d3ce0..2e49d47 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.1-SNAPSHOT</version>
+    <version>2.4-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/.settings/org.eclipse.core.resources.prefs b/gerrit-war/.settings/org.eclipse.core.resources.prefs
index 82eb859..d404b00 100644
--- a/gerrit-war/.settings/org.eclipse.core.resources.prefs
+++ b/gerrit-war/.settings/org.eclipse.core.resources.prefs
@@ -1,3 +1,5 @@
-#Tue Sep 02 16:59:24 PDT 2008
+#Thu Jul 28 11:02:37 PDT 2011
 eclipse.preferences.version=1
+encoding//src/main/java=UTF-8
+encoding//src/main/resources=UTF-8
 encoding/<project>=UTF-8
diff --git a/gerrit-war/.settings/org.eclipse.jdt.core.prefs b/gerrit-war/.settings/org.eclipse.jdt.core.prefs
index 04afc7f..a2d8fcb 100644
--- a/gerrit-war/.settings/org.eclipse.jdt.core.prefs
+++ b/gerrit-war/.settings/org.eclipse.jdt.core.prefs
@@ -1,4 +1,4 @@
-#Tue May 12 17:44:13 PDT 2009
+#Thu Jul 28 11:02:37 PDT 2011
 eclipse.preferences.version=1
 org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled
 org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
@@ -9,6 +9,7 @@
 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
diff --git a/gerrit-war/pom.xml b/gerrit-war/pom.xml
index 2d85b74..733d976a 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.1-SNAPSHOT</version>
+    <version>2.4-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/SitePathFromSystemConfigProvider.java b/gerrit-war/src/main/java/com/google/gerrit/httpd/SitePathFromSystemConfigProvider.java
new file mode 100644
index 0000000..b97df3f
--- /dev/null
+++ b/gerrit-war/src/main/java/com/google/gerrit/httpd/SitePathFromSystemConfigProvider.java
@@ -0,0 +1,61 @@
+// 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.reviewdb.client.SystemConfig;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.config.SitePath;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.io.File;
+import java.util.List;
+
+/** Provides {@link java.io.File} annotated with {@link SitePath}. */
+class SitePathFromSystemConfigProvider implements Provider<File> {
+  private final File path;
+
+  @Inject
+  SitePathFromSystemConfigProvider(SchemaFactory<ReviewDb> schemaFactory)
+      throws OrmException {
+    path = read(schemaFactory);
+  }
+
+  @Override
+  public File get() {
+    return path;
+  }
+
+  private static File read(SchemaFactory<ReviewDb> schemaFactory)
+      throws OrmException {
+    ReviewDb db = schemaFactory.open();
+    try {
+      List<SystemConfig> all = db.systemConfig().all().toList();
+      switch (all.size()) {
+        case 1:
+          return new File(all.get(0).sitePath);
+        case 0:
+          throw new OrmException("system_config table is empty");
+        default:
+          throw new OrmException("system_config must have exactly 1 row;"
+              + " found " + all.size() + " rows instead");
+      }
+    } finally {
+      db.close();
+    }
+  }
+}
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 19c16ca..01b4a44 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,17 +17,30 @@
 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.client.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.config.SitePathFromSystemConfigProvider;
+import com.google.gerrit.server.contact.HttpContactStoreConnection;
+import com.google.gerrit.server.git.LocalDiskRepositoryManager;
+import com.google.gerrit.server.git.PushReplication;
+import com.google.gerrit.server.git.ReceiveCommitsExecutorModule;
+import com.google.gerrit.server.git.WorkQueue;
+import com.google.gerrit.server.mail.SignedTokenEmailTokenVerifier;
+import com.google.gerrit.server.mail.SmtpEmailSender;
 import com.google.gerrit.server.schema.DataSourceProvider;
 import com.google.gerrit.server.schema.DatabaseModule;
+import com.google.gerrit.server.schema.SchemaModule;
+import com.google.gerrit.server.schema.SchemaVersionCheck;
 import com.google.gerrit.sshd.SshModule;
 import com.google.gerrit.sshd.commands.MasterCommandModule;
 import com.google.inject.AbstractModule;
@@ -114,6 +127,7 @@
 
       manager = new LifecycleManager();
       manager.add(dbInjector);
+      manager.add(cfgInjector);
       manager.add(sysInjector);
       manager.add(sshInjector);
       manager.add(webInjector);
@@ -166,13 +180,23 @@
       });
       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);
   }
 
   private Injector createSysInjector() {
     final List<Module> modules = new ArrayList<Module>();
+    modules.add(new WorkQueue.Module());
+    modules.add(new ChangeHookRunner.Module());
+    modules.add(new ReceiveCommitsExecutorModule());
     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() {
@@ -192,7 +216,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 496230f..c36da4e 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.1-SNAPSHOT</version>
+  <version>2.4-SNAPSHOT</version>
 
   <name>Gerrit Code Review - Parent</name>
   <url>http://code.google.com/p/gerrit/</url>
@@ -46,13 +46,13 @@
   </issueManagement>
 
   <properties>
-    <jgitVersion>0.12.1.54-gc125448</jgitVersion>
-    <gwtormVersion>1.1.5</gwtormVersion>
-    <gwtjsonrpcVersion>1.2.3</gwtjsonrpcVersion>
-    <gwtexpuiVersion>1.2.2</gwtexpuiVersion>
-    <gwtVersion>2.1.1</gwtVersion>
+    <jgitVersion>1.3.0.201202151440-r.76-g0135ed8</jgitVersion>
+    <gwtormVersion>1.4</gwtormVersion>
+    <gwtjsonrpcVersion>1.3</gwtjsonrpcVersion>
+    <gwtexpuiVersion>1.2.5</gwtexpuiVersion>
+    <gwtVersion>2.3.0</gwtVersion>
     <slf4jVersion>1.6.1</slf4jVersion>
-    <guiceVersion>2.0</guiceVersion>
+    <guiceVersion>3.0</guiceVersion>
     <jettyVersion>7.2.1.v20101111</jettyVersion>
 
     <gwt.compileReport>false</gwt.compileReport>
@@ -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>
@@ -324,6 +326,12 @@
 
         <plugin>
           <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-source-plugin</artifactId>
+          <version>2.1.2</version>
+        </plugin>
+
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-shade-plugin</artifactId>
           <version>1.4</version>
         </plugin>
@@ -355,13 +363,53 @@
         <plugin>
           <groupId>org.codehaus.mojo</groupId>
           <artifactId>gwt-maven-plugin</artifactId>
-          <version>2.1.0-1</version>
+          <version>2.3.0</version>
         </plugin>
 
         <plugin>
           <groupId>org.codehaus.mojo</groupId>
           <artifactId>build-helper-maven-plugin</artifactId>
-          <version>1.6</version>
+          <version>1.5</version>
+        </plugin>
+
+        <!--This plugin's configuration is used to store Eclipse 
+            m2e settings only. It has no influence on the Maven build itself. -->
+        <plugin>
+          <groupId>org.eclipse.m2e</groupId>
+          <artifactId>lifecycle-mapping</artifactId>
+          <version>1.0.0</version>
+          <configuration>
+            <lifecycleMappingMetadata>
+              <pluginExecutions>
+                <pluginExecution>
+                  <pluginExecutionFilter>
+                    <groupId>org.apache.maven.plugins</groupId>
+                    <artifactId>maven-antrun-plugin</artifactId>
+                    <versionRange>[1.0,)</versionRange>
+                    <goals>
+                      <goal>run</goal>
+                    </goals>
+                  </pluginExecutionFilter>
+                  <action>
+                    <ignore/>
+                  </action>
+                </pluginExecution>
+                <pluginExecution>
+                  <pluginExecutionFilter>
+                    <groupId>org.codehaus.mojo</groupId>
+                    <artifactId>build-helper-maven-plugin</artifactId>
+                    <versionRange>[1.0,)</versionRange>
+                    <goals>
+                      <goal>add-source</goal>
+                    </goals>
+                  </pluginExecutionFilter>
+                  <action>
+                    <ignore/>
+                  </action>
+                </pluginExecution>
+              </pluginExecutions>
+            </lifecycleMappingMetadata>
+          </configuration>
         </plugin>
       </plugins>
     </pluginManagement>
@@ -376,19 +424,6 @@
           <encoding>UTF-8</encoding>
         </configuration>
       </plugin>
-
-      <plugin>
-        <groupId>org.apache.maven.plugins</groupId>
-        <artifactId>maven-source-plugin</artifactId>
-        <version>2.1.2</version>
-        <executions>
-          <execution>
-            <goals>
-              <goal>jar</goal>
-            </goals>
-          </execution>
-        </executions>
-      </plugin>
     </plugins>
   </build>
 
@@ -451,6 +486,11 @@
         <type>pom</type>
         <exclusions>
           <exclusion>
+            <!-- conflicts with our use of guice 3.0 -->
+            <groupId>com.google.code.guice</groupId>
+            <artifactId>guice</artifactId>
+          </exclusion>
+          <exclusion>
             <!-- jug-1.1 is LGPL, and the source has been lost -->
             <groupId>jug</groupId>
             <artifactId>jug</artifactId>
@@ -486,7 +526,7 @@
       <dependency>
         <groupId>com.jcraft</groupId>
         <artifactId>jsch</artifactId>
-        <version>0.1.44</version>
+        <version>0.1.44-1</version>
       </dependency>
 
       <dependency>
@@ -508,19 +548,33 @@
       </dependency>
 
       <dependency>
-        <groupId>com.google.code.guice</groupId>
+        <groupId>javax.validation</groupId>
+        <artifactId>validation-api</artifactId>
+        <version>1.0.0.GA</version>
+        <scope>provided</scope>
+      </dependency>
+      <dependency>
+        <groupId>javax.validation</groupId>
+        <artifactId>validation-api</artifactId>
+        <version>1.0.0.GA</version>
+        <classifier>sources</classifier>
+        <scope>provided</scope>
+      </dependency>
+
+      <dependency>
+        <groupId>com.google.inject</groupId>
         <artifactId>guice</artifactId>
         <version>${guiceVersion}</version>
       </dependency>
 
       <dependency>
-        <groupId>com.google.code.guice</groupId>
+        <groupId>com.google.inject.extensions</groupId>
         <artifactId>guice-servlet</artifactId>
         <version>${guiceVersion}</version>
       </dependency>
 
       <dependency>
-        <groupId>com.google.code.guice</groupId>
+        <groupId>com.google.inject.extensions</groupId>
         <artifactId>guice-assistedinject</artifactId>
         <version>${guiceVersion}</version>
       </dependency>
@@ -533,12 +587,6 @@
       </dependency>
 
       <dependency>
-        <groupId>com.google.code.gson</groupId>
-        <artifactId>gson</artifactId>
-        <version>1.6</version>
-      </dependency>
-
-      <dependency>
         <groupId>commons-net</groupId>
         <artifactId>commons-net</artifactId>
         <version>2.2</version>
@@ -749,7 +797,13 @@
       <dependency>
         <groupId>dk.brics.automaton</groupId>
         <artifactId>automaton</artifactId>
-        <version>1.11.2</version>
+        <version>1.11.8</version>
+      </dependency>
+
+      <dependency>
+        <groupId>com.googlecode.prolog-cafe</groupId>
+        <artifactId>PrologCafe</artifactId>
+        <version>1.3</version>
       </dependency>
     </dependencies>
   </dependencyManagement>
@@ -767,7 +821,7 @@
 
     <repository>
       <id>gson</id>
-      <url>http://google-gson.googlecode.com/svn/mavenrepo/</url>
+      <url>https://google-gson.googlecode.com/svn/mavenrepo/</url>
     </repository>
 
     <repository>
diff --git a/tools/gitlog2asciidoc.py b/tools/gitlog2asciidoc.py
new file mode 100755
index 0000000..dfbad82
--- /dev/null
+++ b/tools/gitlog2asciidoc.py
@@ -0,0 +1,107 @@
+#!/usr/bin/python
+from optparse import OptionParser
+import re
+import subprocess
+import sys
+
+"""
+This script generates a release note from the output of git log
+between the specified tags.
+
+Options:
+--issues          Show output the commits with issues associated with them.
+--issue-numbers   Show outputs issue numbers of the commits with issues
+                  associated with them
+
+Arguments:
+since -- tag name
+until -- tag name
+
+Example Input:
+
+   * <commit subject>
+   +
+   <commit message>
+
+   Bug: issue 123
+   Change-Id: <change id>
+   Signed-off-by: <name>
+
+Expected Output:
+
+   * issue 123 <commit subject>
+   +
+   <commit message>
+"""
+
+parser = OptionParser(usage='usage: %prog [options] <since> <until>')
+
+parser.add_option('-i', '--issues', action='store_true',
+                  dest='issues_only', default=False,
+                  help='only output the commits with issues association')
+
+parser.add_option('-n', '--issue-numbers', action='store_true',
+                  dest='issue_numbers_only', default=False,
+                  help='only outputs issue numbers of the commits with \
+                        issues association')
+
+(options, args) = parser.parse_args()
+
+if len(args) != 2:
+    parser.error("wrong number of arguments")
+
+issues_only = options.issues_only
+issue_numbers_only = options.issue_numbers_only
+
+since_until = args[0] + '..' + args[1]
+proc = subprocess.Popen(['git', 'log', '--reverse', '--no-merges',
+                         since_until, "--format=* %s%n+%n%b"],
+                         stdout=subprocess.PIPE,
+                         stderr=subprocess.STDOUT,)
+
+stdout_value = proc.communicate()[0]
+
+subject = ""
+message = []
+is_issue = False
+
+# regex pattern to match following cases such as Bug: 123, Issue Bug: 123,
+# Bug: GERRIT-123, Bug: issue 123, Bug issue: 123, issue: 123, issue: bug 123
+p = re.compile('bug: GERRIT-|bug(:? issue)?:? |issue(:? bug)?:? ',
+               re.IGNORECASE)
+
+if issue_numbers_only:
+    for line in stdout_value.splitlines(True):
+        if p.match(line):
+            sys.stdout.write(p.sub('', line))
+else:
+    for line in stdout_value.splitlines(True):
+        # Move issue number to subject line
+        if p.match(line):
+            line = p.sub('issue ', line).replace('\n',' ')
+            subject = subject[:2] + line + subject[2:]
+            is_issue = True
+        elif line.startswith('* '):
+            # Write change log for a commit
+            if subject != "":
+                if (not issues_only or is_issue):
+                    # Write subject
+                    sys.stdout.write(subject)
+                    # Write message lines
+                    if message != []:
+                        # Clear + from last line in commit message
+                        message[-1] = '\n'
+                    for m in message:
+                        sys.stdout.write(m)
+            # Start new commit block
+            message = []
+            subject = line
+            is_issue = False
+        # Remove commit footers
+        elif re.match(r'((\w+-)+\w+:)', line):
+            continue
+        # Don't add extra blank line if last one is already blank
+        elif line == '\n' and message and message[-1] != '+\n':
+                message.append('+\n')
+        elif line != '\n':
+            message.append(line)
diff --git a/tools/gwtui_dbg.launch b/tools/gwtui_dbg.launch
index 58f5ec8..ea76ee1 100644
--- a/tools/gwtui_dbg.launch
+++ b/tools/gwtui_dbg.launch
@@ -20,17 +20,17 @@
 <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;"/>
 <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/target/classes&quot; path=&quot;3&quot; type=&quot;2&quot;/&gt;&#10;"/>
 </listAttribute>
-<stringAttribute key="org.eclipse.jdt.launching.CLASSPATH_PROVIDER" value="org.maven.ide.eclipse.launchconfig.classpathProvider"/>
+<stringAttribute key="org.eclipse.jdt.launching.CLASSPATH_PROVIDER" value="org.eclipse.m2e.launchconfig.classpathProvider"/>
 <booleanAttribute key="org.eclipse.jdt.launching.DEFAULT_CLASSPATH" value="false"/>
 <stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value="com.google.gwt.dev.DevMode"/>
 <stringAttribute key="org.eclipse.jdt.launching.PROGRAM_ARGUMENTS" value="-startupUrl /&#10;-war ${resource_loc:/gerrit-gwtui/target}/gwt-hosted-mode&#10;-server com.google.gerrit.gwtdebug.GerritDebugLauncher&#10;com.google.gerrit.GerritGwtUI"/>
 <stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="gerrit-gwtdebug"/>
-<stringAttribute key="org.eclipse.jdt.launching.SOURCE_PATH_PROVIDER" value="org.maven.ide.eclipse.launchconfig.sourcepathProvider"/>
+<stringAttribute key="org.eclipse.jdt.launching.SOURCE_PATH_PROVIDER" value="org.eclipse.m2e.launchconfig.sourcepathProvider"/>
 <stringAttribute key="org.eclipse.jdt.launching.VM_ARGUMENTS" value="-Xmx256M&#10;&#10;-Dgerrit.site_path=${resource_loc:/gerrit-parent}/../test_site"/>
 </launchConfiguration>
diff --git a/tools/pgm_daemon.launch b/tools/pgm_daemon.launch
index f2dc73e..fd7b50f 100644
--- a/tools/pgm_daemon.launch
+++ b/tools/pgm_daemon.launch
@@ -11,10 +11,10 @@
 <stringAttribute key="org.eclipse.debug.core.source_locator_id" value="org.eclipse.jdt.launching.sourceLocator.JavaSourceLookupDirector"/>
 <stringAttribute key="org.eclipse.debug.core.source_locator_memento" value="&lt;?xml version=&quot;1.0&quot; encoding=&quot;UTF-8&quot; standalone=&quot;no&quot;?&gt;&#10;&lt;sourceLookupDirector&gt;&#10;&lt;sourceContainers duplicates=&quot;false&quot;&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-common&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-httpd&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-patch-commonsnet&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-patch-jgit&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-pgm&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-server&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-sshd&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-util-cli&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-util-ssl&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-war&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-reviewdb&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;javaProject name=&amp;quot;gerrit-main&amp;quot;/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.jdt.launching.sourceContainer.javaProject&quot;/&gt;&#10;&lt;container memento=&quot;&amp;lt;?xml version=&amp;quot;1.0&amp;quot; encoding=&amp;quot;UTF-8&amp;quot; standalone=&amp;quot;no&amp;quot;?&amp;gt;&amp;#10;&amp;lt;default/&amp;gt;&amp;#10;&quot; typeId=&quot;org.eclipse.debug.core.containerType.default&quot;/&gt;&#10;&lt;/sourceContainers&gt;&#10;&lt;/sourceLookupDirector&gt;&#10;"/>
 <booleanAttribute key="org.eclipse.jdt.debug.ui.CONSIDER_INHERITED_MAIN" value="true"/>
-<stringAttribute key="org.eclipse.jdt.launching.CLASSPATH_PROVIDER" value="org.maven.ide.eclipse.launchconfig.classpathProvider"/>
+<stringAttribute key="org.eclipse.jdt.launching.CLASSPATH_PROVIDER" value="org.eclipse.m2e.launchconfig.classpathProvider"/>
 <stringAttribute key="org.eclipse.jdt.launching.MAIN_TYPE" value="Main"/>
 <stringAttribute key="org.eclipse.jdt.launching.PROGRAM_ARGUMENTS" value="daemon&#10;--console-log&#10;--show-stack-trace&#10;-d ${resource_loc:/gerrit-parent}/../test_site"/>
 <stringAttribute key="org.eclipse.jdt.launching.PROJECT_ATTR" value="gerrit-war"/>
-<stringAttribute key="org.eclipse.jdt.launching.SOURCE_PATH_PROVIDER" value="org.maven.ide.eclipse.launchconfig.sourcepathProvider"/>
+<stringAttribute key="org.eclipse.jdt.launching.SOURCE_PATH_PROVIDER" value="org.eclipse.m2e.launchconfig.sourcepathProvider"/>
 <stringAttribute key="org.eclipse.jdt.launching.VM_ARGUMENTS" value="-Xmx256M"/>
 </launchConfiguration>