Merge branch 'stable-2.5' into stable-2.6

* stable-2.5:
  Bump openid4java dependency to 0.9.8

Change-Id: Ia946c4a5a81106d6158016ac336ba7c9b7247911
diff --git a/.gitignore b/.gitignore
index 465893d..c87c26f 100644
--- a/.gitignore
+++ b/.gitignore
@@ -6,3 +6,4 @@
 /test_site
 /.idea
 /gerrit-parent.iml
+*.sublime-*
diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000..32483d6
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,11 @@
+[submodule "plugins/replication"]
+	path = plugins/replication
+	url = ../plugins/replication
+
+[submodule "plugins/reviewnotes"]
+	path = plugins/reviewnotes
+	url = ../plugins/reviewnotes
+
+[submodule "plugins/commit-message-length-validator"]
+	path = plugins/commit-message-length-validator
+	url = ../plugins/commit-message-length-validator
diff --git a/Documentation/Makefile b/Documentation/Makefile
index 4c64dfe..59de209 100644
--- a/Documentation/Makefile
+++ b/Documentation/Makefile
@@ -14,6 +14,7 @@
 
 ASCIIDOC       ?= asciidoc
 ASCIIDOC_EXTRA ?=
+ASCIIDOC_VER   ?= 8.6.3
 SVN            ?= svn
 PUB_ROOT       ?= https://gerrit-documentation.googlecode.com/svn/Documentation
 
@@ -23,6 +24,16 @@
 	rm -f *.html
 	rm -rf $(LOCAL_ROOT)
 
+ASCIIDOC_EXE := $(shell which $(ASCIIDOC))
+ifeq ($(wildcard $(ASCIIDOC_EXE)),)
+  $(error $(ASCIIDOC) must be available)
+else
+  ASCIIDOC_OK := $(shell expr `asciidoc --version | cut -f2 -d' '` \>= $(ASCIIDOC_VER))
+  ifeq ($(ASCIIDOC_OK),0)
+    $(error $(ASCIIDOC) version $(ASCIIDOC_VER) or higher is required)
+  endif
+endif
+
 ifeq ($(origin VERSION), undefined)
   VERSION := $(shell ./GEN-DOC-VERSION 2>/dev/null)
 endif
diff --git a/Documentation/access-control.txt b/Documentation/access-control.txt
index 879d1ac..100e473 100644
--- a/Documentation/access-control.txt
+++ b/Documentation/access-control.txt
@@ -15,6 +15,7 @@
 in the `system_config` table within the database, so the groups
 can be renamed after installation if desired.
 
+
 [[administrators]]
 Administrators
 ~~~~~~~~~~~~~~
@@ -33,6 +34,7 @@
 to permit administrative users to otherwise access Gerrit as any
 other normal user would, without needing two different accounts.
 
+
 [[anonymous_users]]
 Anonymous Users
 ~~~~~~~~~~~~~~~
@@ -48,12 +50,13 @@
 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.
+to perform interactive operations on the Gerrit web front-end.
 
 However, sometimes such a user may need a separate thread pool in
 order to prevent it from grabbing threads from the interactive users.
@@ -63,6 +66,7 @@
 users. This ensures that the interactive users can keep working when
 resources are tight.
 
+
 [[project_owners]]
 Project Owners
 ~~~~~~~~~~~~~~
@@ -80,6 +84,7 @@
 avoid the need to initially configure access rights for
 newly created child projects.
 
+
 [[registered_users]]
 Registered Users
 ~~~~~~~~~~~~~~~~
@@ -93,7 +98,7 @@
 group is very easy.  Caution should be taken when assigning any
 permissions to this group.
 
-It is typical to assign `Code Review -1..+1` to this group,
+It is typical to assign `Code-Review -1..+1` to this group,
 allowing signed-in users to vote on a change, but not actually
 cause it to become approved or rejected.
 
@@ -135,6 +140,18 @@
 `Foo-admin` typically do not need to have such rights.
 
 
+[[ldap_groups]]
+LDAP Groups
+-----------
+
+LDAP groups are Account Groups that are maintained inside of your
+LDAP instance. If you are using LDAP to manage your groups they will
+not appear in the Groups list. However you can use them just like
+regular Account Groups by prefixing your group with "ldap/" in the
+Access Control for a project. For example "ldap/foo-project" will
+add the LDAP "foo-project" group to the access list.
+
+
 Project Access Control Lists
 ----------------------------
 
@@ -145,16 +162,16 @@
 Per-project access control lists are also supported.
 
 Users are permitted to use the maximum range granted to any of their
-groups in an approval category.  For example, a user is a member of
-`Foo Leads`, and the following ACLs are granted on a project:
+groups on a label.  For example, a user is a member of `Foo Leads`, and
+the following ACLs are granted on a project:
 
 [options="header"]
-|=================================================
-|Group           |Reference Name |Category|Range
-|Anonymous Users |refs/heads/*|Code Review|-1..+1
-|Registered Users|refs/heads/*|Code Review|-1..+2
-|Foo Leads       |refs/heads/*|Code Review|-2..0
-|=================================================
+|===================================================
+|Group           |Reference Name |Label      |Range
+|Anonymous Users |refs/heads/*   |Code-Review|-1..+1
+|Registered Users|refs/heads/*   |Code-Review|-1..+2
+|Foo Leads       |refs/heads/*   |Code-Review|-2..0
+|===================================================
 
 Then the effective range permitted to be used by the user is
 `-2..+2`, as the user is a member of all three groups (see above
@@ -195,10 +212,10 @@
 
 [options="header"]
 |===============================================================
-|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 |
+|Group            |Reference Name|Label      |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
@@ -219,31 +236,32 @@
 
 [options="header"]
 |==============================================================
-|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
+|Group           |Reference Name|Label      |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,
+Then this user will not have `Code-Review` rights on that change,
 since there is an exclusive access right in place for the
 `refs/heads/qa` branch. This allows locking down access for a
 particular branch to a limited set of users, bypassing inherited
 rights and wildcards.
 
-In order to grant the ability to `Code Review` to the members of
+In order to grant the ability to `Code-Review` to the members of
 `Foo Leads`, in `refs/heads/qa` then the following access rights
 would be needed:
 
 [options="header"]
 |==============================================================
 |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 |
+|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
 ~~~~~~~~~~~~~~~~~~~~~
 
@@ -253,6 +271,7 @@
 of its OpenID identities match one or more of the patterns listed
 in the `auth.trustedOpenID` list from `gerrit.config`.
 
+
 All Projects
 ~~~~~~~~~~~~
 
@@ -272,6 +291,7 @@
 `Administrators` does, as group members would be able to alter
 permissions for every managed project including global capabilities.
 
+
 Per-Project
 ~~~~~~~~~~~
 
@@ -281,13 +301,114 @@
 granting 'DENY' within a specific project to deny a group access.
 
 
-[[access_category]]
+[[references]]
+Special and magic references
+----------------------------
+
+The reference namespaces used in git are generally two, one for branches and
+one for tags:
+
+* +refs/heads/*+
+* +refs/tags/*+
+
+However, every reference under +refs/*+ is really available, and in Gerrit this
+opportunity for giving other refs a special meaning is used.  In Gerrit they
+are sometimes used as magic/virtual references that give the push to Gerrit a
+special meaning.
+
+
+[[references_special]]
+Special references
+~~~~~~~~~~~~~~~~~~
+
+The special references have content that's either generated by Gerrit or
+contains important project configuration that Gerrit needs. When making
+changes to these references, Gerrit will take extra precautions to verify the
+contents compatibility at upload time.
+
+
+refs/changes/*
+^^^^^^^^^^^^^^
+
+Under this namespace each uploaded patch set for every change gets a static
+reference in their git.  The format is convenient but still intended to scale to
+hundreds of thousands of patch sets.  To access a given patch set you will
+need the change number and patch set number.
+
+[verse]
+'refs/changes/'<last two digits of change number>/
+  <change number>/
+  <patch set number>
+
+You can also find these static references linked on the page of each change.
+
+
+refs/meta/config
+^^^^^^^^^^^^^^^^
+
+This is where the Gerrit configuration of each project is residing.  This
+branch contains several files of importance: +project.config+, +groups+ and
++rules.pl+.  Torgether they control access and behaviour during the change
+review process.
+
+
+refs/meta/dashboards/*
+^^^^^^^^^^^^^^^^^^^^^^
+
+There's a dedicated page where you can read more about
+link:user-dashboards.html[User Dashboards].
+
+
+refs/notes/review
+^^^^^^^^^^^^^^^^^
+
+Autogenerated copy of review notes for all changes in the git.  Each log entry
+on the refs/notes/review branch also references the patch set on which the
+review is made.  This functionality is provided by the review-notes plugin.
+
+
+[[references_magic]]
+Magic references
+~~~~~~~~~~~~~~~~
+
+These are references with added functionality to them compared to a regular
+git push operation.
+
+
+refs/for/<branch ref>
+^^^^^^^^^^^^^^^^^^^^^
+
+Most prominent is the `refs/for/<branch ref>` reference which is the reference
+upon which we build the code review intercept before submitting a commit to
+the branch it's uploaded to.
+
+Further documentation on how to push can be found on the
+link:user-upload.html#push_create[Upload changes] page.
+
+
+refs/publish/*
+^^^^^^^^^^^^^^
+
+`refs/publish/*` is an alternative name to `refs/for/*` when pushing new changes
+and patch sets.
+
+
+refs/drafts/*
+^^^^^^^^^^^^^
+
+Push to `refs/drafts/*` creates a change like push to `refs/for/*`, except the
+resulting change remains hidden from public review.  You then have the option
+of adding individual reviewers before making the change public to all.  The
+change page will have a 'Publish' button which allows you to convert individual
+draft patch sets of a change into public patch sets for review.
+
+
+[[access_categories]]
 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.
+Gerrit has several permission categories that can be granted to groups
+within projects, enabling functionality for that group's members.
 
 With the release of the Gerrit 2.2.x series, the web GUI for ACL
 configuration was rewritten from scratch.  Use this
@@ -295,155 +416,6 @@
 conversions from the Gerrit 2.1.x to the Gerrit 2.2.x series.
 
 
-[[category_label-Verified]]
-Label: Verified
-~~~~~~~~~~~~~~~
-
-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.  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.*
-
-* 0 No score
-+
-Didn't try to perform the verification tasks.
-
-* +1 Verified
-+
-Compiled (and ran tests) successfully.
-+
-*Any +1 enables submit.*
-
-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.
-
-If a Gerrit installation does not wish to use this category in any
-project, it can be deleted from the database:
-
-====
-  DELETE FROM approval_categories      WHERE category_id = 'VRIF';
-  DELETE FROM approval_category_values WHERE category_id = 'VRIF';
-====
-
-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
-`approval_category_values` table.
-
-Additional values could also be added to this category, to allow it
-to behave more like `Code Review` (below).  Insert -2 and +2 value
-rows into the `approval_category_values` with `category_id` set to
-`VRIF` to get the same behavior.
-
-[NOTE]
-A restart is required after making database changes.
-See <<restart_changes,below>>.
-
-[[category_label-Code-Review]]
-Label: Code Review
-~~~~~~~~~~~~~~~~~~
-
-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.  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.*
-
-* -1 I would prefer that you didn't submit this
-+
-The code doesn't look right, or could be done differently, but
-the reviewer is willing to live with it as-is if another reviewer
-accepts it, perhaps because it is better than what is currently in
-the project.  Often this is also used by contributors who don't like
-the change, but also aren't responsible for the project long-term
-and thus don't have final say on change submission.
-+
-Does not block submit.
-
-* 0 No score
-+
-Didn't try to perform the code review task, or glanced over it but
-don't have an informed opinion yet.
-
-* +1 Looks good to me, but someone else must approve
-+
-The code looks right to this reviewer, but the reviewer doesn't
-have access to the `+2` value for this category.  Often this is
-used by contributors to a project who were able to review the change
-and like what it is doing, but don't have final approval over what
-gets submitted.
-
-* +2 Looks good to me, approved
-+
-Basically the same as `+1`, but for those who have final say over
-how the project will develop.
-+
-*Any +2 enables submit.*
-
-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:
-
-====
-  DELETE FROM approval_categories      WHERE category_id = 'CRVW';
-  DELETE FROM approval_category_values WHERE category_id = 'CRVW';
-====
-
-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
-`approval_category_values` table.
-
-Additional values could be inserted into `approval_category_values`
-to further extend the negative and positive range, but there is
-likely little value in doing so as this only expands the middle
-region.  This category is a `MaxWithBlock` type, which means that
-the lowest negative value if present blocks a submit, while the
-highest positive value is required to enable submit.
-
-[[function_MaxNoBlock]]
-There is also a `MaxNoBlock` category which still requires the
-highest positive value to submit, but the lowest negative value will
-not block the change, and does not carry over between patch sets.
-This level is mostly useful for automated code-reviews that may
-have false-negatives that shouldn't block the change.
-
-[NOTE]
-A restart is required after making database changes.
-See <<restart_changes,below>>.
-
 [[category_abandon]]
 Abandon
 ~~~~~~~
@@ -455,6 +427,7 @@
 This also grants the permission to restore a change if the change
 can be uploaded.
 
+
 [[category_create]]
 Create reference
 ~~~~~~~~~~~~~~~~
@@ -562,7 +535,7 @@
 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
+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.
@@ -585,7 +558,7 @@
 ^^^^^^^^^^^
 
 Any existing branch can be fast-forwarded to a new commit.
-Creation of new branches is controlled by the 
+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.
@@ -634,7 +607,7 @@
 ~~~~~~~~~~~~~~~~~~~~
 
 The `Push Merge Commit` access right permits the user to upload merge
-commits.  It's an addon to the <<category_push,Push>> access right, and
+commits.  It's an add-on 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
@@ -647,20 +620,27 @@
 the intention of the `Push Merge Commit` entry is to allow direct pushes
 of merge commits.
 
+
 [[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:
+This category permits users to push an annotated tag object 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.
+Or:
+
+====
+  git push https://HOST/PROJECT tag v1.0
+====
+
+Tags must be annotated (created with `git tag -a`), 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.
@@ -682,6 +662,28 @@
 requires the same permission as deleting a branch.
 
 
+[[category_push_signed]]
+Push Signed Tag
+~~~~~~~~~~~~~~~
+
+This category permits users to push a PGP signed tag object 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
+====
+
+Or:
+
+====
+  git push https://HOST/PROJECT tag v1.0
+====
+
+Tags must be signed (created with `git tag -s`), should exist in the
+`refs/tags/` namespace, and should be new.
+
+
 [[category_read]]
 Read
 ~~~~
@@ -729,6 +731,35 @@
 patch set.
 
 
+[[category_remove_reviewer]]
+Remove Reviewer
+~~~~~~~~~~~~~~~
+
+This category permits users to remove other users from the list of
+reviewers on a change.
+
+The change owner, project owner and site administrator can always
+remove reviewers (even without having the `Remove Reviewer` access
+right assigned).
+
+Users without this access right can only remove themselves from the
+reviewer list on a change.
+
+
+[[category_review_labels]]
+Review Labels
+~~~~~~~~~~~~~
+
+For every configured label `My-Name` in the project, there is a
+corresponding permission `label-My-Name` with a range corresponding to
+the defined values.
+
+Gerrit comes pre-configured with a default 'Code-Review' label that can
+be granted to groups within projects, enabling functionality for that
+group's members. link:config-labels.html[Custom labels] may also be
+defined globally or on a per-project basis.
+
+
 [[category_submit]]
 Submit
 ~~~~~~
@@ -740,85 +771,55 @@
 branch as soon as possible, making it a permanent part of the
 project's history.
 
-In order to submit, all approval categories (such as `Verified` and
-`Code Review`, above) must enable submit, and also must not block it.
-See above for details on each category.
+In order to submit, all labels (such as `Verified` and `Code-Review`,
+above) must enable submit, and also must not block it.  See above for
+details on each label.
 
 
-[[category_makeoneup]]
-Your Category Here
-~~~~~~~~~~~~~~~~~~
+[[category_view_drafts]]
+View Drafts
+~~~~~~~~~~~
 
-Gerrit administrators can also make up their own categories.
+This category permits users to view draft changes uploaded by other
+users.
 
-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.
+The change owner and any explicitly added reviewers can always see
+draft changes (even without having the `View Drafts` access right
+assigned).
 
-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 `position` column of `approval_categories` controls which column
-of the 'Approvals' table the category appears in, providing some
-layout control to the administrator.
+[[category_publish_drafts]]
+Publish Drafts
+~~~~~~~~~~~~~~
 
-All `MaxWithBlock` categories must have at least one positive value
-in the `approval_category_values` table, or else submit will never
-be enabled.
+This category permits users to publish draft changes uploaded by other
+users.
 
-To permit blocking submits, ensure a negative value is defined for
-your new category.  If you do not wish to have a blocking submit
-level for your category, do not define values less than 0.
+The change owner can always publish draft changes (even without having
+the `Publish Drafts` access right assigned).
 
-Keep in mind that category definitions are currently global to
-the entire Gerrit instance, and affect all projects hosted on it.
-Any change to a category definition affects everyone.
 
-For example, to define a new 3-valued category that behaves exactly
-like `Verified`, but has different names/labels:
+[[category_delete_drafts]]
+Delete Drafts
+~~~~~~~~~~~~~
 
-====
-  INSERT INTO approval_categories
-    (name
-    ,position
-    ,function_name
-    ,category_id)
-  VALUES
-    ('Copyright Check'
-    ,3
-    ,'MaxWithBlock'
-    ,'copy');
+This category permits users to delete draft changes uploaded by other
+users.
 
-  INSERT INTO approval_category_values
-    (category_id,value,name)
-  VALUES
-    ('copy', -1, 'Do not have copyright');
+The change owner can always delete draft changes (even without having
+the `Delete Drafts` access right assigned).
 
-  INSERT INTO approval_category_values
-    (category_id,value,name)
-  VALUES
-    ('copy', 0, 'No score');
 
-  INSERT INTO approval_category_values
-    (category_id,value,name)
-  VALUES
-    ('copy', 1, 'Copyright clear');
-====
+[[category_edit_topic_name]]
+Edit Topic Name
+~~~~~~~~~~~~~~~
 
-The new column will appear at the end of the table (in position 3),
-and `-1 Do not have copyright` will block submit, while `+1 Copyright
-clear` is required to enable submit.
+This category permits users to edit the topic name of a change that
+is uploaded for review.
 
-[[restart_changes]]
-[NOTE]
-Restart the Gerrit web application and reload all browsers after
-making any database changes to approval categories.  Browsers are
-sent the list of known categories when they first visit the site,
-and don't notice changes until the page is closed and opened again,
-or is reloaded.
+The change owner, branch owners, project owners, and site administrators
+can always edit the topic name (even without having the `Edit Topic Name`
+access right assigned).
 
 
 Examples of typical roles in a project
@@ -829,6 +830,7 @@
 general guidelines for a typical way to set up your project on a
 brand new Gerrit instance.
 
+
 [[examples_contributor]]
 Contributor
 ~~~~~~~~~~~
@@ -840,9 +842,9 @@
 
 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'
+* xref:category_read[`Read`] on 'refs/heads/\*' and 'refs/tags/*'
+* xref:category_push[`Push`] to 'refs/for/refs/heads/*'
+* link:config-labels.html#label_Code-Review[`Code-Review`] with range '-1' to '+1' for 'refs/heads/*'
 
 
 [[examples_developer]]
@@ -864,13 +866,13 @@
 
 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`>>
+* xref:category_read[`Read`] on 'refs/heads/\*' and 'refs/tags/*'
+* xref:category_push[`Push`] to 'refs/for/refs/heads/*'
+* xref:category_push_merge[`Push merge commit`] to 'refs/for/refs/heads/*'
+* xref:category_forge_author[`Forge Author Identity`] to 'refs/heads/*'
+* link:config-labels.html#label_Code-Review[`Label: Code-Review`] with range '-2' to '+2' for 'refs/heads/*'
+* link:config-labels.html#label_Verified[`Label: Verify`] with range '-1' to '+1' for 'refs/heads/*'
+* xref: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.
@@ -885,7 +887,7 @@
 CI system
 ~~~~~~~~~
 
-A typical Continous Integration system should be able to download new changes
+A typical Continuous Integration system should be able to download new changes
 to build and then leave a verdict somehow.
 
 As an example, the popular
@@ -920,14 +922,14 @@
 
 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'
+* xref:category_read[`Read`] on 'refs/heads/\*' and 'refs/tags/*'
+* link:config-labels.html#label_Code-Review[`Label: Code-Review`] with range '-1' to '0' for 'refs/heads/*'
+* link:config-labels.html#label_Verified[`Label: Verify`] with range '0' to '+1' for 'refs/heads/*'
 
 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/*'
+* link:config-labels.html#label_Code-Review[`Label: Code-Review`] with range '-1' to '+1' for 'refs/heads/*'
+* xref:category_push[`Push`] to 'refs/for/refs/heads/*'
 
 
 [[examples_integrator]]
@@ -944,7 +946,7 @@
 * <<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_forge_committer,`Forge Committer Identity`>> to 'refs/for/refs/heads/*'
 * <<category_create,`Create Reference`>> to 'refs/heads/*'
 * <<category_push_annotated,`Push Annotated Tag`>> to 'refs/tags/*'
 
@@ -958,7 +960,7 @@
 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
+These users should be really knowledgeable 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!
@@ -972,6 +974,7 @@
 
 * <<category_owner,`Owner`>> in the gits they mostly work with.
 
+
 [[examples_administrator]]
 Administrator
 ~~~~~~~~~~~~~
@@ -990,6 +993,128 @@
 * <<examples_project-owner,Project owner rights>>
 
 
+Enforcing site wide access policies
+-----------------------------------
+
+By granting the <<category_owner,`Owner`>> access right on the `refs/*` to a
+group, Gerrit administrators can delegate the responsibility of maintaining
+access rights for that project to that group.
+
+In a corporate deployment it is often necessary to enforce some access
+policies. An example could be that no-one can update or delete a tag, not even
+the project owners. The 'ALLOW' and 'DENY' rules are not enough for this
+purpose as project owners can grant themselves any access right they wish and,
+thus, effectively override any inherited access rights from the
+"`All-Projects`" or some other common parent project.
+
+What is needed is a mechanism to block a permission in a parent project so
+that even project owners cannot allow a blocked permission in their child
+project. Still, project owners should retain the possibility to manage all
+non-blocked rules as they wish. This gives best of both worlds:
+
+* Gerrit administrators can concentrate on enforcing site wide policies
+  and providing a meaningful set of default access permissions
+* Project owners can manage access rights of their projects without a danger
+  of violating a site wide policy
+
+
+[[block]]
+'BLOCK' access rule
+~~~~~~~~~~~~~~~~~~~
+
+The 'BLOCK' rule blocks a permission globally. An inherited 'BLOCK' rule cannot
+be overridden in the inheriting project. Any 'ALLOW' rule, from a different
+access section or from an inheriting project, which conflicts with an
+inherited 'BLOCK' rule will not be honored.  Searching for 'BLOCK' rules, in
+the chain of parent projects, ignores the Exclusive flag that is normally
+applied to access sections.
+
+A 'BLOCK' rule that blocks the 'push' permission blocks any type of push,
+force or not. A blocking force push rule blocks only force pushes, but
+allows non-forced pushes if an 'ALLOW' rule would have permitted it.
+
+It is also possible to block label ranges.  To block a group 'X' from voting
+'-2' and '+2', but keep their existing voting permissions for the '-1..+1'
+range intact we would define:
+
+====
+  [access "refs/heads/*"]
+    label-Code-Review = block -2..+2 group X
+====
+
+The interpretation of the 'min..max' range in case of a blocking rule is: block
+every vote from '-INFINITE..min' and 'max..INFINITE'. For the example above it
+means that the range '-1..+1' is not affected by this block.
+
+'BLOCK' and 'ALLOW' rules in the same access section
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+When an access section of a project contains a 'BLOCK' and an 'ALLOW' rule for
+the same permission then this 'ALLOW' rule overrides the 'BLOCK' rule:
+
+====
+  [access "refs/heads/*"]
+    push = block group X
+    push = group Y
+====
+
+In this case a user which is a member of the group 'Y' will still be allowed to
+push to 'refs/heads/*' even if it is a member of the group 'X'.
+
+NOTE: An 'ALLOW' rule overrides a 'BLOCK' rule only when both of them are
+inside the same access section of the same project. An 'ALLOW' rule in a
+different access section of the same project or in any access section in an
+inheriting project cannot override a 'BLOCK' rule.
+
+
+Examples
+~~~~~~~~
+
+The following examples show some possible use cases for the 'BLOCK' rules.
+
+Make sure no one can update or delete a tag
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+This requirement is quite common in a corporate deployment where
+reproducibility of a build must be guaranteed. To achieve that we block 'push'
+permission for the <<anonymous_users,'Anonymous Users'>> in "`All-Projects`":
+
+====
+  [access "refs/tags/*"]
+    push = block group Anonymous Users
+====
+
+By blocking the <<anonymous_users,'Anonymous Users'>> we effectively block
+everyone as everyone is a member of that group. Note that the permission to
+create a tag is still necessary. Assuming that only <<category_owner,project
+owners>> are allowed to create tags, we would extend the example above:
+
+====
+  [access "refs/tags/*"]
+    push = block group Anonymous Users
+    create = group Project Owners
+    pushTag = group Project Owners
+====
+
+
+Let only a dedicated group vote in a special category
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Assume there is a more restrictive process for submitting changes in stable
+release branches which is manifested as a new voting category
+'Release-Process'. Assume we want to make sure that only a 'Release Engineers'
+group can vote in this category and that even project owners cannot approve
+this category. We have to block everyone except the 'Release Engineers' to vote
+in this category and, of course, allow 'Release Engineers' to vote in that
+category. In the "`All-Projects`" we define the following rules:
+
+====
+  [access "refs/heads/stable*"]
+    label-Release-Process = block -1..+1 group Anonymous Users
+    label-Release-Process = -1..+1 group Release Engineers
+====
+
+
 [[conversion_table]]
 Conversion table from 2.1.x series to 2.2.x series
 --------------------------------------------------
@@ -997,8 +1122,8 @@
 [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>>
+|Code review                  |link:config-labels.html#label_Code-Review[Label: Code-Review]
+|Verify                       |link:config-labels.html#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
@@ -1021,15 +1146,16 @@
 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
+receiving 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
+[[global_capabilities]]
+Global Capabilities
 -------------------
 
-The system capabilities control actions that the administrators of
+The global 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.
@@ -1040,6 +1166,9 @@
 keep fewer users in the administrators group, even while spreading
 much of the server administration burden out to more users.
 
+Global capabilities are assigned to groups in the access rights settings
+of the root project ("`All-Projects`").
+
 Below you find a list of capabilities available:
 
 
@@ -1081,6 +1210,7 @@
 either link:cmd-create-project.html[create new git projects via ssh]
 or via the web UI.
 
+
 [[capability_emailReviewers]]
 Email Reviewers
 ~~~~~~~~~~~~~~~
@@ -1091,6 +1221,7 @@
 emailed.  The allow rules are evaluated before deny rules, however the default
 is to allow emailing, if no explicit rule is matched.
 
+
 [[capability_flushCaches]]
 Flush Caches
 ~~~~~~~~~~~~
@@ -1110,7 +1241,7 @@
 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.
+receive-pack.
 
 
 [[capability_priority]]
@@ -1158,6 +1289,21 @@
 command, but also to the web UI results pagination size.
 
 
+[[capability_accessDatabase]]
+Access Database
+~~~~~~~~~~~~~~~
+
+Allow users to access the database using the `gsql` command.
+
+
+[[capability_runGC]]
+Run Garbage Collection
+~~~~~~~~~~~~~~~~~~~~~~
+
+Allow users to run the Git garbage collection for the repositories of
+all projects.
+
+
 [[capability_startReplication]]
 Start Replication
 ~~~~~~~~~~~~~~~~~
diff --git a/Documentation/asciidoc.conf b/Documentation/asciidoc.conf
index 9242f09..2fe6213 100644
--- a/Documentation/asciidoc.conf
+++ b/Documentation/asciidoc.conf
@@ -16,3 +16,14 @@
   margin-top: 1.2em;
   margin-bottom: 0.5em;
 ">
+
+[macros]
+(?u)^(?P<name>get)::(?P<target>\S*?)$=#
+
+[get-blockmacro]
+<a id="{target}" onmousedown="javascript:
+  var i =  document.URL.lastIndexOf('/Documentation/');
+  var url = document.URL.substring(0, i) + '{target}';
+  document.getElementById('{target}').href = url;">
+    GET {target} HTTP/1.0
+</a>
diff --git a/Documentation/cmd-create-account.txt b/Documentation/cmd-create-account.txt
index 16b2eb5..85b9b56 100644
--- a/Documentation/cmd-create-account.txt
+++ b/Documentation/cmd-create-account.txt
@@ -18,9 +18,11 @@
 
 DESCRIPTION
 -----------
-Creates a new internal only user account for batch/role access, such
-as from an automated build system or event monitoring over
-link:cmd-stream-events.html[gerrit stream-events].
+Creates a new internal-only user account.
+
+If the account is created without an email address, it may only be
+used for batch/role access, such as from an automated build system
+or event monitoring over link:cmd-stream-events.html[gerrit stream-events].
 
 If LDAP authentication is being used, the user account is created
 without checking the LDAP directory.  Consequently users can be
diff --git a/Documentation/cmd-gc.txt b/Documentation/cmd-gc.txt
new file mode 100644
index 0000000..07b899a
--- /dev/null
+++ b/Documentation/cmd-gc.txt
@@ -0,0 +1,72 @@
+gerrit gc
+=========
+
+NAME
+----
+gerrit gc - Run the Git garbage collection
+
+SYNOPSIS
+--------
+[verse]
+'ssh' -p <port> <host> 'gerrit gc'
+  [--all]
+  <NAME> ...
+
+DESCRIPTION
+-----------
+Runs the Git garbage collection for the specified projects.
+
+A Gerrit system administrator can define the default parameters that
+should be used for running the garbage collection in the user global
+Git configuration file of the system user that runs the Gerrit
+server.
+
+Since the user global Git configuration file is overlaid with the Git
+configuration on repository level it is possible to specify
+repository specific parameters for the garbage collection in the Git
+repository configuration of every project.
+
+ACCESS
+------
+Caller must be a member of the privileged 'Administrators' group,
+or have been granted the
+link:access-control.html#capability_runGC[Run Garbage Collection]
+global capability.
+
+SCRIPTING
+---------
+This command is intended to be used in scripts.
+
+OPTIONS
+-------
+<NAME>::
+	Name of the projects for which the Git garbage collection should be run.
+
+--all::
+	If specified the Git garbage collection is run for all projects
+	sequentially.
+
+EXAMPLES
+--------
+
+Run the Git garbage collection for the projects 'myProject' and
+'yourProject':
+=====
+	$ ssh -p 29418 review.example.com gerrit gc myProject yourProject
+	collecting garbage for "myProject":
+	...
+	done.
+
+	collecting garbage for "yourProject":
+	...
+	done.
+=====
+
+Run the Git garbage collection for all projects:
+=====
+	$ ssh -p 29418 review.example.com gerrit gc --all
+=====
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/cmd-gsql.txt b/Documentation/cmd-gsql.txt
index 3c4f1b5..b53670b 100644
--- a/Documentation/cmd-gsql.txt
+++ b/Documentation/cmd-gsql.txt
@@ -49,7 +49,7 @@
 
 	Type '\h' for help.  Type '\r' to clear the buffer.
 
-	gerrit> update accounts set ssh_user_name = 'alice' where account_id=1;       
+	gerrit> update accounts set ssh_user_name = 'alice' where account_id=1;
 	UPDATE 1; 1 ms
 	gerrit> \q
 	Bye
diff --git a/Documentation/cmd-index.txt b/Documentation/cmd-index.txt
index ccd9ffc..780231c 100644
--- a/Documentation/cmd-index.txt
+++ b/Documentation/cmd-index.txt
@@ -81,6 +81,9 @@
 link:cmd-stream-events.html[gerrit stream-events]::
 	Monitor events occurring in real time.
 
+link:cmd-version.html[gerrit version]::
+	Show the currently executing version of Gerrit.
+
 git upload-pack::
 	Standard Git server side command for client side `git fetch`.
 
@@ -111,12 +114,18 @@
 link:cmd-flush-caches.html[gerrit flush-caches]::
 	Flush some/all server caches from memory.
 
+link:cmd-gc.html[gerrit gc]::
+	Run the Git garbage collection.
+
 link:cmd-gsql.html[gerrit gsql]::
 	Administrative interface to active database.
 
 link:cmd-set-project-parent.html[gerrit set-project-parent]::
 	Change the project permissions are inherited from.
 
+link:cmd-ls-user-refs.html[gerrit ls-user-refs]::
+	Lists refs visible for a specified user.
+
 link:cmd-show-caches.html[gerrit show-caches]::
 	Display current cache statistics.
 
@@ -147,9 +156,12 @@
 link:cmd-plugin-remove.html[gerrit plugin rm]::
     Alias for 'gerrit plugin remove'.
 
-link:cmd-test-submit-rule.html[gerrit test-submit-rule]::
+link:cmd-test-submit-rule.html[gerrit test-submit rule]::
 	Test prolog submit rules.
 
+link:cmd-test-submit-type.html[gerrit test-submit type]::
+	Test prolog submit type.
+
 link:cmd-kill.html[kill]::
 	Kills a scheduled or running task.
 
diff --git a/Documentation/cmd-ls-groups.txt b/Documentation/cmd-ls-groups.txt
index 306bb92..17ebba1 100644
--- a/Documentation/cmd-ls-groups.txt
+++ b/Documentation/cmd-ls-groups.txt
@@ -11,8 +11,10 @@
 'ssh' -p <port> <host> 'gerrit ls-groups'
   [--project <NAME> | -p <NAME>]
   [--user <NAME> | -u <NAME>]
+  [--owned]
   [--visible-to-all]
   [--type {internal | system}]
+  [-q <GROUP>]
   [--verbose | -v]
 
 DESCRIPTION
@@ -61,6 +63,11 @@
 +
 This option can't be used together with the '--project' option.
 
+--owned::
+	Lists only the groups that are owned by the user that was specified
+	by the `--user` option or if no user was specified the groups that
+	are owned by the calling user.
+
 --visible-to-all::
 	Displays only groups that are visible to all registered users
 	(groups that are explicitly marked as visible to all registered
@@ -75,12 +82,19 @@
 `system`:: Any system defined and managed group.
 --
 
+-q::
+	Group that should be inspected. The `-q` option can be specified
+	multiple times to define several groups to be inspected. If
+	specified the listed groups will only contain groups that were
+	specified to be inspected. This is e.g. useful in combination with
+	the `--owned` and `--user` options to check whether a group is
+	owned by a user.
+
 --verbose::
 -v::
 	Enable verbose output with tab-separated columns for the
-	group name, UUID, description, type (`SYSTEM` or `INTERNAL`),
-	owner group name, owner group UUID and whether the group is
-	visible to all (`true` or `false`).
+	group name, UUID, description, owner group name, owner group UUID
+	and whether the group is visible to all (`true` or `false`).
 +
 If a group has been "orphaned", i.e. its owner group UUID refers to a
 nonexistent group, the owner group name field will read `n/a`.
@@ -107,6 +121,21 @@
 	Registered Users
 =====
 
+List all groups which are owned by the calling user:
+=====
+	$ ssh -p 29418 review.example.com gerrit ls-groups --owned
+	MyProject_Committers
+	MyProject_Verifiers
+=====
+
+Check if the calling user owns the group `MyProject_Committers`. If
+`MyProject_Committers` is returned the calling user owns this group.
+If the result is empty, the calling user doesn't own the group.
+=====
+	$ ssh -p 29418 review.example.com gerrit ls-groups --owned -q MyProject_Committers
+	MyProject_Committers
+=====
+
 Extract the UUID of the 'Administrators' group:
 
 =====
diff --git a/Documentation/cmd-ls-projects.txt b/Documentation/cmd-ls-projects.txt
index d7d5aa5..26530bd 100644
--- a/Documentation/cmd-ls-projects.txt
+++ b/Documentation/cmd-ls-projects.txt
@@ -16,6 +16,7 @@
   [--format {text | json | json_compact}]
   [--all]
   [--limit <N>]
+  [--has-acl-for GROUP]
 
 DESCRIPTION
 -----------
@@ -91,6 +92,13 @@
 --limit::
 	Cap the number of results to the first N matches.
 
+--has-acl-for::
+	Display only projects on which access rights for this group are
+	directly assigned. Projects which only inherit access rights for
+	this group are not listed.
++
+With this option you can find out on which projects a group is used.
+
 HTTP
 ----
 This command is also available over HTTP, as `/projects/` for
diff --git a/Documentation/cmd-ls-user-refs.txt b/Documentation/cmd-ls-user-refs.txt
new file mode 100644
index 0000000..25a99d1
--- /dev/null
+++ b/Documentation/cmd-ls-user-refs.txt
@@ -0,0 +1,55 @@
+gerrit ls-user-refs
+===================
+
+NAME
+----
+gerrit ls-user-refs - List refs visible to a specific user
+
+SYNOPSIS
+--------
+[verse]
+'ssh' -p <port> <host> 'gerrit ls-user-refs'
+  [--project PROJECT> | -p <PROJECT>]
+  [--user <USER> | -u <USER>]
+  [--only-refs-heads]
+
+DESCRIPTION
+-----------
+Displays all refs that the specified user can see.
+
+Allows an administrator to query which refs are visible for
+a user. The command is helpful for admins when debugging why a
+user cannot access certain refs and also to help admins
+verify that certain secret refs are not exposed to the wrong
+groups.
+
+ACCESS
+------
+Administrators
+
+OPTIONS
+-------
+--project::
+-p::
+	Required; Name of the project for which the refs should be listed.
+
+--user::
+-u::
+	Required; User for which the visible refs should be listed. Gerrit
+	will query the database to find matching users, so the
+	full identity/name does not need to be specified.
+
+--only-refs-heads::
+	Only list the refs found under refs/heads/*
+
+EXAMPLES
+--------
+
+List visible refs for the user "mr.developer" in project "gerrit"
+=====
+	$ ssh -p 29418 review.example.com gerrit ls-user-refs -p gerrit -u mr.developer
+=====
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/cmd-query.txt b/Documentation/cmd-query.txt
index 2feea11..66bd845 100644
--- a/Documentation/cmd-query.txt
+++ b/Documentation/cmd-query.txt
@@ -28,7 +28,9 @@
 Queries the change database and returns results describing changes
 that match the input query.  More recently updated changes appear
 before older changes, which is the same order presented in the
-web interface.
+web interface.  For each matching change, the result contains data
+for the change's latest patch set, even if the query matched on an
+older patch set (for example an older patch set's sha1 revision).
 
 A query may be limited on the number of results it returns with the
 'limit:' operator.  If no limit is supplied an internal default
@@ -37,16 +39,21 @@
 to resume the query at the change that follows the last change of
 the prior result set.
 
-Non-option arguments to this command are joined with spaces and then
-parsed as a query.  This simplifies calling conventions over SSH
-by permitting operators to appear in different arguments without
-multiple levels of quoting required.
+Non-option arguments to this command are joined with spaces and
+then parsed as a query. This simplifies calling conventions over
+SSH by permitting operators to appear in different arguments.
+
+Query operators may quote values using matched curly braces
+(e.g. `reviewerin:{Developer Group}`) to sidestep issues with 2
+levels of shell quoting (caller shell invoking SSH, and the SSH
+command line parser in the server).
 
 OPTIONS
 -------
 --format::
-	Formatting method for the results. TEXT is the default,
-	presenting a human readable display. JSON creates one line
+	Formatting method for the results. `TEXT` is the default,
+	presenting a human readable display. `JSON` returns
+	link:json.html#change[change attributes], one line
 	per matching record, with embedded LFs escaped.
 
 --current-patch-set::
@@ -65,13 +72,14 @@
 
 --files::
 	Support for listing files with patch sets and their
-	attributes (ADDED, MODIFIED, DELETED, RENAMED, COPIED).
+	attributes (ADDED, MODIFIED, DELETED, RENAMED, COPIED)
+	and size information (number of insertions and deletions).
 	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
+	--patch-sets flag then all inline/file comments are included for
 	each patch set.
 
 --commit-message::
diff --git a/Documentation/cmd-receive-pack.txt b/Documentation/cmd-receive-pack.txt
index 68f686d..92bb65e 100644
--- a/Documentation/cmd-receive-pack.txt
+++ b/Documentation/cmd-receive-pack.txt
@@ -32,28 +32,12 @@
 
 --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
-	also be emailed to the reviewer.
-+
-May be specified more than once to request multiple reviewers.
-+
-This is a Gerrit Code Review specific extension.
+	Automatically add <address> as a reviewer to any change.
+	Deprecated, use `refs/for/branch%r=address` instead.
 
 --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
-	dashboard, and will be emailed to them.
-+
-May be specified more than once to specify multiple CCs.
-+
-This is a Gerrit Code Review specific extension.
-
-Above <address> may be the complete email address, or, if Gerrit is
-configured with HTTP authentication (e.g. within a single domain),
-just the local part (typically username).
+	Carbon-copy <address> on the created or updated changes.
+	Deprecated, use `refs/for/branch%cc=address` instead.
 
 ACCESS
 ------
@@ -64,32 +48,30 @@
 
 Send a review for a change on the master branch to charlie@example.com:
 =====
-	git push --receive-pack='git receive-pack --reviewer charlie@example.com' ssh://review.example.com:29418/project HEAD:refs/for/master
+	git push ssh://review.example.com:29418/project HEAD:refs/for/master%r=charlie@example.com
 =====
 
 Send reviews, but tagging them with the topic name 'bug42':
 =====
-	git push --receive-pack='git receive-pack --reviewer charlie@example.com' ssh://review.example.com:29418/project HEAD:refs/for/master/bug42
+	git push ssh://review.example.com:29418/project HEAD:refs/for/master%r=charlie@example.com,topic=bug42
 =====
 
 Also CC two other parties:
 =====
-	git push --receive-pack='git receive-pack --reviewer charlie@example.com --cc alice@example.com --cc bob@example.com' ssh://review.example.com:29418/project HEAD:refs/for/master
+	git push ssh://review.example.com:29418/project HEAD:refs/for/master%r=charlie@example.com,cc=alice@example.com,cc=bob@example.com
 =====
 
 Configure a push macro to perform the last action:
 ====
 	git config remote.charlie.url ssh://review.example.com:29418/project
-	git config remote.charlie.push HEAD:refs/for/master
-	git config remote.charlie.receivepack 'git receive-pack --reviewer charlie@example.com --cc alice@example.com --cc bob@example.com'
+	git config remote.charlie.push HEAD:refs/for/master%r=charlie@example.com,cc=alice@example.com,cc=bob@example.com
 ====
 
 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
+  push = HEAD:refs/for/master%r=charlie@example.com,cc=alice@example.com,cc=bob@example.com
 ----
 
 and now sending a new change for review to charlie, CC'ing both
diff --git a/Documentation/cmd-review.txt b/Documentation/cmd-review.txt
index 513bc6e..65c21db 100644
--- a/Documentation/cmd-review.txt
+++ b/Documentation/cmd-review.txt
@@ -17,6 +17,7 @@
   [--publish]
   [--delete]
   [--verified <N>] [--code-review <N>]
+  [--label Label-Name=<N>]
   {COMMIT | CHANGEID,PATCHSET}...
 
 DESCRIPTION
@@ -94,10 +95,14 @@
 
 --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.
+        Set the label 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.  These options are only available for these
+        built-in labels; for other labels, see --label.
+
+--label::
+        Set a label by name to the value 'N'.
 
 ACCESS
 ------
@@ -122,7 +127,7 @@
 	$ ssh -p 29418 review.example.com gerrit review -m '"Build Successful"' c0ff33
 =====
 
-Mark the unmerged commits both "Verified +1" and "Code Review +2" and
+Mark the unmerged commits both "Verified +1" and "Code-Review +2" and
 submit them for merging:
 ====
   $ ssh -p 29418 review.example.com gerrit review \
diff --git a/Documentation/cmd-set-project.txt b/Documentation/cmd-set-project.txt
index 059f063..a0af910 100644
--- a/Documentation/cmd-set-project.txt
+++ b/Documentation/cmd-set-project.txt
@@ -11,11 +11,11 @@
 'ssh' -p <port> <host> 'gerrit set-project'
   [--description <DESC> | -d <DESC>]
   [--submit-type <TYPE> | -t <TYPE>]
-  [--use|no-contributor-agreements | --ca|nca]
-  [--use|no-signed-off-by | --so|nso]
-  [--use|no-content-merge]
-  [--require|no-change-id | --id|nid]
-  [--project-state | --ps]
+  [--contributor-agreements <true|false|inherit>]
+  [--signed-off-by <true|false|inherit>]
+  [--content-merge <true|false|inherit>]
+  [--change-id <true|false|inherit>]
+  [--project-state <STATE> | --ps <STATE>]
   <NAME>
 
 DESCRIPTION
@@ -63,26 +63,23 @@
 For more details see
 link:project-setup.html#submit_type[Change Submit Actions].
 
---use|no-content-merge::
+--content-merge::
     If enabled, Gerrit will try to perform a 3-way merge of text
     file content when a file has been modified by both the
     destination branch and the change being submitted.  This
     option only takes effect if submit type is not
     FAST_FORWARD_ONLY.
 
---use|no-contributor-agreements::
---ca|nca::
+--contributor-agreements::
     If enabled, authors must complete a contributor agreement
     on the site before pushing any commits or changes to this
     project.
 
---use|no-signed-off-by::
---so|nso:
+--signed-off-by::
     If enabled, each change must contain a Signed-off-by line
     from either the author or the uploader in the commit message.
 
---require|no-change-id::
---id|nid::
+--change-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.
@@ -103,7 +100,7 @@
 
 ====
     $ ssh -p 29418 review.example.com gerrit set-project example --submit-type MERGE_IF_NECESSARY\
-    --require-change-id --no-content-merge --project-state HIDDEN
+    --change-id true --content-merge false --project-state HIDDEN
 ====
 
 GERRIT
diff --git a/Documentation/cmd-stream-events.txt b/Documentation/cmd-stream-events.txt
index a8cf3b0..ce23da6 100644
--- a/Documentation/cmd-stream-events.txt
+++ b/Documentation/cmd-stream-events.txt
@@ -44,7 +44,8 @@
 *patchSet*, *account* involved, and other attributes as appropriate.
 The currently supported message types are *patchset-created*,
 *draft-published*, *change-abandoned*, *change-restored*,
-*change-merged*, *comment-added* and *ref-updated*.
+*change-merged*, *merge-failed*, *comment-added*, *ref-updated* and
+*reviewer-added*.
 
 Note that any field may be missing in the JSON messages, so consumers of
 this JSON stream should deal with that appropriately.
@@ -105,6 +106,18 @@
 
 submitter:: link:json.html#account[account attribute]
 
+Merge Failed
+^^^^^^^^^^^^
+type:: "merge-failed"
+
+change:: link:json.html#change[change attribute]
+
+patchSet:: link:json.html#patchSet[patchSet attribute]
+
+submitter:: link:json.html#account[account attribute]
+
+reason:: Reason that the merge failed.
+
 Comment Added
 ^^^^^^^^^^^^^
 type:: "comment-added"
@@ -127,6 +140,16 @@
 
 refUpdate:: link:json.html#refUpdate[refUpdate attribute]
 
+Reviewer Added
+^^^^^^^^^^^^^^
+type:: "reviewer-added"
+
+change:: link:json.html#change[change attribute]
+
+patchset:: link:json.html#patchSet[patchset attribute]
+
+reviewer:: link:json.html#account[account attribute]
+
 
 SEE ALSO
 --------
diff --git a/Documentation/cmd-test-submit-rule.txt b/Documentation/cmd-test-submit-rule.txt
index 5b70bd1..ae68b80 100644
--- a/Documentation/cmd-test-submit-rule.txt
+++ b/Documentation/cmd-test-submit-rule.txt
@@ -1,17 +1,16 @@
-gerrit test-submit-rule
+gerrit test-submit rule
 =======================
 
 NAME
 ----
-gerrit test-submit-rule - Test prolog submit rules with a chosen changeset.
+gerrit test-submit rule - Test prolog submit rules with a chosen changeset.
 
 SYNOPSIS
 --------
 [verse]
-'ssh' -p <port> <host> 'gerrit test-submit-rule'
+'ssh' -p <port> <host> 'gerrit test-submit rule'
   [-s]
   [--no-filters]
-  [--format {TEXT | JSON}]
   CHANGE
 
 DESCRIPTION
@@ -26,15 +25,6 @@
 --no-filters::
 	Don't run the submit_filter/2 from the parent projects of the specified change.
 
---format::
-  What output format to display the results in.
-+
---
-`text`:: Simple text based format.
-`json`:: A JSON object described in link:json.html#submitRecord[submit record].
-`json_compact`:: Minimized JSON output.
---
-
 ACCESS
 ------
 Can be used by anyone that has permission to read the specified changeset.
@@ -42,46 +32,31 @@
 EXAMPLES
 --------
 
-
-Test submit_rule from stdin.
-====
- $ cat non-author-codereview.pl | ssh -p 29418 review.example.com gerrit test-submit-rule -s I78f2c6673db24e4e92ed32f604c960dc952437d9
- Non-Author-Code-Review: NOT_READY
- Verified: NOT_READY
- Code-Review: NOT_READY by Anonymous Coward <test@email.com>
-
- NOT_READY
-====
-
 Test submit_rule from stdin and return the results as JSON.
 ====
- cat non-author-codereview.pl | ssh -p 29418 review.example.com gerrit test-submit-rule --format=JSON -s I78f2c6673db24e4e92ed32f604c960dc952437d9
- {
-  "approvals": [
-    {
-      "type": "Verified",
-      "value": "NEED"
-    },
-    {
-      "type": "Code-Review",
-      "value": "OK",
-      "by": {
-        "email": "test@email.com",
-        "username": "test"
-      }
-    }
-  ],
-  "value": "NOT_READY"
- }
+ cat rules.pl | ssh -p 29418 review.example.com gerrit test-submit rule -s I78f2c6673db24e4e92ed32f604c960dc952437d9
+ [
+   {
+     "status": "NOT_READY",
+     "reject": {
+       "Any-Label-Name": {}
+     }
+   }
+ ]
 ====
 
 Test the active submit_rule from the refs/meta/config branch, ignoring filters in the project parents.
 ====
- $ ssh -p 29418 review.example.com gerrit test-submit-rule I78f2c6673db24e4e92ed32f604c960dc952437d9 --no-filters
- Verified: NOT_READY
- Code-Review: NOT_READY by Anonymous Coward <test@email.com>
-
- NOT_READY
+ $ ssh -p 29418 review.example.com gerrit test-submit rule I78f2c6673db24e4e92ed32f604c960dc952437d9 --no-filters
+ [
+   {
+     "status": "NOT_READY",
+     "need": {
+       "Code-Review": {}
+       "Verified": {}
+     }
+   }
+ ]
 ====
 
 SCRIPTING
diff --git a/Documentation/cmd-test-submit-type.txt b/Documentation/cmd-test-submit-type.txt
new file mode 100644
index 0000000..f6d5fba
--- /dev/null
+++ b/Documentation/cmd-test-submit-type.txt
@@ -0,0 +1,53 @@
+gerrit test-submit type
+=======================
+
+NAME
+----
+gerrit test-submit type - Test prolog submit type with a chosen change.
+
+SYNOPSIS
+--------
+[verse]
+'ssh' -p <port> <host> 'gerrit test-submit type'
+  [-s]
+  [--no-filters]
+  CHANGE
+
+DESCRIPTION
+-----------
+Provides a way to test prolog submit type.
+
+OPTIONS
+-------
+-s::
+	Reads a rules.pl file from stdin instead of rules.pl in refs/meta/config.
+
+--no-filters::
+	Don't run the submit_type_filter/2 from the parent projects of the specified change.
+
+ACCESS
+------
+Can be used by anyone that has permission to read the specified change.
+
+EXAMPLES
+--------
+
+Test submit_type from stdin and return the submit type.
+====
+ cat rules.pl | ssh -p 29418 review.example.com gerrit test-submit type -s I78f2c6673db24e4e92ed32f604c960dc952437d9
+ "MERGE_IF_NECESSARY"
+====
+
+Test the active submit_type from the refs/meta/config branch, ignoring filters in the project parents.
+====
+ $ ssh -p 29418 review.example.com gerrit test-submit type I78f2c6673db24e4e92ed32f604c960dc952437d9 --no-filters
+ "MERGE_IF_NECESSARY"
+====
+
+SCRIPTING
+---------
+Can be used either interactively for testing new prolog submit type, or from a script to check the submit type of a change.
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/cmd-version.txt b/Documentation/cmd-version.txt
new file mode 100644
index 0000000..d1f94ea
--- /dev/null
+++ b/Documentation/cmd-version.txt
@@ -0,0 +1,48 @@
+gerrit version
+================
+
+NAME
+----
+gerrit version - Show the version of the currently executing Gerrit server
+
+SYNOPSIS
+--------
+[verse]
+'ssh' -p <port> <host> 'gerrit version'
+
+DESCRIPTION
+-----------
+Displays a one-line response with the string `gerrit version` followed
+by the currently executing version of Gerrit.
+
+The `git describe` command is used to generate the version string based
+on the Git commit used to build Gerrit. For official releases of Gerrit,
+the version string will be equal to the Git tag set in the Gerrit source
+code, which in turn is equal to the name of the release (for example
+2.4.2). When building Gerrit from another commit (one that doesn't have
+an official-looking tag pointing to it), the version string has the form
+`<tagname>-<n>-g<sha1>`, where `<n>` is an integer indicating the number
+of commits ahead of the `<tagname>` tag the commit is, and `<sha1>` is
+the seven-character abbreviated SHA-1 of the commit. See the `git
+describe` documentation for details on how `<tagname>` is chosen and how
+`<n>` is computed.
+
+ACCESS
+------
+Any user who has configured an SSH key.
+
+SCRIPTING
+---------
+This command is intended to be used in scripts.
+
+EXAMPLES
+--------
+
+=====
+	$ ssh -p 29418 review.example.com gerrit version
+	gerrit version 2.4.2
+=====
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/config-cla.txt b/Documentation/config-cla.txt
new file mode 100644
index 0000000..6404d4e
--- /dev/null
+++ b/Documentation/config-cla.txt
@@ -0,0 +1,82 @@
+Gerrit Code Review - Contributor Agreements
+===========================================
+
+Users can be required to sign one or more contributor agreements before
+being able to submit a change in a project.
+
+Contributor agreements are global and can be configured by modifying
+the `project.config` file on the `All-Projects` project. Push permission
+needs to be granted for the `refs/meta/config` branch to be able to push
+back the `project.config` file. Consult
+link:access-control.html[access controls] for details on how access
+permissions work.
+
+To retrieve the `project.config` file, initialize a temporary Git
+repository to edit the configuration:
+====
+  mkdir cfg_dir
+  cd cfg_dir
+  git init
+====
+
+Download the existing configuration from Gerrit:
+====
+  git fetch ssh://localhost:29418/All-Projects refs/meta/config
+  git checkout FETCH_HEAD
+====
+
+Contributor agreements are defined as contributor-agreement sections in
+`project.config`:
+====
+  [contributor-agreement "Individual"]
+    description = If you are going to be contributing code on your own, this is the one you want. You can sign this one online.
+    requireContactInformation = true
+    agreementUrl = static/cla_individual.html
+    autoVerify = group CLA Accepted - Individual
+    accepted = group CLA Accepted - Individual
+====
+
+Each `contributor-agreement` section within the `project.config` file must
+have a unique name. The section name will appear in the web UI.
+
+If not already present, add the UUID of the groups used in the
+`autoVerify` and `accepted` variables in the groups file.
+
+Commit the configuration change, and push it back:
+====
+  git commit -a -m "Add Individual contributor agreement"
+  git push ssh://localhost:29418/All-Projects HEAD:refs/meta/config
+====
+
+[[contributor-agreement.name.description]]contributor-agreement.<name>.description::
++
+Short text describing the contributor agreement. This text will appear
+when the user selects an agreement.
+
+[[contributor-agreement.name.requireContactInformation]]contributor-agreement.<name>.requireContactInformation::
++
+True if the user must provide contact information when signing a
+contributor agreement. Default is false.
+
+[[contributor-agreement.name.agreementUrl]]contributor-agreement.<name>.agreementUrl::
++
+An absolute URL or a relative path to an HTML file containing the text
+of the contributor agreement. The URL must use the http or https
+scheme. The path is relative to the `gerrit.basePath` variable in
+`gerrit.config`.
+
+[[contributor-agreement.name.autoVerify]]contributor-agreement.<name>.autoVerify::
++
+If present, the user can sign the contributor agreement online. The
+value is the group to which the user will be added after signing the
+agreement. The group's UUID must also appear in the `groups` file.
+
+[[contributor-agreement.name.accepted]]contributor-agreement.<name>.accepted::
++
+List of groups that will be considered when verifying that a
+contributor agreement has been accepted. The groups' UUID must also
+appear in the `groups` file.
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/config-gerrit.txt b/Documentation/config-gerrit.txt
index de2aa02..6228a94 100644
--- a/Documentation/config-gerrit.txt
+++ b/Documentation/config-gerrit.txt
@@ -19,9 +19,6 @@
 
 [cache]
   directory = /var/cache/gerrit2
-
-[cache "diff"]
-  diskbuffer = 10 m
 ----
 
 [[accounts]]Section accounts
@@ -133,7 +130,8 @@
 The actual username used in the LDAP simple bind request is the
 account's full DN, which is discovered by first querying the
 directory using either an anonymous request, or the configured
-<<ldap.username>> identity.
+<<ldap.username,ldap.username>> identity. Gerrit can also use kerberos if
+<<ldap.authentication,ldap.authentication>> is set to `GSSAPI`.
 
 * `LDAP_BIND`
 +
@@ -144,7 +142,7 @@
 +
 Unlike LDAP above, the username used to perform the LDAP simple bind
 request is the exact string supplied by in the dialog by the user.
-The configured <<ldap.username>> identity is not used to obtain
+The configured <<ldap.username,ldap.username>> identity is not used to obtain
 account information.
 +
 * `DEVELOPMENT_BECOME_ANY_ACCOUNT`
@@ -195,6 +193,16 @@
 By default, the list contains two values, `http://` and `https://`,
 allowing Gerrit to trust any OpenID it receives.
 
+[[auth.openIdDomain]]auth.openIdDomain::
++
+List of allowed OpenID email address domains. Only used if
+`auth.type` is set to "OPENID" or "OPENID_SSO".
++
+Domain is case insensitive and must be in the same form as it
+appears in the email address, for example, "example.com".
++
+By default, any domain is accepted.
+
 [[auth.maxOpenIdSessionAge]]auth.maxOpenIdSessionAge::
 +
 Time in seconds before an OpenID provider must force the user
@@ -263,6 +271,24 @@
 +
 If not set, no "Register" link is displayed.
 
+[[auth.registerText]]auth.registerText::
++
+Text for the "Register" link in the upper right corner.  Used only
+when auth.type is `LDAP`.
++
+If not set, defaults to "Register".
+
+[[auth.editFullNameUrl]]auth.editFullNameUrl::
++
+Target for the "Edit" button when the user is allowed to edit their
+full name.
+
+[[auth.httpPasswordUrl]]auth.httpPasswordUrl::
++
+Target for the "Obtain Password" link.  Used only when auth.type is
+`LDAP`, `LDAP_BIND` or `CUSTOM_EXTENSION`.
++
+
 [[auth.cookiePath]]auth.cookiePath::
 +
 Sets "path" attribute of the authentication cookie.
@@ -297,8 +323,9 @@
 enabled for the Gerrit site.  If enabled a user must complete a
 contributor agreement before they can upload changes.
 +
-If enabled, the admin must also insert one or more rows into
-`contributor_agreements` and create agreement files under
+If enabled, the admin must also add one or more
+link:config-cla.html[contributor-agreement sections]
+in project.config and create agreement files under
 `'$site_path'/static`, so users can actually complete one or
 more agreements.
 +
@@ -392,7 +419,7 @@
 * y, year, years (`1 year` is treated as `365 days`)
 
 +
-If a unit suffix is not specified, `minutes` is assumed.  If 0 is
+If a unit suffix is not specified, `seconds` is assumed.  If 0 is
 supplied, the maximum age is infinite and items are never purged
 except when the cache is full.
 +
@@ -475,6 +502,19 @@
 requires two HTTP requests, and this cache tries to carry state from
 the first request into the second to ensure it can complete.
 
+cache `"changes"`::
++
+The size of `memoryLimit` determines the number of projects for which
+all changes will be cached. If the cache is set to 1024, this means all
+changes for up to 1024 projects can be held in the cache.
++
+Default value is 0 (disabled). It is disabled by default due to the fact
+that change updates are not communicated between Gerrit servers. Hence
+this cache should be disabled in an multi-master/multi-slave setup.
++
+The cache should be flushed whenever the database changes table is modified
+outside of gerrit.
+
 cache `"diff"`::
 +
 Each item caches the differences between two commits, at both the
@@ -608,8 +648,11 @@
 Maximum number of milliseconds to wait for intraline difference data
 before giving up and disabling it for a particular file pair.  This is
 a work around for an infinite loop bug in the intraline difference
-implementation.  If computation takes longer than the timeout the
-worker thread is terminated and no intraline difference is displayed.
+implementation.
++
+If computation takes longer than the timeout, the worker thread is
+terminated, an error message is shown, and no intraline difference is
+displayed for the file pair.
 +
 Values should use common unit suffixes to express their setting:
 +
@@ -644,6 +687,7 @@
 ('ms', 'sec', 'min', etc.).
 +
 If set to 0, checks occur every time, which may slow down operations.
+If set to 'disabled' or 'off', no check will ever be done.
 Administrators may force the cache to flush with
 link:cmd-flush-caches.html[gerrit flush-caches].
 +
@@ -652,23 +696,31 @@
 [[changeMerge]]Section changeMerge
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
+changeMerge.checkFrequency::
++
+How often the database should be rescanned for changes that have been
+submitted but not merged due to transient errors. Values can be
+specified using standard time unit abbreviations ('ms', 'sec', 'min',
+etc.). Set to 0 to disable periodic rescanning, only scanning once on
+master node startup.
++
+Default is 300 seconds (5 minutes).
+
+changeMerge.test::
++
 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.
+patch comments, in-line code comments and approval category value descriptions
+to turn set strings into hyperlinks.  One common use is for linking to
+bug-tracking systems.
 
 In the following example configuration the 'changeid' comment link
 will match typical Gerrit Change-Id values and create a hyperlink
@@ -892,6 +944,14 @@
 +
 Default is 64 entries.
 
+[[core.useRecursiveMerge]]core.useRecursiveMerge::
++
+Use JGit's new, experimental recursive merger for three-way merges.
+This only affects projects configured to automatically resolve
+conflicts.
++
+Default is false, but in a future release may default to true.
+
 [[database]]Section database
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -969,6 +1029,13 @@
 isn't necessary as it can be constructed from the all of the
 above properties.
 
+[[database.connectionPool]]database.connectionPool::
++
+If true, use connection pooling for database connections. Otherwise, a
+new database connection is opened for each request.
++
+Default is false for MySQL, and true for other database backends.
+
 [[database.poolLimit]]database.poolLimit::
 +
 Maximum number of open database connections.  If the server needs
@@ -980,11 +1047,17 @@
 need multiple connections.
 +
 Default is 8.
++
+This setting only applies if
+<<database.connectionPool,database.connectionPool>> is true.
 
 [[database.poolMinIdle]]database.poolMinIdle::
 +
 Minimum number of connections to keep idle in the pool.
 Default is 4.
++
+This setting only applies if
+<<database.connectionPool,database.connectionPool>> is true.
 
 [[database.poolMaxIdle]]database.poolMaxIdle::
 +
@@ -992,6 +1065,9 @@
 are more idle connections, connections will be closed instead of
 being returned back to the pool.
 Default is 4.
++
+This setting only applies if
+<<database.connectionPool,database.connectionPool>> is true.
 
 [[database.poolMaxWait]]database.poolMaxWait::
 +
@@ -1010,6 +1086,9 @@
 If a unit suffix is not specified, `milliseconds` is assumed.
 +
 Default is `30 seconds`.
++
+This setting only applies if
+<<database.connectionPool,database.connectionPool>> is true.
 
 [[download]]Section download
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -1243,6 +1322,16 @@
 +
 Valid values are the characters '*', '(' and ')'.
 
+[[groups]]Section groups
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+[[groups.newGroupsVisibleToAll]]groups.newGroupsVisibleToAll::
++
+Controls whether newly created groups should be by default visible to
+all registered users.
++
+By default, false.
+
 [[hooks]]Section hooks
 ~~~~~~~~~~~~~~~~~~~~~~
 
@@ -1272,6 +1361,11 @@
 Optional filename for the change merged hook, if not specified then
 `change-merged` will be used.
 
+[[hooks.mergeFailedHook]]hooks.mergeFailedHook::
++
+Optional filename for the merge failed hook, if not specified then
+`merge-failed` will be used.
+
 [[hooks.changeAbandonedHook]]hooks.changeAbandonedHook::
 +
 Optional filename for the change abandoned hook, if not specified then
@@ -1287,11 +1381,26 @@
 Optional filename for the ref updated hook, if not specified then
 `ref-updated` will be used.
 
+[[hooks.reviewerAddedHook]]hooks.reviewerAddedHook::
++
+Optional filename for the reviewer added hook, if not specified then
+`reviewer-added` will be used.
+
 [[hooks.claSignedHook]]hooks.claSignedHook::
 +
 Optional filename for the CLA signed hook, if not specified then
 `cla-signed` will be used.
 
+[[hooks.refUpdateHook]]hooks.refUpdateHook::
++
+Optional filename for the ref update hook, if not specified then
+`ref-update` will be used.
+
+[[hooks.syncHookTimeout]]hooks.syncHookTimeout::
++
+Optional timeout value in seconds for synchronous hooks, if not specified
+then 30 seconds will be used.
+
 [[http]]Section http
 ~~~~~~~~~~~~~~~~~~~~
 
@@ -1304,13 +1413,13 @@
 [[http.proxyUsername]]http.proxyUsername::
 +
 Optional username to authenticate to the HTTP proxy with.
-This property is honored only if the username does not 
+This property is honored only if the username does not
 appear in the http.proxy property above.
 
 [[http.proxyPassword]]http.proxyPassword::
 +
 Optional password to authenticate to the HTTP proxy with.
-This property is honored only if the password does not 
+This property is honored only if the password does not
 appear in the http.proxy property above.
 
 
@@ -1706,6 +1815,21 @@
 Default is `(memberUid=${username})` for RFC 2307,
 and unset (disabled) for Active Directory.
 
+[[ldap.groupName]]ldap.groupName::
++
+_(Optional)_ Name of the attribute on the group object which contains
+the value to use as the group name in Gerrit.
++
+Typically the attribute name is `cn` for RFC 2307 and Active Directory
+servers.  For other servers the attribute name may differ, for example
+`apple-group-realname` on Apple MacOS X Server.
++
+It is also possible to specify a literal string containing a pattern of
+attribute values.  For example to create a Gerrit group name consisting of
+LDAP group name and group ID, use the pattern `${cn} (${gidNumber})`.
++
+Default is `cn`.
+
 [[ldap.localUsernameToLowerCase]]ldap.localUsernameToLowerCase::
 +
 Converts the local username, that is used to login into the Gerrit
@@ -1725,6 +1849,38 @@
 +
 By default, unset/false.
 
+[[ldap.authentication]]ldap.authentication::
++
+Defines how Gerrit authenticates with the server. When set to `GSSAPI`
+Gerrit will use Kerberos. To use kerberos the
+`java.security.auth.login.config` system property must point to a
+login to a JAAS configuration file and, if Java 6 is used, the system
+property `java.security.krb5.conf` must point to the appropriate
+krb5.ini file with references to the KDC.
+
+Typical jaas.conf.
+
+----
+KerberosLogin {
+    com.sun.security.auth.module.Krb5LoginModule
+            required
+            useTicketCache=true
+            doNotPrompt=true
+            renewTGT=true;
+};
+----
+
+See Java documentation on how to create the krb5.ini file.
+
+Note the `renewTGT` property to make sure the TGT does not expire,
+and `useTicketCache` to use the TGT supplied by the operating system. As
+the whole point of using GSSAPI is to have passwordless authentication
+to the LDAP service, this option does not aquire a new TGT on its own.
+
+On Windows servers the registry key `HKEY_LOCAL_MACHINE\System\CurrentControlSet\Control\Lsa\Kerberos\Parameters`
+must have the DWORD value `allowtgtsessionkey` set to 1 and the account must not
+have local administrator privileges.
+
 [[mimetype]]Section mimetype
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -1810,6 +1966,34 @@
   maxObjectSizeLimit = 40 m
 ----
 
+[[receive.checkMagicRefs]]receive.checkMagicRefs::
++
+If true, Gerrit will verify the destination repository has
+no references under the magic 'refs/drafts', 'refs/for', or
+'refs/publish' branch namespaces. Names under these locations
+confuse clients when trying to upload code reviews so Gerrit
+requires them to be empty.
++
+If false Gerrit skips the sanity check and assumes administrators
+have ensured the repository does not contain any magic references.
+Setting to false to skip the check can decrease latency during push.
++
+Default is true.
+
+[[receive.checkReferencedObjectsAreReachable]]receive.checkReferencedObjectsAreReachable::
++
+If set to true, Gerrit will validate that all referenced objects that
+are not included in the received pack are reachable by the user.
++
+Carrying out this check on gits with many refs and commits can be a
+very CPU-heavy operation. For non public Gerrit-servers this check may
+be overkill.
++
+Only disable this check if you trust the clients not to forge SHA1
+references to access commits intended to be hidden from the user.
++
+Default is true.
+
 [[receive.allowGroup]]receive.allowGroup::
 +
 Name of the groups of users that are allowed to execute
@@ -1839,6 +2023,18 @@
 +
 Defaults to the number of available CPUs according to the Java runtime.
 
+[[receive.changeUpdateThreads]]receive.changeUpdateThreads::
++
+Number of threads to perform change creation or patch set updates
+concurrently. Each thread uses its own database connection from
+the database connection pool, and if all threads are busy then
+main receive thread will also perform a change creation or patch
+set update.
++
+Defaults to 1, using only the main receive thread. This feature is for
+databases with very high latency that can benfit from concurrent
+operations when multiple changes are impacted at once.
+
 [[receive.timeout]]receive.timeout::
 +
 Overall timeout on the time taken to process the change data in
@@ -1978,9 +2174,10 @@
 
 [[sendemail.includeDiff]]sendemail.includeDiff::
 +
-If true, new change emails from Gerrit will include the complete
-unified diff of the change. Variable maxmimumDiffSize places an upper
-limit on how large the email can get when this option is enabled.
+If true, new change emails and merged change emails from Gerrit
+will include the complete unified diff of the change.
+Variable maxmimumDiffSize places an upper limit on how large the
+email can get when this option is enabled.
 +
 By default, false.
 
@@ -2193,6 +2390,20 @@
 +
 By default, 2 minutes.
 
+[[sshd.idleTimeout]]sshd.idleTimeout::
++
+Time in seconds after which the server automatically terminates idle
+connections (or 0 to disable closing of idle connections).  Values
+should use common unit suffixes to express their setting:
++
+* s, sec, second, seconds
+* m, min, minute, minutes
+* h, hr, hour, hours
+* d, day, days
+
++
+By default, 0.
+
 [[sshd.maxConnectionsPerUser]]sshd.maxConnectionsPerUser::
 +
 Maximum number of concurrent SSH sessions that a user account
@@ -2248,6 +2459,13 @@
 New configurations should prefer the boolean value for this field
 and an enum value for `accounts.visibility`.
 
+[[suggest.from]]suggest.from::
++
+The number of characters that a user must have typed before suggestions
+are provided. If set to 0, suggestions are always provided.
++
+By default 0.
+
 [[theme]] Section theme
 ~~~~~~~~~~~~~~~~~~~~~~~
 
@@ -2257,14 +2475,15 @@
 open changes table or the account dashboard. The value must be a
 valid HTML hex color code, or standard color name.
 +
-By default `FCFEEF` (a creme color) for signed-out theme and white
-(`FFFFFF`) for signed-in theme.
+By default white, `FFFFFF`.
 
 [[theme.topMenuColor]]theme.topMenuColor::
 +
 This is the color of the main menu bar at the top of the page.
 The value must be a valid HTML hex color code, or standard color
-name.  The value defaults to <<theme.trimColor,trimColor>>.
+name.
++
+By default white, `FFFFFF`.
 
 [[theme.textColor]]theme.textColor::
 +
@@ -2272,7 +2491,7 @@
 open changes table or the account dashboard. The value must be a
 valid HTML hex color code, or standard color name.
 +
-By default black, `000000`.
+By default dark grey, `353535`.
 
 [[theme.trimColor]]theme.trimColor::
 +
@@ -2282,7 +2501,7 @@
 of the page.  The value must be a valid HTML hex color code, or
 standard color name.
 +
-By default a shade of green, `D4E9A9`.
+By default a light grey, `EEEEEE`.
 
 [[theme.selectionColor]]theme.selectionColor::
 +
@@ -2291,7 +2510,7 @@
 currently selected row.  The value must be a valid HTML hex color
 code, or standard color name.
 +
-By default a shade of yellow, `FFFFCC`.
+By default a pale blue, `D8EDF9`.
 
 [[theme.changeTableOutdatedColor]]theme.changeTableOutdatedColor::
 +
@@ -2370,7 +2589,7 @@
 external tracking id part of the footer line. The match can
 result in several entries in the DB.  If grouping is used in the
 regex the first group will be interpreted as the tracking id.
-Tracking ids longer than 20 characters will be ignored.
+Tracking ids longer than 32 characters will be ignored.
 +
 The configuration file parser eats one level of backslashes, so the
 character class `\s` requires `\\s` in the configuration file.  The
diff --git a/Documentation/config-gitweb.txt b/Documentation/config-gitweb.txt
index 35d5c0d..e5edda8 100644
--- a/Documentation/config-gitweb.txt
+++ b/Documentation/config-gitweb.txt
@@ -1,13 +1,12 @@
-Gerrit Code Review - Gitweb Integration
-=======================================
+Gitweb Integration
+------------------
 
 Gerrit Code Review can manage and generate hyperlinks to gitweb,
 allowing users to jump from Gerrit content to the same information,
 but shown by gitweb.
 
-
 Internal/Managed gitweb
------------------------
+~~~~~~~~~~~~~~~~~~~~~~~
 
 In the internal configuration, Gerrit inspects the request, enforces
 its project level access controls, and directly executes `gitweb.cgi`
@@ -39,7 +38,7 @@
 be restarted and clients must reload the host page to see the change.
 
 Configuration
-~~~~~~~~~~~~~
+^^^^^^^^^^^^^
 
 Most of the gitweb configuration file is handled automatically
 by Gerrit Code Review.  Site specific overrides can be placed in
@@ -47,7 +46,7 @@
 part of the generated configuration file.
 
 Logo and CSS
-~~~~~~~~~~~~
+^^^^^^^^^^^^
 
 If the package-manager installed CGI (`/usr/lib/cgi-bin/gitweb.cgi`)
 is being used, the stock CSS and logo files will be served from
@@ -58,17 +57,165 @@
 the default source code distribution, and most custom installations.
 
 Access Control
-~~~~~~~~~~~~~~
+^^^^^^^^^^^^^^
 
 Access controls for internally managed gitweb page views are enforced
 using the standard project READ +1 permission.
 
-
 External/Unmanaged gitweb
--------------------------
+~~~~~~~~~~~~~~~~~~~~~~~~~
 
-In the external configuration, gitweb runs under the control of an
-external web server, and Gerrit access controls are not enforced.
+For the external configuration, gitweb runs under the control of an
+external web server, and Gerrit access controls are not enforced. Gerrit
+provides configuration parameters for integration with GitWeb.
+
+[[linuxGitWeb]]
+Linux Installation
+^^^^^^^^^^^^^^^^^^
+
+Install GitWeb
+++++++++++++++
+
+On Ubuntu:
+
+====
+  sudo apt-get install gitweb
+====
+
+With Yum:
+
+====
+  $ yum install gitweb
+====
+
+Configure GitWeb
+++++++++++++++++
+
+
+Update `/etc/gitweb.conf`, add the public GIT repositories:
+
+----
+$projectroot = "/var/www/repo/";
+
+# directory to use for temp files
+$git_temp = "/tmp";
+
+# target of the home link on top of all pages
+#$home_link = $my_uri || "/";
+
+# html text to include at home page
+$home_text = "indextext.html";
+
+# file with project list; by default, simply scan the projectroot dir.
+$projects_list = $projectroot;
+
+# stylesheet to use
+# I took off the prefix / of the following path to put these files inside gitweb directory directly
+$stylesheet = "gitweb.css";
+
+# logo to use
+$logo = "git-logo.png";
+
+# the ‘favicon’
+$favicon = "git-favicon.png";
+----
+
+Configure & Restart Apache Web Server
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+Configure Apache
+++++++++++++++++
+
+
+Link gitweb to `/var/www/gitweb`, check `/etc/gitweb.conf` if unsure of paths:
+
+====
+  $ sudo ln -s /usr/share/gitweb /var/www/gitweb
+====
+
+Add the gitweb directory to the Apache configuration by creating a "gitweb"
+file inside the Apache conf.d directory:
+
+====
+  $ touch /etc/apache/conf.d/gitweb
+====
+
+Add the following to /etc/apache/conf.d/gitweb:
+
+----
+Alias /gitweb /var/www/gitweb
+
+Options Indexes FollowSymlinks ExecCGI
+DirectoryIndex /cgi-bin/gitweb.cgi
+AllowOverride None
+----
+
+*NOTE* This may have already been added by yum/apt-get. If that's the case, leave as
+is.
+
+Restart the Apache Web Server
++++++++++++++++++++++++++++++
+
+====
+$ sudo /etc/init.d/apache2 restart
+====
+
+Now you should be able to view your repository projects online:
+
+link:http://localhost/gitweb[http://localhost/gitweb]
+
+[[WindowsGitWeb]]
+Windows Installation
+^^^^^^^^^^^^^^^^^^^^
+
+Instructions are available for installing the GitWeb module distributed with
+MsysGit:
+
+link:https://github.com/msysgit/msysgit/wiki/GitWeb[GitWeb]
+
+If you don't have Apache installed, you can download the appropriate build for
+Windows from link:http://www.apachelounge.com/download[apachelounge.org].
+
+After you have installed Apache, you will want to create a link:http://httpd.apache.org/docs/2.0/platform/windows.html#winsvc[new service user
+account] to use with Apache.
+
+If you're still having difficulty setting up permissions, you may find this
+tech note useful for configuring Apache Service to run under another account.
+You must grant the new account link:http://technet.microsoft.com/en-us/library/cc794944(WS.10).aspx["run as service"] permission:
+
+The GitWeb version in msysgit is missing several important and required
+perl modules, including CGI.pm. The perl included with the msysgit distro 1.7.8
+is broken.. The link:http://groups.google.com/group/msysgit/browse_thread/thread/ba3501f1f0ed95af[unicore folder is missing along with utf8_heavy.pl and CGI.pm]. You can
+verify by checking for perl modules. From an msys console, execute the
+following to check:
+
+====
+$ perl -mCGI -mEncode -mFcntl -mFile::Find -mFile::Basename -e ""
+====
+
+You may encounter the following exception:
+
+----
+$ perl -mCGI -mEncode -mFcntl -mFile::Find -mFile::Basename -e ""
+Can't locate CGI.pm in @INC (@INC contains: /usr/lib/perl5/5.8.8/msys
+/usr/lib/p erl5/5.8.8 /usr/lib/perl5/site_perl/5.8.8/msys
+/usr/lib/perl5/site_perl/5.8.8 /u sr/lib/perl5/site_perl .). BEGIN
+failed--compilation aborted.
+----
+
+If you're missing CGI.pm, you'll have to deploy the module to the msys
+environment: You will have to retrieve them from the 5.8.8 distro on :
+
+http://strawberryperl.com/releases.html
+
+File: strawberry-perl-5.8.8.3.zip
+
+contents: `bin/` `lib/` `site/`
+
+copy the contents of lib into `msysgit/lib/perl5/5.8.8` and overwrite existing files.
+
+Enable GitWeb Integration
+^^^^^^^^^^^^^^^^^^^^^^^^^
 
 To enable the external gitweb integration, set
 link:config-gerrit.html#gitweb.url[gitweb.url] with the URL of your
@@ -79,16 +226,27 @@
 being used, ensure it uses a full mirror, so the `refs/changes/*`
 namespace is available.
 
-====
-  git config --file $site_path/etc/gerrit.config gitweb.url http://example.com/gitweb.cgi
-  git config --file $site_path/etc/gerrit.config --unset gitweb.cgi
-====
+----
+$ git config -f $site_path/etc/gerrit.config gitweb.cgi $PATH_TO_GITWEB/gitweb.cgi
+$ git config -f $site_path/etc/gerrit.config gitweb.url https://gitweb.corporation.com
+----
+
+If you're not following the traditional \{projectName\}.git project naming conventions,
+you will want to customize Gerrit to read them. Add the following:
+
+----
+$ git config -f $site_path/etc/gerrit.config gitweb.type custom
+$ git config -f $site_path/etc/gerrit.config gitweb.project ?p=\${project}\;a=summary
+$ git config -f $site_path/etc/gerrit.config gitweb.revision ?p=\${project}\;a=commit\;h=\${commit}
+$ git config -f $site_path/etc/gerrit.config gitweb.branch ?p=\${project}\;a=shortlog\;h=\${branch}
+$ git config -f $site_path/etc/gerrit.config gitweb.filehistory ?p=\${project}\;a=history\;hb=\${branch}\;f=\${file}
+----
 
 After updating `'$site_path'/etc/gerrit.config`, the Gerrit server must
 be restarted and clients must reload the host page to see the change.
 
 Access Control
-~~~~~~~~~~~~~~
+++++++++++++++
 
 Gitweb access controls can be implemented using standard web server
 access controls.  This isn't typically integrated with Gerrit's own
@@ -96,7 +254,7 @@
 consistent if access needs to be restricted.
 
 Caching Gitweb
-~~~~~~~~~~~~~~
+++++++++++++++
 
 If your repository set is large and you are expecting a lot
 of users, you may want to look at the caching forks used by
@@ -112,7 +270,7 @@
 It is also possible to define custom patterns.
 
 See Also
---------
+~~~~~~~~
 
 * link:config-gerrit.html#gitweb[Section gitweb]
 * link:http://hjemli.net/git/cgit/[cgit]
diff --git a/Documentation/config-hooks.txt b/Documentation/config-hooks.txt
index dfdba52..1236077 100644
--- a/Documentation/config-hooks.txt
+++ b/Documentation/config-hooks.txt
@@ -11,15 +11,33 @@
 
 Make sure your hook scripts are executable if running on *nix.
 
-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
-the server is shutdown before the hook can be invoked.
+With the exception of the ref-update hook, 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 the server is shutdown before the hook can be invoked.
 
 Supported Hooks
 ---------------
 
+ref-update
+~~~~~~~~~~
+
+This is called when a push request is received by Gerrit. It allows
+a push to be rejected before it is committed to the Gerrit repository.
+If the script exits with non-zero return code the push will be rejected.
+Any output from the script will be returned to the user, regardless of the
+return code.
+
+This hook is called synchronously so it is recommended that
+it not block.  A default timeout on the hook is set to 30 seconds to avoid
+"runaway" hooks using up server threads.  See link:config-gerrit.html#hooks.syncHookTimeout[hooks.syncHookTimeout]
+for configuration details.
+
+====
+  ref-update --project <project name> --refname <refname> --uploader <uploader> --oldrev <sha1> --newrev <sha1>
+====
+
 patchset-created
 ~~~~~~~~~~~~~~~~
 
@@ -45,7 +63,7 @@
 This is called whenever a comment is added to a change.
 
 ====
-  comment-added --change <change id> --change-url <change url> --project <project name> --branch <branch> --topic <topic> --author <comment author> --commit <commit> --comment <comment> [--<approval category id> <score> --<approval category id> <score> ...]
+  comment-added --change <change id> --is-draft <boolean> --change-url <change url> --project <project name> --branch <branch> --topic <topic> --author <comment author> --commit <commit> --comment <comment> [--<approval category id> <score> --<approval category id> <score> ...]
 ====
 
 change-merged
@@ -57,6 +75,15 @@
   change-merged --change <change id> --change-url <change url> --project <project name> --branch <branch> --topic <topic> --submitter <submitter> --commit <sha1>
 ====
 
+merge-failed
+~~~~~~~~~~~~
+
+Called whenever a change has failed to merge.
+
+====
+  merge-failed --change <change id> --change-url <change url> --project <project name> --branch <branch> --topic <topic> --submitter <submitter> --commit <sha1> --reason <reason>
+====
+
 change-abandoned
 ~~~~~~~~~~~~~~~~
 
@@ -84,6 +111,15 @@
   ref-updated --oldrev <old rev> --newrev <new rev> --refname <ref name> --project <project name> --submitter <submitter>
 ====
 
+reviewer-added
+~~~~~~~~~~~~~~
+
+Called whenever a reviewer is added to a change.
+
+====
+  reviewer-added --change <change id> --change-url <change url> --project <project name> --branch <branch> --reviewer <reviewer>
+====
+
 cla-signed
 ~~~~~~~~~~
 
@@ -104,8 +140,8 @@
 
 For the hook filenames, Gerrit will use the values of hooks.patchsetCreatedHook,
 hooks.draftPublishedHook, hooks.commentAddedHook, hooks.changeMergedHook,
-hooks.changeAbandonedHook, hooks.changeRestoredHook, hooks.refUpdatedHook and
-hooks.claSignedHook.
+hooks.changeAbandonedHook, hooks.changeRestoredHook, hooks.refUpdatedHook,
+hooks.refUpdateHook, hooks.reviewerAddedHook and hooks.claSignedHook.
 
 Missing Change URLs
 -------------------
diff --git a/Documentation/config-labels.txt b/Documentation/config-labels.txt
new file mode 100644
index 0000000..5014242
--- /dev/null
+++ b/Documentation/config-labels.txt
@@ -0,0 +1,255 @@
+Gerrit Code Review - Review Labels
+==================================
+
+As part of the code review process, reviewers score each change with
+values for each label configured for the project.  The label values that
+a given user is allowed to set are defined according to the
+link:access-control.html#category_review_labels[access controls].  Gerrit
+comes pre-configured with the Code-Review label that can be granted to
+groups within projects, enabling functionality for that group's members.
+
+
+[[label_Code-Review]]
+Label: Code-Review
+------------------
+
+The code review label is the second of two default labels 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.  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.*
+
+* -1 I would prefer that you didn't submit this
++
+The code doesn't look right, or could be done differently, but
+the reviewer is willing to live with it as-is if another reviewer
+accepts it, perhaps because it is better than what is currently in
+the project.  Often this is also used by contributors who don't like
+the change, but also aren't responsible for the project long-term
+and thus don't have final say on change submission.
++
+Does not block submit.
+
+* 0 No score
++
+Didn't try to perform the code review task, or glanced over it but
+don't have an informed opinion yet.
+
+* +1 Looks good to me, but someone else must approve
++
+The code looks right to this reviewer, but the reviewer doesn't
+have access to the `+2` value for this category.  Often this is
+used by contributors to a project who were able to review the change
+and like what it is doing, but don't have final approval over what
+gets submitted.
+
+* +2 Looks good to me, approved
++
+Basically the same as `+1`, but for those who have final say over
+how the project will develop.
++
+*Any +2 enables submit.*
+
+For a change to be submittable, the latest patch set must have a
+`+2 Looks good to me, approved` in this category, and no
+`-2 Do not submit`.  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 label in any project,
+the `[label "Code-Review"]` section can be deleted from `project.config`
+in `All-Projects`.
+
+If a Gerrit installation or project wants to modify the description text
+associated with these label values, the text can be updated in the
+`label.Code-Review.value` fields in `project.config`.
+
+Additional entries could be added to `label.Code-Review.value` to
+further extend the negative and positive range, but there is likely
+little value in doing so as this only expands the middle region.  This
+label is a `MaxWithBlock` type, which means that the lowest negative
+value if present blocks a submit, while the highest positive value is
+required to enable submit.
+
+[[label_Verified]]
+Label: Verified
+---------------
+
+The Verified label was originally invented by the Android Open Source
+Project to mean 'compiles, passes basic unit tests'.  Some CI tools
+expect to use the Verified label to vote on a change after running.
+
+Administrators can install the Verified label by adding the following
+text to `project.config`:
+
+====
+  [label "Verified"]
+      function = MaxWithBlock
+      value = -1 Fails
+      value =  0 No score
+      value = +1 Verified
+====
+
+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.
++
+*Any -1 blocks submit.*
+
+* 0 No score
++
+Didn't try to perform the verification tasks.
+
+* +1 Verified
++
+Compiled (and ran tests) successfully.
++
+*Any +1 enables submit.*
+
+For a change to be submittable, the change must have a `+1 Verified`
+in this label, and no `-1 Fails`.  Thus, `-1 Fails` can block a submit,
+while `+1 Verified` enables a submit.
+
+Additional values could also be added to this label, to allow it to
+behave more like `Code-Review` (below).  Add -2 and +2 entries to the
+`label.Verified.value` fields in `project.config` to get the same
+behavior.
+
+
+[[label_custom]]
+Your Label Here
+---------------
+
+Site administrators and project owners can also define their own labels.
+
+See above for descriptions of how <<label_Verified,`Verified`>>
+and <<label_Code-Review,`Code-Review`>> work, and add your own
+label to `project.config` to get the same behavior over your own range
+of values, for any label you desire.
+
+Just like the built-in labels, users need to be given permissions to
+vote on custom labels. Permissions can either be added by manually
+editing project.config when adding the labels, or, once the labels are
+added, permission categories for those labels will show up in the
+permission editor web UI.
+
+Labels may be added to any project's `project.config`; the default
+labels are defined in `All-Projects`. Labels are inherited from parent
+projects; a child project may add, override, or remove labels defined in
+its parents.  Overriding a label in a child project overrides all its
+properties and values.  To remove a label in a child project, add an
+empty label with the same name as in the parent.
+
+Labels are laid out in the order they are specified in project.config,
+with inherited labels appearing first, providing some layout control to
+the administrator.
+
+[[label_name]]
+`label.Label-Name`
+~~~~~~~~~~~~~~~~~~
+
+The name for a label, consisting only of alphanumeric characters and
+`-`.
+
+
+[[label_value]]
+`label.Label-Name.value`
+~~~~~~~~~~~~~~~~~~~~~~~~
+
+A multi-valued key whose values are of the form `"<#> Value description
+text"`. The `<#>` may be any positive or negative number with an
+optional leading `+`.
+
+
+[[label_abbreviatedName]]
+`label.Label-Name.abbreviatedName`
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+An abbreviated name for a label shown as a compact column header, for
+example on project dashboards. Defaults to all the uppercase characters
+in the label name, e.g. `Label-Name` is abbreviated by default as `LN`.
+
+
+[[label_function]]
+`label.Label-Name.function`
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The name of a function for evaluating multiple votes for a label.  This
+function is only applied if the default submit rule is used for a label.
+If you write a link:prolog-cookbook.html#HowToWriteSubmitRules[custom
+submit rule] (and do not call the default rule), the function name is
+ignored and may be treated as optional.
+
+Valid values are:
+
+* `MaxWithBlock` (default)
++
+The lowest possible negative value, if present, blocks a submit, while
+the highest possible positive value is required to enable submit. There
+must be at least one positive value, or else submit will never be
+enabled. To permit blocking submits, ensure a negative value is defined.
+
+* `MaxNoBlock`
++
+The highest possible positive value is required to enable submit, but
+the lowest possible negative value will not block the change.
+
+* `NoBlock`/`NoOp`
++
+The label is purely informational and values are not considered when
+determining whether a change is submittable.
+
+
+[[label_copyMinScore]]
+`label.Label-Name.copyMinScore`
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If true, the lowest possible negative value for the label is copied
+forward when a new patch set is uploaded.
+
+
+[[label_canOverride]]
+`label.Label-Name.canOverride`
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+If false, the label cannot be overridden by child projects. Any
+configuration for this label in child projects will be ignored. Defaults
+to true.
+
+
+[[label_example]]
+Example
+~~~~~~~
+
+To define a new 3-valued category that behaves exactly like `Verified`,
+but has different names/labels:
+
+====
+  [label "Copyright-Check"]
+      function = MaxWithBlock
+      value = -1 Do not have copyright
+      value =  0 No score
+      value = +1 Copyright clear
+====
+
+The new column will appear at the end of the table, and `-1 Do not have
+copyright` will block submit, while `+1 Copyright clear` is required to
+enable submit.
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/config-login-register.txt b/Documentation/config-login-register.txt
new file mode 100644
index 0000000..d0e0fc5
--- /dev/null
+++ b/Documentation/config-login-register.txt
@@ -0,0 +1,138 @@
+[[usersetup]]
+Inital Login
+------------
+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 of your Gerrit server.  You can
+find the url in the settings file.
+
+----
+  gerrit2@host:~$ git config -f ~/gerrit_testsite/etc/gerrit.config gerrit.canonicalWebUrl
+  http://localhost:8080/
+  gerrit2@host:~$
+----
+
+Register a new account in Gerrit through the web interface with the
+email address of your choice.
+
+The default authentication type is OpenID.  If your Gerrit server is behind a
+proxy, and you are using an external OpenID provider, you will need to add the
+proxy settings in the configuration file.
+
+----
+  gerrit2@host:~$ git config -f ~/gerrit_testsite/etc/gerrit.config --add http.proxy http://proxy:8080
+  gerrit2@host:~$ git config -f ~/gerrit_testsite/etc/gerrit.config --add http.proxyUsername username
+  gerrit2@host:~$ git config -f ~/gerrit_testsite/etc/gerrit.config --add http.proxyPassword password
+----
+
+Refer to the Gerrit configuration guide for more detailed information about
+link:config-gerrit.html#auth[authentication] and
+link:config-gerrit.html#http.proxy[proxy] settings.
+
+The first user to sign-in and register an account will be
+automatically placed into the fully privileged Administrators group,
+permitting server management over the web and over SSH.  Subsequent
+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 AAAAB3NzaC1yc2EAAAABIwAAAQEA1bidOd8LAp7Vp95M1b9z+LGO96OEWzdAgBPfZPq05jUh
+  jw0mIdUuvg5lhwswnNsvmnFhGbsUoXZui6jdXj7xPUWOD8feX2NNEjTAEeX7DXOhnozNAkk/Z98WUV2B
+  xUBqhRi8vhVmaCM8E+JkHzAc+7/HVYBTuPUS7lYPby5w95gs3zVxrX8d1++IXg/u/F/47zUxhdaELMw2
+  deD8XLhrNPx2FQ83FxrjnVvEKQJyD2OoqxbC2KcUGYJ/3fhiupn/YpnZsl5+6mfQuZRJEoZ/FH2n4DEH
+  wzgBBBagBr0ZZCEkl74s4KFZp6JJw/ZSjMRXsXXXWvwcTpaUEDii708HGw== John Doe@MACHINE
+  user@host:~$
+----
+
+IMPORTANT: Please take note of the extra line-breaks introduced in the key above
+for formatting purposes. Please be sure to copy and paste your key without
+line-breaks.
+
+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:~$
+----
\ No newline at end of file
diff --git a/Documentation/config-mail.txt b/Documentation/config-mail.txt
index ad0704f..8de8e59 100644
--- a/Documentation/config-mail.txt
+++ b/Documentation/config-mail.txt
@@ -49,7 +49,21 @@
 
 The `Comment.vm` template will determine the contents of the email related to
 a user submitting comments on changes.  It is a `ChangeEmail`: see
-`ChangeSubject.vm` and `ChangeFooter.vm`.
+`ChangeSubject.vm`, `ChangeFooter.vm` and `CommentFooter.vm`.
+
+CommentFooter.vm
+~~~~~~~~~~~~~~~~
+
+The `CommentFooter.vm` template will determine the contents of the footer
+text that will be appended to emails related to a user submitting comments on
+changes.  See `ChangeSubject.vm`, `Comment.vm` and `ChangeFooter.vm`.
+
+CommitMessageEdited.vm
+~~~~~~~~~~~~~~~~~~~~~~
+
+The `CommitMessageEdited.vm` template will determine the contents of the email
+related to a user editing the commit message through the Gerrit UI.  It is a
+`ChangeEmail`: see `ChangeSubject.vm` and `ChangeFooter.vm`.
 
 Merged.vm
 ~~~~~~~~~
diff --git a/Documentation/config-reverseproxy.txt b/Documentation/config-reverseproxy.txt
index 7161c4a..0857442 100644
--- a/Documentation/config-reverseproxy.txt
+++ b/Documentation/config-reverseproxy.txt
@@ -28,33 +28,34 @@
 Apache 2 Configuration
 ----------------------
 
-To run Gerrit behind an Apache server using 'mod_proxy', enable the
+To run Gerrit behind an Apache server we cannot use 'mod_proxy'
+directly, as Gerrit relies on getting unmodified escaped forward
+slashes. Depending on the setting of 'AllowEncodedSlashes',
+'mod_proxy' would either decode encoded slashes, or encode them once
+again. Hence, we resort to using 'mod_rewrite'. To enable the
 necessary Apache2 modules:
 
 ----
-  a2enmod proxy_http
+  a2enmod rewrite
   a2enmod ssl          ; # optional, needed for HTTPS / SSL
 ----
 
-Configure an Apache VirtualHost to proxy to the Gerrit daemon,
-setting the 'ProxyPass' line to use the 'http://' URL configured
-above.  Ensure the path of ProxyPass and httpd.listenUrl match,
-or links will redirect to incorrect locations.
+Configure an Apache VirtualHost to proxy to the Gerrit daemon, setting
+the 'RewriteRule' line to use the 'http://' URL configured above.
+Ensure the path of 'RewriteRule' (the part before '$1') and
+httpd.listenUrl match, or links will redirect to incorrect locations.
+
+Note that this configuration allows to pass encoded characters to the
+virtual host, which is potentially dangerous. Be sure to read up on
+this topic and that you understand the risks.
 
 ----
 	<VirtualHost *>
 	  ServerName review.example.com
 
-	  ProxyRequests Off
-	  ProxyVia Off
-	  ProxyPreserveHost On
-
-	  <Proxy *>
-		Order deny,allow
-		Allow from all
-	  </Proxy>
-
-	  ProxyPass /r/ http://127.0.0.1:8081/r/
+	  AllowEncodedSlashes NoDecode
+	  RewriteEngine On
+	  RewriteRule ^/r/(.*) http://localhost:8081/r/$1 [NE,P]
 	</VirtualHost>
 ----
 
diff --git a/Documentation/config-validation.txt b/Documentation/config-validation.txt
new file mode 100644
index 0000000..ab5d04a
--- /dev/null
+++ b/Documentation/config-validation.txt
@@ -0,0 +1,21 @@
+Gerrit Code Review - Commit Validation
+======================================
+
+Gerrit supports link:dev-plugins.html[plugin-based] validation of
+uploaded commits.
+
+This allows plugins to perform additional validation checks against
+uploaded commits, and send back a warning or error message to the git
+client.
+
+To make use of this feature, a plugin must implement the `CommitValidationListener`
+interface.
+
+Out of the box, Gerrit includes a plugin that checks the length of the
+subject and body lines of commit messages on uploaded commits.
+
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
+
diff --git a/Documentation/database-setup.txt b/Documentation/database-setup.txt
new file mode 100644
index 0000000..c559b0e
--- /dev/null
+++ b/Documentation/database-setup.txt
@@ -0,0 +1,65 @@
+[[createdb]]
+Database Setup
+--------------
+
+During the init phase of Gerrit you will need to specify which database to use.
+
+[[createdb_h2]]
+H2
+~~
+
+If you choose H2, Gerrit will automatically set up the embedded H2 database as
+backend so no set up or 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:
+
+----
+  $ createuser --username=postgres -RDIElPS gerrit2
+  $ createdb --username=postgres -E UTF-8 -O gerrit2 reviewdb
+----
+
+Visit PostgreSQL's link:http://www.postgresql.org/docs/9.1/interactive/index.html[documentation] for further information regarding
+using PostgreSQL.
+
+[[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:
+
+----
+  mysql
+
+  CREATE USER 'gerrit2'@'localhost' IDENTIFIED BY 'secret';
+  CREATE DATABASE reviewdb;
+  ALTER DATABASE reviewdb charset=latin1;
+  GRANT ALL ON reviewdb.* TO 'gerrit2'@'localhost';
+  FLUSH PRIVILEGES;
+----
+
+Visit MySQL's link:http://dev.mysql.com/doc/[documentation] for further
+information regarding using MySQL.
diff --git a/Documentation/dev-contributing.txt b/Documentation/dev-contributing.txt
index 065e9d1..84cb1e0 100644
--- a/Documentation/dev-contributing.txt
+++ b/Documentation/dev-contributing.txt
@@ -31,7 +31,7 @@
 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
+say so, and if you have the privileges 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.
 
@@ -57,6 +57,7 @@
 spans, we really do want your code.
 
 
+[[commit-message]]
 Commit Message
 --------------
 
@@ -68,6 +69,7 @@
   * Followed by a blank line
   * Followed by one or more explanatory paragraphs
   * Use the present tense (fix instead of fixed)
+  * Use the past tense when describing the status before this commit
   * Include a Bug: Issue <#> line if fixing a Gerrit issue
   * Include a Change-Id line
 
@@ -91,6 +93,25 @@
   Change-Id: Ic4a7c07eeb98cdeaf44e9d231a65a51f3fceae52
 ====
 
+The Change-Id is, as usual, created by a local git hook.  To install it, simply
+copy one from the checkout and make it executable:
+
+====
+  cp ./gerrit-server/src/main/resources/com/google/gerrit/server/tools/root/hooks/commit-msg .git/hooks/
+  chmod +x .git/hooks/commit-msg
+====
+
+To set up git's remote for easy pushing, run the following:
+
+====
+  git remote add gerrit https://gerrit.googlesource.com/gerrit
+====
+
+The HTTPS access requires proper username and password; this can be obtained
+by clicking the "Obtain Password" link on the
+link:https://gerrit-review.googlesource.com/#/settings/http-password[HTTP
+Password tab of the user settings page].
+
 
 Style
 -----
@@ -140,7 +161,7 @@
   * 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
+  * Finally instance members, then constructors, and then instance
     methods.
   * Some common exceptions are private helper static methods which
     might appear near the instance methods which they help.
@@ -238,6 +259,18 @@
     and it also makes "git revert" more useful.
   * Use topic branches to link your separate changes together.
 
+[[process]]
+Process
+-------
+
+Backporting to stable branches
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+From time to time bug fix releases are made for existing stable branches.
+
+Developers concerned with stable branches are encouraged to backport or push
+patchsets to these branches, even if no new release is planned.
+
 
 GERRIT
 ------
diff --git a/Documentation/dev-design.txt b/Documentation/dev-design.txt
index ce2868c..2e43b96 100644
--- a/Documentation/dev-design.txt
+++ b/Documentation/dev-design.txt
@@ -8,7 +8,7 @@
 reviews for projects using the Git version control system.
 
 Gerrit makes reviews easier by showing changes in a side-by-side
-display, and allowing inline comments to be added by any reviewer.
+display, and allowing inline/file comments to be added by any reviewer.
 
 Gerrit simplifies Git based project maintainership by permitting
 any authorized user to submit changes to the master Git repository,
@@ -99,11 +99,11 @@
 permissions in the project.
 
 Reviewers use the web interface to read the side-by-side or unified
-diff of a change, and insert draft inline comments where appropriate.
-A draft comment is visible only to the reviewer, until they publish
-those comments.  Published comments are automatically emailed to
-the change author by Gerrit, and are CC'd to all other reviewers
-who have already commented on the change.
+diff of a change, and insert draft inline/file comments where
+appropriate. A draft comment is visible only to the reviewer, until
+they publish those comments.  Published comments are automatically
+emailed to the change author by Gerrit, and are CC'd to all other
+reviewers who have already commented on the change.
 
 When publishing comments reviewers are also given the opportunity
 to score the change, indicating whether they feel the change is
@@ -553,8 +553,8 @@
 
 Gerrit's web UI would require on average `4+F+F*C` HTTP requests to
 review a change and post comments.  Here `F` is the number of files
-modified by the change, and `C` is the number of inline comments left
-by the reviewer per file.  The constant 4 accounts for the request
+modified by the change, and `C` is the number of inline/file comments
+left by the reviewer per file.  The constant 4 accounts for the request
 to load the reviewer's dashboard, to load the change detail page,
 to publish the review comments, and to reload the change detail
 page after comments are published.
diff --git a/Documentation/dev-eclipse.txt b/Documentation/dev-eclipse.txt
index b2bf011..64e5935 100644
--- a/Documentation/dev-eclipse.txt
+++ b/Documentation/dev-eclipse.txt
@@ -8,12 +8,17 @@
 runtime debugging environment.
 
 
+[[maven]]
 Maven Plugin
 ------------
 
-Install the Maven Integration plugins:
+Install the Maven Integration plugins.
 
-http://www.eclipse.org/m2e/download/[m2eclipse]
+In Eclipse version 3.7 (Indigo) and later, these are available in the
+default update site and can be found under the 'Collaboration' category.
+
+For older versions the update site must be manually added; the link can
+be found on the http://www.eclipse.org/m2e/download/[m2eclipse download page].
 
 
 [[Formatting]]
diff --git a/Documentation/dev-plugins.txt b/Documentation/dev-plugins.txt
index dd0c44c..12838fe 100644
--- a/Documentation/dev-plugins.txt
+++ b/Documentation/dev-plugins.txt
@@ -176,6 +176,69 @@
 To reload/restart a plugin the link:cmd-plugin-reload.html[plugin reload]
 command can be used.
 
+[[init_step]]
+Init step
+~~~~~~~~~
+
+Plugins can contribute their own "init step" during the Gerrit init
+wizard. This is useful for guiding the Gerrit administrator through
+the settings needed by the plugin to work propertly.
+
+For instance plugins to integrate Jira issues to Gerrit changes may
+contribute their own "init step" to allow configuring the Jira URL,
+credentials and possibly verify connectivity to validate them.
+
+====
+  Gerrit-InitStep: tld.example.project.MyInitStep
+====
+
+MyInitStep needs to follow the standard Gerrit InitStep syntax
+and behaviour: writing to the console using the injected ConsoleUI
+and accessing / changing configuration settings using Section.Factory.
+
+In addition to the standard Gerrit init injections, plugins receive
+the @PluginName String injection containing their own plugin name.
+
+Bear in mind that the Plugin's InitStep class will be loaded but
+the standard Gerrit runtime environment is not available and the plugin's
+own Guice modules were not initialized.
+This means the InitStep for a plugin is not executed in the same way that
+the plugin executes within the server, and may mean a plugin author cannot
+trivially reuse runtime code during init.
+
+For instance a plugin that wants to verify connectivity may need to statically
+call the constructor of their connection class, passing in values obtained
+from the Section.Factory rather than from an injected Config object.
+
+Plugins InitStep are executing during the "Gerrit Plugin init" phase, after
+the extraction of the plugins embedded in Gerrit.war into $GERRIT_SITE/plugins
+and before the DB Schema initialization or upgrade.
+Plugins InitStep cannot refer to Gerrit DB Schema or any other Gerrit runtime
+objects injected at startup.
+
+====
+public class MyInitStep implements InitStep {
+  private final ConsoleUI ui;
+  private final Section.Factory sections;
+  private final String pluginName;
+
+  @Inject
+  public GitBlitInitStep(final ConsoleUI ui, Section.Factory sections, @PluginName String pluginName) {
+    this.ui = ui;
+    this.sections = sections;
+    this.pluginName = pluginName;
+  }
+
+  @Override
+  public void run() throws Exception {
+    ui.header("\nMy plugin");
+
+    Section mySection = getSection("myplugin", null);
+    mySection.string("Link name", "linkname", "MyLink");
+  }
+}
+====
+
 [[classpath]]
 Classpath
 ---------
@@ -322,10 +385,13 @@
 automatically export these resources over HTTP from the plugin JAR.
 
 Static resources under `static/` directory in the JAR will be
-available as `/plugins/helloworld/static/resource`.
+available as `/plugins/helloworld/static/resource`. This prefix is
+configurable by setting the `Gerrit-HttpStaticPrefix` attribute.
 
 Documentation files under `Documentation/` directory in the JAR
-will be available as `/plugins/helloworld/Documentation/resource`.
+will be available as `/plugins/helloworld/Documentation/resource`. This
+prefix is configurable by setting the `Gerrit-HttpDocumentationPrefix`
+attribute.
 
 Documentation may be written in
 link:http://daringfireball.net/projects/markdown/[Markdown] style
diff --git a/Documentation/dev-readme.txt b/Documentation/dev-readme.txt
index 552b5a8..67b4aad 100644
--- a/Documentation/dev-readme.txt
+++ b/Documentation/dev-readme.txt
@@ -11,10 +11,13 @@
 Create a new client workspace:
 
 ----
-  git clone https://gerrit.googlesource.com/gerrit
+  git clone --recursive https://gerrit.googlesource.com/gerrit
   cd gerrit
 ----
 
+The `--recursive` option is needed on `git clone` to ensure that
+the core plugins, which are included as git submodules, are also
+cloned.
 
 Configuring Eclipse
 -------------------
@@ -31,7 +34,21 @@
 From the command line:
 
 ----
-  mvn package
+  mvn clean package
+----
+
+By default the build will run tests and build the documentation.
+
+To build without tests:
+
+----
+  mvn clean package -DskipTests
+----
+
+To build without documentation:
+
+----
+  mvn clean package -Dgerrit.documentation.skip
 ----
 
 Output executable WAR will be placed in:
@@ -80,6 +97,27 @@
 Testing
 -------
 
+[[run-acceptance-tests]]
+Running the Acceptance Tests
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+Gerrit has a set of integration tests that test the Gerrit daemon via
+REST, SSH and the git protocol.
+
+A new review site is created for each test and the Gerrit daemon is
+started on that site. When the test has finished the Gerrit daemon is
+shutdown.
+
+Since the acceptance tests are too expensive to run every time
+Gerrit is built, they are only executed during the Maven verify phase
+if the Maven profile `acceptance` is enabled.
+
+To execute the acceptance tests run:
+
+----
+  mvn clean verify -Pacceptance
+----
+
 Running the Daemon
 ~~~~~~~~~~~~~~~~~~
 
@@ -109,13 +147,21 @@
 ----
 
 
+[[debug-javascript]]
 Debugging JavaScript
 ~~~~~~~~~~~~~~~~~~~~
 
-When debugging browser specific issues use `-Dgwt.style=DETAILED`
-so the resulting JavaScript more closely matches the Java sources.
-This can be used to help narrow down what code line 30,400 in the
-JavaScript happens to be.
+When debugging browser specific issues add `?dbg=1` to the URL so the
+resulting JavaScript more closely matches the Java sources.  The debug
+pages use the GWT pretty format, where function and variable names
+match the Java sources.
+
+----
+  http://localhost:8080/?dbg=1
+----
+
+To use the GWT DETAILED style the package must be recompiled and
+`?dbg=1` must be omitted from the URL:
 
 ----
   mvn package -Dgwt.style=DETAILED
diff --git a/Documentation/dev-release-deploy-config.txt b/Documentation/dev-release-deploy-config.txt
new file mode 100644
index 0000000..bc52d50
--- /dev/null
+++ b/Documentation/dev-release-deploy-config.txt
@@ -0,0 +1,135 @@
+Deploy Gerrit Artifacts
+=======================
+
+Gerrit Artifacts are stored on
+link:https://developers.google.com/storage/[Google Cloud Storage].
+Via the link:https://code.google.com/apis/console/[API Console] the
+Gerrit maintainers have access to the `Gerrit Code Review` project.
+This projects host several buckets for storing Gerrit artifacts:
+
+* `gerrit-api`:
++
+Bucket to store the Gerrit Extension API Jar and the Gerrit Plugin API
+Jar.
+
+* `gerrit-maven`:
++
+Bucket to store Gerrit Subproject Artifacts (e.g. `gwtexpui`,
+`gwtjsonrpc` etc.).
+
+* `gerrit-plugins`:
++
+Bucket to store Gerrit Core Plugin Artifacts.
+
+[[deploy-configuration-settings-xml]]
+Deploy Configuration in Maven `settings.xml`
+--------------------------------------------
+
+To upload artifacts to a bucket the user must authenticate with a
+username and password. The username and password need to be retrieved
+from the link:https://code.google.com/apis/console/[API Console]:
+
+* Go to the `Gerrit Code Review` project
+* In the menu on the left select `Google Cloud Storage` >
+`Interoperable Access`
+* Use the `Access Key` as username
+* Click under `Secret` on the `Show` button to find the password
+
+To make the username and password known to Maven, they must be
+configured in the `~/.m2/settings.xml` file.
+
+----
+  <settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
+            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+            xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
+    <servers>
+      <server>
+        <id>gerrit-api-repository</id>
+        <username>GOOG..EXAMPLE.....EXAMPLE</username>
+        <password>EXAMPLE..EXAMPLE..EXAMPLE</password>
+      </server>
+      <server>
+        <id>gerrit-maven-repository</id>
+        <username>GOOG..EXAMPLE.....EXAMPLE</username>
+        <password>EXAMPLE..EXAMPLE..EXAMPLE</password>
+      </server>
+      <server>
+        <id>gerrit-plugins-repository</id>
+        <username>GOOG..EXAMPLE.....EXAMPLE</username>
+        <password>EXAMPLE..EXAMPLE..EXAMPLE</password>
+      </server>
+    </servers>
+  </settings>
+----
+
+[[deploy-configuration-subprojects]]
+Gerrit Subprojects
+~~~~~~~~~~~~~~~~~~
+
+* You will need to have the following in the `pom.xml` to make it
+deployable to the `gerrit-maven` storage bucket:
+
+----
+  <distributionManagement>
+    <repository>
+      <id>gerrit-maven-repository</id>
+      <name>Gerrit Maven Repository</name>
+      <url>s3://gerrit-maven@commondatastorage.googleapis.com</url>
+      <uniqueVersion>true</uniqueVersion>
+    </repository>
+  </distributionManagement>
+----
+
+
+* Add this to the `pom.xml` to enable the wagon provider:
+
+----
+  <build>
+    <extensions>
+      <extension>
+        <groupId>net.anzix.aws</groupId>
+        <artifactId>s3-maven-wagon</artifactId>
+        <version>3.2</version>
+      </extension>
+    </extensions>
+  </build>
+----
+
+
+[[deploy-configuration-core-plugins]]
+Gerrit Core Plugins
+~~~~~~~~~~~~~~~~~~~
+
+* You will need to have the following in the `pom.xml` to make it
+deployable to the `gerrit-plugins` storage bucket:
+
+----
+  <distributionManagement>
+    <repository>
+      <id>gerrit-plugins-repository</id>
+      <name>Gerrit Plugins Repository</name>
+      <url>s3://gerrit-plugins@commondatastorage.googleapis.com</url>
+      <uniqueVersion>true</uniqueVersion>
+    </repository>
+  </distributionManagement>
+----
+
+
+* Add this to the `pom.xml` to enable the wagon provider:
+
+----
+  <build>
+    <extensions>
+      <extension>
+        <groupId>net.anzix.aws</groupId>
+        <artifactId>s3-maven-wagon</artifactId>
+        <version>3.2</version>
+      </extension>
+    </extensions>
+  </build>
+----
+
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/dev-release-subproject.txt b/Documentation/dev-release-subproject.txt
index 799ff2d..5e3770d 100644
--- a/Documentation/dev-release-subproject.txt
+++ b/Documentation/dev-release-subproject.txt
@@ -1,96 +1,106 @@
 Making a Release of a Gerrit Subproject / Core Plugin
 =====================================================
 
-Preparing a New Snapshot for Publishing
----------------------------------------
+[[make-snapshot]]
+Make a Snapshot
+---------------
 
-* You will need to have the following in the `pom.xml` to make it
-  deployable to the `gerrit-maven` storage bucket:
+* Only for plugins:
+** In the `pom.xml` update the Gerrit version under `properties` >
+`Gerrit-ApiVersion` to the version of the new Gerrit
+release.
+** Make sure that the URL for the Maven repository with the id
+`gerrit-api-repository` in the `pom.xml` is correct.
++
+If `Gerrit-ApiVersion` references a released Gerrit version it must be
+`https://gerrit-api.commondatastorage.googleapis.com/release/`, if
+`Gerrit-ApiVersion` references a snapshot Gerrit version it must be
+`https://gerrit-api.commondatastorage.googleapis.com/snapshot/`.
 
-----
-  <distributionManagement>
-    <repository>
-      <id>gerrit-maven</id>
-      <name>gerrit Maven Repository</name>
-      <url>s3://gerrit-maven@commondatastorage.googleapis.com</url>
-      <uniqueVersion>true</uniqueVersion>
-    </repository>
-  </distributionManagement>
-----
+* Build the latest snapshot and install it into the local Maven
+repository:
++
+====
+  mvn clean install
+====
+
+* Test Gerrit with this snapshot locally
 
 
-* Add this to the `pom.xml` to enable the wagon provider:
+Publish Snapshot
+----------------
 
-----
-  <build>
-    <extensions>
-      <extension>
-        <groupId>net.anzix.aws</groupId>
-        <artifactId>s3-maven-wagon</artifactId>
-        <version>3.2</version>
-      </extension>
-    </extensions>
-  </build>
-----
+If a Snapshot for a Subproject was created that should be referenced by
+Gerrit while current Gerrit development is ongoing, this Snapshot needs
+to be published.
 
+* Make sure you have done the configuration needed for deployment:
+** link:dev-release-deploy-config.html#deploy-configuration-settings-xml[
+Configuration in Maven `settings.xml`]
+** link:dev-release-deploy-config.html#deploy-configuration-subprojects[
+Configuration for Subprojects in `pom.xml`]
 
-* Add your username and password to your `~/.m2/settings.xml` file.
-  These need to come from the link:https://code.google.com/apis/console/[API Console].
-
-----
-  <settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
-            xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-            xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
-    <servers>
-      <server>
-        <id>gerrit-maven</id>
-        <username>GOOG..EXAMPLE.....EXAMPLE</username>
-        <password>EXAMPLE..EXAMPLE..EXAMPLE</password>
-      </server>
-    </servers>
-  </settings>
-----
-
-
-Making a Snapshot
------------------
-
-* Only for plugins: in the `pom.xml` update the Gerrit version under
-`properties` > `Gerrit-ApiVersion` to the version of the new Gerrit
-release
-* First build and deploy the latest snapshot and ensure that Gerrit
-builds/runs with this snapshot
-
-* Deploy the snapshot:
-
+* Deploy the new snapshot:
++
 ====
   mvn deploy
 ====
 
+* Change the version in the Gerrit parent `pom.xml` for the Subproject
+to the `SNAPSHOT` version
++
+When Gerrit gets released, a release of the Subproject has to be done
+and Gerrit has to reference the released Subproject version.
 
-Making a Release
-----------------
 
-* First deploy (and test) the latest snapshot for the subproject/plugin
+[[prepare-release]]
+Prepare the Release
+-------------------
+
+* link:#make-snapshot[First create (and test) the latest snapshot for
+the subproject/plugin]
 
 * Update the top level `pom.xml` in the subproject/plugin to reflect
 the new project version (the exact value of the tag you will create
 below)
 
-* Commit the pom change and push to the project's repo
-`refs/for/<master/stable>`
-
-* Tag the version you just pushed (and push the tag)
-
+* Create the Release Tag
++
 ====
  git tag -a -m "prolog-cafe 1.3" v1.3
- git push gerrit-review refs/tags/v1.3:refs/tags/v1.3
 ====
 
+* Build and install into local Maven repository:
++
+====
+  mvn clean install
+====
+
+
+[[publish-release]]
+Publish the Release
+-------------------
+
+* Make sure you have done the configuration needed for deployment:
+** link:dev-release-deploy-config.html#deploy-configuration-settings-xml[
+Configuration in Maven `settings.xml`]
+** Configuration in `pom.xml` for
+link:dev-release-deploy-config.html#deploy-configuration-subprojects[Subprojects] or
+link:dev-release-deploy-config.html#deploy-configuration-core-plugins[Core Plugins]
+
 * Deploy the new release:
-
++
 ====
- mvn deploy
+  mvn deploy
+====
+
+* Push the pom change(s) to the project's repository
+`refs/for/<master|stable>`
+
+* Push the Release Tag
++
+====
+  git push gerrit-review refs/tags/v1.3:refs/tags/v1.3
 ====
 
 
diff --git a/Documentation/dev-release.txt b/Documentation/dev-release.txt
index 5ea3042..ae2aed6 100644
--- a/Documentation/dev-release.txt
+++ b/Documentation/dev-release.txt
@@ -11,7 +11,7 @@
 
 To make a Gerrit release involves a great deal of complex
 tasks and it is easy to miss a step so this document should
-hopefuly serve as both a how to for those new to the process
+hopefully serve as both a how to for those new to the process
 and as a checklist for those already familiar with these
 tasks.
 
@@ -20,142 +20,215 @@
 -------------------
 
 Here are some guidelines on release approaches depending on the
-type of release you want to make (stable-fix, stable, RC0, RC1...).
+type of release you want to make (`stable-fix`, `stable`, `RC0`,
+`RC1`...).
 
+[[stable]]
 Stable
 ~~~~~~
 
-A stable release is generally built from the master branch and may need to
-undergo some stabilization before releasing the final release.
+A `stable` release is generally built from the `master` branch and may
+need to undergo some stabilization before releasing the final release.
 
 * Propose the release with any plans/objectives to the mailing list
 
-* Create a Gerrit RC0
+* Create a Gerrit `RC0`
 
-* If needed create a Gerrit RC1
+* If needed create a Gerrit `RC1`
 
 [NOTE]
 ========================================================================
 You may let in a few features to this release
 ========================================================================
 
-* If needed create a Gerrit RC2
+* If needed create a Gerrit `RC2`
 
 [NOTE]
 ========================================================================
 There should be no new features in this release, only bug fixes
 ========================================================================
 
-* Finally create the stable release (no RC)
+* Finally create the `stable` release (no `RC`)
 
 
 Stable-Fix
 ~~~~~~~~~~
 
-Stable-fix releases should likely only contain bug fixes and doc updates.
+`stable-fix` releases should likely only contain bug fixes and doc
+updates.
 
 * Propose the release with any plans/objectives to the mailing list
 
-* This type of release does not need any RCs, release when the objectives
-  are met
+* This type of release does not need any RCs, release when the
+objectives are met
 
 
+[[security]]
+Security-Fix
+~~~~~~~~~~~~
+
+`security-fix` releases should only contain bug fixes for security
+issues.
+
+For security issues it is important that they are only announced
+*after* fixed versions for all relevant releases have been published.
+Because of this, `security-fix` releases can't be prepared in the public
+`gerrit` project.
+
+`security-fix` releases are prepared in the `gerrit-security-fixes`
+project which is only readable by the Gerrit Maintainers. Only after
+a `security-fix` release has been published will the commits/tags made in
+the `gerrit-security-fixes` project be taken over into the public
+`gerrit` project.
+
 
 Create the Actual Release
 ---------------------------
 
-In the example commands below we assume that the last release was '2.4' and that
-we are preparing '2.5' release.
+To create a Gerrit release the following steps have to be done:
 
-Prepare the Subprojects
-~~~~~~~~~~~~~~~~~~~~~~~
-
-* Publish the latest snapshot for all subprojects
-* Freeze all subprojects and link:dev-release-subproject.html[publish]
-  them!
+. link:#subproject[Release Subprojects]
+. link:#prepare-gerrit[Prepare the Gerrit Release]
+.. link:#prepare-war-and-plugin-api[Prepare the Gerrit WAR and the Plugin API Jar]
+.. link:#prepare-core-plugins[Prepare the Core Plugins]
+.. link:#prepare-war-with-plugins[Prepare Gerrit WAR with Core Plugins]
+. link:#publish-gerrit[Publish the Gerrit Release]
+.. link:#extension-and-plugin-api[Publish the Extension and Plugin API Jars]
+.. link:#publish-core-plugins[Publish the Core Plugins]
+.. link:#publish-gerrit-war[Publish the Gerrit WAR (with Core Plugins)]
+.. link:#push-stable[Push the Stable Branch]
+.. link:#push-tag[Push the Release Tag]
+.. link:#upload-documentation[Upload the Documentation]
+.. link:#update-issues[Update the Issues]
+.. link:#announce[Announce on Mailing List]
+. link:#increase-version[Increase Gerrit Version for Current Development]
+. link:#merge-stable[Merge `stable` into `master`]
 
 
-Prepare Gerrit
-~~~~~~~~~~~~~~
+[[subproject]]
+Release Subprojects
+~~~~~~~~~~~~~~~~~~~
 
-* Create a `stable-2.5` branch for making the new release
+The subprojects to be released are:
 
-* In the `master` branch: Update the poms for the Gerrit version, push for
-review, get merged
+* `gwtexpui`
+* `gwtjsonrpc`
+* `gwtorm`
+* `prolog-cafe`
 
+For each subproject do:
+
+* Check the dependency to the Subproject in the Gerrit parent `pom.xml`:
++
+If a `SNAPSHOT` version of the subproject is referenced the subproject
+needs to be released so that Gerrit can reference a released version of
+the subproject.
+
+* link:dev-release-subproject.html#make-snapshot[Make a snapshot and test it]
+* link:dev-release-subproject.html#prepare-release[Prepare the Release]
+* link:dev-release-subproject.html#publish-release[Publish the Release]
+
+* Update the version of the Subproject in the Gerrit parent `pom.xml`
+to the released version
+
+
+[[build-gerrit]]
+Build Gerrit
+~~~~~~~~~~~~
+
+* Build the Gerrit WAR
++
 ====
- tools/version.sh --snapshot=2.5
-====
-
-* Checkout the `stable-2.5` branch
-* Update the top level `pom.xml` in Gerrit to ensure that none of the
-Subprojects point to snapshot releases
-
-* Tag
-
-====
- git tag -a -m "gerrit 2.5-rc0" v2.5-rc0
- git tag -a -m "gerrit 2.5" v2.5
-====
-
-* Build (without plugins)
-
-====
+ rm -f ~/.m2/settings.xml
  ./tools/release.sh
 ====
++
+[WARNING]
+========================================================================
+Make sure you are compiling the release for all browsers. Check in your
+Maven `~/.m2/settings.xml` file that no Maven profile is active that
+limits the compilation to a certain browser.
+========================================================================
 
-[[plugin-api]]
-Publish the Plugin API JAR File
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-
-* Push JAR to `commondatastorage.googleapis.com`
-** Run `tools/deploy_api.sh`
-
-Prepare the Core Plugins
-~~~~~~~~~~~~~~~~~~~~~~~~
-* link:dev-release-subproject.html[Release and publish] the core plugins
-
-Package Gerrit with Plugins
-~~~~~~~~~~~~~~~~~~~~~~~~~~~
-* Ensure that the core plugins listed in `gerrit-package-plugins/pom.xml`
-point to the latest release version (no dependency to snapshot versions)
-* Include core plugins into WAR
-====
- $ ./tools/version.sh --release && mvn clean package -f gerrit-package-plugins/pom.xml
- $ ./tools/version.sh --reset
-====
-
-* Find WAR that includes the core plugins at
-`gerrit-package-plugins\target\gerrit-full-v2.5.war`
 * Sanity check WAR
+* Test the new Gerrit version
 
-Publish to the Project Locations
---------------------------------
+[[publish-gerrit]]
+Publish the Gerrit Release
+~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-WAR File
-~~~~~~~~
 
-* Upload WAR to code.google.com/p/gerrit (manual web browser)
+[[extension-and-plugin-api]]
+Publish the Extension and Plugin API Jars
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+* Make sure you have done the
+link:dev-release-deploy-config.html#deploy-configuration-settings-xml[
+configuration needed for deployment]
+
+* Push the Jars to `commondatastorage.googleapis.com`:
++
+----
+  ./tools/deploy_api.sh
+----
+
+
+[[publish-core-plugins]]
+Publish the Core Plugins
+^^^^^^^^^^^^^^^^^^^^^^^^
+
+* link:dev-release-subproject.html#publish-release[Publish the Release]
+
+
+[[publish-gerrit-war]]
+Publish the Gerrit WAR (with Core Plugins)
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+
+* The WAR file to upload is `gerrit-package-plugins\target\gerrit-full-v2.5.war`
+* Upload WAR to `code.google.com/p/gerrit` (manual via web browser)
 ** Go to http://code.google.com/p/gerrit/downloads/list
-** Use the "New Download" button
+** Use the `New Download` button
 
 * Update labels:
-** new war: [release-candidate], featured...
-** old war: deprecated
+** new war: [`release-candidate`], `featured`...
+** old war: `deprecated`
 
-Tag
-~~~
 
-* Push the New Tag
+[[push-stable]]
+Push the Stable Branch
+^^^^^^^^^^^^^^^^^^^^^^
 
+* create the stable branch `stable-2.5` in the `gerrit` project
++
+Via the link:https://gerrit-review.googlesource.com/#/admin/projects/gerrit,branches[
+Gerrit WebUI] or by push.
+
+* Push the commits done on `stable-2.5` to `refs/for/stable-2.5` and
+get them merged
+
+
+[[push-tag]]
+Push the Release Tag
+^^^^^^^^^^^^^^^^^^^^
+
+* Push the new Release Tag
++
+For an `RC`:
++
 ====
  git push gerrit-review refs/tags/v2.5-rc0:refs/tags/v2.5-rc0
+====
++
+For a final `stable` release:
++
+====
  git push gerrit-review refs/tags/v2.5:refs/tags/v2.5
 ====
 
 
-Docs
-~~~~
+[[upload-documentation]]
+Upload the Documentation
+^^^^^^^^^^^^^^^^^^^^^^^^
 
 ====
  make -C Documentation PRIOR=2.4 update
@@ -167,14 +240,14 @@
 * Update Google Code project links
 ** Go to http://code.google.com/p/gerrit/admin
 ** Point the main page to the new docs. The link to the documentation has to be
-updated at two places: in the project description and also in the Links
+updated at two places: in the project description and also in the `Links`
 section.
 ** Point the main page to the new release notes
 
 [NOTE]
 ========================================================================
-The docs makefile does an svn cp of the prior revision of the docs to branch
-the docs so you have less to upload on the new docs.
+The docs makefile does an `svn cp` of the prior revision of the docs to
+branch the docs so you have less to upload on the new docs.
 
 User and password from here:
 
@@ -188,30 +261,33 @@
 ========================================================================
 
 
-Issues
-~~~~~~
+[[update-issues]]
+Update the Issues
+^^^^^^^^^^^^^^^^^
 
 ====
  How do the issues get updated?  Do you run a script to do
- this?  When do you do it, after the final 2.2.2 is released?
+ this?  When do you do it, after the final 2.5 is released?
 ====
 
 By hand.
 
-Our current process is an issue should be updated to say Status =
-Submitted, FixedIn-2.2.2 once the change is submitted, but before the
+Our current process is an issue should be updated to say `Status =
+Submitted, FixedIn-2.5` once the change is submitted, but before the
 release.
 
 After the release is actually made, you can search in Google Code for
-``Status=Submitted FixedIn=2.2.2'' and then batch update these changes
-to say Status=Released. Make sure the pulldown says ``All Issues''
-because Status=Submitted is considered a closed issue.
+``Status=Submitted FixedIn=2.5'' and then batch update these changes
+to say `Status=Released`. Make sure the pulldown says ``All Issues''
+because `Status=Submitted` is considered a closed issue.
 
 
-Mailing List
-~~~~~~~~~~~~
+[[announce]]
+Announce on Mailing List
+^^^^^^^^^^^^^^^^^^^^^^^^
 
-* Send an email to the mailing list to announce the release, consider including some or all of the following in the email:
+* Send an email to the mailing list to announce the release, consider
+including some or all of the following in the email:
 ** A link to the release and the release notes (if a final release)
 ** A link to the docs
 ** Describe the type of release (stable, bug fix, RC)
@@ -241,7 +317,7 @@
 -Martin
 ----
 
-* Add an entry to the NEWS section of the main Gerrit project web page
+* Add an entry to the `NEWS` section of the main Gerrit project web page
 ** Go to: http://code.google.com/p/gerrit/admin
 ** Add entry like:
 ----
@@ -252,18 +328,33 @@
 ** Go to: http://groups.google.com/group/repo-discuss/topics
 ** Click on the announcement thread
 ** Near the top right, click on options
-** Under options, cick the "Display this top first" checkbox
+** Under options, click the "Display this top first" checkbox
 ** and Save
 
 * Update the previous discussion group announcement to no longer be sticky
 ** See above (unclick checkbox)
 
 
-Merging Stable Fixes to master
-------------------------------
+[[increase-version]]
+Increase Gerrit Version for Current Development
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-After every stable-fix release, stable should be merged to master to
-ensure that none of the fixes ever get lost.
+All new development that is done in the `master` branch will be
+included in the next Gerrit release. Update the Gerrit version in each
+`pom.xml` file to the next `SNAPSHOT`version. Push the change for
+review and get it merged.
+
+====
+ tools/version.sh --snapshot=2.6
+====
+
+
+[[merge-stable]]
+Merge `stable` into `master`
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+After every release, stable should be merged to master to ensure that
+none of the changes/fixes ever get lost.
 
 ====
  git config merge.summary true
diff --git a/Documentation/error-branch-not-found.txt b/Documentation/error-branch-not-found.txt
index 2aad0e1..bd8d090 100644
--- a/Documentation/error-branch-not-found.txt
+++ b/Documentation/error-branch-not-found.txt
@@ -15,14 +15,14 @@
 * that the branch name in the push specification is typed correctly
   (case sensitive) and
 * that the branch really exists for this project (in the Gerrit WebUI
-  go to 'Admin' -> 'Projects' and browse your project, then click on
+  go to 'Projects' > 'List' and browse your project, then click on
   'Branches' to see all existing branches).
 
 If it was your intention to create a new branch you can either
 
 * bypass code review on push as explained link:user-upload.html#bypass_review[here] or
 * create the new branch in the Gerrit WebUI before pushing (go to
-  'Admin' -> 'Projects' and browse your project, in the 'Branches'
+  'Projects' > 'List' and browse your project, in the 'Branches'
   tab you can then create a new branch).
 
 Please note that you need to be granted the
diff --git a/Documentation/error-commit-already-exists.txt b/Documentation/error-commit-already-exists.txt
new file mode 100644
index 0000000..dc32c4c1
--- /dev/null
+++ b/Documentation/error-commit-already-exists.txt
@@ -0,0 +1,14 @@
+commit already exists
+=====================
+
+With this error message Gerrit rejects to push a commit to an
+existing change via `refs/changes/n` if the commit was already
+successfully pushed to the change.  In this case there is no
+new commit and consequently there is nothing for Gerrit to do.
+
+For further information about how to resolve this error, please
+refer to link:error-no-new-changes.html[no new changes].
+
+GERRIT
+------
+Part of link:error-messages.html[Gerrit Error Messages]
diff --git a/Documentation/error-invalid-changeid-line.txt b/Documentation/error-invalid-changeid-line.txt
index 2f57542..9235266 100644
--- a/Documentation/error-invalid-changeid-line.txt
+++ b/Documentation/error-invalid-changeid-line.txt
@@ -1,8 +1,8 @@
-invalid Change-Id line format in commit message
-===============================================
+invalid Change-Id line format in commit message footer
+======================================================
 
 With this error message Gerrit rejects to push a commit if its commit
-message contains an invalid Change-Id line.
+message footer contains an invalid Change-Id line.
 
 You can see the commit messages for existing commits in the history
 by doing a link:http://www.kernel.org/pub/software/scm/git/docs/git-log.html[git log].
diff --git a/Documentation/error-messages.txt b/Documentation/error-messages.txt
index c9df883..58e9e02 100644
--- a/Documentation/error-messages.txt
+++ b/Documentation/error-messages.txt
@@ -13,25 +13,26 @@
 * link:error-change-closed.html[change ... closed]
 * link:error-change-does-not-belong-to-project.html[change ... does not belong to project ...]
 * link:error-change-not-found.html[change ... not found]
+* link:error-commit-already-exists.html[commit already exists]
 * link:error-contains-banned-commit.html[contains banned commit ...]
 * link:error-has-duplicates.html[... has duplicates]
 * link:error-invalid-author.html[invalid author]
-* link:error-invalid-changeid-line.html[invalid Change-Id line format in commit message]
+* link:error-invalid-changeid-line.html[invalid Change-Id line format in commit message footer]
 * link:error-invalid-committer.html[invalid committer]
-* link:error-missing-changeid.html[missing Change-Id in commit message]
-* link:error-multiple-changeid-lines.html[multiple Change-Id lines in commit message]
+* link:error-missing-changeid.html[missing Change-Id in commit message footer]
+* link:error-multiple-changeid-lines.html[multiple Change-Id lines in commit message footer]
 * link:error-no-changes-made.html[no changes made]
 * link:error-no-common-ancestry.html[no common ancestry]
 * link:error-no-new-changes.html[no new changes]
 * link:error-non-fast-forward.html[non-fast forward]
 * link:error-not-a-gerrit-administrator.html[Not a Gerrit administrator]
-* link:error-not-a-gerrit-project.html[not a Gerrit project]
 * link:error-not-permitted-to-create.html[Not permitted to create ...]
-* link:error-not-signed-off-by.html[not Signed-off-by author/committer/uploader]
+* link:error-not-signed-off-by.html[not Signed-off-by author/committer/uploader in commit message footer]
 * link:error-not-valid-ref.html[not valid ref]
 * link:error-change-upload-blocked.html[One or more refs/for/ names blocks change upload]
 * link:error-permission-denied.html[Permission denied (publickey)]
 * link:error-prohibited-by-gerrit.html[prohibited by Gerrit]
+* link:error-project-not-found.html[Project not found: ...]
 * link:error-squash-commits-first.html[squash commits first]
 * link:error-upload-denied.html[Upload denied for project \'...']
 * link:error-not-allowed-to-upload-merges.html[you are not allowed to upload merges]
diff --git a/Documentation/error-missing-changeid.txt b/Documentation/error-missing-changeid.txt
index daa1d46..b13f3b4 100644
--- a/Documentation/error-missing-changeid.txt
+++ b/Documentation/error-missing-changeid.txt
@@ -1,10 +1,10 @@
-missing Change-Id in commit message
-===================================
+missing Change-Id in commit message footer
+==========================================
 
 With this error message Gerrit rejects to push a commit to a project
 which is configured to always require a Change-Id in the commit
 message if the commit message of the pushed commit does not contain
-a Change-Id.
+a Change-Id in the footer (the last paragraph).
 
 This error may happen for two reasons:
 
diff --git a/Documentation/error-multiple-changeid-lines.txt b/Documentation/error-multiple-changeid-lines.txt
index e604974..9fa2b91 100644
--- a/Documentation/error-multiple-changeid-lines.txt
+++ b/Documentation/error-multiple-changeid-lines.txt
@@ -1,8 +1,8 @@
-multiple Change-Id lines in commit message
-==========================================
+multiple Change-Id lines in commit message footer
+=================================================
 
 With this error message Gerrit rejects to push a commit if the commit
-message of the pushed commit contains several Change-Id lines.
+message footer of the pushed commit contains several Change-Id lines.
 
 You can see the commit messages for existing commits in the history
 by doing a link:http://www.kernel.org/pub/software/scm/git/docs/git-log.html[git log].
diff --git a/Documentation/error-not-a-gerrit-project.txt b/Documentation/error-not-a-gerrit-project.txt
deleted file mode 100644
index dac98ae..0000000
--- a/Documentation/error-not-a-gerrit-project.txt
+++ /dev/null
@@ -1,34 +0,0 @@
-not a Gerrit project
-====================
-
-With this error message Gerrit rejects to push a commit if the git
-repository to which the push is done does not exist as a project in
-the Gerrit server or if the pushing user has no read access for this
-project.
-
-The name of the project in Gerrit has the same name as the path of
-its git repository (excluding the '.git' extension).
-
-If you are facing this problem, do the following:
-
-. Verify that the project name specified as git repository in the
-  push command is typed correctly (case sensitive).
-. 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
-  link:access-control.html#category_read['Read'] access for it. This
-  means if you are certain that the project name is right you should
-  contact the Gerrit Administrator or project owner to request access
-  to the project.
-
-This error message might be misleading if the project actually exists
-but the push is failing because the pushing user has no read access
-for the project. The reason that Gerrit in this case denies the
-existence of the project is to prevent users from probing the Gerrit
-server to see if a particular project exists.
-
-
-GERRIT
-------
-Part of link:error-messages.html[Gerrit Error Messages]
diff --git a/Documentation/error-not-signed-off-by.txt b/Documentation/error-not-signed-off-by.txt
index 00f179b..bd1f40d 100644
--- a/Documentation/error-not-signed-off-by.txt
+++ b/Documentation/error-not-signed-off-by.txt
@@ -1,10 +1,10 @@
-not Signed-off-by author/committer/uploader
-===========================================
+not Signed-off-by author/committer/uploader in commit message footer
+====================================================================
 
 Projects in Gerrit can be configured to require a link:user-signedoffby.html#Signed-off-by[Signed-off-by] in
-the commit message to enforce that every change is signed by the
+the footer of the commit message to enforce that every change is signed by the
 author, committer or uploader. If for a project a Signed-off-by is
-required and the commit message does not contain it, Gerrit rejects
+required and the commit message footer does not contain it, Gerrit rejects
 to push the commit with this error message.
 
 This policy can be bypassed by having the access right
@@ -13,11 +13,11 @@
 This error may happen for different reasons if you do not have the
 access right to forge the committer identity:
 
-. missing Signed-off-by in the commit message
-. Signed-off-by is contained in the commit message but it's neither
+. missing Signed-off-by in the commit message footer
+. Signed-off-by is contained in the commit message footer but it's neither
   from the author, committer nor uploader
 . Signed-off-by from the author, committer or uploader is contained
-  in the commit message but not in the last paragraph
+  in the commit message but not in the footer (last paragraph)
 
 To be able to push your commits you have to update the commit
 messages as explained link:error-push-fails-due-to-commit-message.html[here] so that they contain a Signed-off-by from
diff --git a/Documentation/error-project-not-found.txt b/Documentation/error-project-not-found.txt
new file mode 100644
index 0000000..3fc0141
--- /dev/null
+++ b/Documentation/error-project-not-found.txt
@@ -0,0 +1,34 @@
+Project not found: ...
+======================
+
+With this error message Gerrit rejects to push a commit if the git
+repository to which the push is done does not exist as a project in
+the Gerrit server or if the pushing user has no read access for this
+project.
+
+The name of the project in Gerrit has the same name as the path of
+its git repository (excluding the '.git' extension).
+
+If you are facing this problem, do the following:
+
+. Verify that the project name specified as git repository in the
+  push command is typed correctly (case sensitive).
+. Verify that you are pushing to the correct Gerrit server.
+. Go in the Gerrit WebUI to 'Projects' > 'List' and check that the
+  project is listed. If the project is not listed the project either
+  does not exist or you don't have
+  link:access-control.html#category_read['Read'] access for it. This
+  means if you are 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
+for the project. The reason that Gerrit in this case denies the
+existence of the project is to prevent users from probing the Gerrit
+server to see if a particular project exists.
+
+
+GERRIT
+------
+Part of link:error-messages.html[Gerrit Error Messages]
diff --git a/Documentation/i18n-readme.txt b/Documentation/i18n-readme.txt
index a84c3dc..2135598 100644
--- a/Documentation/i18n-readme.txt
+++ b/Documentation/i18n-readme.txt
@@ -5,22 +5,12 @@
 the way the code produces output.  Most of the UI should support
 right-to-left (RTL) languages.
 
+Labels
+------
 
-ApprovalCategory
-----------------
-
-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
-languages and different users want different translations.
-
-ApprovalCategoryValue
----------------------
-
-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
-languages and different users want different translations.
+Labels and their values are defined in project.config by the Gerrit
+administrator or project owners.  Only a single translation of these
+strings is supported.
 
 /Gerrit Gerrit.html
 -------------------
diff --git a/Documentation/index.txt b/Documentation/index.txt
index 4c2335f..9b2d8eb 100644
--- a/Documentation/index.txt
+++ b/Documentation/index.txt
@@ -6,28 +6,36 @@
 
 * link:intro-quick.html[A Quick Introduction To Gerrit]
 
-User Guide
-----------
+End User Guide
+--------------
 
-* link:http://source.android.com/submit-patches/workflow[Default Workflow]
+* External link: link:http://source.android.com/submit-patches/workflow[Default Android Workflow]
 * link:user-search.html[Searching Changes]
 * link:cmd-index.html[Command Line Tools]
-* link:pgm-index.html[Server Programs]
 * link:user-upload.html[Uploading Changes]
 * link:user-changeid.html[Change-Id Lines]
 * link:user-signedoffby.html[Signed-off-by Lines]
-* link:access-control.html[Access Controls]
 * link:error-messages.html[Error Messages]
-* link:rest-api.html[REST API]
-* link:user-custom-dashboards.html[Custom Dashboards]
 * link:user-notify.html[Subscribing to Email Notifications]
+
+Project Owner and Power User Guide
+----------------------------------
+
+* link:access-control.html[Access Controls]
+* link:rest-api.html[REST API]
+* link:user-dashboards.html[Dashboards]
 * link:user-submodules.html[Subscribing to Git Submodules]
-* link:refs-notes-review.html[The `refs/notes/review` namespace]
 * link:prolog-cookbook.html[Prolog Cookbook]
 * link:prolog-change-facts.html[Prolog Facts for Gerrit Changes]
+* link:config-labels.html[Review Labels]
+
+Admin User Guide
+----------------
+
+* link:pgm-index.html[Server Side Administrative Tools]
 
 Installation
-------------
+~~~~~~~~~~~~
 
 * link:licenses.html[Licenses and Notices]
 * link:install.html[Installation Guide]
@@ -35,7 +43,7 @@
 * link:project-setup.html[Project Setup]
 
 Configuration
--------------
+~~~~~~~~~~~~~
 
 * link:config-gerrit.html[System Settings]
 * link:config-contact.html[User Contact Information]
@@ -45,14 +53,16 @@
 * link:config-reverseproxy.html[Reverse Proxy]
 * link:config-hooks.html[Hooks]
 * link:config-mail.html[Mail Templates]
+* link:config-cla.html[Contributor Agreements]
 
-Developer Documentation
------------------------
+Gerrit Developer Documentation
+------------------------------
 
 * link:dev-readme.html[Developer Setup]
 * link:dev-eclipse.html[Eclipse Setup]
 * link:dev-contributing.html[Contributing to Gerrit]
 * link:dev-plugins.html[Developing Plugins]
+* link:config-validation.html[Commit Validation]
 * link:dev-design.html[System Design]
 * link:i18n-readme.html[i18n Support]
 * link:dev-release.html[Developer Release]
diff --git a/Documentation/install-j2ee.txt b/Documentation/install-j2ee.txt
index 96814a0..4927041 100644
--- a/Documentation/install-j2ee.txt
+++ b/Documentation/install-j2ee.txt
@@ -27,12 +27,6 @@
   review_site/bin/gerrit.sh stop
 ----
 
-* Deploy the 'gerrit.war' file to your application server.
-+
-The deployment process differs between servers, but typically this
-can be accomplished by copying 'gerrit.war' into the 'webapps/'
-subdirectory of the container's installation.
-
 * Configure JNDI DataSource 'jdbc/ReviewDb'.
 +
 This DataSource must point to the database you created above.
@@ -40,6 +34,12 @@
 necessary JDBC drivers.  You may wish to ensure connection pooling
 is configured and enabled within the DataSource.
 
+* Deploy the 'gerrit.war' file to your application server.
++
+The deployment process differs between servers, but typically this
+can be accomplished by copying 'gerrit.war' into the 'webapps/'
+subdirectory of the container's installation.
+
 * ('Optional') Install Bouncy Castle Crypto API
 +
 If you enabled Bouncy Castle Crypto during 'init', copy the JAR
@@ -101,8 +101,8 @@
 script and modify it for your configuration:
 
 ----
-  java -jar webapps/gerrit.war --cat extra/jetty7/gerrit-jetty.sh >/etc/init.d/gerrit-jetty.sh
-  vi /etc/init.d/gerrit-jetty.sh
+  java -jar webapps/gerrit.war --cat extra/jetty7/gerrit-jetty.sh >/etc/init.d/gerrit-jetty
+  vi /etc/init.d/gerrit-jetty
 ----
 
 [TIP]
diff --git a/Documentation/install-quick.txt b/Documentation/install-quick.txt
index c09c197..f1bd25c 100644
--- a/Documentation/install-quick.txt
+++ b/Documentation/install-quick.txt
@@ -109,137 +109,7 @@
   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 of your Gerrit server.  You can
-find the url in the settings file.
-
-----
-  gerrit2@host:~$ git config -f ~/gerrit_testsite/etc/gerrit.config gerrit.canonicalWebUrl
-  http://localhost:8080/
-  gerrit2@host:~$
-----
-
-Register a new account in Gerrit through the web interface with the
-email address of your choice.
-
-The default authentication type is OpenID.  If your Gerrit server is behind a
-proxy, and you are using an external OpenID provider, you will need to add the
-proxy settings in the configuration file.
-
-----
-  gerrit2@host:~$ git config -f ~/gerrit_testsite/etc/gerrit.config --add http.proxy http://proxy:8080
-  gerrit2@host:~$ git config -f ~/gerrit_testsite/etc/gerrit.config --add http.proxyUsername username
-  gerrit2@host:~$ git config -f ~/gerrit_testsite/etc/gerrit.config --add http.proxyPassword password
-----
-
-Refer to the Gerrit configuration guide for more detailed information about
-link:config-gerrit.html#auth[authentication] and
-link:config-gerrit.html#http.proxy[proxy] settings.
-
-The first user to sign-in and register an account will be
-automatically placed into the fully privileged Administrators group,
-permitting server management over the web and over SSH.  Subsequent
-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:~$
-----
+include::config-login-register.txt[]
 
 Project creation
 ----------------
diff --git a/Documentation/install.txt b/Documentation/install.txt
index 9926b8f..4b14ff0 100644
--- a/Documentation/install.txt
+++ b/Documentation/install.txt
@@ -1,5 +1,5 @@
-Gerrit Code Review - Installation Guide
-=======================================
+Gerrit Code Review - Standalone Daemon Installation Guide
+=========================================================
 
 [[requirements]]
 Requirements
@@ -29,67 +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
---------------
-
-[[createdb_h2]]
-H2
-~~
-
-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:
-
-----
-  createuser -A -D -P -E gerrit2
-  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:
-
-----
-  mysql
-
-  CREATE USER 'gerrit2'@'localhost' IDENTIFIED BY 'secret';
-  CREATE DATABASE reviewdb;
-  ALTER DATABASE reviewdb charset=latin1;
-  GRANT ALL ON reviewdb.* TO 'gerrit2'@'localhost';
-  FLUSH PRIVILEGES;
-----
-
+include::database-setup.txt[]
 
 [[init]]
 Initialize the Site
@@ -185,8 +125,8 @@
 automatically starts and stops with the operating system:
 
 ====
-  sudo ln -snf `pwd`/review_site/bin/gerrit.sh /etc/init.d/gerrit.sh
-  sudo ln -snf ../init.d/gerrit.sh /etc/rc3.d/S90gerrit
+  sudo ln -snf `pwd`/review_site/bin/gerrit.sh /etc/init.d/gerrit
+  sudo ln -snf /etc/init.d/gerrit /etc/rc3.d/S90gerrit
 ====
 
 To install Gerrit into an existing servlet container instead of using
diff --git a/Documentation/intro-quick.txt b/Documentation/intro-quick.txt
index 25f5d5e..ae2e7e7 100644
--- a/Documentation/intro-quick.txt
+++ b/Documentation/intro-quick.txt
@@ -138,10 +138,10 @@
 Compressing objects: 100% (2/2), done.
 Writing objects: 100% (3/3), 542 bytes, done.
 Total 3 (delta 0), reused 0 (delta 0)
-remote: 
+remote:
 remote: New Changes:
 remote:   http://gerrithost:8080/68
-remote: 
+remote:
 To ssh://gerrithost:29418/RecipeBook.git
  * [new branch]      HEAD -> refs/for/master
 ----
@@ -209,10 +209,12 @@
 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.
+these views you can add inline comments by double clicking on the line
+(or single click the line number) that you want to comment on. Also you
+can add file comment by double clicking anywhere (not just on the
+"Patch Set" words) in the table header or single clicking on the icon
+in the line-number column header. 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]
@@ -375,7 +377,7 @@
 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
+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.
 
diff --git a/Documentation/json.txt b/Documentation/json.txt
index f0588ce..3893e3f 100644
--- a/Documentation/json.txt
+++ b/Documentation/json.txt
@@ -28,7 +28,11 @@
 
 url:: Canonical URL to reach this change.
 
-commitMessage:: The full commit message for the change.
+commitMessage:: The full commit message for the change's current patch
+set.
+
+createdOn:: Time in seconds since the UNIX epoch when this change
+was created.
 
 lastUpdated:: Time in seconds since the UNIX epoch when this change
 was last updated.
@@ -41,6 +45,8 @@
 
   NEW;; Change is still being reviewed.
 
+  DRAFT;; Change is a draft change that only consists of draft patchsets.
+
   SUBMITTED;; Change has been submitted and is in the merge queue.
   It may be waiting for one or more dependencies.
 
@@ -48,14 +54,20 @@
 
   ABANDONED;; Change was abandoned by its owner or administrator.
 
+comments:: All inline/file comments for this change in <<message,message attributes>>.
+
 trackingIds:: Issue tracking system links in
-<<trackingid,trackingid attribute>>, scraped out of the commit
+<<trackingid,trackingid attributes>>, scraped out of the commit
 message based on the server's
 link:config-gerrit.html#trackingid[trackingid] sections.
 
 currentPatchSet:: Current <<patchSet,patchSet attribute>>.
 
-patchSets:: All <<patchSet,patchSet attribute>> for this change.
+patchSets:: All <<patchSet,patchSet attributes>> for this change.
+
+dependsOn:: List of changes that this change depends on in <<dependency,dependency attributes>>.
+
+neededBy:: List of changes that depend on this change in <<dependency,dependency attributes>>.
 
 submitRecords:: The <<submitRecord,submitRecord attribute>> contains
 information about whether this change has been or can be submitted.
@@ -90,14 +102,29 @@
 
 revision:: Git commit for this patchset.
 
+parents:: List of parent revisions.
+
 ref:: Git reference pointing at the revision.  This reference is
 available through the Gerrit Code Review server's Git interface
 for the containing change.
 
 uploader:: Uploader of the patch set in <<account,account attribute>>.
 
+author:: Author of this patchset in <<account,account attribute>>.
+
+createdOn:: Time in seconds since the UNIX epoch when this patchset
+was created.
+
 approvals:: The <<approval,approval attribute>> granted.
 
+comments:: All comments for this patchset in <<patchsetcomment,patchsetComment attributes>>.
+
+files:: All changed files in this patchset in <<patch,patch attributes>>.
+
+sizeInsertions:: Size information of insertions of this patchset.
+
+sizeDeletions:: Size information of deletions of this patchset.
+
 [[approval]]
 approval
 --------
@@ -123,19 +150,9 @@
 
 newRev:: The new value the ref was updated to.
 
-project:: Project path in Gerrit.
-
 refName:: Ref name within project.
 
-[[queryLimit]]
-queryLimit
-----------
-Information about the link:access-control.html#capability_queryLimit[queryLimit]
-of a user.
-
-min:: lower limit
-
-max:: upper limit
+project:: Project path in Gerrit.
 
 [[submitRecord]]
 submitRecord
@@ -178,6 +195,73 @@
 
 by:: The <<account,account>> that applied the label.
 
+[[dependency]]
+dependency
+----------
+Information about a change or patchset dependency.
+
+id:: Change identifier.
+
+number:: Change number.
+
+revision:: Patchset revision.
+
+ref:: Ref name.
+
+isCurrentPatchSet:: If the revision is the current patchset of the change.
+
+[[message]]
+message
+-------
+Comment added on a change by a reviewer.
+
+timestamp:: Time in seconds since the UNIX epoch when this comment
+was added.
+
+reviewer:: The <<account,account>> that added the comment.
+
+message:: The comment text.
+
+[[patchsetcomment]]
+patchsetComment
+---------------
+Comment added on a patchset by a reviewer.
+
+file:: The name of the file on which the comment was added.
+
+line:: The line number at which the comment was added.
+
+reviewer:: The <<account,account>> that added the comment.
+
+message:: The comment text.
+
+[[patch]]
+patch
+-----
+Information about a patch on a file.
+
+file:: The name of the file.  If the file is renamed, the new name.
+
+fileOld:: The old name of the file, if the file is renamed.
+
+type:: The type of change.
+
+  ADDED;; The file is being created/introduced by this patch.
+
+  MODIFIED;; The file already exists, and has updated content.
+
+  DELETED;; The file existed, but is being removed by this patch.
+
+  RENAMED;; The file is renamed.
+
+  COPIED;; The file is copied from another file.
+
+  REWRITE;; Sufficient amount of content changed to claim the file was rewritten.
+
+insertions:: number of insertions of this patch.
+
+deletions::  number of deletions of this patch.
+
 SEE ALSO
 --------
 
diff --git a/Documentation/licenses.txt b/Documentation/licenses.txt
index 7787fe8..69a12c3 100644
--- a/Documentation/licenses.txt
+++ b/Documentation/licenses.txt
@@ -38,6 +38,7 @@
 |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>>
+|JavaEWAH                   | <<apache2,Apache License 2.0>>
 |JGit                       | <<jgit,New-Style BSD>>
 |JSch                       | <<sshd,New-Style BSD>>
 |PostgreSQL JDBC Driver     | <<postgresql,New-Style BSD>>
@@ -407,7 +408,8 @@
 
 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.
+the 1.2.5 release, and offers the corresponding source code at
+link:https://gerrit.googlesource.com/prolog-cafe[].
 
 Prolog Cafe is dual licensed and available under either the
 link:http://opensource.org/licenses/eclipse-1.0.php[Eclipse Public License],
@@ -415,8 +417,7 @@
 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.
+under the EPL.
 
 [[h2]]
 H2 Database - EPL or modified MPL
@@ -643,10 +644,10 @@
  distribute,  sublicense, and/or sell  copies of  the Software,  and to
  permit persons to whom the Software  is furnished to do so, subject to
  the following conditions:
- 
+
  The  above  copyright  notice  and  this permission  notice  shall  be
  included in all copies or substantial portions of the Software.
- 
+
  THE  SOFTWARE IS  PROVIDED  "AS  IS", WITHOUT  WARRANTY  OF ANY  KIND,
  EXPRESS OR  IMPLIED, INCLUDING  BUT NOT LIMITED  TO THE  WARRANTIES OF
  MERCHANTABILITY,    FITNESS    FOR    A   PARTICULAR    PURPOSE    AND
@@ -664,9 +665,9 @@
 
 ----
 (The MIT License)
- 
+
 Copyright (c) 2008 Tom Preston-Werner
- 
+
 Permission is hereby granted, free of charge, to any person obtaining
 a copy of this software and associated documentation files (the
 'Software'), to deal in the Software without restriction, including
diff --git a/Documentation/man/.gitignore b/Documentation/man/.gitignore
new file mode 100644
index 0000000..7993a55
--- /dev/null
+++ b/Documentation/man/.gitignore
@@ -0,0 +1 @@
+gerrit*
diff --git a/Documentation/man/Makefile b/Documentation/man/Makefile
new file mode 100644
index 0000000..75e6533
--- /dev/null
+++ b/Documentation/man/Makefile
@@ -0,0 +1,63 @@
+# Copyright (C) 2013 The Android Open Source Project
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+A2X ?= a2x
+
+all: man
+
+clean:
+	@rm -f gerrit-*
+
+CMD_CORE_SSH_CMD := \
+cmd-ban-commit.txt     \
+cmd-create-account.txt \
+cmd-create-group.txt   \
+cmd-create-project.txt \
+cmd-flush-caches.txt   \
+cmd-gc.txt             \
+cmd-gsql.txt           \
+cmd-ls-groups.txt      \
+cmd-ls-projects.txt    \
+cmd-ls-user-refs.txt   \
+cmd-query.txt          \
+cmd-rename-group.txt   \
+cmd-review.txt         \
+cmd-set-account.txt    \
+cmd-set-project-parent.txt \
+cmd-set-project.txt    \
+cmd-set-reviewers.txt  \
+cmd-show-caches.txt    \
+cmd-show-connections.txt   \
+cmd-show-queue.txt     \
+cmd-stream-events.txt  \
+cmd-test-submit-rule.txt   \
+cmd-version.txt
+
+GERRIT_CORE_SSH_CMD := $(patsubst cmd-%,gerrit-%,$(CMD_CORE_SSH_CMD))
+DOC_MAN := $(patsubst %.txt,%.1,$(GERRIT_CORE_SSH_CMD))
+
+man: $(GERRIT_CORE_SSH_CMD) $(DOC_MAN)
+
+$(GERRIT_CORE_SSH_CMD) : gerrit-%.txt : ../cmd-%.txt
+	@cp $< $@
+
+$(DOC_MAN) : %.1 : %.txt
+	@echo "creating man page for $@ ..."
+	@rm -f $@
+	@$(eval TITLE := $(join $(basename $<),\(1\)))
+	@$(eval SEPERATOR := $(shell echo $(TITLE) | sed 's/./=/g'))
+	@sed -i -re '1s/^.*$//$(TITLE)/' $<
+	@sed -i -re '2s/^=.*/$(SEPERATOR)/' $<
+	@sed -i -re '6s/^gerrit\s+(\w)/gerrit-\1/' $<
+	@$(A2X) --doctype manpage --format manpage $<
diff --git a/Documentation/pgm-ExportReviewNotes.txt b/Documentation/pgm-ExportReviewNotes.txt
deleted file mode 100644
index 1b00213..0000000
--- a/Documentation/pgm-ExportReviewNotes.txt
+++ /dev/null
@@ -1,50 +0,0 @@
-ExportReviewNotes
-=================
-
-NAME
-----
-ExportReviewNotes - Export successful reviews to link:refs-notes-review.html[refs/notes/review]
-
-SYNOPSIS
---------
-[verse]
-'java' -jar gerrit.war 'ExportReviewNotes' -d <SITE_PATH>
-
-DESCRIPTION
------------
-Scans every submitted change and creates an initial notes
-branch detailing the previous submission information for
-each merged change.
-
-This task can take quite some time, but can run in the background
-concurrently to the server if the database is MySQL or PostgreSQL.
-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.  Default: 2.
-
-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.
-
-EXAMPLES
---------
-To generate all review information:
-
-====
-	$ java -jar gerrit.war ExportReviewNotes -d site_path --threads 16
-====
-
-GERRIT
-------
-Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/pgm-daemon.txt b/Documentation/pgm-daemon.txt
index 1c1d343..3ffcb40 100644
--- a/Documentation/pgm-daemon.txt
+++ b/Documentation/pgm-daemon.txt
@@ -14,6 +14,7 @@
 	[\--enable-sshd | \--disable-sshd]
 	[\--console-log]
 	[\--slave]
+	[\--headless]
 
 DESCRIPTION
 -----------
@@ -58,6 +59,10 @@
 	Send log messages to the console, instead of to the standard
 	log file '$site_path/logs/error_log'.
 
+\--headless::
+	Don't start the default Gerrit UI. May be useful when Gerrit is
+	run with an alternative UI.
+
 CONTEXT
 -------
 This command can only be run on a server which has direct
diff --git a/Documentation/pgm-gsql.txt b/Documentation/pgm-gsql.txt
index c3a492a..37fbb74 100644
--- a/Documentation/pgm-gsql.txt
+++ b/Documentation/pgm-gsql.txt
@@ -45,7 +45,7 @@
 
 	Type '\h' for help.  Type '\r' to clear the buffer.
 
-	gerrit> update accounts set ssh_user_name = 'alice' where account_id=1;       
+	gerrit> update accounts set ssh_user_name = 'alice' where account_id=1;
 	UPDATE 1; 1 ms
 	gerrit> \q
 	Bye
diff --git a/Documentation/pgm-index.txt b/Documentation/pgm-index.txt
index 5cbe6ba0..7a1edcf 100644
--- a/Documentation/pgm-index.txt
+++ b/Documentation/pgm-index.txt
@@ -1,13 +1,15 @@
-Gerrit Code Review - Server Programs
-====================================
+Gerrit Code Review - Server Side Administrative Tools
+=====================================================
 
-Server side programs can be started by executing the WAR file
+Server side tools can be started by executing the WAR file
 through the Java command line.  For example:
 
-  $ java -jar gerrit.war program [options]
+  $ java -jar gerrit.war <tool> [<options>]
 
-[[programs]]Programs
---------------------
+Tool should be one of the following names:
+
+Tools
+-----
 
 link:pgm-init.html[init]::
 	Initialize a new Gerrit server installation.
@@ -28,10 +30,7 @@
 	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.
diff --git a/Documentation/project-setup.txt b/Documentation/project-setup.txt
index 3d979d3..36d3c60 100644
--- a/Documentation/project-setup.txt
+++ b/Documentation/project-setup.txt
@@ -54,8 +54,8 @@
 --------------------
 
 The method Gerrit uses to submit a change to a project can be
-modified by any project owner through the project console, `Admin` >
-`Projects`.  The following methods are supported:
+modified by any project owner through the project console, `Projects` >
+`List` > my/project.  The following methods are supported:
 
 * Fast Forward Only
 +
@@ -99,10 +99,20 @@
 the right order since inter-change dependencies will not be
 enforced for them.
 
+[[rebase_if_necessary]]
+* Rebase If Necessary
++
+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 the change is automatically rebased and then the branch is
+fast-forwarded to the change.
+
 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.
+succeed if there is no path conflict.  A path conflict occurs when
+the same file has also been changed on the other side of the merge.
+
+If `Automatically resolve conflicts` is enabled, Gerrit will try
+to do a content merge when a path conflict occurs.
 
 
 Registering Additional Branches
@@ -113,8 +123,8 @@
 
 Additional branches can also be created through the web UI, assuming
 at least one commit already exists in the project repository.
-A project owner can create additional branches under `Admin` >
-`Projects` > `Branches`.  Enter the new branch name, and the
+A project owner can create additional branches under `Projects` >
+`List` > my/project > `Branches`.  Enter the new branch name, and the
 starting Git revision.  Branch names that don't start with `refs/`
 will automatically have `refs/heads/` prefixed to ensure they are
 a standard Git branch name.  Almost any valid SHA-1 expression can
diff --git a/Documentation/prolog-change-facts.txt b/Documentation/prolog-change-facts.txt
index d5f6174..e21da02 100644
--- a/Documentation/prolog-change-facts.txt
+++ b/Documentation/prolog-change-facts.txt
@@ -48,6 +48,9 @@
 |`commit_message/1`   |`commit_message('Fix bug X').`
     |Commit message as string atom
 
+|`commit_stats/3`   |`commit_stats(5,20,50).`
+    |Number of files modified, number of insertions and the number of deletions.
+
 .4+|`current_user/1`  |`current_user(user(1000000)).`
     .4+|Current user as one of the four given possibilities
 
diff --git a/Documentation/prolog-cookbook.txt b/Documentation/prolog-cookbook.txt
index e660067..7912cf2 100644
--- a/Documentation/prolog-cookbook.txt
+++ b/Documentation/prolog-cookbook.txt
@@ -28,6 +28,27 @@
 link:http://gerrit-documentation.googlecode.com/svn/ReleaseNotes/ReleaseNotes-2.2.2.html[Gerrit
 2.2.2 ReleaseNotes] introduces Prolog support in Gerrit.
 
+Submit Type
+-----------
+A 'Submit Type' is a strategy that is used on submit to integrate the
+change into the destination branch. Supported submit types are:
+
+* `Fast Forward Only`
+* `Merge If Necessary`
+* `Merge Always`
+* `Cherry Pick`
+* `Rebase If Necessary`
+
+'Submit Type' is a project global setting. This means that the same submit type
+is used for all changes of one project.
+
+Projects which need more flexibility in choosing, or enforcing, a submit type
+can use Prolog based submit type which replaces the project's default submit
+type.
+
+Prolog based submit type computes a submit type for each change. The computed
+submit type is shown on the change screen for each change.
+
 Prolog Language
 ---------------
 This document is not a complete Prolog tutorial.
@@ -76,6 +97,7 @@
   $ git push origin HEAD:refs/meta/config
 ====
 
+[[HowToWriteSubmitRules]]
 How to write submit rules
 -------------------------
 Whenever Gerrit needs to evaluate submit rules for a change `C` from project `P` it
@@ -171,13 +193,14 @@
 Another aspect of the return result from the `submit_rule` predicate is that
 Gerrit uses it to decide which set of labels to display on the change review
 screen for voting. If the return result contains label `'ABC'` and if the label
-`'ABC'` is one of the (global) voting categories then voting for the label
-`'ABC'` will be displayed. Otherwise, it is not displayed. Note that we don't
-need a (global) voting category for each label contained in the result of
+`'ABC'` is link:config-labels.html[defined for the project] then voting for the
+label `'ABC'` will be displayed. Otherwise, it is not displayed. Note that the
+project doesn't need a defined label for each label contained in the result of
 `submit_rule` predicate.  For example, the decision whether `'Author-is-John-Doe'`
 label is met will probably not be made by explicit voting but, instead, by
 inspecting the facts about the change.
 
+[[SubmitFilter]]
 Submit Filter
 -------------
 Another mechanism of changing the default submit rules is to implement the
@@ -215,7 +238,6 @@
 
 The following "drawing" illustrates the order of the invocation and the chaining
 of the results of the `submit_rule` and `submit_filter` predicates.
-
 ====
   All-Projects
   ^   submit_filter(B, S) :- ...  <4>
@@ -247,6 +269,34 @@
 default implementation of submit rule that is named `gerrit:default_submit` and
 its result will be filtered as described above.
 
+[[HowToWriteSubmitType]]
+How to write submit type
+------------------------
+Writing custom submit type logic in Prolog is the similar top
+xref:HowToWriteSubmitRules[writing submit rules]. The only difference is that
+one has to implement a `submit_type` predicate (instead of the `submit_rule`)
+and that the return result of the `submit_type` has to be an atom that
+represents one of the supported submit types:
+
+* `fast_forward_only`
+* `merge_if_necessary`
+* `merge_always`
+* `cherry_pick`
+* `rebase_if_necessary`
+
+Submit Type Filter
+------------------
+Submit type filter works the same way as the xref:SubmitFilter[Submit Filter]
+where the name of the filter predicate is `submit_type_filter`.
+
+====
+  submit_type_filter(In, Out).
+====
+
+Gerrit will invoke `submit_type_filter` with the `In` parameter containing a
+result of the `submit_type` and will take the value of the `Out` parameter as
+the result.
+
 [[TestingSubmitRules]]
 Testing submit rules
 --------------------
@@ -275,8 +325,11 @@
 the change exposed to them. Gerrit plugins get full access to Gerrit internals
 and can potentially check more things than Prolog based rules.
 
-Examples
---------
+From version 2.6 Gerrit plugins can contribute Prolog predicates. This way, we
+can make use of the plugin provided predicates when writing Prolog based rules.
+
+Examples - Submit Rule
+----------------------
 The following examples should serve as a cookbook for developing own submit rules.
 Some of them are too trivial to be used in production and their only purpose is
 to provide step by step introduction and understanding.
@@ -295,7 +348,8 @@
 .rules.pl
 [caption=""]
 ====
-  submit_rule(submit(label('Any-Label-Name', ok(_)))).
+  submit_rule(submit(W)) :-
+    W = label('Any-Label-Name', ok(_)).
 ====
 
 In this case we make no use of facts about the change. We don't need it as we are simply
@@ -313,7 +367,9 @@
 .rules.pl
 [caption=""]
 ====
-  submit_rule(submit(label('Code-Review', ok(_)), label('Verified', ok(_)))).
+  submit_rule(submit(CR, V)) :-
+    CR = label('Code-Review', ok(_)),
+    V = label('Verified', ok(_)).
 ====
 
 Since for every change all label statuses are `'ok'` every change will be submittable.
@@ -328,7 +384,8 @@
 .rules.pl
 [caption=""]
 ====
-  submit_rule(submit(label('Any-Label-Name', reject(_)))).
+  submit_rule(submit(R)) :-
+    R = label('Any-Label-Name', reject(_)).
 ====
 
 Since for any change we return only one label with status `reject`, no change
@@ -344,13 +401,17 @@
 [caption=""]
 ====
   % In the UI this will show: Need Any-Label-Name
-  submit_rule(submit(label('Any-Label-Name', need(_)))).
+  submit_rule(submit(N)) :-
+    N = label('Any-Label-Name', need(_)).
 
   % We could define more "need" labels by adding more rules
-  submit_rule(submit(label('Another-Label-Name', need(_)))).
+  submit_rule(submit(N)) :-
+    N = label('Another-Label-Name', need(_)).
 
   % or by providing more than one need label in the same rule
-  submit_rule(submit(label('X-Label-Name', need(_)), label('Y-Label-Name', need(_)))).
+  submit_rule(submit(NX, NY)) :-
+    NX = label('X-Label-Name', need(_)),
+    NY = label('Y-Label-Name', need(_)).
 ====
 
 In the UI this will show:
@@ -377,8 +438,11 @@
 .rules.pl
 [caption=""]
 ====
-  submit_rule(label('Some-Condition', need(_))).
-  submit_rule(label('Another-Condition', ok(_))).
+  submit_rule(submit(N)) :-
+    N = label('Some-Condition', need(_)).
+
+  submit_rule(submit(OK)) :-
+    OK = label('Another-Condition', ok(_)).
 ====
 
 The 'Need Some-Condition' will not be show in the UI because of the result of
@@ -389,8 +453,11 @@
 .rules.pl
 [caption=""]
 ====
-  submit_rule(label('Another-Condition', ok(_))).
-  submit_rule(label('Some-Condition', need(_))).
+  submit_rule(submit(OK)) :-
+    OK = label('Another-Condition', ok(_)).
+
+  submit_rule(submit(N)) :-
+    N = label('Some-Condition', need(_)).
 ====
 
 The result of the first rule will stop search for any further solutions.
@@ -407,7 +474,8 @@
 .rules.pl
 [caption=""]
 ====
-  submit_rule(submit(label('Author-is-John-Doe', need(_)))).
+  submit_rule(submit(Author)) :-
+    Author = label('Author-is-John-Doe', need(_)).
 ====
 
 This will show:
@@ -420,9 +488,12 @@
 .rules.pl
 [caption=""]
 ====
-  submit_rule(submit(label('Author-is-John-Doe', need(_)))).
-  submit_rule(submit(label('Author-is-John-Doe', ok(_))))
-    :- gerrit:commit_author(_, 'John Doe', _).
+  submit_rule(submit(Author)) :-
+    Author = label('Author-is-John-Doe', need(_)).
+
+  submit_rule(submit(Author)) :-
+    gerrit:commit_author(_, 'John Doe', _),
+    Author = label('Author-is-John-Doe', ok(_)).
 ====
 
 In the second rule we return `ok` status for the `'Author-is-John-Doe'` label
@@ -438,9 +509,12 @@
 .rules.pl
 [caption=""]
 ====
-  submit_rule(submit(label('Author-is-John-Doe', need(_)))).
-  submit_rule(submit(label('Author-is-John-Doe', ok(_))))
-    :- gerrit:commit_author(_, _, 'john.doe@example.com').
+  submit_rule(submit(Author)) :-
+    Author = label('Author-is-John-Doe', need(_)).
+
+  submit_rule(submit(Author)) :-
+    gerrit:commit_author(_, _, 'john.doe@example.com'),
+    Author = label('Author-is-John-Doe', ok(_)).
 ====
 
 or by user id (assuming it is 1000000):
@@ -448,9 +522,12 @@
 .rules.pl
 [caption=""]
 ====
-  submit_rule(submit(label('Author-is-John-Doe', need(_)))).
-  submit_rule(submit(label('Author-is-John-Doe', ok(_))))
-    :- gerrit:commit_author(user(1000000), _, _).
+  submit_rule(submit(Author)) :-
+    Author = label('Author-is-John-Doe', need(_)).
+
+  submit_rule(submit(Author)) :-
+    gerrit:commit_author(user(1000000), _, _),
+    Author = label('Author-is-John-Doe', ok(_)).
 ====
 
 or by a combination of these 3 attributes:
@@ -458,13 +535,16 @@
 .rules.pl
 [caption=""]
 ====
-  submit_rule(submit(label('Author-is-John-Doe', need(_)))).
-  submit_rule(submit(label('Author-is-John-Doe', ok(_))))
-    :- gerrit:commit_author(_, 'John Doe', 'john.doe@example.com').
+  submit_rule(submit(Author)) :-
+    Author = label('Author-is-John-Doe', need(_)).
+
+  submit_rule(submit(Author)) :-
+    gerrit:commit_author(_, 'John Doe', 'john.doe@example.com'),
+    Author = label('Author-is-John-Doe', ok(_)).
 ====
 
-Example 7: Make change submittable if commit message starts with "Trivial fix"
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Example 7: Make change submittable if commit message starts with "Fix "
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 Besides showing how to make use of the commit message text the purpose of this
 example is also to show how to match only a part of a string symbol. Similarly
 like commit author the commit message is provided as a string symbol which is
@@ -482,9 +562,12 @@
 .rules.pl
 [caption=""]
 ====
-  submit_rule(submit(label('Commit-Message-starts-with-Trivial-Fix', need(_)))).
-  submit_rule(submit(label('Commit-Message-starts-with-Trivial-Fix', ok(_))))
-    :- gerrit:commit_message(M), name(M, L), starts_with(L, "Trivial Fix").
+  submit_rule(submit(Fix)) :-
+    Fix = label('Commit-Message-starts-with-Fix', need(_)).
+
+  submit_rule(submit(Fix)) :-
+    gerrit:commit_message(M), name(M, L), starts_with(L, "Fix "),
+    Fix = label('Commit-Message-starts-with-Fix', ok(_)).
 
   starts_with(L, []).
   starts_with([H|T1], [H|T2]) :- starts_with(T1, T2).
@@ -503,30 +586,74 @@
 .rules.pl
 [caption=""]
 ====
-  submit_rule(submit(label('Commit-Message-starts-with-Trivial-Fix', need(_)))).
-  submit_rule(submit(label('Commit-Message-starts-with-Trivial-Fix', ok(_))))
-    :- gerrit:commit_message_matches('^Trivial Fix').
+  submit_rule(submit(Fix)) :-
+    Fix = label('Commit-Message-starts-with-Fix', need(_)).
+
+  submit_rule(submit(Fix)) :-
+    gerrit:commit_message_matches('^Fix '),
+    Fix = label('Commit-Message-starts-with-Fix', ok(_)).
 ====
 
-Reusing the default submit policy
----------------------------------
+The previous example could also be written so that it first checks if the commit
+message starts with 'Fix '. If true then it sets OK for that category and stops
+further backtracking by using the cut `!` operator:
+.rules.pl
+[caption=""]
+====
+  submit_rule(submit(Fix)) :-
+    gerrit:commit_message_matches('^Fix '),
+    Fix = label('Commit-Message-starts-with-Fix', ok(_)),
+    !.
+
+  % Message does not start with 'Fix ' so Fix is needed to submit
+  submit_rule(submit(Fix)) :-
+    Fix = label('Commit-Message-starts-with-Fix', need(_)).
+====
+
+The default submit policy
+-------------------------
 All examples until now concentrate on one particular aspect of change data.
 However, in real-life scenarios we would rather want to reuse Gerrit's default
-submit policy and extend/change it for our specific purpose. In other words, we
-would like to keep all the default policies (like the `Verified` category,
-vetoing change, etc...) and only extend/change an aspect of it. For example, we
-may want to disable the ability for change authors to approve their own changes
-but keep all other policies the same.
+submit policy and extend/change it for our specific purpose.  This could be
+done in one of the following ways:
 
+* understand how the default submit policy is implemented and use that as a
+  template for implementing custom submit rules,
+* invoke the default submit rule implementation and then perform further
+  actions on its return result.
+
+Default submit rule implementation
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+The default submit rule with the two default categories, `Code-Review` and
+`Verified`, can be implemented as:
+
+.rules.pl
+[caption=""]
+====
+  submit_rule(submit(V, CR)) :-
+    gerrit:max_with_block(-2, 2, 'Code-Review', CR),
+    gerrit:max_with_block(-1, 1, 'Verified', V).
+====
+
+Once this implementation is understood it can be customized to implement
+project specific submit rules. Note, that this implementation hardcodes
+the two default categories. Introducing a new category in the database would
+require introducing the same category here or a `submit_filter` in a parent
+project would have to care about including the new category in the result of
+this `submit_rule`.  On the other side, this example is easy to read and
+understand.
+
+Reusing the default submit policy
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 To get results of Gerrits default submit policy we use the
-`gerrit:default_submit` predicate. This means that if we write a submit rule like:
+`gerrit:default_submit` predicate.  The `gerrit:default_submit(X)` includes all
+categories from the database.  This means that if we write a submit rule like:
 
 .rules.pl
 [caption=""]
 ====
   submit_rule(X) :- gerrit:default_submit(X).
 ====
-
 then this is equivalent to not using `rules.pl` at all. We just delegate to
 default logic. However, once we invoke the `gerrit:default_submit(X)` we can
 perform further actions on the return result `X` and apply our specific
@@ -540,7 +667,7 @@
   project_specific_policy(R, S) :- ...
 ====
 
-The following examples build on top of the default submit policy.
+In the following examples both styles will be shown.
 
 Example 8: Make change submittable only if `Code-Review+2` is given by a non author
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -548,6 +675,8 @@
 satisfied if there is at least one `Code-Review+2` from a non author. All other
 default policies like the `Verified` category and vetoing changes still apply.
 
+Reusing the `gerrit:default_submit`
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
 First, we invoke `gerrit:default_submit` to compute the result for the default
 submit policy and then add the `Non-Author-Code-Review` label to it.  The
 `Non-Author-Code-Review` label is added with status `ok` if such an approval
@@ -563,7 +692,8 @@
     S =.. [submit | R].
 
   add_non_author_approval(S1, S2) :-
-    gerrit:commit_author(A), gerrit:commit_label(label('Code-Review', 2), R),
+    gerrit:commit_author(A),
+    gerrit:commit_label(label('Code-Review', 2), R),
     R \= A, !,
     S2 = [label('Non-Author-Code-Review', ok(R)) | S1].
   add_non_author_approval(S1, [label('Non-Author-Code-Review', need(_)) | S1]).
@@ -586,6 +716,44 @@
 if the `cut` in the first rule is not reached and it only happens if a
 predicate before the `cut` fails.
 
+Don't use `gerrit:default_submit`
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+Let's implement the same submit rule the other way, without reusing the
+`gerrit:default_submit`:
+
+.rules.pl
+[caption=""]
+====
+  submit_rule(submit(CR, V)) :-
+    base(CR, V),
+    CR = label(_, ok(Reviewer)),
+    gerrit:commit_author(Author),
+    Author \= Reviewer,
+    !.
+
+  submit_rule(submit(CR, V, N)) :-
+    base(CR, V),
+    N = label('Non-Author-Code-Review', need(_)).
+
+  base(CR, V) :-
+    gerrit:max_with_block(-2, 2, 'Code-Review', CR),
+    gerrit:max_with_block(-1, 1, 'Verified', V).
+====
+
+The latter implementation is probably easier to understand and the code looks
+cleaner. Note, however, that the latter implementation will always return the
+two standard categories only (`Code-Review` and `Verified`) even if a new
+category has beeen inserted into the database. To include the new category
+the `rules.pl` would need to be modified or a `submit_filter` in a parent
+project would have to care about including the new category in the result
+of this `submit_rule`.
+
+The former example, however, would include any newly added category as it
+invokes the `gerrit:default_submit` and then modifies its result.
+
+Which of these two behaviors is desired will always depend on how a particular
+Gerrit server is managed.
+
 Example 9: Remove the `Verified` category
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 A project has no build and test. It consists of only text files and needs only
@@ -594,6 +762,17 @@
 We also want the UI to not show the `Verified` category in the table with
 votes and on the voting screen.
 
+This is quite simple without reusing the 'gerrit:default_submit`:
+
+.rules.pl
+[caption=""]
+====
+  submit_rule(submit(CR)) :-
+    gerrit:max_with_block(-2, 2, 'Code-Review', CR).
+====
+
+Implementing the same rule by reusing `gerrit:default_submit` is a bit more complex:
+
 .rules.pl
 [caption=""]
 ====
@@ -627,6 +806,27 @@
 The `remove_verified_category` and `add_non_author_approval` predicates are the
 same as defined in the previous two examples.
 
+Without reusing the `gerrit:default_submit` the same example may be implemented
+as:
+
+.rules.pl
+[caption=""]
+====
+  submit_rule(submit(CR)) :-
+    base(CR),
+    CR = label(_, ok(Reviewer)),
+    gerrit:commit_author(Author),
+    Author \= Reviewer,
+    !.
+
+  submit_rule(submit(CR, N)) :-
+    base(CR),
+    N = label('Non-Author-Code-Review', need(_)).
+
+  base(CR) :-
+    gerrit:max_with_block(-2, 2, 'Code-Review', CR),
+====
+
 Example 11: Remove the `Verified` category from all projects
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 Example 9, implements `submit_rule` that removes the `Verified` category from
@@ -648,7 +848,32 @@
   remove_verified_category([H|T], [H|R]) :- remove_verified_category(T, R).
 ====
 
-Example 12: 1+1=2 Code-Review
+Example 12: On release branches require DrNo in addition to project rules
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+A new category 'DrNo' is added to the database and is required for release
+branches. To mark a branch as a release branch we use `drno('refs/heads/branch')`.
+
+.rules.pl
+[caption=""]
+====
+  drno('refs/heads/master').
+  drno('refs/heads/stable-2.3').
+  drno('refs/heads/stable-2.4').
+  drno('refs/heads/stable-2.5').
+  drno('refs/heads/stable-2.5').
+
+  submit_filter(In, Out) :-
+    gerrit:change_branch(Branch),
+    drno(Branch),
+    !,
+    In =.. [submit | I],
+    gerrit:max_with_block(-1, 1, 'DrNo', DrNo),
+    Out =.. [submit, DrNo | I].
+
+  submit_filter(In, Out) :- In = Out.
+====
+
+Example 13: 1+1=2 Code-Review
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 In this example we introduce accumulative voting to determine if a change is
 submittable or not. We modify the standard Code-Review to be accumulative, and make the
@@ -684,7 +909,34 @@
     S =.. [submit | Labels].
 ====
 
-Example 13: Master and apprentice
+Implementing the same example without using `gerrit:default_submit`:
+
+.rules.pl
+[caption=""]
+====
+  submit_rule(submit(CR, V)) :-
+    sum(2, 'Code-Review', CR),
+    gerrit:max_with_block(-1, 1, 'Verified', V).
+
+  % Sum the votes in a category. Uses a helper function score/2
+  % to select out only the score values the given category.
+  sum(VotesNeeded, Category, label(Category, ok(_))) :-
+    findall(Score, score(Category, Score), All),
+    sum_list(All, Sum),
+    Sum >= VotesNeeded,
+    !.
+  sum(VotesNeeded, Category, label(Category, need(VotesNeeded))).
+
+  score(Category, Score) :-
+    gerrit:commit_label(label(Category, Score), User).
+
+  % Simple Prolog routine to sum a list of integers.
+  sum_list(List, Sum)   :- sum_list(List, 0, Sum).
+  sum_list([X|T], Y, S) :- Z is X + Y, sum_list(T, Z, S).
+  sum_list([], S, S).
+====
+
+Example 14: Master and apprentice
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 The master and apprentice example allow you to specify a user (the `master`)
 that must approve all changes done by another user (the `apprentice`).
@@ -721,7 +973,7 @@
   add_apprentice_master(S, S).
 ====
 
-Example 14: Only allow Author to submit change
+Example 15: Only allow Author to submit change
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 This example adds a new needed category `Patchset-Author` for any user that is
 not the author of the patch. This effectively blocks all users except the author
@@ -745,6 +997,68 @@
   only_allow_author_to_submit(S1, [label('Patchset-Author', need(_)) | S1]).
 ====
 
+Examples - Submit Type
+----------------------
+The following examples show how to implement own submit type rules.
+
+Example 1: Set a `Cherry Pick` submit type for all changes
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+This example sets the `Cherry Pick` submit type for all changes. It overrides
+whatever is set as project default submit type.
+
+rules.pl
+[caption=""]
+====
+  submit_type(cherry_pick).
+====
+
+
+Example 2: `Fast Forward Only` for all `refs/heads/stable*` branches
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+For all `refs/heads/stable.*` branches we would like to enforce the `Fast
+Forward Only` submit type. A reason for this decision may be a need to never
+break the build in the stable branches.  For all other branches, those not
+matching the `refs/heads/stable.*` pattern, we would like to use the project's
+default submit type as defined on the project settings page.
+
+.rules.pl
+[caption=""]
+====
+  submit_type(fast_forward_only) :-
+    gerrit:change_branch(B), regex_matches('refs/heads/stable.*', B),
+    !.
+  submit_type(T) :- gerrit:project_default_submit_type(T)
+====
+
+The first `submit_type` predicate defines the `Fast Forward Only` submit type
+for `refs/heads/stable.*` branches. The second `submit_type` predicate returns
+the project's default submit type.
+
+Example 3: Don't require `Fast Forward Only` if only documentation was changed
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+Like in the previous example we want the `Fast Forward Only` submit type for
+the `refs/heads/stable*` branches.  However, if only documentation was changed
+(only `*.txt` files), then we allow project's default submit type for such
+changes.
+
+.rules.pl
+[caption=""]
+====
+  submit_type(fast_forward_only) :-
+    gerrit:commit_delta('(?<!\.txt)$'),
+    gerrit:change_branch(B), regex_matches('refs/heads/stable.*', B),
+    !.
+  submit_type(T) :- gerrit:project_default_submit_type(T)
+====
+
+The `gerrit:commit_delta('(?<!\.txt)$')` succeeds if the change contains a file
+whose name doesn't end with `.txt` The rest of this rule is same like in the
+previous example.
+
+If all file names in the change end with `.txt`, then the
+`gerrit:commit_delta('(?<!\.txt)$')` will fail as no file name will match this
+regular expression.
+
 GERRIT
 ------
 Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/refs-notes-review.txt b/Documentation/refs-notes-review.txt
deleted file mode 100644
index 632f567..0000000
--- a/Documentation/refs-notes-review.txt
+++ /dev/null
@@ -1,111 +0,0 @@
-The refs/notes/review namespace
-===============================
-
-Summary
--------
-
-`refs/notes/review` is a special reference that Gerrit creates on repositories
-to store information about code reviews.
-
-When a repository is cloned from Gerrit, the `refs/notes/review` reference is
-not included by default.  It has to be manually fetched:
-
-====
-  $ git fetch origin refs/notes/review:refs/notes/review
-====
-
-It is also possible to
-link:http://www.kernel.org/pub/software/scm/git/docs/git-config.html[configure git]
-to always fetch `refs/notes/review`:
-
-====
-  $ git config --add remote.origin.fetch refs/notes/review:refs/notes/review
-  $ git fetch
-====
-
-When `refs/notes/review` is fetched on a repository, the Gerrit review
-information can be included in the git log output:
-
-====
-   $ git log --show-notes=review
-====
-
-Content of refs/notes/review
-----------------------------
-
-For each commit, Gerrit stores the following review information in
-`refs/notes/review`:
-
-[[submitted_by]]
-Submitted-by
-~~~~~~~~~~~~
-
-The name and email address of the Gerrit user that submitted the change in
-link:http://www.ietf.org/rfc/rfc2822.txt[RFC 2822] format.
-
-====
-  Submitted-by: Random J Developer <random@developer.example.org>
-====
-
-[[submitted_at]]
-Submitted-at
-~~~~~~~~~~~~
-
-The time the commit was submitted in RFC 2822 time stamp format.
-
-====
-  Submitted-at: Mon, 25 Jun 2012 16:15:57 +0200
-====
-
-[[reviewed_on]]
-Reviewed-on
-~~~~~~~~~~~
-
-The URL to the change on the Gerrit server.
-
-====
-  Reviewed-on: http://path.to.gerrit/12345
-====
-
-[[review_scores]]
-Review Labels and Scores
-~~~~~~~~~~~~~~~~~~~~~~~~
-
-Review label and score, and the name and email address of the Gerrit user that
-gave it in RFC 2822 format:
-
-====
-  Code-Review+2: A. N. Other <another@developer.example.org>
-  Verified+1: A. N. Other <another@developer.example.org>
-====
-
-Commonly used review labels are "Code-Review" and "Verified", but any label
-configured in Gerrit can be included.
-
-All review labels and scores present on the change at the time of submit are
-included.
-
-[[project]]
-Project
-~~~~~~~
-
-The name of the project in which the commit was made.
-
-====
-  Project: kernel/common
-====
-
-[[branch]]
-Branch
-~~~~~~
-
-The name of the branch on which the commit was made.
-
-====
-  Branch: refs/heads/master
-====
-
-
-GERRIT
-------
-Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/rest-api-accounts.txt b/Documentation/rest-api-accounts.txt
new file mode 100644
index 0000000..2ccd39a
--- /dev/null
+++ b/Documentation/rest-api-accounts.txt
@@ -0,0 +1,368 @@
+Gerrit Code Review - /accounts/ REST API
+========================================
+
+This page describes the account related REST endpoints.
+Please also take note of the general information on the
+link:rest-api.html[REST API].
+
+Endpoints
+---------
+
+[[get-account]]
+Get Account
+~~~~~~~~~~~
+[verse]
+'GET /accounts/link:#account-id[\{account-id\}]'
+
+Returns an account as an link:#account-info[AccountInfo] entity.
+
+.Request
+----
+  GET /accounts/self HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "_account_id": 1000096,
+    "name": "John Doe",
+    "email": "john.doe@example.com"
+  }
+----
+
+[[list-account-capabilities]]
+List Account Capabilities
+~~~~~~~~~~~~~~~~~~~~~~~~~
+[verse]
+'GET /accounts/link:#account-id[\{account-id\}]/capabilities'
+
+Returns the global capabilities that are enabled for the specified
+user.
+
+If the global capabilities for the calling user should be listed,
+`self` can be used as account-id. This can be used by UI tools to
+discover if administrative features are available to the caller, so
+they can hide (or show) relevant UI actions.
+
+.Request
+----
+  GET /accounts/self/capabilities HTTP/1.0
+----
+
+As response the global capabilities of the user are returned as a
+link:#capability-info[CapabilityInfo] entity.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "queryLimit": {
+      "min": 0,
+      "max": 500
+    },
+    "emailReviewers": true
+  }
+----
+
+Administrator that has authenticated with digest authentication:
+
+.Request
+----
+  GET /a/accounts/self/capabilities HTTP/1.0
+  Authorization: Digest username="admin", realm="Gerrit Code Review", nonce="...
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "administrateServer": true,
+    "queryLimit": {
+      "min": 0,
+      "max": 500
+    },
+    "createAccount": true,
+    "createGroup": true,
+    "createProject": true,
+    "emailReviewers": true,
+    "killTask": true,
+    "viewCaches": true,
+    "flushCaches": true,
+    "viewConnections": true,
+    "viewQueue": true,
+    "runGC": true,
+    "startReplication": true
+  }
+----
+
+.Get your own capabilities
+****
+get::/accounts/self/capabilities
+****
+
+To filter the set of global capabilities the `q` parameter can be used.
+Filtering may decrease the response time by avoiding looking at every
+possible alternative for the caller.
+
+.Request
+----
+  GET /a/accounts/self/capabilities?q=createAccount&q=createGroup HTTP/1.0
+  Authorization: Digest username="admin", realm="Gerrit Code Review", nonce="...
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "createAccount": true,
+    "createGroup": true
+  }
+----
+
+.Check if you can create groups
+****
+get::/accounts/self/capabilities?q=createGroup
+****
+
+[[check-account-capability]]
+Check Account Capability
+~~~~~~~~~~~~~~~~~~~~~~~~
+[verse]
+'GET /accounts/link:#account-id[\{account-id\}]/capabilities/link:#capability-id[\{capability-id\}]'
+
+Checks if a user has a certain global capability.
+
+.Request
+----
+  GET /a/accounts/self/capabilities/createGroup HTTP/1.0
+----
+
+If the user has the global capability the string `ok` is returned.
+
+.Response
+----
+  HTTP/1.1 200 OK
+
+  ok
+----
+
+If the user doesn't have the global capability the response is
+`404 Not Found`.
+
+.Check if you can create groups
+****
+get::/accounts/self/capabilities/createGroup
+****
+
+[[list-groups]]
+List Groups
+~~~~~~~~~~~
+[verse]
+'GET /accounts/link:#account-id[\{account-id\}]/groups/'
+
+Lists all groups that contain the specified user as a member.
+
+.Request
+----
+  GET /a/accounts/self/groups/ HTTP/1.0
+----
+
+As result a list of link:rest-api-groups.html#group-info[GroupInfo]
+entries is returned.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  [
+    {
+      "kind": "gerritcodereview#group",
+      "id": "global%3AAnonymous-Users",
+      "url": "#/admin/groups/uuid-global%3AAnonymous-Users",
+      "options": {
+      },
+      "description": "Any user, signed-in or not",
+      "group_id": 2,
+      "owner_id": "6a1e70e1a88782771a91808c8af9bbb7a9871389"
+    },
+    {
+      "kind": "gerritcodereview#group",
+      "id": "834ec36dd5e0ed21a2ff5d7e2255da082d63bbd7",
+      "url": "#/admin/groups/uuid-834ec36dd5e0ed21a2ff5d7e2255da082d63bbd7",
+      "options": {
+        "visible_to_all": true,
+      },
+      "group_id": 6,
+      "owner_id": "834ec36dd5e0ed21a2ff5d7e2255da082d63bbd7"
+    },
+    {
+      "kind": "gerritcodereview#group",
+      "id": "global%3ARegistered-Users",
+      "url": "#/admin/groups/uuid-global%3ARegistered-Users",
+      "options": {
+      },
+      "description": "Any signed-in user",
+      "group_id": 3,
+      "owner_id": "6a1e70e1a88782771a91808c8af9bbb7a9871389"
+    }
+  ]
+----
+
+.List all groups that contain you as a member
+****
+get::/accounts/self/groups/
+****
+
+[[get-avatar]]
+Get Avatar
+~~~~~~~~~~
+[verse]
+'GET /accounts/link:#account-id[\{account-id\}]/avatar'
+
+Retrieves the avatar image of the user.
+
+With the `size` option (alias `s`) you can specify the preferred size
+in pixels (height and width).
+
+.Request
+----
+  GET /a/accounts/john.doe@example.com/avatar?s=20 HTTP/1.0
+----
+
+The response redirects to the URL of the avatar image.
+
+.Response
+----
+  HTTP/1.1 302 Found
+  Location: https://profiles/avatar/john_doe.jpeg?s=20x20
+----
+
+
+[[ids]]
+IDs
+---
+
+[[account-id]]
+\{account-id\}
+~~~~~~~~~~~~~~
+Identifier that uniquely identifies one account.
+
+This can be:
+
+* a string of the format "Full Name <email@example.com>"
+* just the email address ("email@example")
+* a full name if it is unique ("Full Name")
+* an account ID ("18419")
+* a user name ("username")
+* `self` for the calling user
+
+[[capability-id]]
+\{capability-id\}
+~~~~~~~~~~~~~~~~~
+Identifier of a global capability. Valid values are all field names of
+the link:#capability-info[CapabilityInfo] entity.
+
+
+[[json-entities]]
+JSON Entities
+-------------
+
+[[account-info]]
+AccountInfo
+~~~~~~~~~~~
+The `AccountInfo` entity contains information about an account.
+
+[options="header",width="50%",cols="1,^1,5"]
+|===========================
+|Field Name    ||Description
+|`_account_id` ||The numeric ID of the account.
+|`name`        |optional|The full name of the user. +
+Only set if detailed account information is requested.
+|`email`       |optional|
+The email address the user prefers to be contacted through. +
+Only set if detailed account information is requested.
+|===========================
+
+[[capability-info]]
+CapabilityInfo
+~~~~~~~~~~~~~~
+The `CapabilityInfo` entity contains information about the global
+capabilities of a user.
+
+[options="header",width="50%",cols="1,^1,5"]
+|=================================
+|Field Name          ||Description
+|`administrateServer`|not set if `false`|Whether the user has the
+link:access-control.html#capability_administrateServer[Administrate
+Server] capability.
+|`queryLimit`||The link:access-control.html#capability_queryLimit[Query
+Limit] of the user as link:#query-limit-info[QueryLimitInfo].
+|`createAccount`     |not set if `false`|Whether the user has the
+link:access-control.html#capability_createAccount[Create Account]
+capability.
+|`createGroup`       |not set if `false`|Whether the user has the
+link:access-control.html#capability_createGroup[Create Group]
+capability.
+|`createProject`     |not set if `false`|Whether the user has the
+link:access-control.html#capability_createProject[Create Project]
+capability.
+|`emailReviewers`    |not set if `false`|Whether the user has the
+link:access-control.html#capability_emailReviewers[Email Reviewers]
+capability.
+|`killTask`          |not set if `false`|Whether the user has the
+link:access-control.html#capability_kill[Kill Task] capability.
+|`viewCaches`        |not set if `false`|Whether the user has the
+link:access-control.html#capability_viewCaches[View Caches] capability.
+|`flushCaches`       |not set if `false`|Whether the user has the
+link:access-control.html#capability_flushCaches[Flush Caches]
+capability.
+|`viewConnections`   |not set if `false`|Whether the user has the
+link:access-control.html#capability_viewConnections[View Connections]
+capability.
+|`viewQueue`         |not set if `false`|Whether the user has the
+link:access-control.html#capability_viewQueue[View Queue] capability.
+|`runGC`  |not set if `false`|Whether the user has the
+link:access-control.html#capability_runGC[Run Garbage Collection]
+capability.
+|`startReplication`  |not set if `false`|Whether the user has the
+link:access-control.html#capability_startReplication[Start Replication]
+capability.
+|=================================
+
+[[query-limit-info]]
+QueryLimitInfo
+~~~~~~~~~~~~~~
+The `QueryLimitInfo` entity contains information about the
+link:access-control.html#capability_queryLimit[Query Limit] of a user.
+
+[options="header",width="50%",cols="1,6"]
+|================================
+|Field Name          |Description
+|`min`               |Lower limit.
+|`max`               |Upper limit.
+|================================
+
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/rest-api-changes.txt b/Documentation/rest-api-changes.txt
new file mode 100644
index 0000000..696a174
--- /dev/null
+++ b/Documentation/rest-api-changes.txt
@@ -0,0 +1,2153 @@
+Gerrit Code Review - /changes/ REST API
+=======================================
+
+This page describes the change related REST endpoints.
+Please also take note of the general information on the
+link:rest-api.html[REST API].
+
+[[change-endpoints]]
+Change Endpoints
+----------------
+
+[[list-changes]]
+Query Changes
+~~~~~~~~~~~~~
+[verse]
+'GET /changes/'
+
+Queries changes visible to the caller. The query string must be
+provided by the `q` parameter. The `n` parameter can be used to limit
+the returned results.
+
+As result a list of link:#change-info[ChangeInfo] entries is returned.
+The change output is sorted by the last update time, most recently
+updated to oldest updated.
+
+Query for open changes of watched projects:
+
+.Request
+----
+  GET /changes/?q=status:open+is:watched&n=2 HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  [
+    {
+      "kind": "gerritcodereview#change",
+      "id": "demo~master~Idaf5e098d70898b7119f6f4af5a6c13343d64b57",
+      "project": "demo",
+      "branch": "master",
+      "change_id": "Idaf5e098d70898b7119f6f4af5a6c13343d64b57",
+      "subject": "One change",
+      "status": "NEW",
+      "created": "2012-07-17 07:18:30.854000000",
+      "updated": "2012-07-17 07:19:27.766000000",
+      "reviewed": true,
+      "mergeable": true,
+      "_sortkey": "001e7057000006dc",
+      "_number": 1756,
+      "owner": {
+        "name": "John Doe"
+      },
+    },
+    {
+      "kind": "gerritcodereview#change",
+      "id": "demo~master~I09c8041b5867d5b33170316e2abc34b79bbb8501",
+      "project": "demo",
+      "branch": "master",
+      "change_id": "I09c8041b5867d5b33170316e2abc34b79bbb8501",
+      "subject": "Another change",
+      "status": "NEW",
+      "created": "2012-07-17 07:18:30.884000000",
+      "updated": "2012-07-17 07:18:30.885000000",
+      "mergeable": true,
+      "_sortkey": "001e7056000006dd",
+      "_number": 1757,
+      "owner": {
+        "name": "John Doe"
+      },
+      "_more_changes": true
+    }
+  ]
+----
+
+If the `n` query parameter is supplied and additional changes exist
+that match the query beyond the end, the last change object has a
+`_more_changes: true` JSON field set. Callers can resume a query with
+the `N` query parameter, supplying the last change's `_sortkey` field
+as the value. When going in the reverse direction with the `P` query
+parameter a `_more_changes: true` is put in the first change object if
+there are results *before* the first change returned.
+
+Clients are allowed to specify more than one query by setting the `q`
+parameter multiple times. In this case the result is an array of
+arrays, one per query in the same order the queries were given in.
+
+.Query for the 25 most recent open changes of the projects that you watch
+****
+get::/changes/?q=status:open+is:watched&n=25
+****
+
+Query that retrieves changes for a user's dashboard:
+
+.Request
+----
+  GET /changes/?q=is:open+owner:self&q=is:open+reviewer:self+-owner:self&q=is:closed+owner:self+limit:5&o=LABELS HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  [
+    [
+      {
+        "kind": "gerritcodereview#change",
+        "id": "demo~master~Idaf5e098d70898b7119f6f4af5a6c13343d64b57",
+        "project": "demo",
+        "branch": "master",
+        "change_id": "Idaf5e098d70898b7119f6f4af5a6c13343d64b57",
+        "subject": "One change",
+        "status": "NEW",
+        "created": "2012-07-17 07:18:30.854000000",
+        "updated": "2012-07-17 07:19:27.766000000",
+        "reviewed": true,
+        "mergeable": true,
+        "_sortkey": "001e7057000006dc",
+        "_number": 1756,
+        "owner": {
+          "name": "John Doe"
+        },
+        "labels": {
+          "Verified": {},
+          "Code-Review": {}
+        }
+      }
+    ],
+    [],
+    []
+  ]
+----
+
+.Query the changes for your user dashboard
+****
+get::/changes/?q=is:open+owner:self&q=is:open+reviewer:self+-owner:self&q=is:closed+owner:self+limit:5&o=LABELS
+****
+
+Additional fields can be obtained by adding `o` parameters, each
+option requires more database lookups and slows down the query
+response time to the client so they are generally disabled by
+default. Optional fields are:
+
+[[labels]]
+--
+* `LABELS`: a summary of each label required for submit, and
+  approvers that have granted (or rejected) with that label.
+--
+
+[[detailed-labels]]
+--
+* `DETAILED_LABELS`: detailed label information, including numeric
+  values of all existing approvals, recognized label values, values
+  permitted to be set by the current user, and reviewers that may be
+  removed by the current user.
+--
+
+[[current-revision]]
+--
+* `CURRENT_REVISION`: describe the current revision (patch set)
+  of the change, including the commit SHA-1 and URLs to fetch from.
+--
+
+[[all-revisions]]
+--
+* `ALL_REVISIONS`: describe all revisions, not just current.
+--
+
+[[current-commit]]
+--
+* `CURRENT_COMMIT`: parse and output all header fields from the
+  commit object, including message. Only valid when the current
+  revision or all revisions are selected.
+--
+
+[[all-commits]]
+--
+* `ALL_COMMITS`: parse and output all header fields from the
+  output revisions. If only `CURRENT_REVISION` was requested
+  then only the current revision's commit data will be output.
+--
+
+[[current-files]]
+--
+* `CURRENT_FILES`: list files modified by the commit, including
+  basic line counts inserted/deleted per file. Only valid when
+  the current revision or all revisions are selected.
+--
+
+[[all-files]]
+--
+* `ALL_FILES`: list files modified by the commit, including
+  basic line counts inserted/deleted per file. If only the
+  `CURRENT_REVISION` was requested the only that commit's
+  modified files will be output.
+--
+
+[[detailed-accounts]]
+--
+* `DETAILED_ACCOUNTS`: include `_account_id` and `email` fields when
+  referencing accounts.
+--
+
+.Request
+----
+  GET /changes/?q=97&o=CURRENT_REVISION&o=CURRENT_COMMIT&o=CURRENT_FILES HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  [
+    {
+      "kind": "gerritcodereview#change",
+      "id": "demo~master~I7ea46d2e2ee5c64c0d807677859cfb7d90b8966a",
+      "project": "gerrit",
+      "branch": "master",
+      "change_id": "I7ea46d2e2ee5c64c0d807677859cfb7d90b8966a",
+      "subject": "Use an EventBus to manage star icons",
+      "status": "NEW",
+      "created": "2012-04-25 00:52:25.580000000",
+      "updated": "2012-04-25 00:52:25.586000000",
+      "mergeable": true,
+      "_sortkey": "001c9bf400000061",
+      "_number": 97,
+      "owner": {
+        "name": "Shawn Pearce"
+      },
+      "current_revision": "184ebe53805e102605d11f6b143486d15c23a09c",
+      "revisions": {
+        "184ebe53805e102605d11f6b143486d15c23a09c": {
+          "_number": 1,
+          "fetch": {
+            "git": {
+              "url": "git://localhost/gerrit",
+              "ref": "refs/changes/97/97/1"
+            },
+            "http": {
+              "url": "http://127.0.0.1:8080/gerrit",
+              "ref": "refs/changes/97/97/1"
+            }
+          },
+          "commit": {
+            "parents": [
+              {
+                "commit": "1eee2c9d8f352483781e772f35dc586a69ff5646",
+                "subject": "Migrate contributor agreements to All-Projects."
+              }
+            ],
+            "author": {
+              "name": "Shawn O. Pearce",
+              "email": "sop@google.com",
+              "date": "2012-04-24 18:08:08.000000000",
+              "tz": -420
+            },
+            "committer": {
+              "name": "Shawn O. Pearce",
+              "email": "sop@google.com",
+              "date": "2012-04-24 18:08:08.000000000",
+              "tz": -420
+            },
+            "subject": "Use an EventBus to manage star icons",
+            "message": "Use an EventBus to manage star icons\n\nImage widgets that need to ..."
+          },
+          "files": {
+            "gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeCache.java": {
+              "lines_deleted": 8
+            },
+            "gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDetailCache.java": {
+              "lines_inserted": 1
+            },
+            "gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java": {
+              "lines_inserted": 11,
+              "lines_deleted": 19
+            },
+            "gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java": {
+              "lines_inserted": 23,
+              "lines_deleted": 20
+            },
+            "gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarCache.java": {
+              "status": "D",
+              "lines_deleted": 139
+            },
+            "gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarredChanges.java": {
+              "status": "A",
+              "lines_inserted": 204
+            },
+            "gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Screen.java": {
+              "lines_deleted": 9
+            }
+          }
+        }
+      }
+    }
+  ]
+----
+
+[[get-change]]
+Get Change
+~~~~~~~~~~
+[verse]
+'GET /changes/link:#change-id[\{change-id\}]'
+
+Retrieves a change.
+
+.Request
+----
+  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940 HTTP/1.0
+----
+
+As response a link:#change-info[ChangeInfo] entity is returned that
+describes the change.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "kind": "gerritcodereview#change",
+    "id": "myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940",
+    "project": "myProject",
+    "branch": "master",
+    "change_id": "I8473b95934b5732ac55d26311a706c9c2bde9940",
+    "subject": "Implementing Feature X",
+    "status": "NEW",
+    "created": "2013-02-01 09:59:32.126000000",
+    "updated": "2013-02-21 11:16:36.775000000",
+    "reviewed": true,
+    "mergeable": true,
+    "_sortkey": "0023412400000f7d",
+    "_number": 3965,
+    "owner": {
+      "name": "John Doe"
+    }
+  }
+----
+
+[[get-change-detail]]
+Get Change Detail
+~~~~~~~~~~~~~~~~~
+[verse]
+'GET /changes/link:#change-id[\{change-id\}]/detail'
+
+Retrieves a change with link:#labels[labels], link:#detailed-labels[
+detailed labels] and link:#detailed-accounts[detailed accounts].
+
+.Request
+----
+  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/detail HTTP/1.0
+----
+
+As response a link:#change-info[ChangeInfo] entity is returned that
+describes the change.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "kind": "gerritcodereview#change",
+    "id": "myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940",
+    "project": "myProject",
+    "branch": "master",
+    "change_id": "I8473b95934b5732ac55d26311a706c9c2bde9940",
+    "subject": "Implementing Feature X",
+    "status": "NEW",
+    "created": "2013-02-01 09:59:32.126000000",
+    "updated": "2013-02-21 11:16:36.775000000",
+    "reviewed": true,
+    "mergeable": true,
+    "_sortkey": "0023412400000f7d",
+    "_number": 3965,
+    "owner": {
+      "_account_id": 1000096,
+      "name": "John Doe",
+      "email": "john.doe@example.com"
+    },
+    "labels": {
+      "Verified": {
+        "all": [
+          {
+            "value": 0,
+            "_account_id": 1000096,
+            "name": "John Doe",
+            "email": "john.doe@example.com"
+          },
+          {
+            "value": 0,
+            "_account_id": 1000097,
+            "name": "Jane Roe",
+            "email": "jane.roe@example.com"
+          }
+        ],
+        "values": {
+          "-1": "Fails",
+          " 0": "No score",
+          "+1": "Verified"
+        }
+      },
+      "Code-Review": {
+        "recommended": {
+          "_account_id": 1000097,
+          "name": "Jane Roe",
+          "email": "jane.roe@example.com"
+        },
+        "disliked": {
+          "_account_id": 1000096,
+          "name": "John Doe",
+          "email": "john.doe@example.com"
+        },
+        "all": [
+          {
+            "value": -1,
+            "_account_id": 1000096,
+            "name": "John Doe",
+            "email": "john.doe@example.com"
+          },
+          {
+            "value": 1,
+            "_account_id": 1000097,
+            "name": "Jane Roe",
+            "email": "jane.roe@example.com"
+          }
+        ]
+        "values": {
+          "-2": "Do not submit",
+          "-1": "I would prefer that you didn\u0027t submit this",
+          " 0": "No score",
+          "+1": "Looks good to me, but someone else must approve",
+          "+2": "Looks good to me, approved"
+        }
+      }
+    },
+    "permitted_labels": {
+      "Verified": [
+        "-1",
+        " 0",
+        "+1"
+      ],
+      "Code-Review": [
+        "-2",
+        "-1",
+        " 0",
+        "+1",
+        "+2"
+      ]
+    },
+    "removable_reviewers": [
+      {
+        "_account_id": 1000096,
+        "name": "John Doe",
+        "email": "john.doe@example.com"
+      },
+      {
+        "_account_id": 1000097,
+        "name": "Jane Roe",
+        "email": "jane.roe@example.com"
+      }
+    ]
+  }
+----
+
+[[get-topic]]
+Get Topic
+~~~~~~~~~
+[verse]
+'GET /changes/link:#change-id[\{change-id\}]/topic'
+
+Retrieves the topic of a change.
+
+.Request
+----
+  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/topic HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  "Documentation"
+----
+
+If the change does not have a topic an empty string is returned.
+
+[[set-topic]]
+Set Topic
+~~~~~~~~~
+[verse]
+'PUT /changes/link:#change-id[\{change-id\}]/topic'
+
+Sets the topic of a change.
+
+The new topic must be provided in the request body inside a
+link:#topic-input[TopicInput] entity.
+
+.Request
+----
+  PUT /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/topic HTTP/1.0
+  Content-Type: application/json;charset=UTF-8
+
+  {
+    "topic": "Documentation"
+  }
+----
+
+As response the new topic is returned.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  "Documentation"
+----
+
+If the topic was deleted the response is "`204 No Content`".
+
+[[delete-topic]]
+Delete Topic
+~~~~~~~~~~~~
+[verse]
+'DELETE /changes/link:#change-id[\{change-id\}]/topic'
+
+Deletes the topic of a change.
+
+The request body does not need to include a link:#topic-input[
+TopicInput] entity if no review comment is added.
+
+Please note that some proxies prohibit request bodies for DELETE
+requests. In this case, if you want to specify a commit message, use
+link:#set-topic[PUT] to delete the topic.
+
+.Request
+----
+  DELETE /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/topic HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 204 No Content
+----
+
+[[abandon-change]]
+Abandon Change
+~~~~~~~~~~~~~~
+[verse]
+'POST /changes/link:#change-id[\{change-id\}]/abandon'
+
+Abandons a change.
+
+The request body does not need to include a link:#abandon-input[
+AbandonInput] entity if no review comment is added.
+
+.Request
+----
+  POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/abandon HTTP/1.0
+----
+
+As response a link:#change-info[ChangeInfo] entity is returned that
+describes the abandoned change.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "kind": "gerritcodereview#change",
+    "id": "myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940",
+    "project": "myProject",
+    "branch": "master",
+    "change_id": "I8473b95934b5732ac55d26311a706c9c2bde9940",
+    "subject": "Implementing Feature X",
+    "status": "ABANDONED",
+    "created": "2013-02-01 09:59:32.126000000",
+    "updated": "2013-02-21 11:16:36.775000000",
+    "reviewed": true,
+    "mergeable": true,
+    "_sortkey": "0023412400000f7d",
+    "_number": 3965,
+    "owner": {
+      "name": "John Doe"
+    }
+  }
+----
+
+If the change cannot be abandoned because the change state doesn't
+allow abandoning of the change, the response is "`409 Conflict`" and
+the error message is contained in the response body.
+
+.Response
+----
+  HTTP/1.1 409 Conflict
+  Content-Disposition: attachment
+  Content-Type: text/plain;charset=UTF-8
+
+  change is merged
+----
+
+[[restore-change]]
+Restore Change
+~~~~~~~~~~~~~~
+[verse]
+'POST /changes/link:#change-id[\{change-id\}]/restore'
+
+Restores a change.
+
+The request body does not need to include a link:#restore-input[
+RestoreInput] entity if no review comment is added.
+
+.Request
+----
+  POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/restore HTTP/1.0
+----
+
+As response a link:#change-info[ChangeInfo] entity is returned that
+describes the restored change.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "kind": "gerritcodereview#change",
+    "id": "myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940",
+    "project": "myProject",
+    "branch": "master",
+    "change_id": "I8473b95934b5732ac55d26311a706c9c2bde9940",
+    "subject": "Implementing Feature X",
+    "status": "NEW",
+    "created": "2013-02-01 09:59:32.126000000",
+    "updated": "2013-02-21 11:16:36.775000000",
+    "reviewed": true,
+    "mergeable": true,
+    "_sortkey": "0023412400000f7d",
+    "_number": 3965,
+    "owner": {
+      "name": "John Doe"
+    }
+  }
+----
+
+If the change cannot be restored because the change state doesn't
+allow restoring the change, the response is "`409 Conflict`" and
+the error message is contained in the response body.
+
+.Response
+----
+  HTTP/1.1 409 Conflict
+  Content-Disposition: attachment
+  Content-Type: text/plain;charset=UTF-8
+
+  change is new
+----
+
+[[revert-change]]
+Revert Change
+~~~~~~~~~~~~~
+[verse]
+'POST /changes/link:#change-id[\{change-id\}]/revert'
+
+Reverts a change.
+
+The request body does not need to include a link:#revert-input[
+RevertInput] entity if no review comment is added.
+
+.Request
+----
+  POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revert HTTP/1.0
+----
+
+As response a link:#change-info[ChangeInfo] entity is returned that
+describes the reverting change.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "kind": "gerritcodereview#change",
+    "id": "myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940",
+    "project": "myProject",
+    "branch": "master",
+    "change_id": "I8473b95934b5732ac55d26311a706c9c2bde9940",
+    "subject": "Revert \"Implementing Feature X\"",
+    "status": "NEW",
+    "created": "2013-02-01 09:59:32.126000000",
+    "updated": "2013-02-21 11:16:36.775000000",
+    "reviewed": true,
+    "mergeable": true,
+    "_sortkey": "0023412400000f7d",
+    "_number": 3965,
+    "owner": {
+      "name": "John Doe"
+    }
+  }
+----
+
+If the change cannot be reverted because the change state doesn't
+allow reverting the change, the response is "`409 Conflict`" and
+the error message is contained in the response body.
+
+.Response
+----
+  HTTP/1.1 409 Conflict
+  Content-Disposition: attachment
+  Content-Type: text/plain;charset=UTF-8
+
+  change is new
+----
+
+[[submit-change]]
+Submit Change
+~~~~~~~~~~~~~
+[verse]
+'POST /changes/link:#change-id[\{change-id\}]/submit'
+
+Submits a change.
+
+The request body only needs to include a link:#submit-input[
+SubmitInput] entity if the request should wait for the merge to
+complete.
+
+.Request
+----
+  POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/submit HTTP/1.0
+  Content-Type: application/json;charset=UTF-8
+
+  {
+    "wait_for_merge": true
+  }
+----
+
+As response a link:#change-info[ChangeInfo] entity is returned that
+describes the submitted/merged change.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "kind": "gerritcodereview#change",
+    "id": "myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940",
+    "project": "myProject",
+    "branch": "master",
+    "change_id": "I8473b95934b5732ac55d26311a706c9c2bde9940",
+    "subject": "Implementing Feature X",
+    "status": "MERGED",
+    "created": "2013-02-01 09:59:32.126000000",
+    "updated": "2013-02-21 11:16:36.775000000",
+    "reviewed": true,
+    "_sortkey": "0023412400000f7d",
+    "_number": 3965,
+    "owner": {
+      "name": "John Doe"
+    }
+  }
+----
+
+If the change cannot be submitted because the submit rule doesn't allow
+submitting the change, the response is "`409 Conflict`" and the error
+message is contained in the response body.
+
+.Response
+----
+  HTTP/1.1 409 Conflict
+  Content-Disposition: attachment
+  Content-Type: text/plain;charset=UTF-8
+
+  blocked by Verified
+----
+
+[[reviewer-endpoints]]
+Reviewer Endpoints
+------------------
+
+[[list-reviewers]]
+List Reviewers
+~~~~~~~~~~~~~~
+[verse]
+'GET /changes/link:#change-id[\{change-id\}]/reviewers/'
+
+Lists the reviewers of a change.
+
+As result a list of link:#reviewer-info[ReviewerInfo] entries is returned.
+
+.Request
+----
+  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/reviewers/ HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  [
+    {
+      "kind": "gerritcodereview#reviewer",
+      "approvals": {
+        "Verified": "+1",
+        "Code-Review": "+2"
+      },
+      "_account_id": 1000096,
+      "name": "John Doe",
+      "email": "john.doe@example.com"
+    },
+    {
+      "kind": "gerritcodereview#reviewer",
+      "approvals": {
+        "Verified": " 0",
+        "Code-Review": "-1"
+      },
+      "_account_id": 1000097,
+      "name": "Jane Roe",
+      "email": "jane.roe@example.com"
+    }
+  ]
+----
+
+[[get-reviewer]]
+Get Reviewer
+~~~~~~~~~~~~
+[verse]
+'GET /changes/link:#change-id[\{change-id\}]/reviewers/link:rest-api-accounts.html#account-id[\{account-id\}]'
+
+Retrieves a reviewer of a change.
+
+As response a link:#reviewer-info[ReviewerInfo] entity is returned that
+describes the reviewer.
+
+.Request
+----
+  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/reviewers/john.doe@example.com HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "kind": "gerritcodereview#reviewer",
+    "approvals": {
+      "Verified": "+1",
+      "Code-Review": "+2"
+    },
+    "_account_id": 1000096,
+    "name": "John Doe",
+    "email": "john.doe@example.com"
+  }
+----
+
+[[add-reviewer]]
+Add Reviewer
+~~~~~~~~~~~~
+[verse]
+'POST /changes/link:#change-id[\{change-id\}]/reviewers'
+
+Adds one user or all members of one group as reviewer to the change.
+
+The reviewer to be added to the change must be provided in the request
+body as a link:#reviewer-input[ReviewerInput] entity.
+
+.Request
+----
+  POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/reviewers HTTP/1.0
+  Content-Type: application/json;charset=UTF-8
+
+  {
+    "reviewer": "john.doe@example.com"
+  }
+----
+
+As response an link:#add-reviewer-result[AddReviewerResult] entity is
+returned that describes the newly added reviewers.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "reviewers": [
+      {
+        "kind": "gerritcodereview#reviewer",
+        "approvals": {
+          "Verified": " 0",
+          "Code-Review": " 0"
+        },
+        "_account_id": 1000096,
+        "name": "John Doe",
+        "email": "john.doe@example.com"
+      }
+    ]
+  }
+----
+
+If a group is specified, adding the group members as reviewers is an
+atomic operation. This means if an error is returned, none of the
+members are added as reviewer.
+
+If a group with many members is added as reviewer a confirmation may be
+required.
+
+.Request
+----
+  POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/reviewers HTTP/1.0
+  Content-Type: application/json;charset=UTF-8
+
+  {
+    "reviewer": "MyProjectVerifiers"
+  }
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "error": "The group My Group has 15 members. Do you want to add them all as reviewers?",
+    "confirm": true
+  }
+----
+
+To confirm the addition of the reviewers, resend the request with the
+`confirmed` flag being set.
+
+.Request
+----
+  POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/reviewers HTTP/1.0
+  Content-Type: application/json;charset=UTF-8
+
+  {
+    "reviewer": "MyProjectVerifiers",
+    "confirmed": true
+  }
+----
+
+[[delete-reviewer]]
+Delete Reviewer
+~~~~~~~~~~~~~~~
+[verse]
+'DELETE /changes/link:#change-id[\{change-id\}]/reviewers/link:rest-api-accounts.html#account-id[\{account-id\}]'
+
+Deletes a reviewer from a change.
+
+.Request
+----
+  DELETE /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/reviewers/John%20Doe HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 204 No Content
+----
+
+[[revision-endpoints]]
+Revision Endpoints
+------------------
+
+[[get-review]]
+Get Review
+~~~~~~~~~~
+[verse]
+'GET /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/review'
+
+Retrieves a review of a revision.
+
+.Request
+----
+  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/review HTTP/1.0
+----
+
+As response a link:#change-info[ChangeInfo] entity with
+link:#detailed-labels[detailed labels] and link:#detailed-accounts[
+detailed accounts] is returned that describes the review of the
+revision. The revision for which the review is retrieved is contained
+in the `revisions` field. In addition the `current_revision` field is
+set if the revision for which the review is retrieved is the current
+revision of the change. Please note that the returned labels are always
+for the current patch set.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "kind": "gerritcodereview#change",
+    "id": "myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940",
+    "project": "myProject",
+    "branch": "master",
+    "change_id": "I8473b95934b5732ac55d26311a706c9c2bde9940",
+    "subject": "Implementing Feature X",
+    "status": "NEW",
+    "created": "2013-02-01 09:59:32.126000000",
+    "updated": "2013-02-21 11:16:36.775000000",
+    "reviewed": true,
+    "mergeable": true,
+    "_sortkey": "0023412400000f7d",
+    "_number": 3965,
+    "owner": {
+      "_account_id": 1000096,
+      "name": "John Doe",
+      "email": "john.doe@example.com"
+    },
+    "labels": {
+      "Verified": {
+        "all": [
+          {
+            "value": 0,
+            "_account_id": 1000096,
+            "name": "John Doe",
+            "email": "john.doe@example.com"
+          },
+          {
+            "value": 0,
+            "_account_id": 1000097,
+            "name": "Jane Roe",
+            "email": "jane.roe@example.com"
+          }
+        ],
+        "values": {
+          "-1": "Fails",
+          " 0": "No score",
+          "+1": "Verified"
+        }
+      },
+      "Code-Review": {
+        "all": [
+          {
+            "value": -1,
+            "_account_id": 1000096,
+            "name": "John Doe",
+            "email": "john.doe@example.com"
+          },
+          {
+            "value": 1,
+            "_account_id": 1000097,
+            "name": "Jane Roe",
+            "email": "jane.roe@example.com"
+          }
+        ]
+        "values": {
+          "-2": "Do not submit",
+          "-1": "I would prefer that you didn\u0027t submit this",
+          " 0": "No score",
+          "+1": "Looks good to me, but someone else must approve",
+          "+2": "Looks good to me, approved"
+        }
+      }
+    },
+    "permitted_labels": {
+      "Verified": [
+        "-1",
+        " 0",
+        "+1"
+      ],
+      "Code-Review": [
+        "-2",
+        "-1",
+        " 0",
+        "+1",
+        "+2"
+      ]
+    },
+    "removable_reviewers": [
+      {
+        "_account_id": 1000096,
+        "name": "John Doe",
+        "email": "john.doe@example.com"
+      },
+      {
+        "_account_id": 1000097,
+        "name": "Jane Roe",
+        "email": "jane.roe@example.com"
+      }
+    ],
+    "current_revision": "674ac754f91e64a0efb8087e59a176484bd534d1",
+    "revisions": {
+      "674ac754f91e64a0efb8087e59a176484bd534d1": {
+      "_number": 2,
+      "fetch": {
+        "http": {
+          "url": "http://gerrit/myProject",
+          "ref": "refs/changes/65/3965/2"
+        }
+      }
+    }
+  }
+----
+
+[[set-review]]
+Set Review
+~~~~~~~~~~
+[verse]
+'POST /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/review'
+
+Sets a review on a revision.
+
+The review must be provided in the request body as a
+link:#review-input[ReviewInput] entity.
+
+.Request
+----
+  POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/review HTTP/1.0
+  Content-Type: application/json;charset=UTF-8
+
+  {
+    "message": "Some nits need to be fixed.",
+    "labels": {
+      "Code-Review": -1
+    },
+    "comments": {
+      "gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java": [
+        {
+          "line": 23,
+          "message": "[nit] trailing whitespace"
+        },
+        {
+          "line": 49,
+          "message": "[nit] s/conrtol/control"
+        }
+      ]
+    }
+  }
+----
+
+As response a link:#review-info[ReviewInfo] entity is returned that
+describes the applied labels.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "labels": {
+      "Code-Review": -1
+    }
+  }
+----
+
+[[submit-revision]]
+Submit Revision
+~~~~~~~~~~~~~~~
+[verse]
+'POST /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/submit'
+
+Submits a revision.
+
+The request body only needs to include a link:#submit-input[
+SubmitInput] entity if the request should wait for the merge to
+complete.
+
+.Request
+----
+  POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/submit HTTP/1.0
+  Content-Type: application/json;charset=UTF-8
+
+  {
+    "wait_for_merge": true
+  }
+----
+
+As response a link:#submit-info[SubmitInfo] entity is returned that
+describes the status of the submitted change.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "status": "MERGED"
+  }
+----
+
+If the revision cannot be submitted, e.g. because the submit rule
+doesn't allow submitting the revision or the revision is not the
+current revision, the response is "`409 Conflict`" and the error
+message is contained in the response body.
+
+.Response
+----
+  HTTP/1.1 409 Conflict
+  Content-Type: text/plain;charset=UTF-8
+
+  "revision 674ac754f91e64a0efb8087e59a176484bd534d1 is not current revision"
+----
+
+[[get-submit-type]]
+Get Submit Type
+~~~~~~~~~~~~~~~
+[verse]
+'GET /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/submit_type'
+
+Gets the method the server will use to submit (merge) the change.
+
+.Request
+----
+  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/current/submit_type HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  "MERGE_IF_NECESSARY"
+----
+
+[[test-submit-type]]
+Test Submit Type
+~~~~~~~~~~~~~~~~
+[verse]
+'POST /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/test.submit_type'
+
+Tests the submit_type Prolog rule in the project, or the one given.
+
+Request body may be either the Prolog code as `text/plain` or a
+link:#rule-input[RuleInput] object. The query parameter `filters`
+may be set to `SKIP` to bypass parent project filters while testing
+a project-specific rule.
+
+.Request
+----
+  POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/current/test.submit_type HTTP/1.0
+  Content-Type: text/plain;charset-UTF-8
+
+  submit_type(cherry_pick).
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  "cherry_pick"
+----
+
+[[test-submit-rule]]
+Test Submit Rule
+~~~~~~~~~~~~~~~~
+[verse]
+'POST /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/test.submit_rule'
+
+Tests the submit_rule Prolog rule in the project, or the one given.
+
+Request body may be either the Prolog code as `text/plain` or a
+link:#rule-input[RuleInput] object. The query parameter `filters`
+may be set to `SKIP` to bypass parent project filters while testing
+a project-specific rule.
+
+.Request
+----
+  POST /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/current/test.submit_type?filters=SKIP HTTP/1.0
+  Content-Type: text/plain;charset-UTF-8
+
+  submit_rule(submit(R)) :-
+    R = label('Any-Label-Name', reject(_)).
+----
+
+The response is a list of link:#submit-record[SubmitRecord] entries
+describing the permutations that satisfy the tested submit rule.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  [
+    {
+      "status": "NOT_READY",
+      "reject": {
+        "Any-Label-Name": {}
+      }
+    }
+  ]
+----
+
+[[list-drafts]]
+List Drafts
+~~~~~~~~~~~
+[verse]
+'GET /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/drafts/'
+
+Lists the draft comments of a revision that belong to the calling
+user.
+
+As result a map is returned that maps the file path to a list of
+link:#comment-info[CommentInfo] entries. The entries in the map are
+sorted by file path.
+
+.Request
+----
+  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/drafts/ HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java": [
+      {
+        "kind": "gerritcodereview#comment",
+        "id": "TvcXrmjM",
+        "line": 23,
+        "message": "[nit] trailing whitespace",
+        "updated": "2013-02-26 15:40:43.986000000"
+      },
+      {
+        "kind": "gerritcodereview#comment",
+        "id": "TveXwFiA",
+        "line": 49,
+        "in_reply_to": "TfYX-Iuo",
+        "message": "Done",
+        "updated": "2013-02-26 15:40:45.328000000"
+      }
+    ]
+  }
+----
+
+[[create-draft]]
+Create Draft
+~~~~~~~~~~~~
+[verse]
+'PUT /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/drafts'
+
+Creates a draft comment on a revision.
+
+The new draft comment must be provided in the request body inside a
+link:#comment-input[CommentInput] entity.
+
+.Request
+----
+  PUT /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/drafts HTTP/1.0
+  Content-Type: application/json;charset=UTF-8
+
+  {
+    "path": "gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java",
+    "line": 23,
+    "message": "[nit] trailing whitespace"
+  }
+----
+
+As response a link:#comment-info[CommentInfo] entity is returned that
+describes the draft comment.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "kind": "gerritcodereview#comment",
+    "id": "TvcXrmjM",
+    "path": "gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java",
+    "line": 23,
+    "message": "[nit] trailing whitespace",
+    "updated": "2013-02-26 15:40:43.986000000"
+  }
+----
+
+[[get-draft]]
+Get Draft
+~~~~~~~~~
+[verse]
+'GET /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/drafts/link:#draft-id[\{draft-id\}]'
+
+Retrieves a draft comment of a revision that belongs to the calling
+user.
+
+.Request
+----
+  GET /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/drafts/TvcXrmjM HTTP/1.0
+----
+
+As response a link:#comment-info[CommentInfo] entity is returned that
+describes the draft comment.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "kind": "gerritcodereview#comment",
+    "id": "TvcXrmjM",
+    "path": "gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java",
+    "line": 23,
+    "message": "[nit] trailing whitespace",
+    "updated": "2013-02-26 15:40:43.986000000"
+  }
+----
+
+[[update-draft]]
+Update Draft
+~~~~~~~~~~~~
+[verse]
+'PUT /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/drafts/link:#draft-id[\{draft-id\}]'
+
+Updates a draft comment on a revision.
+
+The new draft comment must be provided in the request body inside a
+link:#comment-input[CommentInput] entity.
+
+.Request
+----
+  PUT /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/drafts/TvcXrmjM HTTP/1.0
+  Content-Type: application/json;charset=UTF-8
+
+  {
+    "path": "gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java",
+    "line": 23,
+    "message": "[nit] trailing whitespace"
+  }
+----
+
+As response a link:#comment-info[CommentInfo] entity is returned that
+describes the draft comment.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "kind": "gerritcodereview#comment",
+    "id": "TvcXrmjM",
+    "path": "gerrit-server/src/main/java/com/google/gerrit/server/project/RefControl.java",
+    "line": 23,
+    "message": "[nit] trailing whitespace",
+    "updated": "2013-02-26 15:40:43.986000000"
+  }
+----
+
+[[delete-draft]]
+Delete Draft
+~~~~~~~~~~~~
+[verse]
+'DELETE /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/drafts/link:#draft-id[\{draft-id\}]'
+
+Deletes a draft comment from a revision.
+
+.Request
+----
+  DELETE /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/drafts/TvcXrmjM HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 204 No Content
+----
+
+[[set-reviewed]]
+Set Reviewed
+~~~~~~~~~~~~
+[verse]
+'PUT /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/files/link:#patch-id[\{patch-id\}]/reviewed'
+
+Marks a patch of a revision as reviewed by the calling user.
+
+.Request
+----
+  PUT /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/files/gerrit-server%2Fsrc%2Fmain%2Fjava%2Fcom%2Fgoogle%2Fgerrit%2Fserver%2Fproject%2FRefControl.java/reviewed HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 201 Created
+----
+
+If the patch was already marked as reviewed by the calling user the
+response is "`200 OK`".
+
+[[delete-reviewed]]
+Delete Reviewed
+~~~~~~~~~~~~~~~
+[verse]
+'DELETE /changes/link:#change-id[\{change-id\}]/revisions/link:#revision-id[\{revision-id\}]/files/link:#patch-id[\{patch-id\}]/reviewed'
+
+Deletes the reviewed flag of the calling user from a patch of a revision.
+
+.Request
+----
+  DELETE /changes/myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940/revisions/674ac754f91e64a0efb8087e59a176484bd534d1/files/gerrit-server%2Fsrc%2Fmain%2Fjava%2Fcom%2Fgoogle%2Fgerrit%2Fserver%2Fproject%2FRefControl.java/reviewed HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 204 No Content
+----
+
+
+[[ids]]
+IDs
+---
+
+[[account-id]]
+link:rest-api-accounts.html#account-id[\{account-id\}]
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+--
+--
+
+[[change-id]]
+\{change-id\}
+~~~~~~~~~~~~~
+Identifier that uniquely identifies one change.
+
+This can be:
+
+* an ID of the change in the format "'$$<project>~<branch>~<Change-Id>$$'",
+  where for the branch the `refs/heads/` prefix can be omitted
+  ("$$myProject~master~I8473b95934b5732ac55d26311a706c9c2bde9940$$")
+* a Change-Id if it uniquely identifies one change
+  ("I8473b95934b5732ac55d26311a706c9c2bde9940")
+* a legacy numeric change ID ("4247")
+
+[[draft-id]]
+\{draft-id\}
+~~~~~~~~~~~~
+UUID of a draft comment.
+
+[[patch-id]]
+\{patch-id\}
+~~~~~~~~~~~~
+The file path of the patch.
+
+[[revision-id]]
+\{revision-id\}
+~~~~~~~~~~~~~~~
+Identifier that uniquely identifies one revision of a change.
+
+This can be:
+
+* the literal `current` to name the current patch set/revision
+* a commit ID ("674ac754f91e64a0efb8087e59a176484bd534d1")
+* an abbreviated commit ID that uniquely identifies one revision of the
+  change ("674ac754"), at least 4 digits are required
+* a legacy numeric patch number ("1" for first patch set of the change)
+
+
+[[json-entities]]
+JSON Entities
+-------------
+
+[[abandon-input]]
+AbandonInput
+~~~~~~~~~~~~
+The `AbandonInput` entity contains information for abandoning a change.
+
+[options="header",width="50%",cols="1,^1,5"]
+|===========================
+|Field Name    ||Description
+|`message`     |optional|
+Message to be added as review comment to the change when abandoning the
+change.
+|===========================
+
+[[add-reviewer-result]]
+AddReviewerResult
+~~~~~~~~~~~~~~~~~
+The `AddReviewerResult` entity describes the result of adding a
+reviewer to a change.
+
+[options="header",width="50%",cols="1,^1,5"]
+|===========================
+|Field Name    ||Description
+|`reviewers`   |optional|
+The newly added reviewers as a list of link:#reviewer-info[
+ReviewerInfo] entities.
+|`error`       |optional|
+Error message explaining why the reviewer could not be added. +
+If a group was specified in the input and an error is returned, it
+means that none of the members were added as reviewer.
+|`confirm`     |`false` if not set|
+Whether adding the reviewer requires confirmation.
+|===========================
+
+[[approval-info]]
+ApprovalInfo
+~~~~~~~~~~~~
+The `ApprovalInfo` entity contains information about an approval from a
+user for a label on a change.
+
+`ApprovalInfo` has the same fields as
+link:rest-api-accounts.html#account-info[AccountInfo].
+In addition `ApprovalInfo` has the following fields:
+
+[options="header",width="50%",cols="1,^1,5"]
+|===========================
+|Field Name    ||Description
+|`value`       |optional|
+The vote that the user has given for the label. If present and zero, the
+user is permitted to vote on the label. If absent, the user is not
+permitted to vote on that label.
+|===========================
+
+[[change-info]]
+ChangeInfo
+~~~~~~~~~~
+The `ChangeInfo` entity contains information about a change.
+
+[options="header",width="50%",cols="1,^1,5"]
+|==================================
+|Field Name           ||Description
+|`kind`               ||`gerritcodereview#change`
+|`id`                 ||
+The ID of the change in the format "'<project>\~<branch>~<Change-Id>'",
+where 'project', 'branch' and 'Change-Id' are URL encoded. For 'branch' the
+`refs/heads/` prefix is omitted.
+|`project`            ||The name of the project.
+|`branch`             ||
+The name of the target branch. +
+The `refs/heads/` prefix is omitted.
+|`topic`              |optional|The topic to which this change belongs.
+|`change_id`          ||The Change-Id of the change.
+|`subject`            ||
+The subject of the change (header line of the commit message).
+|`status`             ||
+The status of the change (`NEW`, `SUBMITTED`, `MERGED`, `ABANDONED`,
+`DRAFT`).
+|`created`            ||
+The link:rest-api.html#timestamp[timestamp] of when the change was
+created.
+|`updated`            ||
+The link:rest-api.html#timestamp[timestamp] of when the change was last
+updated.
+|`starred`            |not set if `false`|
+Whether the calling user has starred this change.
+|`reviewed`           |not set if `false`|
+Whether the change was reviewed by the calling user.
+|`mergeable`          |optional|
+Whether the change is mergeable. +
+Not set for merged changes.
+|`_sortkey`           ||The sortkey of the change.
+|`_number`            ||The legacy numeric ID of the change.
+|`owner`              ||
+The owner of the change as an link:rest-api-accounts.html#account-info[
+AccountInfo] entity.
+|`labels`             |optional|
+The labels of the change as a map that maps the label names to
+link:#label-info[LabelInfo] entries. +
+Only set if link:#labels[labels] or link:#detailed-labels[detailed
+labels] are requested.
+|`permitted_labels`   |optional|
+A map of the permitted labels that maps a label name to the list of
+values that are allowed for that label. +
+Only set if link:#detailed-labels[detailed labels] are requested.
+|`removable_reviewers`|optional|
+The reviewers that can be removed by the calling user as a list of
+link:rest-api-accounts.html#account-info[AccountInfo] entities. +
+Only set if link:#detailed-labels[detailed labels] are requested.
+|`current_revision`   |optional|
+The commit ID of the current patch set of this change. +
+Only set if link:#current-revision[the current revision] is requested
+or if link:#all-revisions[all revisions] are requested.
+|`revisions`          |optional|
+All patch sets of this change as a map that maps the commit ID of the
+patch set to a link:#revision-info[RevisionInfo] entity. +
+Only set if link:#all-revisions[all revisions] are requested.
+|`_more_changes`      |optional, not set if `false`|
+Whether the query would deliver more results if not limited. +
+Only set on either the last or the first change that is returned.
+|==================================
+
+[[comment-info]]
+CommentInfo
+~~~~~~~~~~~
+The `CommentInfo` entity contains information about an inline comment.
+
+[options="header",width="50%",cols="1,^1,5"]
+|===========================
+|Field Name    ||Description
+|`kind`        ||`gerritcodereview#comment`
+|`id`          ||The URL encoded UUID of the draft comment.
+|`path`        |optional|
+The path of the file for which the inline comment was done. +
+Not set if returned in a map where the key is the file path.
+|`side`        |optional|
+The side on which the comment was added. +
+Allowed values are `REVISION` and `PARENT`. +
+If not set, the default is `REVISION`.
+|`line`        |optional|
+The number of the line for which the comment was done. +
+If not set, it's a file comment.
+|`in_reply_to` |optional|
+The URL encoded UUID of the comment to which this comment is a reply.
+|`message`     |optional|The comment message.
+|`updated`     ||
+The link:rest-api.html#timestamp[timestamp] of when this comment was
+written.
+|===========================
+
+[[comment-input]]
+CommentInput
+~~~~~~~~~~~~
+The `CommitInput` entity contains information for creating an inline
+comment.
+
+[options="header",width="50%",cols="1,^1,5"]
+|===========================
+|Field Name    ||Description
+|`kind`        |optional|
+Must be `gerritcodereview#comment` if provided.
+|`id`          |optional|
+The URL encoded UUID of the comment if an existing draft comment should
+be updated.
+|`path`        |optional|
+The path of the file for which the inline comment should be added. +
+Doesn't need to be set if contained in a map where the key is the file
+path.
+|`side`        |optional|
+The side on which the comment should be added. +
+Allowed values are `REVISION` and `PARENT`. +
+If not set, the default is `REVISION`.
+|`line`        |optional|
+The number of the line for which the comment should be added. +
+`0` if it is a file comment. +
+If not set, a file comment is added.
+|`in_reply_to` |optional|
+The URL encoded UUID of the comment to which this comment is a reply.
+|`updated`     |optional|
+The link:rest-api.html#timestamp[timestamp] of this comment. +
+Accepted but ignored.
+|`message`     |optional|
+The comment message. +
+If not set and an existing draft comment is updated, the existing draft
+comment is deleted.
+|===========================
+
+[[commit-info]]
+CommitInfo
+~~~~~~~~~~
+The `CommitInfo` entity contains information about a commit.
+
+[options="header",width="50%",cols="1,6"]
+|==========================
+|Field Name    |Description
+|`commit`      |The commit ID.
+|`parent`      |
+The parent commits of this commit as a list of
+link:#commit-info[CommitInfo] entities.
+|`author`      |The author of the commit as a
+link:#git-person-info[GitPersonInfo] entity.
+|`committer`   |The committer of the commit as a
+link:#git-person-info[GitPersonInfo] entity.
+|`subject`     |
+The subject of the commit (header line of the commit message).
+|`message`     |The commit message.
+|==========================
+
+[[fetch-info]]
+FetchInfo
+~~~~~~~~~
+The `FetchInfo` entity contains information about how to fetch a patch
+set via a certain protocol.
+
+[options="header",width="50%",cols="1,6"]
+|==========================
+|Field Name    |Description
+|`url`         |The URL of the project.
+|`ref`         |The ref of the patch set.
+|==========================
+
+[[file-info]]
+FileInfo
+~~~~~~~~
+The `FileInfo` entity contains information about a file in a patch set.
+
+[options="header",width="50%",cols="1,^1,5"]
+|=============================
+|Field Name      ||Description
+|`status`        |optional|
+The status of the file ("`A`"=Added, "`D`"=Deleted, "`R`"=Renamed,
+"`C`"=Copied, "`W`"=Rewritten). +
+Not set if the file was Modified ("`M`").
+|`binary`        |not set if `false`|Whether the file is binary.
+|`old_path`      |optional|
+The old file path. +
+Only set if the file was renamed or copied.
+|`lines_inserted`|optional|
+Number of inserted lines. +
+Not set for binary files or if no lines were inserted.
+|`lines_deleted` |optional|
+Number of deleted lines. +
+Not set for binary files or if no lines were deleted.
+|=============================
+
+[[git-person-info]]
+GitPersonInfo
+~~~~~~~~~~~~~
+The `GitPersonInfo` entity contains information about the
+author/committer of a commit.
+
+[options="header",width="50%",cols="1,6"]
+|==========================
+|Field Name    |Description
+|`name`        |The name of the author/committer.
+|`email`       |The email address of the author/committer.
+|`date`        |The link:rest-api.html#timestamp[timestamp] of when
+this identity was constructed.
+|`tz`          |The timezone offset from UTC of when this identity was
+constructed.
+|==========================
+
+[[label-info]]
+LabelInfo
+~~~~~~~~~
+The `LabelInfo` entity contains information about a label on a change.
+
+[options="header",width="50%",cols="1,^1,5"]
+|===========================
+|Field Name    ||Description
+|`approved`    |optional|The user who approved this label on the change
+as a link:rest-api-accounts.html#account-info[AccountInfo] entity. +
+Only set if link:#labels[labels] are requested.
+|`rejected`    |optional|The user who rejected this label on the change
+as a link:rest-api-accounts.html#account-info[AccountInfo] entity. +
+Only set if link:#labels[labels] are requested.
+|`recommended` |optional|The user who recommended this label on the
+change as a link:rest-api-accounts.html#account-info[AccountInfo] entity. +
+Only set if link:#labels[labels] are requested.
+|`disliked`    |optional|The user who disliked this label on the change
+as a link:rest-api-accounts.html#account-info[AccountInfo] entity. +
+Only set if link:#labels[labels] are requested.
+|`value`       |optional|The voting value of the user who
+recommended/disliked this label on the change if it is not
+"`+1`"/"`-1`". +
+Only set if link:#labels[labels] are requested.
+|`optional`    |not set if `false`|
+Whether the label is optional. Optional means the label may be set, but
+it's neither necessary for submission nor does it block submission if
+set.
+|`all`         |optional|List of all approvals for this label as a list
+of link:#approval-info[ApprovalInfo] entities. +
+Only set if link:#detailed-labels[detailed labels] are requested.
+|`values`      |optional|A map of all values that are allowed for this
+label. The map maps the values ("`-2`", "`-1`", " `0`", "`+1`", "`+2`")
+to the value descriptions. +
+Only set if link:#detailed-labels[detailed labels] are requested.
+|===========================
+
+[[restore-input]]
+RestoreInput
+~~~~~~~~~~~~
+The `RestoreInput` entity contains information for restoring a change.
+
+[options="header",width="50%",cols="1,^1,5"]
+|===========================
+|Field Name    ||Description
+|`message`     |optional|
+Message to be added as review comment to the change when restoring the
+change.
+|===========================
+
+[[revert-input]]
+RevertInput
+~~~~~~~~~~~
+The `RevertInput` entity contains information for reverting a change.
+
+[options="header",width="50%",cols="1,^1,5"]
+|===========================
+|Field Name    ||Description
+|`message`     |optional|
+Message to be added as review comment to the change when reverting the
+change.
+|===========================
+
+[[review-info]]
+ReviewInfo
+~~~~~~~~~~
+The `ReviewInfo` entity contains information about a review.
+
+[options="header",width="50%",cols="1,6"]
+|===========================
+|Field Name     |Description
+|`labels`       |
+The labels of the review as a map that maps the label names to the
+voting values.
+|===========================
+
+[[review-input]]
+ReviewInput
+~~~~~~~~~~~
+The `ReviewInput` entity contains information for adding a review to a
+revision.
+
+[options="header",width="50%",cols="1,^1,5"]
+|============================
+|Field Name     ||Description
+|`message`      |optional|
+The message to be added as review comment.
+|`labels`       |optional|
+The votes that should be added to the revision as a map that maps the
+label names to the voting values.
+|`comments`     |optional|
+The comments that should be added as a map that maps a file path to a
+list of link:#comment-input[CommentInput] entities.
+|`strict_labels`|`true` if not set|
+Whether all labels are required to be within the user's permitted ranges
+based on access controls. +
+If `true`, attempting to use a label not granted to the user will fail
+the entire modify operation early. +
+If `false`, the operation will execute anyway, but the proposed labels
+will be modified to be the "best" value allowed by the access controls.
+|`drafts`      |optional|
+Draft handling that defines how draft comments are handled that are
+already in the database but that were not also described in this
+input. +
+Allowed values are `DELETE`, `PUBLISH` and `KEEP`. +
+If not set, the default is `DELETE`.
+|`notify`      |optional|
+Notify handling that defines to whom email notifications should be sent
+after the review is stored. +
+Allowed values are `NONE`, `OWNER`, `OWNER_REVIEWERS` and `ALL`. +
+If not set, the default is `ALL`.
+|============================
+
+[[reviewer-info]]
+ReviewerInfo
+~~~~~~~~~~~~
+The `ReviewerInfo` entity contains information about a reviewer and its
+votes on a change.
+
+`ReviewerInfo` has the same fields as
+link:rest-api-accounts.html#account-info[AccountInfo] and includes
+link:#detailed-accounts[detailed account information].
+In addition `ReviewerInfo` has the following fields:
+
+[options="header",width="50%",cols="1,6"]
+|==========================
+|Field Name    |Description
+|`kind`        |`gerritcodereview#reviewer`
+|`approvals`   |
+The approvals of the reviewer as a map that maps the label names to the
+approval values ("`-2`", "`-1`", " `0`", "`+1`", "`+2`").
+|==========================
+
+[[reviewer-input]]
+ReviewerInput
+~~~~~~~~~~~~~
+The `ReviewerInput` entity contains information for adding a reviewer
+to a change.
+
+[options="header",width="50%",cols="1,^1,5"]
+|===========================
+|Field Name    ||Description
+|`reviewer`    ||
+The link:rest-api-accounts.html#account-id[ID] of one account that
+should be added as reviewer or the link:rest-api-groups.html#group-id[
+ID] of one group for which all members should be added as reviewers. +
+If an ID identifies both an account and a group, only the account is
+added as reviewer to the change.
+|`confirmed`   |optional|
+Whether adding the reviewer is confirmed. +
+The Gerrit server may be configured to
+link:config-gerrit.html#addreviewer.maxWithoutConfirmation[require a
+confirmation] when adding a group as reviewer that has many members.
+|===========================
+
+[[revision-info]]
+RevisionInfo
+~~~~~~~~~~~~
+The `RevisionInfo` entity contains information about a patch set.
+
+[options="header",width="50%",cols="1,^1,5"]
+|===========================
+|Field Name    ||Description
+|`draft`       |not set if `false`|Whether the patch set is a draft.
+|`_number`     ||The patch set number.
+|`fetch`       ||
+Information about how to fetch this patch set. The fetch information is
+provided as a map that maps the protocol name ("`git`", "`http`",
+"`ssh`") to link:#fetch-info[FetchInfo] entities.
+|`commit`      ||The commit of the patch set as
+link:#commit-info[CommitInfo] entity.
+|`files`       ||
+The files of the patch set as a map that maps the file names to
+link:#file-info[FileInfo] entities.
+|===========================
+
+[[rule-input]]
+RuleInput
+~~~~~~~~~
+The `RuleInput` entity contains information to test a Prolog rule.
+
+[options="header",width="50%",cols="1,^1,5"]
+|===========================
+|Field Name      ||Description
+|`rule`||
+Prolog code to execute instead of the code in `refs/meta/config`.
+|`filters`|`RUN` if not set|
+When `RUN` filter rules in the parent projects are called to
+post-process the results of the project specific rule. This
+behavior matches how the rule will execute if installed. +
+If `SKIP` the parent filters are not called, allowing the test
+to return results from the input rule.
+|===========================
+
+[[submit-info]]
+SubmitInfo
+~~~~~~~~~~
+The `SubmitInfo` entity contains information about the change status
+after submitting.
+
+[options="header",width="50%",cols="1,6"]
+|==========================
+|Field Name    |Description
+|`status`      |
+The status of the change after submitting, can be `MERGED` or
+`SUBMITTED`. +
+If `wait_for_merge` in the link:#submit-input[SubmitInput] was set to
+`false` the returned status is `SUBMITTED` and the caller can't know
+whether the change could be merged successfully.
+|==========================
+
+[[submit-input]]
+SubmitInput
+~~~~~~~~~~~
+The `SubmitInput` entity contains information for submitting a change.
+
+[options="header",width="50%",cols="1,^1,5"]
+|===========================
+|Field Name      ||Description
+|`wait_for_merge`|`false` if not set|
+Whether the request should wait for the merge to complete. +
+If `false` the request returns immediately after the change has been
+added to the merge queue and the caller can't know whether the change
+could be merged successfully.
+|===========================
+
+[[submit-record]]
+SubmitRecord
+~~~~~~~~~~~~
+The `SubmitRecord` entity describes results from a submit_rule.
+
+[options="header",width="50%",cols="1,^1,5"]
+|===========================
+|Field Name      ||Description
+|`status`||
+`OK`, the change can be submitted. +
+`NOT_READY`, additional labels are required before submit. +
+`CLOSED`, closed changes cannot be submitted. +
+`RULE_ERROR`, rule code failed with an error.
+|`ok`|optional|
+Map of labels that are approved; an
+link:rest-api-accounts.html#account-info[AccountInfo] identifies the
+voter chosen by the rule.
+|`reject`|optional|
+Map of labels that are preventing submit;
+link:rest-api-accounts.html#account-info[AccountInfo] identifies voter.
+|`need`|optional|
+Map of labels that need to be given to submit. The value is
+currently an empty object.
+|`may`|optional|
+Map of labels that can be used, but do not affect submit.
+link:rest-api-accounts.html#account-info[AccountInfo] identifies voter,
+if the label has been applied.
+|`impossible`|optional|
+Map of labels that should have been in `need` but cannot be
+used by any user because of access restrictions. The value
+is currently an empty object.
+|`error_message`|optional|
+When status is RULE_ERROR this message provides some text describing
+the failure of the rule predicate.
+|===========================
+
+[[topic-input]]
+TopicInput
+~~~~~~~~~~
+The `TopicInput` entity contains information for setting a topic.
+
+[options="header",width="50%",cols="1,^1,5"]
+|===========================
+|Field Name    ||Description
+|`topic`       |optional|The topic. +
+The topic will be deleted if not set.
+|`message`     |optional|
+Message to be added as review comment to the change when setting the
+topic.
+|===========================
+
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/rest-api-groups.txt b/Documentation/rest-api-groups.txt
new file mode 100644
index 0000000..e800d56
--- /dev/null
+++ b/Documentation/rest-api-groups.txt
@@ -0,0 +1,1229 @@
+Gerrit Code Review - /groups/ REST API
+======================================
+
+This page describes the group related REST endpoints.
+Please also take note of the general information on the
+link:rest-api.html[REST API].
+
+[[group-endpoints]]
+Group Endpoints
+---------------
+
+[[list-groups]]
+List Groups
+~~~~~~~~~~~
+[verse]
+'GET /groups/'
+
+Lists the groups accessible by the caller. This is the same as
+using the link:cmd-ls-groups.html[ls-groups] command over SSH,
+and accepts the same options as query parameters.
+
+As result a map is returned that maps the group names to
+link:#group-info[GroupInfo] entries. The entries in the map are sorted
+by group name.
+
+.Request
+----
+  GET /groups/ HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "Administrators": {
+      "kind": "gerritcodereview#group",
+      "id": "6a1e70e1a88782771a91808c8af9bbb7a9871389",
+      "url": "#/admin/groups/uuid-6a1e70e1a88782771a91808c8af9bbb7a9871389",
+      "options": {
+      },
+      "description": "Gerrit Site Administrators",
+      "group_id": 1,
+      "owner": "Administrators",
+      "owner_id": "6a1e70e1a88782771a91808c8af9bbb7a9871389"
+    },
+    "Anonymous Users": {
+      "kind": "gerritcodereview#group",
+      "id": "global%3AAnonymous-Users",
+      "url": "#/admin/groups/uuid-global%3AAnonymous-Users",
+      "options": {
+      },
+      "description": "Any user, signed-in or not",
+      "group_id": 2,
+      "owner": "Administrators",
+      "owner_id": "6a1e70e1a88782771a91808c8af9bbb7a9871389"
+    },
+    "MyProject_Committers": {
+      "kind": "gerritcodereview#group",
+      "id": "834ec36dd5e0ed21a2ff5d7e2255da082d63bbd7",
+      "url": "#/admin/groups/uuid-834ec36dd5e0ed21a2ff5d7e2255da082d63bbd7",
+      "options": {
+        "visible_to_all": true,
+      },
+      "group_id": 6,
+      "owner": "MyProject_Committers",
+      "owner_id": "834ec36dd5e0ed21a2ff5d7e2255da082d63bbd7"
+    },
+    "Non-Interactive Users": {
+      "kind": "gerritcodereview#group",
+      "id": "5057f3cbd3519d6ab69364429a89ffdffba50f73",
+      "url": "#/admin/groups/uuid-5057f3cbd3519d6ab69364429a89ffdffba50f73",
+      "options": {
+      },
+      "description": "Users who perform batch actions on Gerrit",
+      "group_id": 4,
+      "owner": "Administrators",
+      "owner_id": "6a1e70e1a88782771a91808c8af9bbb7a9871389"
+    },
+    "Project Owners": {
+      "kind": "gerritcodereview#group",
+      "id": "global%3AProject-Owners",
+      "url": "#/admin/groups/uuid-global%3AProject-Owners",
+      "options": {
+      },
+      "description": "Any owner of the project",
+      "group_id": 5,
+      "owner": "Administrators",
+      "owner_id": "6a1e70e1a88782771a91808c8af9bbb7a9871389"
+    },
+    "Registered Users": {
+      "kind": "gerritcodereview#group",
+      "id": "global%3ARegistered-Users",
+      "url": "#/admin/groups/uuid-global%3ARegistered-Users",
+      "options": {
+      },
+      "description": "Any signed-in user",
+      "group_id": 3,
+      "owner": "Administrators",
+      "owner_id": "6a1e70e1a88782771a91808c8af9bbb7a9871389"
+    }
+  }
+----
+
+.Get all groups
+****
+get::/groups/
+****
+
+[[group-options]]
+Group Options
+^^^^^^^^^^^^^
+Additional fields can be obtained by adding `o` parameters, each option
+requires more lookups and slows down the query response time to the
+client so they are generally disabled by default. Optional fields are:
+
+[[includes]]
+--
+* `INCLUDES`: include list of directly included groups.
+--
+
+[[members]]
+--
+* `MEMBERS`: include list of direct group members.
+--
+
+Check if a group is owned by the calling user
+^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
+By setting the option `owned` and specifying a group to inspect with
+the option `q`, it is possible to find out, if this group is owned by
+the calling user.
+
+.Request
+----
+  GET /groups/?owned&q=MyProject-Committers HTTP/1.0
+----
+
+If the group is owned by the calling user, the returned map contains
+this group. If the calling user doesn't own this group an empty map is
+returned.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "MyProject-Committers": {
+      "kind": "gerritcodereview#group",
+      "id": "9999c971bb4ab872aab759d8c49833ee6b9ff320",
+      "url": "#/admin/groups/uuid-9999c971bb4ab872aab759d8c49833ee6b9ff320",
+      "options": {
+        "visible_to_all": true
+      },
+      "description":"contains all committers for MyProject",
+      "group_id": 551,
+      "owner": "MyProject-Owners",
+      "owner_id": "7ca042f4d5847936fcb90ca91057673157fd06fc"
+    }
+  }
+----
+
+[[get-group]]
+Get Group
+~~~~~~~~~
+[verse]
+'GET /groups/link:#group-id[\{group-id\}]'
+
+Retrieves a group.
+
+.Request
+----
+  GET /groups/6a1e70e1a88782771a91808c8af9bbb7a9871389 HTTP/1.0
+----
+
+As response a link:#group-info[GroupInfo] entity is returned that
+describes the group.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "kind": "gerritcodereview#group",
+    "id": "6a1e70e1a88782771a91808c8af9bbb7a9871389",
+    "name": "Administrators",
+    "url": "#/admin/groups/uuid-6a1e70e1a88782771a91808c8af9bbb7a9871389",
+    "options": {
+    },
+    "description": "Gerrit Site Administrators",
+    "group_id": 1,
+    "owner": "Administrators",
+    "owner_id": "6a1e70e1a88782771a91808c8af9bbb7a9871389"
+  }
+----
+
+[[create-group]]
+Create Group
+~~~~~~~~~~~~
+[verse]
+'PUT /groups/link:#group-name[\{group-name\}]'
+
+Creates a new Gerrit internal group.
+
+In the request body additional data for the group can be provided as
+link:#group-input[GroupInput].
+
+.Request
+----
+  PUT /groups/MyProject-Committers HTTP/1.0
+  Content-Type: application/json;charset=UTF-8
+
+  {
+    "description": "contains all committers for MyProject",
+    "visible_to_all": true,
+    "owner": "MyProject-Owners",
+    "owner_id": "7ca042f4d5847936fcb90ca91057673157fd06fc"
+  }
+----
+
+As response the link:#group-info[GroupInfo] entity is returned that
+describes the created group.
+
+.Response
+----
+  HTTP/1.1 201 Created
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "kind": "gerritcodereview#group",
+    "id": "9999c971bb4ab872aab759d8c49833ee6b9ff320",
+    "name": "MyProject-Committers",
+    "url": "#/admin/groups/uuid-9999c971bb4ab872aab759d8c49833ee6b9ff320",
+    "options": {
+      "visible_to_all": true
+    },
+    "description":"contains all committers for MyProject",
+    "group_id": 551,
+    "owner": "MyProject-Owners",
+    "owner_id": "7ca042f4d5847936fcb90ca91057673157fd06fc"
+  }
+----
+
+If the group creation fails because the name is already in use the
+response is "`409 Conflict`".
+
+[[get-group-detail]]
+Get Group Detail
+~~~~~~~~~~~~~~~~
+[verse]
+'GET /groups/link:#group-id[\{group-id\}]/detail'
+
+Retrieves a group with the direct link:#members[members] and the
+directly link:#includes[included groups].
+
+.Request
+----
+  GET /groups/6a1e70e1a88782771a91808c8af9bbb7a9871389/detail HTTP/1.0
+----
+
+As response a link:#group-info[GroupInfo] entity is returned that
+describes the group.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "kind": "gerritcodereview#group",
+    "id": "6a1e70e1a88782771a91808c8af9bbb7a9871389",
+    "name": "Administrators",
+    "url": "#/admin/groups/uuid-6a1e70e1a88782771a91808c8af9bbb7a9871389",
+    "options": {
+    },
+    "description": "Gerrit Site Administrators",
+    "group_id": 1,
+    "owner": "Administrators",
+    "owner_id": "6a1e70e1a88782771a91808c8af9bbb7a9871389",
+    "members": [
+      {
+        "_account_id": 1000097,
+        "name": "Jane Roe",
+        "email": "jane.roe@example.com"
+      },
+      {
+        "_account_id": 1000096,
+        "name": "John Doe",
+        "email": "john.doe@example.com"
+      }
+    ],
+    "includes": []
+  }
+----
+
+[[get-group-name]]
+Get Group Name
+~~~~~~~~~~~~~~
+[verse]
+'GET /groups/link:#group-id[\{group-id\}]/name'
+
+Retrieves the name of a group.
+
+.Request
+----
+  GET /groups/9999c971bb4ab872aab759d8c49833ee6b9ff320/name HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  "MyProject-Committers"
+----
+
+[[rename-group]]
+Rename Group
+~~~~~~~~~~~~
+[verse]
+'PUT /groups/link:#group-id[\{group-id\}]/name'
+
+Renames a Gerrit internal group.
+
+The new group name must be provided in the request body.
+
+.Request
+----
+  PUT /groups/MyProject-Committers/name HTTP/1.0
+  Content-Type: application/json;charset=UTF-8
+
+  {
+    "name": "My-Project-Committers"
+  }
+----
+
+As response the new group name is returned.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  "My-Project-Committers"
+----
+
+If renaming the group fails because the new name is already in use the
+response is "`409 Conflict`".
+
+[[get-group-description]]
+Get Group Description
+~~~~~~~~~~~~~~~~~~~~~
+[verse]
+'GET /groups/link:#group-id[\{group-id\}]/description'
+
+Retrieves the description of a group.
+
+.Request
+----
+  GET /groups/9999c971bb4ab872aab759d8c49833ee6b9ff320/description HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  "contains all committers for MyProject"
+----
+
+If the group does not have a description an empty string is returned.
+
+[[set-group-description]]
+Set Group Description
+~~~~~~~~~~~~~~~~~~~~~
+[verse]
+'PUT /groups/link:#group-id[\{group-id\}]/description'
+
+Sets the description of a Gerrit internal group.
+
+The new group description must be provided in the request body.
+
+.Request
+----
+  PUT /groups/9999c971bb4ab872aab759d8c49833ee6b9ff320/description HTTP/1.0
+  Content-Type: application/json;charset=UTF-8
+
+  {
+    "description": "The committers of MyProject."
+  }
+----
+
+As response the new group description is returned.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  "The committers of MyProject."
+----
+
+If the description was deleted the response is "`204 No Content`".
+
+[[delete-group-description]]
+Delete Group Description
+~~~~~~~~~~~~~~~~~~~~~~~~
+[verse]
+'DELETE /groups/link:#group-id[\{group-id\}]/description'
+
+Deletes the description of a Gerrit internal group.
+
+.Request
+----
+  DELETE /groups/9999c971bb4ab872aab759d8c49833ee6b9ff320/description HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 204 No Content
+----
+
+[[get-group-options]]
+Get Group Options
+~~~~~~~~~~~~~~~~~
+[verse]
+'GET /groups/link:#group-id[\{group-id\}]/options'
+
+Retrieves the options of a group.
+
+.Request
+----
+  GET /groups/9999c971bb4ab872aab759d8c49833ee6b9ff320/options HTTP/1.0
+----
+
+As response a link:#group-options-info[GroupOptionsInfo] entity is
+returned that describes the options of the group.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "visible_to_all": true
+  }
+----
+
+[[set-group-options]]
+Set Group Options
+~~~~~~~~~~~~~~~~~
+[verse]
+'PUT /groups/link:#group-id[\{group-id\}]/options'
+
+Sets the options of a Gerrit internal group.
+
+The new group options must be provided in the request body as a
+link:#group-options-input[GroupOptionsInput] entity.
+
+.Request
+----
+  PUT /groups/9999c971bb4ab872aab759d8c49833ee6b9ff320/options HTTP/1.0
+  Content-Type: application/json;charset=UTF-8
+
+  {
+    "visible_to_all": true
+  }
+----
+
+As response the new group options are returned as a
+link:#group-options-info[GroupOptionsInfo] entity.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "visible_to_all": true
+  }
+----
+
+[[get-group-owner]]
+Get Group Owner
+~~~~~~~~~~~~~~~
+[verse]
+'GET /groups/link:#group-id[\{group-id\}]/owner'
+
+Retrieves the owner group of a Gerrit internal group.
+
+.Request
+----
+  GET /groups/9999c971bb4ab872aab759d8c49833ee6b9ff320/owner HTTP/1.0
+----
+
+As response a link:#group-info[GroupInfo] entity is returned that
+describes the owner group.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "kind": "gerritcodereview#group",
+    "id": "6a1e70e1a88782771a91808c8af9bbb7a9871389",
+    "name": "Administrators",
+    "url": "#/admin/groups/uuid-6a1e70e1a88782771a91808c8af9bbb7a9871389",
+    "options": {
+    },
+    "description": "Gerrit Site Administrators",
+    "group_id": 1,
+    "owner": "Administrators",
+    "owner_id": "6a1e70e1a88782771a91808c8af9bbb7a9871389"
+  }
+----
+
+[[set-group-owner]]
+Set Group Owner
+~~~~~~~~~~~~~~~
+[verse]
+'PUT /groups/link:#group-id[\{group-id\}]/owner'
+
+Sets the owner group of a Gerrit internal group.
+
+The new owner group must be provided in the request body.
+
+The new owner can be specified by name, by group UUID or by the legacy
+numeric group ID.
+
+.Request
+----
+  PUT /groups/9999c971bb4ab872aab759d8c49833ee6b9ff320/description HTTP/1.0
+  Content-Type: application/json;charset=UTF-8
+
+  {
+    "owner": "6a1e70e1a88782771a91808c8af9bbb7a9871389"
+  }
+----
+
+As response a link:#group-info[GroupInfo] entity is returned that
+describes the new owner group.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "kind": "gerritcodereview#group",
+    "id": "6a1e70e1a88782771a91808c8af9bbb7a9871389",
+    "name": "Administrators",
+    "url": "#/admin/groups/uuid-6a1e70e1a88782771a91808c8af9bbb7a9871389",
+    "options": {
+    },
+    "description": "Gerrit Site Administrators",
+    "group_id": 1,
+    "owner": "Administrators",
+    "owner_id": "6a1e70e1a88782771a91808c8af9bbb7a9871389"
+  }
+----
+
+[[group-member-endpoints]]
+Group Member Endpoints
+----------------------
+
+[[group-members]]
+List Group Members
+~~~~~~~~~~~~~~~~~~
+[verse]
+'GET /groups/link:#group-id[\{group-id\}]/members/'
+
+Lists the direct members of a Gerrit internal group.
+
+As result a list of detailed link:rest-api-accounts.html#account-info[
+AccountInfo] entries is returned. The entries in the list are sorted by
+full name, preferred email and id.
+
+.Request
+----
+  GET /groups/834ec36dd5e0ed21a2ff5d7e2255da082d63bbd7/members/ HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  [
+    {
+      "_account_id": 1000097,
+      "name": "Jane Roe",
+      "email": "jane.roe@example.com"
+    },
+    {
+      "_account_id": 1000096,
+      "name": "John Doe",
+      "email": "john.doe@example.com"
+    }
+  ]
+----
+
+.Get all members of the 'Administrators' group (normally group id = 1)
+****
+get::/groups/1/members/
+****
+
+To resolve the included groups of a group recursively and to list all
+members the parameter `recursive` can be set.
+
+Members from included external groups and from included groups which
+are not visible to the calling user are ignored.
+
+.Request
+----
+  GET /groups/834ec36dd5e0ed21a2ff5d7e2255da082d63bbd7/members/?recursive HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  [
+    {
+      "_account_id": 1000097,
+      "name": "Jane Roe",
+      "email": "jane.roe@example.com"
+    },
+    {
+      "_account_id": 1000096,
+      "name": "John Doe",
+      "email": "john.doe@example.com"
+    },
+    {
+      "_account_id": 1000098,
+      "name": "Richard Roe",
+      "email": "richard.roe@example.com"
+    }
+  ]
+----
+
+[[get-group-member]]
+Get Group Member
+~~~~~~~~~~~~~~~~
+[verse]
+'GET /groups/link:#group-id[\{group-id\}]/members/link:rest-api-accounts.html#account-id[\{account-id\}]'
+
+Retrieves a group member.
+
+.Request
+----
+  GET /groups/834ec36dd5e0ed21a2ff5d7e2255da082d63bbd7/members/1000096 HTTP/1.0
+----
+
+As response a detailed link:rest-api-accounts.html#account-info[
+AccountInfo] entity is returned that describes the group member.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "_account_id": 1000096,
+    "name": "John Doe",
+    "email": "john.doe@example.com"
+  }
+----
+
+[[add-group-member]]
+Add Group Member
+~~~~~~~~~~~~~~~~
+[verse]
+'PUT /groups/link:#group-id[\{group-id\}]/members/link:rest-api-accounts.html#account-id[\{account-id\}]'
+
+Adds a user as member to a Gerrit internal group.
+
+.Request
+----
+  PUT /groups/MyProject-Committers/members/John%20Doe HTTP/1.0
+----
+
+As response a detailed link:rest-api-accounts.html#account-info[
+AccountInfo] entity is returned that describes the group member.
+
+.Response
+----
+  HTTP/1.1 201 Created
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "_account_id": 1000037,
+    "name": "John Doe",
+    "email": "john.doe@example.com"
+  }
+----
+
+The request also succeeds if the user is already a member of this
+group, but then the HTTP response code is `200 OK`.
+
+Add Group Members
+~~~~~~~~~~~~~~~~~
+[verse]
+'POST /groups/link:#group-id[\{group-id\}]/members'
+
+OR
+
+[verse]
+'POST /groups/link:#group-id[\{group-id\}]/members.add'
+
+Adds one or several users to a Gerrit internal group.
+
+The users to be added to the group must be provided in the request body
+as a link:#members-input[MembersInput] entity.
+
+.Request
+----
+  POST /groups/MyProject-Committers/members.add HTTP/1.0
+  Content-Type: application/json;charset=UTF-8
+
+  {
+    "members": {
+      "jane.roe@example.com",
+      "john.doe@example.com"
+    }
+  }
+----
+
+As response a list of detailed link:rest-api-accounts.html#account-info[
+AccountInfo] entities is returned that describes the group members that
+were specified in the link:#members-input[MembersInput]. An
+link:rest-api-accounts.html#account-info[AccountInfo] entity
+is returned for each user specified in the input, independently of
+whether the user was newly added to the group or whether the user was
+already a member of the group.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  [
+    {
+      "_account_id": 1000057,
+      "name": "Jane Roe",
+      "email": "jane.roe@example.com"
+    },
+    {
+      "_account_id": 1000037,
+      "name": "John Doe",
+      "email": "john.doe@example.com"
+    }
+  ]
+----
+
+[[delete-group-member]]
+Delete Group Member
+~~~~~~~~~~~~~~~~~~~
+[verse]
+'DELETE /groups/link:#group-id[\{group-id\}]/members/link:rest-api-accounts.html#account-id[\{account-id\}]'
+
+Deletes a user from a Gerrit internal group.
+
+.Request
+----
+  DELETE /groups/MyProject-Committers/members/John%20Doe HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 204 No Content
+----
+
+[[delete-group-members]]
+Delete Group Members
+~~~~~~~~~~~~~~~~~~~~
+[verse]
+'POST /groups/link:#group-id[\{group-id\}]/members.delete'
+
+Delete one or several users from a Gerrit internal group.
+
+The users to be deleted from the group must be provided in the request
+body as a link:#members-input[MembersInput] entity.
+
+.Request
+----
+  POST /groups/MyProject-Committers/members.delete HTTP/1.0
+  Content-Type: application/json;charset=UTF-8
+
+  {
+    "members": {
+      "jane.roe@example.com",
+      "john.doe@example.com"
+    }
+  }
+----
+
+.Response
+----
+  HTTP/1.1 204 No Content
+----
+
+[[group-include-endpoints]]
+Group Include Endpoints
+-----------------------
+
+[[included-groups]]
+List Included Groups
+~~~~~~~~~~~~~~~~~~~~
+[verse]
+'GET /groups/link:#group-id[\{group-id\}]/groups/'
+
+Lists the directly included groups of a group.
+
+As result a list of link:#group-info[GroupInfo] entries is returned.
+The entries in the list are sorted by group name and UUID.
+
+.Request
+----
+  GET /groups/834ec36dd5e0ed21a2ff5d7e2255da082d63bbd7/groups/ HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  [
+    {
+      "kind": "gerritcodereview#group",
+      "id": "7ca042f4d5847936fcb90ca91057673157fd06fc",
+      "name": "MyProject-Verifiers",
+      "url": "#/admin/groups/uuid-7ca042f4d5847936fcb90ca91057673157fd06fc",
+      "options": {
+      },
+      "group_id": 38,
+      "owner": "MyProject-Verifiers",
+      "owner_id": "7ca042f4d5847936fcb90ca91057673157fd06fc"
+    }
+  ]
+----
+
+[[get-included-group]]
+Get Included Group
+~~~~~~~~~~~~~~~~~~
+[verse]
+'GET /groups/link:#group-id[\{group-id\}]/groups/link:#group-id[\{group-id\}]'
+
+Retrieves an included group.
+
+.Request
+----
+  GET /groups/834ec36dd5e0ed21a2ff5d7e2255da082d63bbd7/groups/7ca042f4d5847936fcb90ca91057673157fd06fc HTTP/1.0
+----
+
+As response a link:#group-info[GroupInfo] entity is returned that
+describes the included group.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "kind": "gerritcodereview#group",
+    "id": "7ca042f4d5847936fcb90ca91057673157fd06fc",
+    "name": "MyProject-Verifiers",
+    "url": "#/admin/groups/uuid-7ca042f4d5847936fcb90ca91057673157fd06fc",
+    "options": {
+    },
+    "group_id": 38,
+    "owner": "Administrators",
+    "owner_id": "6a1e70e1a88782771a91808c8af9bbb7a9871389"
+  }
+----
+
+[[include-group]]
+Include Group
+~~~~~~~~~~~~~
+[verse]
+'PUT /groups/link:#group-id[\{group-id\}]/groups/link:#group-id[\{group-id\}]'
+
+Includes a group into a Gerrit internal group.
+
+.Request
+----
+  PUT /groups/MyProject-Committers/groups/MyGroup HTTP/1.0
+----
+
+As response a link:#group-info[GroupInfo] entity is returned that
+describes the included group.
+
+.Response
+----
+  HTTP/1.1 201 Created
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "kind": "gerritcodereview#group",
+    "id": "6a1e70e1a88782771a91808c8af9bbb7a9871389",
+    "name": "MyGroup",
+    "url": "#/admin/groups/uuid-6a1e70e1a88782771a91808c8af9bbb7a9871389",
+    "options": {
+    },
+    "group_id": 8,
+    "owner": "Administrators",
+    "owner_id": "6a1e70e1a88782771a91808c8af9bbb7a9871389"
+  }
+----
+
+The request also succeeds if the group is already included in this
+group, but then the HTTP response code is `200 OK`.
+
+[[include-groups]]
+Include Groups
+~~~~~~~~~~~~~~
+[verse]
+'POST /groups/link:#group-id[\{group-id\}]/groups'
+
+OR
+
+[verse]
+'POST /groups/link:#group-id[\{group-id\}]/groups.add'
+
+Includes one or several groups into a Gerrit internal group.
+
+The groups to be included into the group must be provided in the
+request body as a link:#groups-input[GroupsInput] entity.
+
+.Request
+----
+  POST /groups/MyProject-Committers/groups.add HTTP/1.0
+  Content-Type: application/json;charset=UTF-8
+
+  {
+    "groups": {
+      "MyGroup",
+      "MyOtherGroup"
+    }
+  }
+----
+
+As response a list of link:#group-info[GroupInfo] entities is
+returned that describes the groups that were specified in the
+link:#groups-input[GroupsInput]. A link:#group-info[GroupInfo] entity
+is returned for each group specified in the input, independently of
+whether the group was newly included into the group or whether the
+group was already included in the group.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  [
+    {
+      "kind": "gerritcodereview#group",
+      "id": "6a1e70e1a88782771a91808c8af9bbb7a9871389",
+      "name": "MyGroup",
+      "url": "#/admin/groups/uuid-6a1e70e1a88782771a91808c8af9bbb7a9871389",
+      "options": {
+      },
+      "group_id": 8,
+      "owner": "Administrators",
+      "owner_id": "6a1e70e1a88782771a91808c8af9bbb7a9871389"
+    },
+    {
+      "kind": "gerritcodereview#group",
+      "id": "5057f3cbd3519d6ab69364429a89ffdffba50f73",
+      "name": "MyOtherGroup",
+      "url": "#/admin/groups/uuid-5057f3cbd3519d6ab69364429a89ffdffba50f73",
+      "options": {
+      },
+      "group_id": 10,
+      "owner": "MyOtherGroup",
+      "owner_id": "5057f3cbd3519d6ab69364429a89ffdffba50f73"
+    }
+  ]
+----
+
+[[delete-included-group]]
+Delete Included Group
+~~~~~~~~~~~~~~~~~~~~~
+[verse]
+'DELETE /groups/link:#group-id[\{group-id\}]/groups/link:#group-id[\{group-id\}]'
+
+Deletes an included group from a Gerrit internal group.
+
+.Request
+----
+  DELETE /groups/MyProject-Committers/groups/MyGroup HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 204 No Content
+----
+
+[[delete-included-groups]]
+Delete Included Groups
+~~~~~~~~~~~~~~~~~~~~~~
+[verse]
+'POST /groups/link:#group-id[\{group-id\}]/groups.delete'
+
+Delete one or several included groups from a Gerrit internal group.
+
+The groups to be deleted from the group must be provided in the request
+body as a link:#groups-input[GroupsInput] entity.
+
+.Request
+----
+  POST /groups/MyProject-Committers/groups.delete HTTP/1.0
+  Content-Type: application/json;charset=UTF-8
+
+  {
+    "members": {
+      "MyGroup",
+      "MyOtherGroup"
+    }
+  }
+----
+
+.Response
+----
+  HTTP/1.1 204 No Content
+----
+
+
+[[ids]]
+IDs
+---
+
+[[account-id]]
+link:rest-api-accounts.html#account-id[\{account-id\}]
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+--
+--
+
+[[group-id]]
+\{group-id\}
+~~~~~~~~~~~~
+Identifier for a group.
+
+This can be:
+
+* the UUID of the group
+* the legacy numeric ID of the group
+* the name of the group if it is unique
+
+[[group-name]]
+\{group-name\}
+~~~~~~~~~~~~~~
+Group name that uniquely identifies one group.
+
+
+[[json-entities]]
+JSON Entities
+-------------
+
+[[group-info]]
+GroupInfo
+~~~~~~~~~
+The `GroupInfo` entity contains information about a group. This can be
+a Gerrit internal group, or an external group that is known to Gerrit.
+
+[options="header",width="50%",cols="1,^1,5"]
+|===========================
+|Field Name    ||Description
+|`kind`        ||`gerritcodereview#group`
+|`id`          ||The URL encoded UUID of the group.
+|`name`        |
+not set if returned in a map where the group name is used as map key|
+The name of the group.
+|`url`         |optional|
+URL to information about the group. Typically a URL to a web page that
+permits users to apply to join the group, or manage their membership.
+|`options`     ||link:#group-options-info[Options of the group]
+|`description` |only for internal groups|The description of the group.
+|`group_id`    |only for internal groups|The numeric ID of the group.
+|`owner`       |only for internal groups|The name of the owner group.
+|`owner_id`    |only for internal groups|The URL encoded UUID of the owner group.
+|`members`     |optional, only for internal groups|
+A list of link:rest-api-accounts.html#account-info[AccountInfo]
+entities describing the direct members. +
+Only set if link:#members[members] are requested.
+|`includes`    |optional, only for internal groups|
+A list of link:#group-info[GroupInfo] entities describing the directly
+included groups. +
+Only set if link:#includes[included groups] are requested.
+|===========================
+
+The type of a group can be deduced from the group's UUID:
+[width="50%"]
+|============
+|UUID matches "^[0-9a-f]\{40\}$"|Gerrit internal group
+|UUID starts with "global:"|Gerrit system group
+|UUID starts with "ldap:"|LDAP group
+|UUID starts with "<prefix>:"|other external group
+|============
+
+[[group-input]]
+GroupInput
+~~~~~~~~~~
+The 'GroupInput' entity contains information for the creation of
+a new internal group.
+
+[options="header",width="50%",cols="1,^1,5"]
+|===========================
+|Field Name      ||Description
+|`name`          |optional|The name of the group (not encoded). +
+If set, must match the group name in the URL.
+|`description`   |optional|The description of the group.
+|`visible_to_all`|optional|
+Whether the group is visible to all registered users. +
+`false` if not set.
+|`owner_id`|optional|The URL encoded ID of the owner group. +
+This can be a group UUID, a legacy numeric group ID or a unique group
+name. +
+If not set, the new group will be self-owned.
+|===========================
+
+[[groups-input]]
+GroupsInput
+~~~~~~~~~~~
+The `GroupsInput` entity contains information about groups that should
+be included into a group or that should be deleted from a group.
+
+[options="header",width="50%",cols="1,^1,5"]
+|==========================
+|Field Name   ||Description
+|`_one_group` |optional|
+The link:#group-id[id] of one group that should be included or deleted.
+|`groups`     |optional|
+A list of link:#group-id[group ids] that identify the groups that
+should be included or deleted.
+|==========================
+
+[[group-options-info]]
+GroupOptionsInfo
+~~~~~~~~~~~~~~~~
+Options of the group.
+
+[options="header",width="50%",cols="1,^1,5"]
+|=============================
+|Field Name      ||Description
+|`visible_to_all`|not set if `false`|
+Whether the group is visible to all registered users.
+|=============================
+
+[[group-options-input]]
+GroupOptionsInput
+~~~~~~~~~~~~~~~~~
+New options for a group.
+
+[options="header",width="50%",cols="1,^1,5"]
+|=============================
+|Field Name      ||Description
+|`visible_to_all`|not set if `false`|
+Whether the group is visible to all registered users.
+|=============================
+
+[[members-input]]
+MembersInput
+~~~~~~~~~~~
+The `MembersInput` entity contains information about accounts that should
+be added as members to a group or that should be deleted from the group.
+
+[options="header",width="50%",cols="1,^1,5"]
+|==========================
+|Field Name   ||Description
+|`_one_member`|optional|
+The link:#account-id[id] of one account that should be added or
+deleted.
+|`members`    |optional|
+A list of link:#account-id[account ids] that identify the accounts that
+should be added or deleted.
+|==========================
+
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/rest-api-projects.txt b/Documentation/rest-api-projects.txt
new file mode 100644
index 0000000..9aba0e9
--- /dev/null
+++ b/Documentation/rest-api-projects.txt
@@ -0,0 +1,902 @@
+Gerrit Code Review - /projects/ REST API
+========================================
+
+This page describes the project related REST endpoints.
+Please also take note of the general information on the
+link:rest-api.html[REST API].
+
+Endpoints
+---------
+
+[[project-endpoints]]
+Project Endpoints
+-----------------
+
+[[list-projects]]
+List Projects
+~~~~~~~~~~~~~
+[verse]
+'GET /projects/'
+
+Lists the projects accessible by the caller. This is the same as
+using the link:cmd-ls-projects.html[ls-projects] command over SSH,
+and accepts the same options as query parameters.
+
+As result a map is returned that maps the project names to
+link:#project-info[ProjectInfo] entries. The entries in the map are sorted
+by project name.
+
+.Request
+----
+  GET /projects/?d HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "external/bison": {
+      "kind": "gerritcodereview#project",
+      "id": "external%2Fbison",
+      "description": "GNU parser generator"
+    },
+    "external/gcc": {
+      "kind": "gerritcodereview#project",
+      "id": "external%2Fgcc",
+    },
+    "external/openssl": {
+      "kind": "gerritcodereview#project",
+      "id": "external%2Fopenssl",
+      "description": "encryption\ncrypto routines"
+    },
+    "test": {
+      "kind": "gerritcodereview#project",
+      "id": "test",
+      "description": "\u003chtml\u003e is escaped"
+    }
+  }
+----
+
+.Get all projects with their description
+****
+get::/projects/?d
+****
+
+[[suggest-projects]]
+The `/projects/` URL also accepts a prefix string in the `p` parameter.
+This limits the results to those projects that start with the specified
+prefix.
+List all projects that start with `platform/`:
+
+.Request
+----
+  GET /projects/?p=platform%2F HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "platform/drivers": {
+      "kind": "gerritcodereview#project",
+      "id": "platform%2Fdrivers",
+    },
+    "platform/tools": {
+      "kind": "gerritcodereview#project",
+      "id": "platform%2Ftools",
+    }
+  }
+----
+E.g. this feature can be used by suggestion client UI's to limit results.
+
+[[get-project]]
+Get Project
+~~~~~~~~~~~
+[verse]
+'GET /projects/link:#project-name[\{project-name\}]'
+
+Retrieves a project.
+
+.Request
+----
+  GET /projects/plugins%2Freplication HTTP/1.0
+----
+
+As response a link:#project-info[ProjectInfo] entity is returned that
+describes the project.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "kind": "gerritcodereview#project",
+    "id": "plugins%2Freplication",
+    "name": "plugins/replication",
+    "parent": "Public-Plugins",
+    "description": "Copies to other servers using the Git protocol"
+  }
+----
+
+[[create-project]]
+Create Project
+~~~~~~~~~~~~~~
+[verse]
+'PUT /projects/link:#project-name[\{project-name\}]'
+
+Creates a new project.
+
+In the request body additional data for the project can be provided as
+link:#project-input[ProjectInput].
+
+.Request
+----
+  PUT /projects/MyProject HTTP/1.0
+  Content-Type: application/json;charset=UTF-8
+
+  {
+    "description": "This is a demo project.",
+    "submit_type": "CHERRY_PICK",
+    "owners": [
+      "MyProject-Owners"
+    ]
+  }
+----
+
+As response the link:#project-info[ProjectInfo] entity is returned that
+describes the created project.
+
+.Response
+----
+  HTTP/1.1 201 Created
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "kind": "gerritcodereview#project",
+    "id": "MyProject",
+    "name": "MyProject",
+    "parent": "All-Projects",
+    "description": "This is a demo project."
+  }
+----
+
+[[get-project-description]]
+Get Project Description
+~~~~~~~~~~~~~~~~~~~~~~~
+[verse]
+'GET /projects/link:#project-name[\{project-name\}]/description'
+
+Retrieves the description of a project.
+
+.Request
+----
+  GET /projects/plugins%2Freplication/description HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  "Copies to other servers using the Git protocol"
+----
+
+If the project does not have a description an empty string is returned.
+
+[[set-project-description]]
+Set Project Description
+~~~~~~~~~~~~~~~~~~~~~~~
+[verse]
+'PUT /projects/link:#project-name[\{project-name\}]/description'
+
+Sets the description of a project.
+
+The new project description must be provided in the request body inside
+a link:#project-description-input[ProjectDescriptionInput] entity.
+
+.Request
+----
+  PUT /projects/plugins%2Freplication/description HTTP/1.0
+  Content-Type: application/json;charset=UTF-8
+
+  {
+    "description": "Plugin for Gerrit that handles the replication.",
+    "commit_message": "Update the project description"
+  }
+----
+
+As response the new project description is returned.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  "Plugin for Gerrit that handles the replication."
+----
+
+If the description was deleted the response is "`204 No Content`".
+
+[[delete-project-description]]
+Delete Project Description
+~~~~~~~~~~~~~~~~~~~~~~~~~~
+[verse]
+'DELETE /projects/link:#project-name[\{project-name\}]/description'
+
+Deletes the description of a project.
+
+The request body does not need to include a
+link:#project-description-input[ProjectDescriptionInput] entity if no
+commit message is specified.
+
+Please note that some proxies prohibit request bodies for DELETE
+requests. In this case, if you want to specify a commit message, use
+link:#set-project-description[PUT] to delete the description.
+
+.Request
+----
+  DELETE /projects/plugins%2Freplication/description HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 204 No Content
+----
+
+[[get-project-parent]]
+Get Project Parent
+~~~~~~~~~~~~~~~~~~
+[verse]
+'GET /projects/link:#project-name[\{project-name\}]/parent'
+
+Retrieves the name of a project's parent project. For the
+`All-Projects` root project an empty string is returned.
+
+.Request
+----
+  GET /projects/plugins%2Freplication/parent HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  "All-Projects"
+----
+
+[[set-project-parent]]
+Set Project Parent
+~~~~~~~~~~~~~~~~~~
+[verse]
+'PUT /projects/link:#project-name[\{project-name\}]/parent'
+
+Sets the parent project for a project.
+
+The new name of the parent project must be provided in the request body
+inside a link:#project-parent-input[ProjectParentInput] entity.
+
+.Request
+----
+  PUT /projects/plugins%2Freplication/parent HTTP/1.0
+  Content-Type: application/json;charset=UTF-8
+
+  {
+    "parent": "Public-Plugins",
+    "commit_message": "Update the project parent"
+  }
+----
+
+As response the new parent project name is returned.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  "Public-Plugins"
+----
+
+[[get-head]]
+Get HEAD
+~~~~~~~~
+[verse]
+'GET /projects/link:#project-name[\{project-name\}]/HEAD'
+
+Retrieves for a project the name of the branch to which `HEAD` points.
+
+.Request
+----
+  GET /projects/plugins%2Freplication/HEAD HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  "refs/heads/master"
+----
+
+[[set-head]]
+Set HEAD
+~~~~~~~~
+[verse]
+'PUT /projects/link:#project-name[\{project-name\}]/HEAD'
+
+Sets `HEAD` for a project.
+
+The new ref to which `HEAD` should point must be provided in the
+request body inside a link:#head-input[HeadInput] entity.
+
+.Request
+----
+  PUT /projects/plugins%2Freplication/HEAD HTTP/1.0
+  Content-Type: application/json;charset=UTF-8
+
+  {
+    "ref": "refs/heads/stable"
+  }
+----
+
+As response the new ref to which `HEAD` points is returned.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  "refs/heads/stable"
+----
+
+[[get-repository-statistics]]
+Get Repository Statistics
+~~~~~~~~~~~~~~~~~~~~~~~~~
+[verse]
+'GET /projects/link:#project-name[\{project-name\}]/statistics.git'
+
+Return statistics for the repository of a project.
+
+.Request
+----
+  GET /projects/plugins%2Freplication/statistics.git HTTP/1.0
+----
+
+The repository statistics are returned as a
+link:#repository-statistics-info[RepositoryStatisticsInfo] entity.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "number_of_loose_objects": 127,
+    "number_of_loose_refs": 15,
+    "number_of_pack_files": 15,
+    "number_of_packed_objects": 67,
+    "number_of_packed_refs": 0,
+    "size_of_loose_objects": 29466,
+    "size_of_packed_objects": 9646
+  }
+----
+
+[[run-gc]]
+Run GC
+~~~~~~
+[verse]
+'POST /projects/link:#project-name[\{project-name\}]/gc'
+
+Run the Git garbage collection for the repository of a project.
+
+.Request
+----
+  POST /projects/plugins%2Freplication/gc HTTP/1.0
+----
+
+The response is the streamed output of the garbage collection.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: text/plain;charset=UTF-8
+
+  collecting garbage for "plugins/replication":
+  Pack refs:              100% (21/21)
+  Counting objects:       20
+  Finding sources:        100% (20/20)
+  Getting sizes:          100% (13/13)
+  Compressing objects:     83% (5/6)
+  Writing objects:        100% (20/20)
+  Selecting commits:      100% (7/7)
+  Building bitmaps:       100% (7/7)
+  Finding sources:        100% (41/41)
+  Getting sizes:          100% (25/25)
+  Compressing objects:     52% (12/23)
+  Writing objects:        100% (41/41)
+  Prune loose objects also found in pack files: 100% (36/36)
+  Prune loose, unreferenced objects: 100% (36/36)
+  done.
+----
+
+[[dashboard-endpoints]]
+Dashboard Endpoints
+-------------------
+
+[[list-dashboards]]
+List Dashboards
+~~~~~~~~~~~~~~~
+[verse]
+'GET /projects/link:#project-name[\{project-name\}]/dashboards/'
+
+List custom dashboards for a project.
+
+As result a list of link:#dashboard-info[DashboardInfo] entries is
+returned.
+
+List all dashboards for the `work/my-project` project:
+
+.Request
+----
+  GET /projects/work%2Fmy-project/dashboards/ HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  [
+    {
+      "kind": "gerritcodereview#dashboard",
+      "id": "main:closed",
+      "ref": "main",
+      "path": "closed",
+      "description": "Merged and abandoned changes in last 7 weeks",
+      "url": "/dashboard/?title\u003dClosed+changes\u0026Merged\u003dstatus:merged+age:7w\u0026Abandoned\u003dstatus:abandoned+age:7w",
+      "default": true,
+      "title": "Closed changes",
+      "sections": [
+        {
+          "name": "Merged",
+          "query": "status:merged age:7w"
+        },
+        {
+          "name": "Abandoned",
+          "query": "status:abandoned age:7w"
+        }
+      ]
+    }
+  ]
+----
+
+.Get all dashboards of the 'All-Projects' project
+****
+get::/projects/All-Projects/dashboards/
+****
+
+[[get-dashboard]]
+Get Dashboard
+~~~~~~~~~~~~~
+[verse]
+'GET /projects/link:#project-name[\{project-name\}]/dashboards/link:#dashboard-id[\{dashboard-id\}]'
+
+Retrieves a project dashboard. The dashboard can be defined on that
+project or be inherited from a parent project.
+
+.Request
+----
+  GET /projects/work%2Fmy-project/dashboards/main:closed HTTP/1.0
+----
+
+As response a link:#dashboard-info[DashboardInfo] entity is returned
+that describes the dashboard.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "kind": "gerritcodereview#dashboard",
+    "id": "main:closed",
+    "ref": "main",
+    "path": "closed",
+    "description": "Merged and abandoned changes in last 7 weeks",
+    "url": "/dashboard/?title\u003dClosed+changes\u0026Merged\u003dstatus:merged+age:7w\u0026Abandoned\u003dstatus:abandoned+age:7w",
+    "default": true,
+    "title": "Closed changes",
+    "sections": [
+      {
+        "name": "Merged",
+        "query": "status:merged age:7w"
+      },
+      {
+        "name": "Abandoned",
+        "query": "status:abandoned age:7w"
+      }
+    ]
+  }
+----
+
+To retrieve the default dashboard of a project use `default` as
+dashboard-id.
+
+.Request
+----
+  GET /projects/work%2Fmy-project/dashboards/default HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "kind": "gerritcodereview#dashboard",
+    "id": "main:closed",
+    "ref": "main",
+    "path": "closed",
+    "description": "Merged and abandoned changes in last 7 weeks",
+    "url": "/dashboard/?title\u003dClosed+changes\u0026Merged\u003dstatus:merged+age:7w\u0026Abandoned\u003dstatus:abandoned+age:7w",
+    "default": true,
+    "title": "Closed changes",
+    "sections": [
+      {
+        "name": "Merged",
+        "query": "status:merged age:7w"
+      },
+      {
+        "name": "Abandoned",
+        "query": "status:abandoned age:7w"
+      }
+    ]
+  }
+----
+
+[[set-dashboard]]
+Set Dashboard
+~~~~~~~~~~~~~
+[verse]
+'PUT /projects/link:#project-name[\{project-name\}]/dashboards/link:#dashboard-id[\{dashboard-id\}]'
+
+Updates/Creates a project dashboard.
+
+Currently only supported for the `default` dashboard.
+
+The creation/update information for the dashboard must be provided in
+the request body as a link:#dashboard-input[DashboardInput] entity.
+
+.Request
+----
+  PUT /projects/work%2Fmy-project/dashboards/default HTTP/1.0
+  Content-Type: application/json;charset=UTF-8
+
+  {
+    "id": "main:closed",
+    "commit_message": "Define the default dashboard"
+  }
+----
+
+As response the new/updated dashboard is returned as a
+link:#dashboard-info[DashboardInfo] entity.
+
+.Response
+----
+  HTTP/1.1 200 OK
+  Content-Disposition: attachment
+  Content-Type: application/json;charset=UTF-8
+
+  )]}'
+  {
+    "kind": "gerritcodereview#dashboard",
+    "id": "main:closed",
+    "ref": "main",
+    "path": "closed",
+    "description": "Merged and abandoned changes in last 7 weeks",
+    "url": "/dashboard/?title\u003dClosed+changes\u0026Merged\u003dstatus:merged+age:7w\u0026Abandoned\u003dstatus:abandoned+age:7w",
+    "default": true,
+    "title": "Closed changes",
+    "sections": [
+      {
+        "name": "Merged",
+        "query": "status:merged age:7w"
+      },
+      {
+        "name": "Abandoned",
+        "query": "status:abandoned age:7w"
+      }
+    ]
+  }
+----
+
+[[delete-dashboard]]
+Delete Dashboard
+~~~~~~~~~~~~~~~~
+[verse]
+'DELETE /projects/link:#project-name[\{project-name\}]/dashboards/link:#dashboard-id[\{dashboard-id\}]'
+
+Deletes a project dashboard.
+
+Currently only supported for the `default` dashboard.
+
+The request body does not need to include a link:#dashboard-input[
+DashboardInput] entity if no commit message is specified.
+
+Please note that some proxies prohibit request bodies for DELETE
+requests.
+
+.Request
+----
+  DELETE /projects/work%2Fmy-project/dashboards/default HTTP/1.0
+----
+
+.Response
+----
+  HTTP/1.1 204 No Content
+----
+
+
+[[ids]]
+IDs
+---
+
+[[dashboard-id]]
+\{dashboard-id\}
+~~~~~~~~~~~~~~~~
+The ID of a dashboard in the format '<ref>:<path>'.
+
+A special dashboard ID is `default` which represents the default
+dashboard of a project.
+
+[[project-name]]
+\{project-name\}
+~~~~~~~~~~~~~~~~
+The name of the project.
+
+
+[[json-entities]]
+JSON Entities
+-------------
+
+[[dashboard-info]]
+DashboardInfo
+~~~~~~~~~~~~~
+The `DashboardInfo` entity contains information about a project
+dashboard.
+
+[options="header",width="50%",cols="1,^2,4"]
+|===============================
+|Field Name        ||Description
+|`kind`            ||`gerritcodereview#dashboard`
+|`id`              ||
+The ID of the dashboard. The ID has the format '<ref>:<path>',
+where ref and path are URL encoded.
+|`project`         ||
+The name of the project for which this dashboard is returned.
+|`defining_project`||
+The name of the project in which this dashboard is defined.
+This is different from `project` if the dashboard is inherited from a
+parent project.
+|`ref`             ||
+The name of the ref in which the dashboard is defined, without the
+`refs/meta/dashboards/` prefix, which is common for all dashboard refs.
+|`path`            ||
+The path of the file in which the dashboard is defined.
+|`description`     |optional|The description of the dashboard.
+|`foreach`         |optional|
+Subquery that applies to all sections in the dashboard. +
+Tokens such as `${project}` are not resolved.
+|`url`             ||
+The URL under which the dashboard can be opened in the Gerrit WebUI. +
+The URL is relative to the canonical web URL. +
+Tokens in the queries such as `${project}` are resolved.
+|`default`         |not set if `false`|
+Whether this is the default dashboard of the project.
+|`title`           |optional|The title of the dashboard.
+|`sections`        ||
+The list of link:#dashboard-section-info[sections] in the dashboard.
+|===============================
+
+[[dashboard-input]]
+DashboardInput
+~~~~~~~~~~~~~~
+The `DashboardInput` entity contains information to create/update a
+project dashboard.
+
+[options="header",width="50%",cols="1,^2,4"]
+|=============================
+|Field Name      ||Description
+|`id`            |optional|
+URL encoded ID of a dashboard to which this dashboard should link to.
+|`commit_message`|optional|
+Message that should be used to commit the change of the dashboard.
+|=============================
+
+[[dashboard-section-info]]
+DashboardSectionInfo
+~~~~~~~~~~~~~~~~~~~~
+The `DashboardSectionInfo` entity contains information about a section
+in a dashboard.
+
+[options="header",width="50%",cols="1,6"]
+|===========================
+|Field Name    |Description
+|`name`        |The title of the section.
+|`query`       |The query of the section. +
+Tokens such as `${project}` are not resolved.
+|===========================
+
+[[head-input]]
+HeadInput
+~~~~~~~~~
+The `HeadInput` entity contains information for setting `HEAD` for a
+project.
+
+[options="header",width="50%",cols="1,6"]
+|============================
+|Field Name      |Description
+|`ref`           |
+The ref to which `HEAD` should be set, the `refs/heads` prefix can be
+omitted.
+|============================
+
+[[project-description-input]]
+ProjectDescriptionInput
+~~~~~~~~~~~~~~~~~~~~~~~
+The `ProjectDescriptionInput` entity contains information for setting a
+project description.
+
+[options="header",width="50%",cols="1,^2,4"]
+|=============================
+|Field Name      ||Description
+|`description`   |optional|The project description. +
+The project description will be deleted if not set.
+|`commit_message`|optional|
+Message that should be used to commit the change of the project
+description in the `project.config` file to the `refs/meta/config`
+branch.
+|=============================
+
+[[project-info]]
+ProjectInfo
+~~~~~~~~~~~
+The `ProjectInfo` entity contains information about a project.
+
+[options="header",width="50%",cols="1,^2,4"]
+|===========================
+|Field Name    ||Description
+|`kind`        ||`gerritcodereview#project`
+|`id`          ||The URL encoded project name.
+|`name`        |
+not set if returned in a map where the project name is used as map key|
+The name of the project.
+|`parent`      |optional|
+The name of the parent project. +
+`?-<n>` if the parent project is not visible (`<n>` is a number which
+is increased for each non-visible project).
+|`description` |optional|The description of the project.
+|`branches`    |optional|Map of branch names to HEAD revisions.
+|===========================
+
+[[project-input]]
+ProjectInput
+~~~~~~~~~~~~
+The `ProjectInput` entity contains information for the creation of
+a new project.
+
+[options="header",width="50%",cols="1,^2,4"]
+|=========================================
+|Field Name                  ||Description
+|`name`                      |optional|
+The name of the project (not encoded). +
+If set, must match the project name in the URL.
+|`parent`                    |optional|
+The name of the parent project. +
+If not set, the `All-Projects` project will be the parent project.
+|`description`               |optional|The description of the project.
+|`permissions_only`          |`false` if not set|
+Whether a permission-only project should be created.
+|`create_empty_commit`       |`false` if not set|
+Whether an empty initial commit should be created.
+|`submit_type`               |optional|
+The submit type that should be set for the project
+(`MERGE_IF_NECESSARY`, `REBASE_IF_NECESSARY`, `FAST_FORWARD_ONLY`,
+`MERGE_ALWAYS`, `CHERRY_PICK`). +
+If not set, `MERGE_IF_NECESSARY` is set as submit type.
+|`branches`                  |optional|
+A list of branches that should be initially created. +
+For the branch names the `refs/heads/` prefix can be omitted.
+|`owners`                    |optional|
+A list of groups that should be assigned as project owner. +
+Each group in the list must be specified as
+link:rest-api-groups.html#group-id[group-id]. +
+If not set, the link:config-gerrit.html#repository.name.ownerGroup[
+groups that are configured as default owners] are set as project
+owners.
+|`use_contributor_agreements`|`INHERIT` if not set|
+Whether contributor agreements should be used for the project  (`TRUE`,
+`FALSE`, `INHERIT`).
+|`use_signed_off_by`         |`INHERIT` if not set|
+Whether the usage of 'Signed-Off-By' footers is required for the
+project (`TRUE`, `FALSE`, `INHERIT`).
+|`use_content_merge`         |`INHERIT` if not set|
+Whether content merge should be enabled for the project (`TRUE`,
+`FALSE`, `INHERIT`). +
+`FALSE`, if the `submit_type` is `FAST_FORWARD_ONLY`.
+|`require_change_id`         |`INHERIT` if not set|
+Whether the usage of Change-Ids is required for the project (`TRUE`,
+`FALSE`, `INHERIT`).
+|=========================================
+
+[[project-parent-input]]
+ProjectParentInput
+~~~~~~~~~~~~~~~~~~
+The `ProjectParentInput` entity contains information for setting a
+project parent.
+
+[options="header",width="50%",cols="1,^2,4"]
+|=============================
+|Field Name      ||Description
+|`parent`        ||The name of the parent project.
+|`commit_message`|optional|
+Message that should be used to commit the change of the project parent
+in the `project.config` file to the `refs/meta/config` branch.
+|=============================
+
+[[repository-statistics-info]]
+RepositoryStatisticsInfo
+~~~~~~~~~~~~~~~~~~~~~~~~
+The `RepositoryStatisticsInfo` entity contains information about
+statistics of a Git repository.
+
+[options="header",width="50%",cols="1,6"]
+|======================================
+|Field Name                |Description
+|`number_of_loose_objects` |Number of loose objects.
+|`number_of_loose_refs`    |Number of loose refs.
+|`number_of_pack_files`    |Number of pack files.
+|`number_of_packed_objects`|Number of packed objects.
+|`number_of_packed_refs`   |Number of packed refs.
+|`size_of_loose_objects`   |Size of loose objects in bytes.
+|`size_of_packed_objects`  |Size of packed objects in bytes.
+|======================================
+
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/rest-api.txt b/Documentation/rest-api.txt
index 2f9d03f..303fc4b 100644
--- a/Documentation/rest-api.txt
+++ b/Documentation/rest-api.txt
@@ -5,6 +5,17 @@
 The API is suitable for automated tools to build upon, as well as
 supporting some ad-hoc scripting use cases.
 
+Endpoints
+---------
+link:rest-api-accounts.html[/accounts/]::
+  Account related REST endpoints
+link:rest-api-changes.html[/changes/]::
+  Change related REST endpoints
+link:rest-api-groups.html[/groups/]::
+  Group related REST endpoints
+link:rest-api-projects.html[/projects/]::
+  Project related REST endpoints
+
 Protocol Details
 ----------------
 
@@ -21,11 +32,19 @@
 prefix the endpoint URL with `/a/`. For example to authenticate to
 `/projects/` request URL `/a/projects/`.
 
+[[preconditions]]
+Preconditions
+~~~~~~~~~~~~~
+Clients can request PUT to create a new resource and not overwrite
+an existing one by adding `If-None-Match: *` to the request HTTP
+headers. If the named resource already exists the server will respond
+with HTTP 412 Precondition Failed.
+
 [[output]]
 Output Format
 ~~~~~~~~~~~~~
-Most APIs return text format by default. JSON can be requested
-by setting the `Accept` HTTP request header to include
+Most APIs return pretty printed JSON by default. Compact JSON can be
+requested by setting the `Accept` HTTP request header to include
 `application/json`, for example:
 
 ----
@@ -43,350 +62,99 @@
   [ ... valid JSON ... ]
 ----
 
-The default JSON format is `JSON_COMPACT`, which skips unnecessary
-whitespace. This is not the easiest format for a human to read. Many
-examples in this documentation use `format=JSON` as a query parameter
-to obtain pretty formatting in the response. Producing (and parsing)
-the compact format is more efficient, so most tools should prefer the
-default compact format.
+The default JSON format is pretty, which uses extra whitespace to make
+the output more readable for a human. Producing (and parsing) the
+non-pretty compact format is more efficient so tools should request it
+by using the `Accept: application/json` header or `pp=0` query
+parameter whenever possible.
 
 Responses will be gzip compressed by the server if the HTTP
 `Accept-Encoding` request header is set to `gzip`. This may
 save on network transfer time for larger responses.
 
-Endpoints
----------
+[[timestamp]]
+Timestamp
+~~~~~~~~~
+Timestamps are given in UTC and have the format
+"'yyyy-mm-dd hh:mm:ss.fffffffff'" where "'ffffffffff'" indicates the
+nanoseconds.
 
-[[accounts_self_capabilities]]
-/accounts/self/capabilities (Account Capabilities)
-~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
-Returns the global capabilities (such as `createProject` or
-`createGroup`) that are enabled for the calling user. This can be used
-by UI tools to discover if administrative features are available
-to the caller, so they can hide (or show) relevant UI actions.
+[[encoding]]
+Encoding
+~~~~~~~~
+All IDs that appear in the URL of a REST call (e.g. project name, group name)
+must be URL encoded.
 
-----
-  GET /accounts/self/capabilities?format=JSON HTTP/1.0
+[[response-codes]]
+Response Codes
+~~~~~~~~~~~~~~
+HTTP status codes are well defined and the Gerrit REST endpoints use
+them as described in the HTTP spec.
 
-  )]}'
-  {
-    "queryLimit": {
-      "min": 0,
-      "max": 500
-    }
-  }
-----
+Here are examples for some HTTP status codes that show how they are
+used in the context of the Gerrit REST API.
 
-Administrator that has authenticated with digest authentication:
-----
-  GET /a/accounts/self/capabilities?format=JSON HTTP/1.0
-  Authorization: Digest username="admin", realm="Gerrit Code Review", nonce="...
+400 Bad Request
+^^^^^^^^^^^^^^^
+`400 Bad Request` is used if the request is not understood by the
+server due to malformed syntax.
 
-  )]}'
-  {
-    "administrateServer": true,
-    "queryLimit": {
-      "min": 0,
-      "max": 500
-    },
-    "createAccount": true,
-    "createGroup": true,
-    "createProject": true,
-    "killTask": true,
-    "viewCaches": true,
-    "flushCaches": true,
-    "viewConnections": true,
-    "viewQueue": true,
-    "startReplication": true
-  }
-----
+E.g. `400 Bad Request` is returned if JSON input is expected but the
+'Content-Type' of the request is not 'application/json' or the request
+body doesn't contain valid JSON.
 
-To filter the set of global capabilities the `q` parameter can be used.
-Filtering may decrease the response time by avoiding looking at every
-possible alternative for the caller.
+`400 Bad Request` is also used if required input fields are not set or
+if options are set which cannot be used together.
 
-----
-  GET /a/accounts/self/capabilities?format=JSON&q=createAccount&q=createGroup HTTP/1.0
-  Authorization: Digest username="admin", realm="Gerrit Code Review", nonce="...
+403 Forbidden
+^^^^^^^^^^^^^
+`403 Forbidden` is used if the operation is not allowed because the
+calling user has no sufficient permissions.
 
-  )]}'
-  {
-    "createAccount": true,
-    "createGroup": true
-  }
-----
+E.g. some REST endpoints require that the calling user has certain
+link:access-control.html#global_capabilities[global capabilities]
+assigned.
 
-Most results are boolean, and a field is only present when its value
-is `true`. link:json.html#queryLimit[`queryLimit`] is a range and is
-presented as a nested JSON object with `min` and `max` members.
+`403 Forbidden` is also used if `self` is used as account ID and the
+REST call was done without authentication.
 
-[[projects]]
-/projects/ (List Projects)
-~~~~~~~~~~~~~~~~~~~~~~~~~~
-Lists the projects accessible by the caller. This is the same as
-using the link:cmd-ls-projects.html[ls-projects] command over SSH,
-and accepts the same options as query parameters.
+404 Not Found
+^^^^^^^^^^^^^
+`404 Not Found` is returned if the resource that is specified by the
+URL is not found or is not visible to the calling user. A resource
+cannot be found if the URL contains a non-existing ID or view.
 
-----
-  GET /projects/?format=JSON&d HTTP/1.0
+405 Method Not Allowed
+^^^^^^^^^^^^^^^^^^^^^^
+`405 Method Not Allowed` is used if the resource exists but doesn't
+support the operation.
 
-  HTTP/1.1 200 OK
-  Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
-   
-  )]}'
-  {
-    "external/bison": {
-      "description": "GNU parser generator"
-    },
-    "external/gcc": {},
-    "external/openssl": {
-      "description": "encryption\ncrypto routines"
-    },
-    "test": {
-      "description": "\u003chtml\u003e is escaped"
-    }
-  }
-----
+E.g. some of the `/groups/` endpoints are only supported for Gerrit
+internal groups, if they are invoked for an external group the response
+is `405 Method Not Allowed`.
 
-[[suggest-projects]]
-The `/projects/` URL also accepts a prefix string as part of the URL.
-This limits the results to those projects that start with the specified
-prefix.
-List all projects that start with `platform/`:
-----
-GET /projects/platform/?format=JSON HTTP/1.0
-HTTP/1.1 200 OK
-Content-Disposition: attachment
-Content-Type: application/json;charset=UTF-8
-)]}'
-{
-"platform/drivers": {},
-"platform/tools": {}
-}
-----
-E.g. this feature can be used by suggestion client UI's to limit results.
+409 Conflict
+^^^^^^^^^^^^
+`409 Conflict` is used if the request cannot be completed because the
+current state of the resource doesn't allow the operation.
 
-[[changes]]
-/changes/ (Query Changes)
-~~~~~~~~~~~~~~~~~~~~~~~~~
-Queries changes visible to the caller. The query string must be
-provided by the `q` parameter. The `n` parameter can be used to limit
-the returned results.
+E.g. if you try to submit a change that is abandoned, this fails with
+`409 Conflict` because the state of the change doesn't allow the submit
+operation.
 
-Query for open changes of watched projects:
-----
-  GET /changes/?format=JSON&q=status:open+is:watched&n=2 HTTP/1.0
+`409 Conflict` is also used if you try to create a resource but the
+name is already occupied by an existing resource.
 
-  HTTP/1.1 200 OK
-  Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
+412 Precondition Failed
+^^^^^^^^^^^^^^^^^^^^^^^
+`412 Precondition Failed` is used if a precondition from the request
+header fields is not fulfilled as described in the link:#preconditions[
+Preconditions] section.
 
-  )]}'
-  {
-    "project": "demo",
-    "branch": "master",
-    "id": "Idaf5e098d70898b7119f6f4af5a6c13343d64b57",
-    "subject": "One change",
-    "status": "NEW",
-    "created": "2012-07-17 07:18:30.854000000",
-    "updated": "2012-07-17 07:19:27.766000000",
-    "reviewed": true,
-    "_sortkey": "001e7057000006dc",
-    "_number": 1756,
-    "owner": {
-      "name": "John Doe"
-    },
-  },
-  {
-    "project": "demo",
-    "branch": "master",
-    "id": "I09c8041b5867d5b33170316e2abc34b79bbb8501",
-    "subject": "Another change",
-    "status": "NEW",
-    "created": "2012-07-17 07:18:30.884000000",
-    "updated": "2012-07-17 07:18:30.885000000",
-    "_sortkey": "001e7056000006dd",
-    "_number": 1757,
-    "owner": {
-      "name": "John Doe"
-    },
-    "_more_changes": true
-  }
-----
-
-The change output is sorted by the last update time, most recently
-updated to oldest update.
-
-If the `n` query parameter is supplied and additional changes exist
-that match the query beyond the end, the last change object has a
-`_more_changes: true` JSON field set. Callers can resume a query with
-the `n` query parameter, supplying the last change's `_sortkey` field
-as the value. When going in the reverse direction with the `p` query
-parameter a `_more_changes: true` is put in the first change object if
-there are results *before* the first change returned.
-
-Clients are allowed to specify more than one query by setting the `q`
-parameter multiple times. In this case the result is an array of
-arrays, one per query in the same order the queries were given in.
-
-Query that retrieves changes for a user's dashboard:
-----
-  GET /changes/?format=JSON&q=is:open+owner:self&q=is:open+reviewer:self+-owner:self&q=is:closed+owner:self+limit:5&o=LABELS HTTP/1.0
-
-  HTTP/1.1 200 OK
-  Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
-
-  )]}'
-  [
-    [
-      {
-        "project": "demo",
-        "branch": "master",
-        "id": "Idaf5e098d70898b7119f6f4af5a6c13343d64b57",
-        "subject": "One change",
-        "status": "NEW",
-        "created": "2012-07-17 07:18:30.854000000",
-        "updated": "2012-07-17 07:19:27.766000000",
-        "reviewed": true,
-        "_sortkey": "001e7057000006dc",
-        "_number": 1756,
-        "owner": {
-          "name": "John Doe"
-        },
-        "labels": {
-          "Verified": {},
-          "Code-Review": {}
-        }
-      }
-    ],
-    [],
-    []
-  ]
-----
-
-Additional fields can be obtained by adding `o` parameters, each
-option requires more database lookups and slows down the query
-response time to the client so they are generally disabled by
-default. Optional fields are:
-
-* `LABELS`: a summary of each label required for submit, and
-  approvers that have granted (or rejected) with that label.
-
-* `CURRENT_REVISION`: describe the current revision (patch set)
-  of the change, including the commit SHA-1 and URLs to fetch from.
-
-* `ALL_REVISIONS`: describe all revisions, not just current.
-
-* `CURRENT_COMMIT`: parse and output all header fields from the
-  commit object, including message. Only valid when the current
-  revision or all revisions are selected.
-
-* `ALL_COMMITS`: parse and output all header fields from the
-  output revisions. If only `CURRENT_REVISION` was requested
-  then only the current revision's commit data will be output.
-
-* `CURRENT_FILES`: list files modified by the commit, including
-  basic line counts inserted/deleted per file. Only valid when
-  the current revision or all revisions are selected.
-
-* `ALL_FILES`: list files modified by the commit, including
-  basic line counts inserted/deleted per file. If only the
-  `CURRENT_REVISION` was requested the only that commit's
-  modified files will be output.
-
-----
-  GET /changes/?q=97&format=JSON&o=CURRENT_REVISION&o=CURRENT_COMMIT&o=CURRENT_FILES HTTP/1.0
-
-  HTTP/1.1 200 OK
-  Content-Disposition: attachment
-  Content-Type: application/json;charset=UTF-8
-
-  )]}'
-  [
-    {
-      "project": "gerrit",
-      "branch": "master",
-      "id": "I7ea46d2e2ee5c64c0d807677859cfb7d90b8966a",
-      "subject": "Use an EventBus to manage star icons",
-      "status": "NEW",
-      "created": "2012-04-25 00:52:25.580000000",
-      "updated": "2012-04-25 00:52:25.586000000",
-      "_sortkey": "001c9bf400000061",
-      "_number": 97,
-      "owner": {
-        "name": "Shawn Pearce"
-      },
-      "current_revision": "184ebe53805e102605d11f6b143486d15c23a09c",
-      "revisions": {
-        "184ebe53805e102605d11f6b143486d15c23a09c": {
-          "_number": 1,
-          "fetch": {
-            "git": {
-              "url": "git://localhost/gerrit",
-              "ref": "refs/changes/97/97/1"
-            },
-            "http": {
-              "url": "http://127.0.0.1:8080/gerrit",
-              "ref": "refs/changes/97/97/1"
-            }
-          },
-          "commit": {
-            "parents": [
-              {
-                "commit": "1eee2c9d8f352483781e772f35dc586a69ff5646",
-                "subject": "Migrate contributor agreements to All-Projects."
-              }
-            ],
-            "author": {
-              "name": "Shawn O. Pearce",
-              "email": "sop@google.com",
-              "date": "2012-04-24 18:08:08.000000000",
-              "tz": -420
-            },
-            "committer": {
-              "name": "Shawn O. Pearce",
-              "email": "sop@google.com",
-              "date": "2012-04-24 18:08:08.000000000",
-              "tz": -420
-            },
-            "subject": "Use an EventBus to manage star icons",
-            "message": "Use an EventBus to manage star icons\n\nImage widgets that need to ..."
-          },
-          "files": {
-            "gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeCache.java": {
-              "lines_deleted": 8
-            },
-            "gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeDetailCache.java": {
-              "lines_inserted": 1
-            },
-            "gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeScreen.java": {
-              "lines_inserted": 11,
-              "lines_deleted": 19
-            },
-            "gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable.java": {
-              "lines_inserted": 23,
-              "lines_deleted": 20
-            },
-            "gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarCache.java": {
-              "status": "D",
-              "lines_deleted": 139
-            },
-            "gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarredChanges.java": {
-              "status": "A",
-              "lines_inserted": 204
-            },
-            "gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/Screen.java": {
-              "lines_deleted": 9
-            }
-          }
-        }
-      }
-    }
-  ]
-----
-
+422 Unprocessable Entity
+^^^^^^^^^^^^^^^^^^^^^^^^
+`422 Unprocessable Entity` is returned if the ID of a resource that is
+specified in the request body cannot be resolved.
 
 GERRIT
 ------
diff --git a/Documentation/user-changeid.txt b/Documentation/user-changeid.txt
index a3015e1..a4224bd 100644
--- a/Documentation/user-changeid.txt
+++ b/Documentation/user-changeid.txt
@@ -10,10 +10,10 @@
 Gerrit can automatically associate a new version of a change back
 to its original review, even across cherry-picks and rebases.
 
-To be picked up by Gerrit, a Change-Id line must be in the bottom
-portion (last paragraph) of a commit message, and may be mixed
-together with the Signed-off-by, Acked-by, or other such footers.
-For example:
+To be picked up by Gerrit, a Change-Id line must be in the footer
+(last paragraph) of a commit message, and may be mixed
+together with link:user-signedoffby.html[Signed-off-by], Acked-by,
+or other such lines. For example:
 
 ----
   $ git log -1
@@ -40,6 +40,7 @@
 To avoid confusion with commit names, Change-Ids are typically prefixed with
 an uppercase `I`.
 
+[[creation]]
 Creation
 --------
 
@@ -54,6 +55,10 @@
 
   $ curl -o .git/hooks/commit-msg http://review.example.com/tools/hooks/commit-msg
 
+Then ensure that the execute bit is set on the hook script:
+
+  $ chmod u+x .git/hooks/commit-msg
+
 For more details, see link:cmd-hook-commit-msg.html[commit-msg].
 
 Change Upload
diff --git a/Documentation/user-custom-dashboards.txt b/Documentation/user-custom-dashboards.txt
deleted file mode 100644
index a015e4c..0000000
--- a/Documentation/user-custom-dashboards.txt
+++ /dev/null
@@ -1,48 +0,0 @@
-Gerrit Code Review - Custom Dashboards
-======================================
-
-Description
------------
-
-A custom dashboard is shown in a layout similar to the per-user
-dashboard, but the sections are entirely configured from the URL.
-Because of this custom dashboards are stateless on the server side.
-Users or projects can simply trade URLs using an external system like
-a project wiki, or site administrators can put the links into the
-site's `GerritHeader.html` or `GerritFooter.html`.
-
-Dashboards are available via URLs like:
-----
-  /#/dashboard/?title=Custom+View&To+Review=reviewer:john.doe@example.com&Pending+In+myproject=project:myproject+is:open
-----
-This opens a view showing the title "Custom View" with two sections,
-"To Review" and "Pending in myproject":
-----
-  Custom View
-
-  To Review
-
-    Results of `reviewer:john.doe@example.com`
-
-  Pending In myproject
-
-    Results of `project:myproject is:open`
-----
-
-The dashboard URLs are easy to configure. All keys and values in the
-URL are encoded as query parameters. Set the page and window title
-using an optional `title=Text` parameter.
-
-Each section's title is defined by the parameter name, the section
-display order is defined by the order the parameters appear in the
-URL, and the query results are defined by the parameter value. To
-limit the number of rows in a query use `limit:N`, otherwise the
-entire result set will be shown (up to the user's query limit).
-
-Parameters may be separated from each other using any of the following
-characters, as some users may find one more readable than another:
-`&` or `;` or `,`
-
-GERRIT
-------
-Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/user-dashboards.txt b/Documentation/user-dashboards.txt
new file mode 100644
index 0000000..3a12b76
--- /dev/null
+++ b/Documentation/user-dashboards.txt
@@ -0,0 +1,169 @@
+Gerrit Code Review - Dashboards
+===============================
+
+Custom Dashboards
+-----------------
+
+A custom dashboard is shown in a layout similar to the per-user
+dashboard, but the sections are entirely configured from the URL.
+Because of this custom dashboards are stateless on the server side.
+Users or projects can simply trade URLs using an external system like
+a project wiki, or site administrators can put the links into the
+site's `GerritHeader.html` or `GerritFooter.html`.
+
+Dashboards are available via URLs like:
+----
+  /#/dashboard/?title=Custom+View&To+Review=reviewer:john.doe@example.com&Pending+In+myproject=project:myproject+is:open
+----
+This opens a view showing the title "Custom View" with two sections,
+"To Review" and "Pending in myproject":
+----
+  Custom View
+
+  To Review
+
+    Results of `reviewer:john.doe@example.com`
+
+  Pending In myproject
+
+    Results of `project:myproject is:open`
+----
+
+The dashboard URLs are easy to configure. All keys and values in the
+URL are encoded as query parameters. Set the page and window title
+using an optional `title=Text` parameter.
+
+Each section's title is defined by the parameter name, the section
+display order is defined by the order the parameters appear in the
+URL, and the query results are defined by the parameter value. To
+limit the number of rows in a query use `limit:N`, otherwise the
+entire result set will be shown (up to the user's query limit).
+
+Parameters may be separated from each other using any of the following
+characters, as some users may find one more readable than another:
+`&` or `;` or `,`
+
+The special `foreach=...` parameter is designed to facilitate
+more easily writting similar queries in a dashboard.  The value of the
+foreach parameter will be used in every query in the dashboard by
+appending it to their ends with a space (ANDing it with the queries).
+
+Example custom dashboard using foreach to constrain a dashboard
+to changes for the current user:
+
+----
+  /#/dashboard/?title=Mine&foreach=owner:self&My+Pending=is:open&My+Merged=is:merged
+----
+
+
+[[project-dashboards]]
+Project Dashboards
+------------------
+
+It is possible to share custom dashboards at a project level. To do
+this define the dashboards in a `refs/meta/dashboards/*` branch of the
+project. For each dashboard create a config file. The file path/name
+will be used as name (equivalent to a title in a custom dashboard) for
+the dashboard.
+
+Example dashboard config file `MyProject Dashboard`:
+
+----
+[dashboard]
+  description = Most recent open and merged changes.
+[section "Open Changes"]
+  query = status:open project:myProject limit:15
+[section "Merged Changes"]
+  query = status:merged project:myProject limit:15
+----
+
+Once defined, project dashboards are accessible using stable URLs by
+using the project name, refname and pathname of the dashboard via URLs
+like:
+----
+  /#/projects/All-Projects,dashboards/Site:Main
+----
+
+Project dashboards are inherited from ancestor projects unless
+overridden by dashboards with the same ref and name.  This makes
+it easy to define common dashboards for every project by simply
+defining project dashboards on the All-Projects project.
+
+Token `${project}`
+~~~~~~~~~~~~~~~~~~
+
+Project dashboard queries may contain the special `${project}` token
+which will be replaced with the name of the project to which the
+dashboard is being applied.  This is useful for defining dashboards
+designed to be inherited.  With this token, it is possible to cause a
+query in a project dashboard to be restricted to only changes for the
+project in which an inherited dashboard is being applied by simply
+adding `project:${project}` to the query in the dashboard.
+
+The `${project}` token can also be used in the link:#dashboard.title[
+dashboard title] and in the link:#dashboard.description[dashboard
+description].
+
+Section `dashboard`
+~~~~~~~~~~~~~~~~~~~
+
+[[dashboard.title]]dashboard.title::
++
+The title of the dashboard.
++
+If not specified the path of the dashboard config file is used as
+title.
+
+[[dashboard.description]]dashboard.description::
++
+The description of the dashboard.
+
+dashboard.foreach::
++
+The value of the foreach parameter gets appended to every query in the
+dashboard.
++
+Example dashboard config section to constrain the entire dashboard to
+the project to which it is applied:
++
+----
+[dashboard]
+  foreach = project:${project}
+----
+
+
+Section `section`
+~~~~~~~~~~~~~~~~~
+
+section.<name>.query::
++
+The change query that should be used to populate the section with the
+given name.
+
+[[project-default-dashboard]]
+Project Default Dashboard
+-------------------------
+
+It is possible to define a default dashboard for a project in the
+projects `project.config` file in the `refs/meta/config` branch:
+
+----
+[dashboard]
+  default = refs/meta/dashboards/main:default
+----
+
+The dashboard set as the default dashboard will be inherited as the
+default dashboard by child projects if they do not define their own
+default dashboard. The `local-default` entry makes it possible to
+define a different default dashboard that is only used by this project
+but not inherited to the child projects.
+
+----
+[dashboard]
+  default = refs/meta/dashboards/main:default
+  local-default = refs/meta/dashboards/main:local
+----
+
+GERRIT
+------
+Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/user-notify.txt b/Documentation/user-notify.txt
index ae3c2d09..711d1ac 100644
--- a/Documentation/user-notify.txt
+++ b/Documentation/user-notify.txt
@@ -18,9 +18,9 @@
 `All-Projects` can be watched to watch all projects that
 are visible to the user.
 
-Change search expressions can be used to filter change notifications
-to specific subsets, for example `branch:master` to only see changes
-proposed for the master branch.
+link:user-search.html[Change search expressions] can be used to filter
+change notifications to specific subsets, for example `branch:master`
+to only see changes proposed for the master branch.
 
 Project Level Settings
 ----------------------
@@ -92,13 +92,25 @@
 are sent.
 +
 * `new_changes`: Only newly created changes.
+* `new_patchsets`: Only newly created patch sets.
 * `all_comments`: Only comments on existing changes.
 * `submitted_changes`: Only changes that have been submitted.
+* `abandoned_changes`: Only changes that have been abandoned.
 * `all`: All notifications.
 
 +
 Like email, this variable may be a list of options.
 
+[[notify.name.header]]notify.<name>.header::
++
+Email header used to list the destination. If not set BCC is used.
+Only one value may be specified. To use different headers for each
+address list them in different notify blocks.
++
+* `to`: The standard To field is used; addresses are visible to all.
+* `cc`: The standard CC field is used; addresses are visible to all.
+* `bcc`: SMTP RCPT TO is used to hide the address.
+
 [[notify.name.filter]]notify.<name>.filter::
 +
 link:user-search.html[Change search expression] to match changes that
diff --git a/Documentation/user-search.txt b/Documentation/user-search.txt
index 8a231fe..f4328be 100644
--- a/Documentation/user-search.txt
+++ b/Documentation/user-search.txt
@@ -1,7 +1,7 @@
 Gerrit Code Review - Searching Changes
 ======================================
 
-Default Searches 
+Default Searches
 ----------------
 
 Most basic searches can be viewed by clicking on a link along the top
@@ -23,6 +23,7 @@
 |Open changes in Foo  | status:open project:Foo
 |=================================================
 
+
 Basic Change Search
 -------------------
 
@@ -36,7 +37,7 @@
 |Full or abbreviated Change-Id    | Ic0ff33
 |Full or abbreviated commit SHA-1 | d81b32ef
 |Email address                    | user@example.com
-|Approval requirement             | CodeReview>=+2, Verified=1
+|Approval requirement             | Code-Review>=+2, Verified=1
 |=============================================================
 
 
@@ -109,7 +110,6 @@
 link:http://www.brics.dk/automaton/[dk.brics.automaton
 library] is used for evaluation of such patterns.
 
-
 [[branch]]
 branch:'BRANCH'::
 +
@@ -271,6 +271,17 @@
 Change has been abandoned.
 
 
+Argument Quoting
+----------------
+
+Operator values that are not bare words (roughly A-Z, a-z, 0-9, @,
+hypen, dot and underscore) must be quoted for the query parser.
+
+Quoting is accepted as either double quotes
+(e.g.  `message:"the value"`) or as matched
+curly braces (e.g. `message:{the value}`).
+
+
 Boolean Operators
 -----------------
 
@@ -306,18 +317,12 @@
 ------
 Label operators can be used to match approval scores given during
 a code review.  The specific set of supported labels depends on
-the server configuration, however `CodeReview` and `Verified`
+the server configuration, however `Code-Review` and `Verified`
 are the default labels provided out of the box.
 
 A label name is any of the following:
 
-* The category name.  If the category name contains spaces,
-  it must be wrapped in double quotes.  Example: `label:"Code Review"`.
-
-* The name, without spaces.  This avoids needing to use double quotes
-  for the common category Code Review.  Example: `label:CodeReview`.
-
-* The internal short name.  Example: `label:CRVW`, or `label:VRIF`.
+* The label name.  Example: `label:Code-Review`.
 
 * The one or two character abbreviation shown in the column header
   of change list pages.  Example: `label:R` or `label:V`.
@@ -325,38 +330,38 @@
 A label name must be followed by a score, or an operator and a score.
 The easiest way to explain this is by example.
 
-`label:CodeReview=2`::
-`label:CodeReview=+2`::
-`label:CodeReview+2`::
+`label:Code-Review=2`::
+`label:Code-Review=+2`::
+`label:Code-Review+2`::
 +
-Matches changes where there is at least one +2 score for Code Review.
+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`::
-`label:CodeReview-2`::
+`label:Code-Review=-2`::
+`label:Code-Review-2`::
 +
-Matches changes where there is at least one -2 score for Code Review.
+Matches changes where there is at least one -2 score for Code-Review.
 Because the negative sign is required, the = operator is optional.
 
-`label:CodeReview=1`::
+`label:Code-Review=1`::
 +
-Matches changes where there is at least one +1 score for Code Review.
+Matches changes where there is at least one +1 score for Code-Review.
 Scores of +2 are not matched, even though they are higher.
 
-`label:CodeReview>=1`::
+`label:Code-Review>=1`::
 +
 Matches changes with either a +1, +2, or any higher score.
 
-`label:CodeReview<=-1`::
+`label:Code-Review<=-1`::
 +
 Matches changes with either a -1, -2, or any lower score.
 
-`is:open CodeReview+2 Verified+1 -Verified-1 -CodeReview-2`::
+`is:open Code-Review+2 Verified+1 -Verified-1 -Code-Review-2`::
 +
 Matches changes that are ready to be submitted.
 
-`is:open (Verified-1 OR CodeReview-2)`::
+`is:open (Verified-1 OR Code-Review-2)`::
 +
 Changes that are blocked from submission due to a blocking score.
 
@@ -427,6 +432,7 @@
 of a query.  Including either value in a web query may lead to
 unpredictable results.
 
+
 GERRIT
 ------
 Part of link:index.html[Gerrit Code Review]
diff --git a/Documentation/user-submodules.txt b/Documentation/user-submodules.txt
index cfaf3e9..6aab43f 100644
--- a/Documentation/user-submodules.txt
+++ b/Documentation/user-submodules.txt
@@ -1,5 +1,5 @@
-Gerrit Code Review - Superprojects subscribed to submodules updates
-===================================================================
+Gerrit Code Review - Superproject subscription to submodules updates
+====================================================================
 
 Description
 -----------
@@ -25,7 +25,7 @@
 Git Submodules Overview
 -----------------------
 
-It is a git feature that allows an external repository to be
+Submodules are 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.
@@ -37,20 +37,20 @@
 '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
+.gitmodules is created (it is a configuration 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
+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'),
@@ -63,11 +63,11 @@
 Defining the Submodule Branch
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
 
-This is required because Submodule subscription is actually the
+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
+Since Gerrit 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'
@@ -77,22 +77,23 @@
 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.
+As the branch field is a Gerrit specific field it will not be filled
+automatically by the git submodule command, so one needs to edit it
+manually. 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
+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.
+If the intention is to make use of the Gerrit feature described
+here, one should always be sure to update the .gitmodules file after
+adding submodules to a super project.
 
-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.
+If a git submodule is added but the branch field is not added to the
+.gitmodules file, Gerrit will not create a subscription for the
+submodule and there will be no automatic updates to the superproject.
 
 Detecting and Subscribing Submodules
 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
@@ -118,17 +119,62 @@
 creates a new commit on branch 'dev' of 'super' updating the gitlink
 to point to the just merged commit.
 
-Canonical Web Url
-~~~~~~~~~~~~~~~~~
+Subscription Limitations
+~~~~~~~~~~~~~~~~~~~~~~~~
 
-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).
+Gerrit will only automatically update superprojects where the
+submodules are hosted on the same Gerrit instance as the
+superproject. Gerrit determines this by checking the hostname of the
+submodule specified in the .gitmodules file and comparing it to the
+hostname from the canonical web URL.
+
+It is currently not possible to use the submodule subscription feature
+with a canonical web URL hostname that differs from the hostname of
+the submodule. Instead relative submodules should be used.
 
 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.
+provide the canonical web URL value in its configuration file. Users
+should certify to use the correct hostname of the running Gerrit
+instance to add/subscribe submodules.
+
+Relative submodules
+~~~~~~~~~~~~~~~~~~~
+
+To enable easier usage of Gerrit mirrors and/or distribution over
+several protocols, such as plain git and HTTP(S) as well as SSH, one
+can use relative submodules. This means that instead of providing the
+entire URL to the submodule a relative path is stated in the
+.gitmodules file.
+
+Gerrit will try to match the entire project name of the submodule
+including directories. Therefore it is important to supply the full
+path name of the Gerrit project, not only relative to the super
+repository. See the following example:
+
+We have a super repository placed under a sub directory.
+
+  product/super_repository.git
+
+To this repository we wish add a submodule "deeper" into the directory
+structure.
+
+  product/framework/subcomponent.git
+
+Now we need to edit the .gitmodules to include the complete path to
+the Gerrit project. Observe that we need to use two "../" to include
+the complete Gerrit project path.
+
+  path = subcomponent.git
+  url = ../../product/framework/subcomponent.git
+  branch = master
+
+In contrast the following will not setup proper submodule
+subscription, even if the submodule will be successfully cloned by git
+from Gerrit.
+
+  path = subcomponent.git
+  url = ../framework/subcomponent.git
+  branch = master
 
 Removing Subscriptions
 ----------------------
diff --git a/Documentation/user-upload.txt b/Documentation/user-upload.txt
index 6bdff7a..55ab895 100644
--- a/Documentation/user-upload.txt
+++ b/Documentation/user-upload.txt
@@ -7,8 +7,31 @@
 * Use `git push`, to create changes for review
 * Use `git push`, and bypass code review
 
-All three methods rely on SSH public key authentication, which must
-first be configured by the uploading user.
+All three methods rely on authentication, which must first be configured
+by the uploading user.
+
+Gerrit supports two methods of authenticating the uploading user.  SSH
+public key, and HTTP/HTTPS.
+
+[[http]]
+HTTP/HTTPS
+----------
+
+On Gerrit installations that do not support SSH authentication, the
+user must authenticate via HTTP/HTTPS.
+
+When link:config-gerrit.html#auth.gitBasicAuth[gitBasicAuth] is enabled,
+the user is authenticated using standard BasicAuth and credentials validated
+using the same authentication method configured for the Gerrit Web UI.
+
+When gitBasicAuth is not configured, the user's HTTP credentials can be
+accessed within Gerrit by going to `Settings`, and then accessing the `HTTP
+Password` tab.
+
+For Gerrit installations where an link:config-gerrit.html#auth.httpPasswordUrl[HTTP password URL]
+is configured, the password can be obtained by clicking on `Obtain Password`
+and then following the site-specific instructions.  On sites where this URL is
+not configured, the password can be obtained by clicking on `Generate Password`.
 
 SSH
 ---
@@ -108,7 +131,7 @@
 magical `refs/for/'branch'` ref using any Git client tool:
 
 ====
-  git push ssh://sshusername@hostname:29418/projectname HEAD:refs/for/branchname
+  git push ssh://sshusername@hostname:29418/projectname HEAD:refs/for/branch
 ====
 
 E.g. `john.doe` can use git push to upload new changes for the
@@ -130,12 +153,12 @@
 
 To include a short tag associated with all of the changes in the
 same group, such as the local topic branch name, append it after
-the destination branch name.  In this example the short topic tag
+the destination branch name. In this example the short topic tag
 'driver/i42' will be saved on each change this push creates or
 updates:
 
 ====
-  git push ssh://john.doe@git.example.com:29418/kernel/common HEAD:refs/for/experimental/driver/i42
+  git push ssh://john.doe@git.example.com:29418/kernel/common HEAD:refs/for/experimental%topic=driver/i42
 ====
 
 If you are frequently uploading changes to the same Gerrit server,
@@ -156,17 +179,17 @@
 
 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`:
+as options in the reference
 
 ====
-  git push --receive-pack='git receive-pack --reviewer=a@a.com --cc=b@o.com' tr:kernel/common HEAD:refs/for/experimental
+  git push tr:kernel/common HEAD:refs/for/experimental%r=a@a.com,cc=b@o.com
 ====
 
-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
-addresses had also requested to receive all new change notifications.
+The `r='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 addresses had also requested
+to receive all new change notifications.
 
 If you are frequently sending changes to the same parties and/or
 branches, consider adding a custom remote block to your project's
@@ -175,12 +198,11 @@
 ====
   $ cat .git/config
   ...
-  [remote "for-a-exp"]
+  [remote "exp"]
     url = tr:kernel/common
-    receivepack = git receive-pack --reviewer=a@a.com --cc=b@o.com
-    push = HEAD:refs/for/experimental
+    push = HEAD:refs/for/experimental%r=a@a.com,cc=b@o.com
 
-  $ git push for-a-exp
+  $ git push exp
 ====
 
 
diff --git a/ReleaseNotes/ReleaseNotes-2.0.21.txt b/ReleaseNotes/ReleaseNotes-2.0.21.txt
index 7dd9ef4..47ba654 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.21.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.21.txt
@@ -201,7 +201,7 @@
 Gerrit no longer forges the From header in notification emails.
 To enable the prior forging behavior, set `sendemail.from`
 to `USER` in gerrit.config.  For more details see
-[http://gerrit.googlecode.com/svn/documentation/2.0/config-gerrit.html#sendemail.from sendemail.from]
+link:http://gerrit.googlecode.com/svn/documentation/2.0/config-gerrit.html#sendemail.from[sendemail.from]
 
 Bug Fixes
 ---------
diff --git a/ReleaseNotes/ReleaseNotes-2.0.24.txt b/ReleaseNotes/ReleaseNotes-2.0.24.txt
index 9481a3a..7e0a617 100644
--- a/ReleaseNotes/ReleaseNotes-2.0.24.txt
+++ b/ReleaseNotes/ReleaseNotes-2.0.24.txt
@@ -67,7 +67,7 @@
 * issue 300    Support SMTP over SSL/TLS
 +
 Encrypted SMTP is now supported natively within Gerrit, see
-[http://gerrit.googlecode.com/svn/documentation/2.0/config-gerrit.html#sendemail.smtpEncryption sendemail.smtpEncryption]
+link:http://gerrit.googlecode.com/svn/documentation/2.0/config-gerrit.html#sendemail.smtpEncryption[sendemail.smtpEncryption]
 
 Bug Fixes
 ---------
diff --git a/ReleaseNotes/ReleaseNotes-2.5.1.txt b/ReleaseNotes/ReleaseNotes-2.5.1.txt
index 967d78d..3a640d1 100644
--- a/ReleaseNotes/ReleaseNotes-2.5.1.txt
+++ b/ReleaseNotes/ReleaseNotes-2.5.1.txt
@@ -38,9 +38,10 @@
 administrators.
 +
 In addition it is now no longer possible to
-- set a non-existing project as parent (as this would make the project
++
+** set a non-existing project as parent (as this would make the project
   be orphaned)
-- set a parent project for the `All-Projects` root project (the root
+** set a parent project for the `All-Projects` root project (the root
   project by definition has no parent)
 by pushing changes of the `project.config` file to `refs/meta/config`.
 
diff --git a/ReleaseNotes/ReleaseNotes-2.5.2.txt b/ReleaseNotes/ReleaseNotes-2.5.2.txt
index 5e99ebf..08b4f86 100644
--- a/ReleaseNotes/ReleaseNotes-2.5.2.txt
+++ b/ReleaseNotes/ReleaseNotes-2.5.2.txt
@@ -7,7 +7,7 @@
 
 There are no schema changes from 2.5, or 2.5.1.
 
-However, if upgrading from anything earlier version, follow the upgrade
+However, if upgrading from any earlier version, follow the upgrade
 procedure in the 2.5 link:ReleaseNotes-2.5.html[Release Notes].
 
 Bug Fixes
diff --git a/ReleaseNotes/ReleaseNotes-2.5.txt b/ReleaseNotes/ReleaseNotes-2.5.txt
index 55fbb60..023b4bf 100644
--- a/ReleaseNotes/ReleaseNotes-2.5.txt
+++ b/ReleaseNotes/ReleaseNotes-2.5.txt
@@ -270,11 +270,11 @@
 * link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/rest-api.html[Documentation of the REST API]
 
 * Support REST endpoints to
-** link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/rest-api.html#changes[query changes]
-** link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/rest-api.html#projects[list projects]
-** link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/rest-api.html#suggest-projects[suggest
+** link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/rest-api-changes.html[query changes]
+** link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/rest-api-projects.html[list projects]
+** link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/rest-api-projects.html#suggest-projects[suggest
    projects]
-** link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/rest-api.html#accounts_self_capabilities[query
+** link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/rest-api-accounts.html#list-account-capabilities[query
    the global capabilities of the calling user]
 
 * Support link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/rest-api.html#authentication[anonymous
@@ -1376,7 +1376,7 @@
 * Avoid second remote call to lookup approvals when loading change
   results
 +
-By using the new link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/rest-api.html#changes[`/changes/`]
+By using the new link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/rest-api-changes.html[`/changes/`]
 REST endpoint the web UI client now obtains the label information
 during the query and avoids a second round trip to lookup the current
 approvals for each displayed change. For most users this should improve
diff --git a/ReleaseNotes/ReleaseNotes-2.6.txt b/ReleaseNotes/ReleaseNotes-2.6.txt
new file mode 100644
index 0000000..85f2844
--- /dev/null
+++ b/ReleaseNotes/ReleaseNotes-2.6.txt
@@ -0,0 +1,1702 @@
+Release notes for Gerrit 2.6
+============================
+
+Gerrit 2.6 is now available:
+
+link:http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.6.war[http://code.google.com/p/gerrit/downloads/detail?name=gerrit-2.6.war]
+
+Gerrit 2.6 includes the bug fixes done with
+link:ReleaseNotes-2.5.1.html[Gerrit 2.5.1] and
+link:ReleaseNotes-2.5.2.html[Gerrit 2.5.2]. These bug fixes are *not*
+listed in these release notes.
+
+Schema Change
+-------------
+*WARNING:* This release contains schema changes.  To upgrade:
+----
+  java -jar gerrit.war init -d site_path
+----
+
+*WARNING:* Upgrading to 2.6.x requires the server be first upgraded to 2.1.7 (or
+a later 2.1.x version), and then to 2.6.x.  If you are upgrading from 2.2.x.x or
+newer, you may ignore this warning and upgrade directly to 2.6.x.
+
+Release Highlights
+------------------
+* 42x improvement on `git clone` and `git fetch`
++
+Running link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/cmd-gc.html[
+gerrit gc] allows JGit to optimize a repository to serve clone and fetch
+faster than C Git can, with massively lower server CPU required. Typically
+Gerrit 2.6 can completely transfer a project to a client faster than C Git
+can finish "Counting" the objects.
+
+* Completely customizable workflow
++
+Individual projects can add (or remove) score categories through
+link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/config-labels.html[
+labels] and link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/prolog-cookbook.html[
+Prolog rules].
+
+New Features
+------------
+
+Web UI
+~~~~~~
+
+Global
+^^^^^^
+
+* New Login Screens
++
+New form based HTML screens for login allow browsers to offer the
+choice to save the login data locally in the user's password store.
+
+* Rename "Groups" top-level menu to "People"
+
+* Move "Draft Comments" link next to "Drafts" link
+
+* Highlight the active menu item
+
+* Move user info, settings, and logout to popup dialog
+
+* Show a small version of the avatar image next to the user's name.
+
+* Show avatar image in user info popup dialog
+
+* Always show 'Working ...' message
++
+The 'Working ...' message is relatively positioned from the top of
+the browser, so that the message is always visible, even if the user
+has scrolled down the page.
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/config-gerrit.html#suggest.from[
+  suggest.from] configures a minimum number of characters before
+  matches for reviewers, accounts, groups or projects are offered.
+
+* Make the default font size "small".
+
+* Mark all CSS classes as external so users can rely on them.
+
+Search
+^^^^^^
+* Animate search bar by expanding & unexpanding
++
+When the search bar is used, expand it to allow for more text to be
+visible. When it is blurred, shrink it back to the original size.
+
+* Suggest projects, groups and users in search panel
++
+Suggest projects, groups and users in the search panel as parameter for
+those search operators that expect a project, group or user.
+
+* In search panel suggest 'self' as value for operators that expect a user
+
+* Quote values suggested for search operators only if needed
++
+The values that are suggested for the search operators in the search
+panel are now only quoted if they contain a whitespace.
+
+Change Screens
+^^^^^^^^^^^^^^
+
+* A change's commit message can be edited from the change screen.
+
+* A change's topic can be added, removed or changed from the
+  change screen.
+
+* An "Add Comment" button is added to change screen
+
+* The reviewer matrix on a change displays gray boxes where permissions
+  do not allow voting in that category.
++
+The coloring enables authors to quickly identify if another reviewer
+is necessary to continue the change.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=353[Issue 353] &
+  link:https://code.google.com/p/gerrit/issues/detail?id=1123[Issue 1123]:
+  New link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/project-setup.html#rebase_if_necessary[
+  Rebase If Necessary] submit type
++
+This is similar to cherry pick, but honors change dependency
+information.
+
+* The rebase button is hidden when the patch set is current.
+
+* Improved review message when a change is rebased in the UI
++
+When a change is rebased in the UI by pressing the rebase button, a
+comment is added onto the review. Instead of only saying 'Rebased' the
+message is now more verbose, e.g. 'Patch Set 1 was rebased'.
+
+* The submit type that is used for submitting a change is shown on the
+  change screen in the info block.
++
+This is useful because the submit type of a change can now be
+link:#submit-type-from-prolog[controlled by Prolog].
+
+* Replace the All Diff buttons on the change screen with links
++
+The action buttons to open the diff for all files in own tabs consumed
+too much space due to the long label texts.
+
+* The patch set review screen can include radio buttons for custom
+  labels if enabled by
+  link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/prolog-cookbook.html#_how_to_write_submit_rules[submit rules].
+
+* Voting on draft changes is now possible.
+
+* Recommend rebase on Path Conflict
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=1685[Issue 1685]:
+  After 'Up to change' expand the patch set that was just reviewed
++
+After clicking on the 'Up to change' link on a patch screen, the patch
+set that was just reviewed is automatically expanded on the change
+screen.
+
+* Allow direct change URLs to end with '/'.
+
+* Slightly increase commit message text size from 8px to 9px.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=1381[Issue 1381]:
+  Remove the ID column from change tables
++
+Users don't really need the ID column present. For most changes the
+subject is descriptive and unique enough to identify the correct
+change.
+
+* Do not wrap project/branch/owner fields in change table
++
+This makes it easier to use Gerrit on narrow screens.
+
+Patch Screens
+^^^^^^^^^^^^^
+
+* Support for file comments
++
+It is now possible to comment on a whole file in a patch.
+
+* Have the reviewed panel also at the bottom of the patch screen
++
+Reviewers normally review patches top down, finishing the review when
+they reach the bottom of the patch. To use the streamlined review
+workflow they now don't need to scroll back to the top to find the
+reviewed checkbox and link.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=1494[Issue 1494]:
+  Use mono-font for displaying the file contents
++
+This avoids alignment errors when syntax highlighting is enabled.
+
+Project Screens
+^^^^^^^^^^^^^^^
+
+* Support filtering of projects in the project list screen
++
+Filter matches are highlighted by bold printing.
++
+The filter is reflected by the `filter` URL parameter.
+
+* Support filtering of projects in ProjectListPopup
++
+Filter matches are highlighted by bold printing.
+
+* Display a query icon for each project in the project list screen that
+  links to the default query/dashboard of that project.
+
+* Replace projects side menus with top menus
++
+The top menus are submenus to the Project Menu and they appear only
+when a project has been selected.
+
+* Remember the last Project Screen used
++
+Remember the last project screen used every time a project screen is
+loaded. Go to the remembered screen when selecting a new project from
+the project list instead of always going to the project info screen.
+
+* Remember the last project viewed
++
+Remember the last project viewed when navigating away from a project
+screen.  If there is a remembered project, then the extra project links
+are not hidden.
+
+* Add clone panel to the project general screen
+
+* New screen for listing and accessing the project dashboards.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=1677[Issue 1677]:
+  Place the 'Browse' button to select a watched project next to input field
+
+* Ask user to login if project is not found
++
+Accessing a project URL was failing with 'Not Found - The page you
+requested was not found, or you do not have permission to view this
+page' if the user was not signed in and the project was not visible to
+'Anonymous Users'. Instead Gerrit now asks the user to login and
+afterwards shows the project to the user if it exists and is visible.
+If the project doesn't exist or is not visible, the user will still get
+the Not Found screen after sign in.
+
+* Improve error handling on branch creation
++
+Improve the error messages that are displayed in the WebUI if the
+creation of a branch fails due to invalid user input.
+
+Group Screens
+^^^^^^^^^^^^^
+
+* Support filtering of groups in the group list screen
++
+Filter matches are highlighted by bold printing.
++
+The filter is reflected by the `filter` URL parameter.
+
+* Remove group type from group info screen
++
+The information about the group type was not much helpful. All groups
+that can be seen in Gerrit are of type 'INTERNAL', except a few
+well-known system groups which are of type 'SYSTEM'. The system groups
+are so well-known that there is no need to display the type for them.
+
+Dashboard Screens
+^^^^^^^^^^^^^^^^^
+
+* Link dashboard title to a URL version of itself
++
+When using a stable project dashboard URL, the URL obfuscates the
+content of the dashboard which can make it hard to debug a dashboard or
+copy and modify it. In the special case of stable dashboards, make the
+title a link to an unstable URL version of the dashboard with the URL
+reflecting the actual dashboard contents the way a custom dashboard
+does.
+
+* Increase time span for "Recently Closed" section in user dashboard to 4 weeks.
+
+Account Screens
+^^^^^^^^^^^^^^^
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=1740[Issue 1740]:
+  Display description how to generate SSH Key in SshPanel
++
+Display a description of how to generate an SSH Key in an expandable
+section in the SshPanel instead of linking to the GitHub SSH tutorial.
+The GitHub SSH tutorial was partially not relevant and confused users.
+
+* Make the text for "Register" customizable
+
+Plugin Screens
+^^^^^^^^^^^^^^
+
+* Show status for enabled plugins in the WebUI as 'Enabled'
++
+Earlier no status was shown for enabled plugins, which was confusing to
+some users.
+
+REST API
+~~~~~~~~
+
+* A big chunk of the Gerrit functionality is now available via the
+  link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/rest-api.html[REST API].
++
+The REST API is *NOT* complete yet and some functionality is still missing.
++
+To find out which functionality is available, check the REST endpoint documentation for
+link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/rest-api-projects.html[projects],
+link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/rest-api-changes.html[changes],
+link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/rest-api-groups.html[groups] and
+link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/rest-api-accounts.html[accounts].
+
+* Support setting `HEAD` of a project
+  link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/rest-api-projects.html#set-head[via REST].
+
+* Audit support for REST API.
++
+Allow generating Audit events related to REST API execution. The
+structure of the AuditEvent has been extended to support the new
+name-multivalue pairs used in the REST API.
++
+This is breaking compatibility with the 2.5 API as it changes the
+params data type, this is needed anyway as the previous list of
+Objects was not providing all the necessary information of
+"what relates to what" in terms of parameters info.
++
+Existing support for SSH and JSON-RPC events have been adapted in
+order to fit into the new name-multivalue syntax: this allow a
+generic audit plug-in to capture all parameters regardless of where
+they have been generated.
+
+* Remove support for deprecated `--format` option when listing changes
++
+Querying changes via REST is now always producing JSON output.
+
+* Introduce `id` property on REST entities
++
+The `/changes/` entities now use `id` to include a triplet of the
+project, branch and change-id string to uniquely identify that change
+on the server. This moves the old `id` field to be named `change_id`,
+which is a breaking change.
+
+* Accept common forms of malformed JSON
++
+Some clients may send JSON-ish instead of JSON. Be nice to those
+clients and accept various useful forms of incorrect syntax:
++
+** End of line comments starting with `//` or `#` and ending with a
+   newline character.
+** C-style comments starting with `/*` and ending with `*/`
+   Such comments may not be nested.
+** Names that are unquoted or single quoted.
+** Strings that are unquoted or single quoted.
+** Array elements separated by `;` instead of `,`.
+** Unnecessary array separators. These are interpreted as if null was
+   the omitted value.
+** Names and values separated by `=` or `=>` instead of `:`.
+** Name/value pairs separated by `;` instead of `,`.
+
+* Be more liberal about parsing JSON responses
++
+If the response begins with the JSON magic string, remove it before
+parsing. If a response is missing this leading string, parse the
+response as-is.
+
+* Accept simple form encoded data for REST APIs
++
+Simple cases like `/review` or `/abandon` can now accept standard form
+values for basic properties, making it simple for tools to directly
+post data:
++
+----
+  curl -n --digest \
+  --data 'message=Does not compile.' \
+  --data labels.Verified=-1 \
+  http://localhost:8080/a/changes/3/revisions/1/review
+----
++
+Form field names are JSON field names in the top level object.  If dot
+appears in the name the part to the left is taken as the JSON field
+name and the part to the right as the key for a Map. This nicely fits
+with the labels structure used by `/review`, but doesn't support the
+much more complex inline comment case. Clients that need to use more
+complex fields must use JSON formatting for the request body.
+
+* Allow administrators to see other user capabilities
++
+Expand `/accounts/{id}/capabilities` to permit an administrator
+to inspect another user's effective capabilities.
+
+* Declare kind in JSON API results
++
+This is recommended to hint to clients what the entity type is when
+processing the JSON payload.
+
+* Format h/help output as plain text not JSON
++
+The output produced when the client requested the h or help property
+from a JSON API is always produced from constant compiled into the
+server. Assume this safe to return to the client as text/plain content
+and avoid wrapping it into an HTML escaped JSON string.
+
+* Use string for JSON encoded plain text replies
++
+Instead of wrapping the value into an object, just return the
+string by itself. This better matches what happens with the plain
+text return format.
+
+* Wrap possible HTML plain text in JSON on GET
++
+If the HTML appears like MSIE might guess it is HTML (such as if it
+contains `<`) encode the response as a JSON object instead of as a
+simple plain text string. This won't show up very often for clients,
+and protects MSIE users stuck on ancient versions (pre MSIE 8).
+
+* Ask MSIE to never sniff content types on REST API responses
++
+Newer versions of MSIE can disable the content sniffing feature if the
+server asks it to by setting an extension header. It is annoying, but
+necessary, that a server needs to say "No really, I _am_ telling you
+the right Content-Type, trust it."
++
+This feature was added in MSIE 8 Beta 2 so it doesn't protect users
+running MSIE 6 or 7, but those are ancient and users should upgrade.
++
+Enable this on the REST API responses because we sometimes send back
+text/plain results that are really just plain text. Existing JSON
+responses are protected from accidential sniffing and treatment as
+HTML thanks to Gson encoding HTML control characters using Unicode
+character escapes within JSON strings.
+
+* Apache reverse proxies must switch to mod_rewrite
++
+When Apache is used as a reverse proxy the server must be reconfigured
+to use mod_rewrite and AllowEncodedSlashes.  For updated information
+link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/config-reverseproxy.html#_apache_2_configuration[
+review the Apache 2 Configuration documentation].
+
+Project Dashboards
+~~~~~~~~~~~~~~~~~~
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/user-dashboards.html#project-dashboards[
+  Support for storing custom dashboards for projects]
++
+Custom dashboards can now be stored in the projects
+`refs/meta/dashboards/*` branches.
++
+The project dashboards are shown in a new project screen and can be
+link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/rest-api-projects.html#dashboard-endpoints[
+accessed via REST].
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/user-dashboards.html#project-default-dashboard[
+  Allow defining a default dashboard for projects]
+
+* Support inheritance for project dashboards.
++
+In dashboards queries the `${project}` token can be used as placeholder
+for the project name. This token will be replaced with the project to
+which a dashboard is being applied.
+
+* On the project list screen a query icon is displayed for each project
+  that links to the default dashboard of that project.
+
+* Support a `foreach` parameter for custom dashboards.
++
+The `foreach` parameter which will get appended to all the queries in
+the dashboard.
+
+Access Controls
+~~~~~~~~~~~~~~~
+* Allow to overrule `BLOCK` permissions on the same project
++
+It was impossible to block a permission for a group and allow the same
+permission for a sub-group of that group as the `BLOCK` permission
+always won over any `ALLOW` permission. For example, it was impossible
+to block the "Forge Committer" permission for all users and then allow
+it only for a couple of privileged users.
++
+An `ALLOW` permission has now  priority over a `BLOCK` permission when
+they are defined in the same access section of a project. To achieve the
+above mentioned policy the following could be defined:
++
+  [access "refs/heads/*"]
+    forgeCommitter = block group Anonymous Users
+    forgeCommitter = group Privileged Users
++
+Across projects the `BLOCK` permission still wins over any `ALLOW`
+permission. This way one cannot override an inherited `BLOCK`
+permission in a subproject.
++
+Overruling of `BLOCK` permissions with `ALLOW` permissions also works
+for labels i.e. permission ranges. If a dedicated 'Verifiers' group
+need to be the only group who can vote in the 'Verified' label and it
+must be ensured that even project owners cannot change this policy,
+then the following can be defined in a common parent project:
++
+  [access "refs/heads/*"]
+    label-Verified = block -1..+1 group Anonymous Users
+    label-Verified = -1..+1 group Verifiers
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=1516[issue 1516]:
+  Show global capabilities to all users that can read `refs/meta/config`
++
+Users can now propose changes to the global capabilities for review
+from the WebUI.
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/access-control.html#category_remove_reviewer[
+  Remove Reviewer] is a new permission.
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/access-control.html#category_push_signed[
+  Pushing a signed tag] is a new permission.
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/access-control.html#category_edit_topic_name[
+  Editing the topic name] is a new permission.
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/access-control.html#capability_accessDatabase[
+  Raw database access] with the `gsql` command is a new global capability.
++
+Previously site administrators had this capability by default.  Now it has
+to be explicitly assigned, even for site administrators.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=1585[Issue 1585]:
+  link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/access-control.html#category_view_drafts[
+  Viewing other users' draft changes] is a new permission.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=1675[Issue 1675]:
+  link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/access-control.html#category_delete_drafts[Deleting] and
+  link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/access-control.html#category_publish_drafts[publishing]
+  other users' draft changes is a new permission.
+
+* Grant most permissions when creating `All-Projects`
++
+Make Gerrit more like a Git server out-of-the box by granting both
+Administrators and Project Owners permissions to review changes, submit
+them, create branches, create tags, and push directly to branches.
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/config-gerrit.html#ldap.groupName[
+  LDAP group names] are configurable, `cn` is still the default.
+
+* Kerberos authentication to LDAP servers is now supported.
+
+* Basic project properties are now inherited by default from parent
+  projects: Use Content Merge, Require Contributor Agreement, Require
+  Change Id, Require Signed Off By.
+
+* Allow assigning `Push` for `refs/meta/config` on `All-Projects`
++
+The `refs/meta/config` branch of the `All-Projects` project should only
+be modified by Gerrit administrators because being able to do
+modifications on this branch means that the user could assign himself
+administrator permissions.
++
+In addition to being administrator Gerrit requires that the
+administrator has the `Push` access right for `refs/meta/config` in
+order to be able to modify it (just as with all other branches
+administrators do not have edit permissions by default).
++
+The problem was that assigning the `Push` access right for
+`refs/meta/config` on the `All-Projects` project was not allowed.
++
+Having the `Push` access right for `refs/meta/config` on the
+`All-Projects` project without being administrator has no effect.
+
+Hooks
+~~~~~
+* Change topic is passed to hooks as `--topic NAME`.
+* link:https://code.google.com/p/gerrit/issues/detail?id=1200[Issue 1200]:
+New `reviewer-added` hook and stream event when a reviewer is added.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=1237[Issue 1237]:
+New `merge-failed` hook and stream event when a change cannot be submitted due to failed merge.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=925[Issue 925]:
+New `ref-update` hook run before a push is accepted by Gerrit.
+
+* Add `--is-draft` parameter to `comment-added` hook
+
+Git
+~~~
+* Add options to `refs/for/` magic branch syntax
++
+Git doesn't want to modify the network protocol to support passing
+data from the git push client to the server. Work around this by
+embedding option data into a new style of reference specification:
++
+----
+  refs/for/master%r=alice,cc=bob,cc=charlie,topic=options
+----
++
+is now parsed by the server as:
++
+--
+** set topic to "options"
+** CC charlie and bob
+** add reviewer alice
+** for branch refs/heads/master
+--
++
+If `%` is used the extra information after the branch name is
+parsed as options with args4j. Each option is delimited by `,`.
++
+Selecting publish vs. draft should be done with the options `draft` or
+`publish`, appearing anywhere in the refspec after the `%` marker:
++
+----
+  refs/for/master%draft
+  refs/for/master%draft,r=alice
+  refs/for/master%r=alice,draft
+  refs/for/master%r=alice,publish
+----
+
+* Enable content merge by default
++
+Most teams seem to expect Gerrit to manage simple merges within a
+source code file. Enable this out-of-the-box.
+
+* Added a link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/config-gerrit.html#core.useRecursiveMerge[
+  server-level option] to use JGit's new, experimental recursive merger.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=1608[Issue 1608]:
+Commits pushed without a Change-Id now warn with instructions on how
+to download and install the commit-msg hook.
+
+* Add `oldObjectId` and `newObjectId` to the `GitReferenceUpdatedListener.Update`
+
+SSH
+~~~
+* New SSH command to http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/cmd-gc.html[
+  run Git garbage collection]
++
+All GC runs are logged in a GC log file.
+
+* Descriptions are added to ssh commands.
++
+If `gerrit` is called without arguments, it will now show a list of available
+commands with their descriptions.
+
+* `create-account --http-password` enables setting/resetting the
+  HTTP password of role accounts, for Git or REST API access.
+
+* http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/cmd-ls-user-refs.html[
+  ls-user-refs] lists which refs are visible for a given user.
+
+* `ls-projects --has-acl-for` lists projects that mention a group
+  in an ACL, identifying where rights are granted.
+
+* `review` command supports project-specific labels
+
+* `test-submit-rule` was renamed to `test-submit rule`:
++
+`rule` is now a subcommand of the `test-submit` command.
+
+* http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/cmd-test-submit-type.html[
+  test-submit type] tests the Prolog submit type with a chosen change.
+
+Query
+~~~~~
+* Allow `{}` to be used for quoting in query expressions
++
+This makes it a little easier to query for group names that contain
+a space over SSH:
++
+  ssh srv gerrit query " 'status:open NOT reviewerin:{Developer Group}' "
+
+* The query summary block includes `resumeSortKey`.
+
+* Query results include author and change size information when certain
+  options are specified.
+
+* When a file is renamed the old file name is included in the Patch
+  attribute
+
+Plugins
+~~~~~~~
+* Plugins can contribute Prolog facts/predicates from Java.
+* Plugins can prompt for parameters during `init` with `InitStep`.
+* Plugins can now contribute JavaScript to the web UI. UI plugins can
+  also be written and compiled with GWT.
+* New Maven archetypes for JavaScript and GWT plugins.
+* Plugins can contribute validation steps to received commits.
+* Commit message length checks are moved to the `commit-message-length-validator`
+  plugin which is included as a core plugin in the Gerrit distribution and
+  can be installed during site initialization.
+* Creation of code review notes is moved to the `reviewnotes` plugin
+  which is included as a core plugin in the Gerrit distribution and can
+  be installed during site initialization.
+* A plugin extension point for avatar images was added.
+* Allow HTTP plugins to change `static` or `docs` prefixes
++
+An HTTP plugin may want more control over its URL space, but still
+delegate to the plugin servlet's magic handling for static files and
+documentation. Add JAR attributes to configure these prefixes.
+
+Prolog
+~~~~~~
+[[submit-type-from-prolog]]
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/prolog-cookbook.html#HowToWriteSubmitType[
+  Support controlling the submit type for changes from Prolog]
++
+Similarly like the `submit_rule` there is now a `submit_type` predicate
+which returns the allowed submit type for a change. When the
+`submit_type` predicate is not provided in the `rules.pl` then the
+project default submit type is used for all changes of that project.
++
+Filtering the results of the `submit_type` is also supported in the
+same way like filtering the results of the `submit_rule`. Using a
+`submit_type_filter` predicate one can enforce a particular submit type
+from a parent project.
+
+* Plugins can contribute Prolog facts/predicates from Java.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=288[Issue 288]:
+  Expose basic commit statistics for the Prolog rule engine
++
+A new method `gerrit:commit_stats(-Files,-Insertions, -Deletions)` was
+added.
+
+* A new `max_with_block` predicate was added for more convenient usage
+
+Email
+~~~~~
+* Notify project watchers if draft change is published
+* Notify users mentioned in commit footer on draft publish
+* Add new notify type that allows watching of new patch sets
+* link:https://code.google.com/p/gerrit/issues/detail?id=1686[Issue 1686]:
+  Add new notify type that allows watching abandoning of changes
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/user-notify.html[
+  Notifications configured in `project.config`] can now be addressed
+  using any of To, CC, or BCC headers.
+* link:https://code.google.com/p/gerrit/issues/detail?id=1531[Issue 1531]:
+Email footers now include `Gerrit-HasComments: {Yes|No}`.
+* `#if($email.hasInlineComments())` can be used in templates to test
+  if there are comments to be included in this email.
+* Notification emails are sent to included groups.
+* Comment notification emails are sent to project watchers.
+* "Change Merged" emails include the diff output when `sendemail.includeDiff` is enabled.
+* When link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/rest-api-changes.html#set-review[
+  posting a review via REST] the caller can control email delivery
++
+This may help automated systems to be less noisy. Tools can now choose
+which review updates should send email, and which categories of users
+on a change should get that email.
+
+Labels
+~~~~~~
+* Approval categories stored in the database have been replaced with labels
+  configured in `project.config`. Existing categories are migrated to
+  `project.config` in `All-Projects` as part of the schema upgrade; no user
+  action is required.
+* Labels are no longer global;
+  link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/config-labels.html[
+  projects may define their own labels], with inheritance.
+* Don't create `Verify` category by default
++
+Most project teams seem confused with the out-of-the-box experience
+needing to vote on both `Code-Review` and `Verified` categories in
+order to submit a change. Simplify the out-of-the-box workflow to only
+have `Code-Review`. When a team installs the Hudson/Jenkins integration
+or their own build system they can now trivially add the `Verified`
+category by pasting 5 lines into `project.config`.
+
+Dev
+~~~
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/dev-readme.html#debug-javascript[
+  Support loading debug JavaScript]
+
+* Gerrit acceptance tests
++
+An infrastructure for testing the Gerrit daemon via REST and/or SSH
+protocols has been added. Gerrit daemon is run in the headless mode and
+in the same JVM where the tests run. Besides using REST/SSH, the tests
+can also access Gerrit server internals to prepare the test environment
+and to perform assertions.
++
+A new review site is created for each test and the Gerrit daemon is
+started on that site. When the test has finished the Gerrit daemon is
+shutdown.
+
+* Lightweight LDAP server for debugging
+
+* Add asciidoc checks in the documentation makefile
++
+Exit with error if the asciidoc executable is not available or has
+version lower than 8.6.3.
++
+The release script is aborted if asciidoc is missing.
+
+* Added sublime project files to `.gitignore`
+
+* Exclude all `pom.xml` files that are archetype resources in `version.sh`
+
+Performance
+~~~~~~~~~~~
+* Bitmap Optimizations
++
+On running the http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/cmd-gc.html[
+garbage collection] JGit creates bitmap data that is saved to an
+auxiliary file. The bitmap optimizations improve the clone and fetch
+performance. git-core will ignore the bitmap data.
+
+* Improve suggest user performance when adding a reviewer.
++
+Do not check the visibility of the change for each suggested account if
+the ref is visible by all registered users.
++
+On a system with about 2-3000 users, where most of the projects are
+visible by every registered user, this improves the performance of the
+suggesting reviewer by a factor of 1000 at least.
+
+* Cache RefControl.isVisible()
++
+For Git repositories with many changes the time for calculating visible
+refs is reduced by 30-50%.
+
+* Allow admins to disable magic ref check on upload
++
+Some sites manage to run their repositories in a way that prevents
+users from ever being able to create `refs/for`, `refs/drafts` or
+`refs/publish` names in a repository. Allow admins on those servers
+to disable this (somewhat) expensive check before every upload.
+
+* Permit ProjectCacheClock to be completely disabled
++
+Some admins may just want to require all updates to projects to be
+made through the web interface, and avoid the small expense of a
+background thread ticking off changes.
+
+* Batch read Change objects during query
+
+* Default `core.streamFileThreshold` to a larger value
++
+If this value is not configured by the server administrator
+performance on larger text files suffers considerably and
+Gerrit may grind to a halt and be unable to answer users.
++
+Default to either 25% of the available JVM heap or ~2048m.
+
+* Improve performance of ReceiveCommits for repositories with many refs
++
+Avoid adding `refs/changes/` and `refs/tags/` to RevWalk's as
+uninteresting since JGit RevWalk doesn't perform well when a large
+number of objects is marked as uninteresting.
+
+* PatchSet.isRef()-optimizations.
++
+PatchSet.isRef() is used extensively when preparing for a ref
+advertisment and the regular expression used by isRefs() was notably
+costly in these circumstances, especially since it could not be
+pre-compiled.
++
+The regular expression is removed and the check is now directly
+implemented. As result the performance of `git ls-remote` could be
+increased upto 15%.
+
+* New config option `receive.checkReferencedObjectsAreReachable`
++
+If set to true, Gerrit will validate that all referenced objects that
+are not included in the received pack are reachable by the user.
++
+Carrying out this check on Git repositories with many refs and commits
+can be a very CPU-heavy operation. For non public Gerrit servers it may
+make sense to disable this check, which is now possible.
+
+* Cache config value in LdapAuthBackend
+
+* Perform a single /accounts/self/capabilities on page load
++
+This joins up 3 requests into a single call, which should speed up
+initial page load for most users.
+
+* Only gzip compress responses that are smaller compressed
+
+* Caching of changes
++
+During Ref Advertisments (via VisibleRefFilter), all changes need to
+be fetched from the database to allow Gerrit to figure out which change
+refs are visible and should be advertised to the user. To reduce
+database traffic a cache for changes was introduced. This cache is
+disabled by default since it can mess-up multi-server setups.
+
+Misc
+~~~~
+* Add config parameter to make new groups by default visible to all
++
+Add a new http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/config-gerrit.html#groups.newGroupsVisibleToAll[
+Gerrit configuration parameter] that controls whether newly
+created groups should be by default visible to all registered users.
+
+* Support for OpenID domain filtering
++
+Added the ability to only allow email addresses under specific domains
+to be used for OpenID login.
++
+The allowed domains can be configured by setting
+http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/config-gerrit.html#auth.openIdDomain[
+auth.openIdDomain] in the Gerrit configuration.
+
+* Always configure
+  http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/config-gerrit.html#gerrit.canonicalWebUrl[
+  gerrit.canonicalWebUrl] on init
++
+Gerrit has been requiring this field for several versions now, but init
+did not configure it. Ensure there is a value set so the server is not
+confused at runtime.
+
+* Add submodule subscriptions fetching by projects
++
+While submodule subscriptions can be fetched by branch, some plugins
+(e.g.: delete-project) would rather need to access all submodule
+subscriptions of a project (regardless of the branch). Instead of
+iterating over all branches of a project, and fetching the
+subscription for each branch separately, we allow fetching of
+subscriptions directly by projects.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=1805[Issue 1805]:
+  Make client SSL certificates that contain an email address work
++
+Authentication with CLIENT_SSL_CERT_LDAP didn't work if the certificate
+contained email address.
+
+* Guess LDAP type of Active Directory LDS as ActiveDirectory
++
+If Gerrit connects to an AD LDS [1] server it will guess its type as
+RCF_2307 instead of ActiveDirectory. The reason is that an AD LDS
+doesn't support the "1.2.840.113556.1.4.800" capability.  However,
+AD LDS behaves like ActiveDirectory and Gerrit also needs to guess
+its type as ActiveDirectory to make the default query patterns work
+properly.
++
+Extend the LDAP server type guessing by checking for presence of the
+"1.2.840.113556.1.4.1851" capability which indicates that this LDAP
+server runs ActiveDirectory as AD LDS [2].
++
+Also remove the check for the presence of the "defaultNamingContext"
+attribute as we don't use it anywhere and, by default, this attribute is
+not set on an AD LDS [3]
++
+[1] http://msdn.microsoft.com/en-us/library/aa705886(VS.85).aspx +
+[2] http://msdn.microsoft.com/en-us/library/cc223364.aspx +
+[3] http://technet.microsoft.com/en-us/library/cc816929(v=ws.10).aspx
+
+* Allow group descriptions to supply email and URL
++
+Some backends have external management interfaces that are not
+embedded into Gerrit Code Review. Allow those backends to supply
+a URL to the web management interface for a group, so a user can
+manage their membership, view current members, or do whatever other
+features the group system might support.
++
+Some backends also have an email address associated with every
+group. Sending email to that address will distribute the message to
+the group's members. Permit backends to supply an optional email
+address, and use this in the project level notification system if
+a group is selected as the target for a message.
+
+* Allow group backends to guess on relevant UUIDs
++
+Expose all cheaply known group UUIDs from the ProjectCache,
+enumerating groups used by access controls. This allows a backend
+that has a large number of groups to filter its getKnownGroups()
+output to only groups that may be relevant for this Gerrit server.
++
+The best use case to consider is an LDAP server at a large
+organization. A typical user may belong to 50 LDAP groups, but only
+3 are relevant to this Gerrit server. Taking the intersection of
+the two groups limits the output Gerrit displays to users, or uses
+when considering same group visibility.
+
+* Add more forbidden characters for project names
++
+`?`, `%`, `*`, `:`, `<`, `>`, `|`, `$`, `\r` are now forbidden in
+project names.
+
+* Make `gerrit.sh` LSB compliant
++
+** Add LSB headers
+** Add 'status' as synonym for 'check'
+** Fix exit status codes according to http://refspecs.linux-foundation.org/LSB_3.2.0/LSB-Core-generic/LSB-Core-generic/iniscrptact.html
+
+* Option to start headless Gerrit daemon
++
+Add `--headless` option to the Daemon which will start Gerrit daemon
+without the Web UI front end (headless mode).
++
+This may be useful for running Gerrit server with an alternative (rest
+based) UI or when starting Gerrit server for the purpose of automated
+REST/SSH based testing.
++
+Currently this option is only supported via the `--headless` option of
+the daemon program. We would need to introduce a config option in order
+to support this feature for deployed war mode.
+
+* Show path to gerrit.war in command for upgrade schema
+
+Upgrades
+~~~~~~~~
+* link:https://code.google.com/p/gerrit/issues/detail?id=1619[Issue 1619]:
+Embedded Jetty is now 8.1.7.v20120910.
+
+* ASM bytecode library is now 4.0.
+* JGit is now 2.3.1.201302201838-r.78-g8fcde4b.
+* asciidoc 8.6.3 is now required to build the documentation.
+* link:https://code.google.com/p/gerrit/issues/detail?id=1155[Issue 1155]:
+prettify is now r225
+
+* The used GWT version is now 2.5.0
++
+Fixes some issues with IE9 and IE10.
+
+Bug Fixes
+---------
+
+Web UI
+~~~~~~
+* link:https://code.google.com/p/gerrit/issues/detail?id=1662[Issue 1662]:
+  Don't show error on ACL modification if empty permissions are added
++
+This error message was incorrectly displayed if a permission without
+rules was added, although the save was actually successful.
+
+* Don't show error on ACL modification if a section is added more than once
++
+This error message was incorrectly displayed if multiple sections for
+the same ref were added, although the save was actually successful.
+
+* Links to CGit were broken when `remove-suffix` was enabled.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=926[Issue 926]:
+Internet Explorer versions 9 and 10 are supported.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=1664[Issue 1664]:
+  Reverting a change did not preserve the change's topic
+
+* Fix: User could get around restrictions by reverting a commit
++
+The Gerrit server may enforce several restrictions on the commit
+message (change-id required, signed-off-by, etc). A user was able to
+get around these restrictions by reverting a commit using the UI.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=1518[Issue 1518]:
+  Reset 'Old Version History' if dependent change is opened
++
+Following the navigation link in the dependencies table on the
+change screen, the user can directly navigate to dependent changes.
+The value for 'Old Version History' of the current change was
+incorrectly applied to the new change. If the value was invalid for
+the new change the 'Old Version History' field became blank.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=1736[Issue 1736]:
+  Clear 'Old Version History' ListBox before populating it
++
+The ListBox was not always cleared and as result the same entries were
+sometimes added multiple times e.g. after rebasing a change in the
+WebUI.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=1673[Issue 1673]:
+  Fix disappearance of patch headers when compared patches are identical
++
+When two patches were compared that were identical 'No Differences' is
+displayed to the user. In this case the patch headers disappeared and
+as result the user couldn't change the patch selection anymore.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=1759[Issue 1759]:
+  Fix ArrayIndexOutOfBoundsException on intraline diff
++
+In some cases displaying the intraline diff failed with an exception like
+this:
++
+----
+java.lang.ArrayIndexOutOfBoundsException: Array index out of range: 10
+  at com.google.gerrit.prettify.common.SparseFileContent.mapIndexToLine(SparseFileContent.java:149)
+  at com.google.gerrit.prettify.common.PrettyFormatter.format(PrettyFormatter.java:188)
+  at com.google.gerrit.client.patches.AbstractPatchContentTable.getSparseHtmlFileB(AbstractPatchContentTable.java:287)
+  at com.google.gerrit.client.patches.SideBySideTable.render(SideBySideTable.java:113)
+  at com.google.gerrit.client.patches.AbstractPatchContentTable.display(AbstractPatchContentTable.java:238)
+  at com.google.gerrit.client.patches.PatchScreen.onResult(PatchScreen.java:444)
+...
+----
++
+This happened when the old line was:
++
+----
+  foo-old<LF>
+----
++
+and the new line was:
++
+----
+  foo-new<CRLF>
+----
+
+* Prevent leading and trailing spaces on user's Full Name
++
+Strip off the leading and trailing spaces from the Full Name that the
+user enters on the Contact Information form.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=1480[Issue 1480]:
+  Show proper error message if registering email address fails
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=816[Issue 816]:
+  Due to issues with the diff_intraline cache the file indention in the
+  Side-By-Side diff was sometimes wrong.
+
+* Make rebase failed and merge failed messages consistent
+
+* Select 'Projects' menu on loading of a project screen
++
+If in the top level menu 'All' is selected and the user navigates to
+a change and then from the change to the project by clicking on the
+project link in the ChangeInfoBlock, the project screen is loaded but
+the 'Projects' menu was not selected.
+
+* Fix display issues for inline comments and inline comment editors
++
+** Sometimes a second comment editor was shown instead of using the
+   existing comment editor.
+** Fix doublicated border line between comments.
+** Sometimes the parts of the border were missing when a comment was
+   expanded.
+** Fix displaying the blue line that marks the current line when there
+   are several published comments.
+** Sometimes on discard of a comment some frames of the comment editor
+   stayed and some border lines of neighbour comments disappeared.
+
+* In diff view don't let arrow column accept clicks.
+
+* Fix display of commit message
++
+If there are no HTML tags in the text, just break on lines.
+
+* Upon selection in AddMemberBox, focus the box's text field
++
+In the change screen, after clicking on an element of the 'Add
+Reviewer' suggestion list, users expect to be able to add the reviewer
+by hitting enter. This did not work in Firefox.
+
+* Fix enter key detection in project creation screen
++
+The enter key detection was not working in all browsers (e.g. Firefox).
+
+* Display a tooltip for all tiny icons and ensure that the cursor is
+  shown as pointer when hovering over them.
+
+* Clean query string when switching pages
++
+If we load a page without a query string, such as Projects->List,
+My->Changes, or Settings, it might be confusing to show the old query
+string from the previous page. The query string is now cleared out
+when switching pages, leaving the help text visible.
+
+* Fix highlighting in search suggestions
++
+The provided suggestions should highlight the part that the user has
+already typed as bold text. This only worked for the first operator.
+For suggestions of any further operator no hightlighting was done.
+
+* Fix style of hint text in search box on initial page load
++
+The hint text should be a light gray on the white background,
+but was coming up black on initial page load due to a style setup
+ordering issue between the SearchPanel and the HintTextBox.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=1661[Issue 1661]:
+  Update links to Change-Id and Signed-off-by docu on project info
+  screen
+
+* Use href="javascript;" for All {Side-by-Side,Unified} links
++
+These links shouldn't have an anchor location. There is nothing for
+the browser to remember or visit if it were opened in a new tab for
+example.
+
+* Improve message for unsatisfiable dependencies
++
+If a change cannot be merged due to unsatisfiable dependencies a
+comment is added to the change that lists the missing commits and says
+that a rebase is necessary.
++
+For each missing commit the comment said "Depends on patch set X
+of ..., however the current patch set is Y."
++
+If multiple commits are missing it may be that for some commits the
+dependency is not outdated (X == Y). In this case the message was
+confusing.
++
+", however the current patch set is Y." is now skipped if Y == X.
+
+REST API
+~~~~~~~~
+* Fix returning of 'Email Reviewers' capability via REST
++
+The `/accounts/self/capabilities/` didn't return the 'Email Reviewers'
+capability when it was not explicitly assigned, although by default
+everyone has the 'Email Reviewers' capability.
++
+If 'Email Reviewers' capability was allowed or denied,
+`/accounts/self/capabilities/` returned the 'Email Reviewers'
+capability always as true, which was wrong for the DENY case.
+
+* Provide a more descriptive error message for unauthenticated REST
+  API access
+
+Git
+~~~
+* The wildcard `.` is now permitted in reference regex rules.
+
+* Checking if a change is mergeable no longer writes to the repository.
+
+* Submitted but unmerged changes are periodically retried. This is
+  necessary for a multi-master configuration where the second master
+  may need to retry a change not yet merged by the first. Please note
+  we still do not believe this is sufficient to enable multi-master.
+
+* Retry merge after LOCK_FAILURE when updating branch
++
+If the project requires fast-forwards, the merge cannot succeed once
+a lock failure occurs, but in other cases, it is safe to retry the
+merge immediately.
+
+* Do not automatically add reviewers from footer lines to draft patch sets
++
+Gerrit already avoids adding reviewers from footer lines when a new
+draft change is created. Now the same is done for draft patch sets.
+
+* Add users mentioned in commit footer as reviewers on draft publish
+
+* Hide any existing magic branches during push
++
+If there is a magic branch visible during push, just hide it from the
+client. Administrators can clear these by accessing the repository
+directly.
+
+* Prevent from deleting `refs/changes/`
++
+Everything under `refs/changes/` should be protected by Gerrit, users
+shouldn't be able to delete a particular patch set or a whole change
+from the review process.
+
+* Update description file in Git
++
+When writing the description to `project.config`, it is also necessary
+to write it to the description file in the repository so the same text
+is visible in CGit or GitWeb.
+
+* Write valid reflog for `HEAD` when creating the `All-Projects`
+  project
++
+When the `All-Projects` project is created during the schema
+initialization, `HEAD` is set to point to the `refs/meta/config`
+branch. When `HEAD` is updated an entry into the reflog is written.
+This ref log entry should contain the ID of the initial commit as
+target, but instead the target was the zero ID.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=1702[Issue 1702]:
+  Fix: 'internal server error' when pushing the same commit twice
++
+On the second push of the same commit to `refs/for/<branch name>`, Gerrit
+returns 'no new changes'.
++
+However if the user pushed to 'refs/changes/<change id>', Gerrit returned
+'internal server error'.
+
+* Match all git fetch/clone/push commands to the command executor
++
+Route not just `/p/` but any Git access to the same thread pool as the
+SSH server is using, allowing all requests to compete fairly for
+resources.
+
+* Fix auto closing of changes on direct push
++
+When a commit was directly pushed into a repository (bypassing code
+review) and this commit had a Change-Id in its commit message then the
+corresponding change was not automatically closed if it was open.
+
+* Set change state to NEW if merge fails due to non-existing dest branch
++
+If a submitted change failed to merge because the destination branch
+didn't exist anymore, it stayed in state 'Submitted, Merge Pending'.
+This meant Gerrit was re-attempting to merge this change (e.g. on
+startup), but this didn't make sense. Either the branch did still not
+exist (then there was no need to try merging it) or a new branch with
+the old name was created (then it was questionable if the change should
+still be merged into this branch). This is why it's better to set the
+change back to the 'Review in Progress' state and update it with a
+message saying that it couldn't be merged because the destination
+branch doesn't exist anymore.
++
+In addition Gerrit was writing an error into the error log if a change
+couldn't be merged because the destination branch wass missing.
+That was not really a server error and is not logged anymore.
+
+* Fix NPE when pushing a patch with an invalid author with
+  `Forge Author` permissions
+
+* Fix duplicated GitReferenceUpdated event on project creation.
++
+Creating a new Gerrit project was firing the GitReferenceUpdated event
+for the `refs/meta/config` branch two times.
+
+* Fix error log message in ReceiveCommits
++
+When the creation of one or more references failed ReceiveCommits failed
+with 'internal server error' and wrote the following error log:
+"Only X of Y new change refs created in xxx; aborting"
+The printed value for Y could be wrong since it didn't include the
+replaceCount. As a result, a confusing message like
+"Only 0 of 0 new change refs created in xxx; aborting"
+could appear in the error log.
+
+SSH
+~~~
+* `review --restore` allows a review score to be added on the restored change.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=1721[Issue 1721]:
+  `review --message` only adds the message once.
+
+* `ls-groups` prints "N/A" if the group's name is not set.
+
+* `set-project-parent --children-of`: Fix getting parent for level 1 projects
++
+For direct child projects of the `All-Projects` project the name of the
+parent project was incorrectly retrieved if the parent name was not
+explicitly stored as `All-Projects` in the project.config file.
+
+* Fix NPE when abandoning change with invalid author
++
+If the author of a change isn't known to Gerrit (pushed with
+`Forge Author` permissions), trying to abandon that change over SSH
+failed with an NPE.
+
+Query
+~~~~~
+* link:https://code.google.com/p/gerrit/issues/detail?id=1729[Issue 1729]:
+  Fix query by 'label:Verified=0'
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=1772[Issue 1772]:
+  Set `_more_changes` if result is limited due to configured query limit
+
+* Fix query cost for "status:merged commit:c0ffee"
+
+Plugins
+~~~~~~~
+* Skip disabled plugins on rescan
++
+In a background thread Gerrit periodically scans for new or changed
+plugins. On every such a rescan disabled plugins were loaded and a new
+copy of their jar files was stored in the review site's tmp folder.
+
+* Fix cleanup of plugins from tmp folder on graceful Gerrit shutdown
++
+Loaded plugin jars are copied to the review site's tmp folder to support
+hot updates of the plugin jars in the plugins folder. On Gerrit shutdown
+these copies of the jar files should be cleaned up. For this purpose a
+CleanupHandle is created, but the CleanupHandle wasn't enqueued in the
+cleanupQueue which is why cleanup on Gerrit shutdown didn't happen.
+
+* Reattempt deletion of plugin jars from tmp folder on JVM termination
++
+Loaded plugin jars are copied to the review site's tmp folder to support
+hot updates of the plugin jars in the plugins folder. On Gerrit shutdown
+these copies of the jar files should be cleaned up. For this purpose a
+CleanupHandle is created. The deletion of the tmp file in the
+CleanupHandle can fail although the jar file was closed. In this case
+reattempt the deletion on termination of the virtual machine. This
+normally succeeds.
+
+* Fix unloading of plugins
++
+When two plugins, say pluginA, and pluginB had been loaded, and pluginA
+was removed from $sitePath/plugins, pluginA got stopped, and a cleaning
+run was ordered. But this cleaning run cleaned both plugins and both
+plugins had their jars removed. This left pluginB visible to Gerrit
+although it's backing jar was gone. Upon calling not yet initialized
+parts of pluginB (e.g.: viewing not yet viewed Documentation pages of
+pluginB), exceptions as following were thrown:
++
+----
+  java.lang.IllegalStateException: zip file closed
+          at java.util.zip.ZipFile.ensureOpen(ZipFile.java:420)
+          at java.util.zip.ZipFile.getEntry(ZipFile.java:165)
+----
+
+* Fix double bound exception when loading extensions
++
+ServerInformation class was already bound, therefore it shouldn't be
+bound a second time for Gerrit extensions.
+
+* Do not call onModuleLoad() second time
++
+onModuleLoad() method is automatically called by GWT framework. Calling
+it once again in PluginGenerator caused double plugin initialization.
+
+* Require `Administrate Server` capability to GET /plugins/
++
+Listing plugins requires being an administrator. This was missed in the
+REST API.
+
+Email
+~~~~~
+* Merge failure emails are only sent once per day.
+* Unused macros are removed from the mail templates.
+* Unnecessary ellipses are no longer applied to email subjects.
+* The empty diff output from an "octopus merge" is now explained in change notification emails.
+* link:https://code.google.com/p/gerrit/issues/detail?id=1480[Issue 1480]:
+Proper error message is shown when registering an email address fails.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=1692[Issue 1692]:
+Review comments are sorted before being added to notification emails.
+
+* Fix watching of 'All Comments' on `All-Projects`
++
+If a user is watching 'All Comments' on `All-Projects` this should
+apply to all projects.
+
+Misc
+~~~~
+* Provide more descriptive message for NoSuchProjectException
+
+* On internal error due to receive timeout include the value of
+  `receive.timeout` into the log message
+
+* Silence INFO/DEBUG output from apache.http
++
+This spammed the log when using OpenID, for each and every login.
+
+* Remove `mysql_nextval` script
++
+This function does not work on binary logging enabled servers,
+as MySQL is unable to execute the function on slaves without
+causing possible corruption. Drop the function since it was only
+created to help administrators, and is unsafe.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=1312[Issue 1312]:
+  Fix relative URL detection in submodules
++
+Relative submodules do not start with `/`. Instead they start with
+`../`.  Fix the Submodule Subscriptions engine to recognize relative
+submodules.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=1622[Issue 1622]:
+  Fix NPE in LDAP Helper class if username is null
+
+* Fix commit-msg hook failure with spaces in path
++
+If the project absolute path had any whitespace, the commit
+hook failed to complete because a script variable was not
+enclosed in double quotes.
+
+* Drop the trailing ".git" suffix of the name of new project
+
+* Prevent possible NPE when running `change-merged` hook
++
+It's possible that the submitter is null. Add a check for this
+before invoking the `change-merged` hook with it.
+
+* Keep change open if its commit is pushed to another branch.
+
+* Fire GitReferenceUpdated event when BanCommit updates the
+  `refs/meta/reject-commits` branch.
+
+* Fix GitWeb Caching
++
+GitWeb Caching was not working when its cgi file was executed from
+outside. The same approach will also work with vanilla GitWeb.
+
+* Fix infinite loops when walking project hierarchy
+
+* Fix resource leak in MarkdownFormatter
+
+* Query all external groups for internal group memberships
++
+When asking for the known groups a user belongs to they may belong
+to an internal group by way of membership in a non-internal group,
+such as LDAP. Cache in memory the complete list of any non-internal
+group UUIDs used as members of an internal group. These must get
+checked for membership before completing the known group data from
+the internal backend.
+
+* Handle sorting groups with no name to avoid NPE
+
+* `gerrit.sh`
+** Don't suggest site init if schema version is newer than expected
+** Improve error messages in schema check
+** Suggest changing `gerrit.config` when JDK not found
+** Explicitly set a shell
+** Determine `GERRIT_SITE` from current working directory.
+** Fix `gerrit.sh restart` for relative paths
+** Fix site path computation if '.' occurs in path
+** Whitespace fixes
+
+* Display the reason of an Init injection failure.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=1821[Issue 1821]:
+  Warn if `cache.web_sessions.maxAge` is to small
++
+Setting `maxAge` to a small value can result in the browser endlessly
+redirecting trying to setup a new valid session. Warn administrators
+that the value is set smaller than 5 minutes.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=1821[Issue 1821]:
+  Support `cache.web_sessions.maxAge` < 1 minute
+
+* Use SECONDS as default time unit for `cache.web_sessions.maxAge`
++
+DefaultCacheFactory already uses SECONDS as default time unit for
+`cache.*.maxAge`.
++
+Update the described default time unit for `cache.*.maxAge` in the
+documentation.
++
+Administrators may need to update their configuration to for the new
+default time unit.
+
+* Add pylint configuration for contributed Python scripts
+
+* Various fixes and improvements of the `contrib/trivial_rebase.py`
+  script
++
+** Adapt options to Gerrit 2.6
+** Use change-url flag for ChangeId
+** Prevent exception for empty commit
+** Fix pylint errors
+** Call `gerrit review` instead of `gerrit approve`
+** Make the private key argument optional
+** Support alternative ssh executable, for example `plink`
+** Support custom review labels
+** Correctly handle empty patch ID
++
+If only one of the patch IDs is empty, it should not be considered
+a trivial rebase.
+
+** Use plain python instead of python2.6
++
+Windows installation only has python.exe
+
+* Correct MIME type of `favicon.ico` reference
++
+This is not a GIF, it is an "MS Windows icon resource".
+Some browsers may skip the image if the type is wrong.
+
+* Use `<link rel="shortcut icon">` for `favicon.ico` reference
++
+IE looks for a two-word "shortcut icon" relationship.  Other browsers
+interpret this as two relationships, one of which is "icon", so they
+can handle this syntax as well.
++
+See:
++
+** http://msdn.microsoft.com/en-us/library/ms537656(VS.85).aspx
+** http://jeffcode.blogspot.com/2007/12/why-doesnt-favicon-for-my-site-appear.html
+
+* Remove `servlet-api` from `WAR/lib`
++
+It is wrong to include the servlet API in a WAR's `WEB-INF/lib`
+directory. This confuses some servlet containers who refuse to
+load the Gerrit WAR. Instead package the Jetty runtime and the
+servlet API in a new `WEB-INF/pgm-lib` directory.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=1822[Issue 1822]:
+  Verify session matches container authentiation header
++
+If the user alters their identity in the container invalidate
+the Gerrit user session and force a new one to begin.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=1743[Issue 1743]:
+  Move RPC auth token from `Authorization` header to `X-Gerrit-Auth`
++
+Servers that run with auth.type = HTTP or HTTP_LDAP are unable to
+use the web UI because the Authorization code supplied by the UI
+overrides the browser's native `Authorization` header and causes the
+request to be blocked at the HTTP reverse proxy, before Gerrit even
+sees the request.
++
+Instead insert a unique token into `X-Gerrit-Auth`, leaving the HTTP
+standard `Authorization` header unspecified and available for use in
+HTTP reverse proxies.
+
+Documentation
+-------------
+
+User Documentation
+~~~~~~~~~~~~~~~~~~
+* Split link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/rest-api.html[
+  REST API documentation] and have one page per top level resource
+
+* Add executable examples for GET requests to
+  link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/rest-api.html[
+  REST API documentation]
++
+Add examples for GET requests to the REST API documentation on which
+the user can click to fire the requests. This allows users to
+immediately try out the requests and play around with them.
+
+* Document the link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/access-control.html#block[
+  BLOCK access rule].
+
+* Added documentation of
+  link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/user-upload.html#http[
+  how to authenticate uploads over HTTP].
+
+* Added documentation of the
+  link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/config-gerrit.html#auth.editFullNameUrl[auth.editFullNameUrl] and
+  link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/config-gerrit.html#auth.httpPasswordUrl[auth.httpPasswordUrl]
+  configuration parameters.
+
+* Add link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/prolog-cookbook.html[
+  submit_rule examples] from Gerrit User Summit 2012.
+
+* Improved the push tag examples in the access control documentation.
+
+* Improved documentation of error messages related to commit message footer content.
+
+* Added documentation of the
+  link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/error-commit-already-exists.html[
+  commit already exists] error message.
+
+* Added missing documentation of the ssh
+  link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/cmd-version.html[
+  version] command.
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=1369[Issue 1369]:
+  link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/config-gitweb.html[
+  Gitweb Instruction Updates]
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=1594[Issue 1594]:
+  Document execute permission for commit-msg in
+  link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/user-changeid.html#creation[
+  Change-Id docs]
+
+* link:https://code.google.com/p/gerrit/issues/detail?id=1602[Issue 1602]:
+Corrected references to `refs/changes` in the access control documentation.
+
+* Update documentation of
+  link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/config-gerrit.html#trackingid.name.match[
+  maximal length for tracking ids]
+
+* Added missing documentation of
+  link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/json.html[JSON attributes].
+
+* Rename `custom-dashboards.html` to
+  link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/user-dashboards.html[user-dashboards.html]
++
+This document no longer deals exclusively with custom dashboards, it now describes project level dashboards also.
+
+* Separate the
+  link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/config-login-register.html[
+  initial user setup instructions] to a shared file
+
+* Separate the
+  link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/database-setup.html[
+  database setup instructions] to a shared file
+
+* Improve the link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/database-setup.html[
+  instructions for PgSQL setup]
+
+* Fix the order of steps in the
+  link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/install-j2ee.html[
+  J2EE Installation document]
++
+It is better to first define the JNDI data source in the application
+server and then deploy Gerrit than opposite. This should avoid errors
+like "No DataSource" on the first deployment.
+
+* Clarify documentation of
+  link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/config-gerrit.html#ldap.groupName[
+  LDAP group name setting]
+
+* Adapt documentation to having 'Projects' as top level menu
+* Added missing documentation of mail templates.
+* Added documentation of contributor agreements.
+* Fix `init.d` symbolic link commands.
+* Remove obsolete diskbuffer setting from example config file.
+* Various minor grammatical and formatting corrections.
+* Fix external links in 2.0.21 and 2.0.24 release notes
+* Manual pages can be optionally created/installed for core gerrit ssh commands.
+
+Developer And Maintainer Documentation
+~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+* Updated the link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/dev-eclipse.html#maven[
+  Maven plugin installation instructions] for Eclipse 3.7 (Indigo).
+
+* Document link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/dev-contributing.html#commit-message[
+  usage of the past tense in commit messages]
+
+* Add link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/dev-contributing.html[
+  instructions] on how to configure git for pushing to Gerrit's Gerrit
+
+* link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/dev-contributing.html#process[
+  Stable branches process documentation]
+
+* Improved the
+  link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/dev-release.html[
+  release documentation].
+
+* Document that plans for
+  link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/dev-release.html#stable[
+  stable-fix releases] should be announced
+
+* Document process for
+  link:http://gerrit-documentation.googlecode.com/svn/Documentation/2.6/dev-release.html#security[
+  security-fix releases]
+
+* The release notes are now made when a release is created by running the `tools/release.sh` script.
+
diff --git a/ReleaseNotes/index.txt b/ReleaseNotes/index.txt
index e2487bc..26ccae7 100644
--- a/ReleaseNotes/index.txt
+++ b/ReleaseNotes/index.txt
@@ -1,6 +1,11 @@
 Gerrit Code Review - Release Notes
 ==================================
 
+[[2_6]]
+Version 2.6.x
+-------------
+* link:ReleaseNotes-2.6.html[2.6]
+
 [[2_5]]
 Version 2.5.x
 -------------
diff --git a/contrib/.pylintrc b/contrib/.pylintrc
new file mode 100644
index 0000000..9e8882e
--- /dev/null
+++ b/contrib/.pylintrc
@@ -0,0 +1,301 @@
+# lint Python modules using external checkers.
+#
+# This is the main checker controling the other ones and the reports
+# generation. It is itself both a raw checker and an astng checker in order
+# to:
+# * handle message activation / deactivation at the module level
+# * handle some basic but necessary stats'data (number of classes, methods...)
+#
+[MASTER]
+
+# Specify a configuration file.
+#rcfile=
+
+# Python code to execute, usually for sys.path manipulation such as
+# pygtk.require().
+#init-hook=
+
+# Profiled execution.
+profile=no
+
+# Add <file or directory> to the black list. It should be a base name, not a
+# path. You may set this option multiple times.
+ignore=SVN
+
+# Pickle collected data for later comparisons.
+persistent=yes
+
+# Set the cache size for astng objects.
+cache-size=500
+
+# List of plugins (as comma separated values of python modules names) to load,
+# usually to register additional checkers.
+load-plugins=
+
+
+[MESSAGES CONTROL]
+
+# Enable only checker(s) with the given id(s). This option conflicts with the
+# disable-checker option
+#enable-checker=
+
+# Enable all checker(s) except those with the given id(s). This option
+# conflicts with the enable-checker option
+#disable-checker=
+
+# Enable all messages in the listed categories.
+#enable-msg-cat=
+
+# Disable all messages in the listed categories.
+#disable-msg-cat=
+
+# Enable the message(s) with the given id(s).
+enable=RP0004
+
+# Disable the message(s) with the given id(s).
+disable=R0903,R0912,R0913,R0914,R0915,W0141,C0111,C0103,W0603,W0703,R0911,C0301,C0302,R0902,R0904,W0142,W0212,E1101,E1103,R0201,W0201,W0122,W0232,RP0001,RP0003,RP0101,RP0002,RP0401,RP0701,RP0801
+
+[REPORTS]
+
+# set the output format. Available formats are text, parseable, colorized, msvs
+# (visual studio) and html
+output-format=text
+
+# Include message's id in output
+include-ids=yes
+
+# Put messages in a separate file for each module / package specified on the
+# command line instead of printing them on stdout. Reports (if any) will be
+# written in a file name "pylint_global.[txt|html]".
+files-output=no
+
+# Tells whether to display a full report or only the messages
+reports=yes
+
+# Python expression which should return a note less than 10 (10 is the highest
+# note).You have access to the variables errors warning, statement which
+# respectivly contain the number of errors / warnings messages and the total
+# number of statements analyzed. This is used by the global evaluation report
+# (R0004).
+evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10)
+
+# Add a comment according to your evaluation note. This is used by the global
+# evaluation report (R0004).
+comment=no
+
+# checks for
+# * unused variables / imports
+# * undefined variables
+# * redefinition of variable from builtins or from an outer scope
+# * use of variable before assigment
+#
+[VARIABLES]
+
+# Tells whether we should check for unused import in __init__ files.
+init-import=no
+
+# A regular expression matching names used for dummy variables (i.e. not used).
+dummy-variables-rgx=_|dummy
+
+# List of additional names supposed to be defined in builtins. Remember that
+# you should avoid to define new builtins when possible.
+additional-builtins=
+
+
+# try to find bugs in the code using type inference
+#
+[TYPECHECK]
+
+# Tells whether missing members accessed in mixin class should be ignored. A
+# mixin class is detected if its name ends with "mixin" (case insensitive).
+ignore-mixin-members=yes
+
+# List of classes names for which member attributes should not be checked
+# (useful for classes with attributes dynamicaly set).
+ignored-classes=SQLObject
+
+# When zope mode is activated, consider the acquired-members option to ignore
+# access to some undefined attributes.
+zope=no
+
+# List of members which are usually get through zope's acquisition mecanism and
+# so shouldn't trigger E0201 when accessed (need zope=yes to be considered).
+acquired-members=REQUEST,acl_users,aq_parent
+
+
+# checks for :
+# * doc strings
+# * modules / classes / functions / methods / arguments / variables name
+# * number of arguments, local variables, branchs, returns and statements in
+# functions, methods
+# * required module attributes
+# * dangerous default values as arguments
+# * redefinition of function / method / class
+# * uses of the global statement
+#
+[BASIC]
+
+# Required attributes for module, separated by a comma
+required-attributes=
+
+# Regular expression which should only match functions or classes name which do
+# not require a docstring
+no-docstring-rgx=_main|__.*__
+
+# Regular expression which should only match correct module names
+module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$
+
+# Regular expression which should only match correct module level names
+const-rgx=(([A-Z_][A-Z1-9_]*)|(__.*__))|(log)$
+
+# Regular expression which should only match correct class names
+class-rgx=[A-Z_][a-zA-Z0-9]+$
+
+# Regular expression which should only match correct function names
+function-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct method names
+method-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct instance attribute names
+attr-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct argument names
+argument-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct variable names
+variable-rgx=[a-z_][a-z0-9_]{2,30}$
+
+# Regular expression which should only match correct list comprehension /
+# generator expression variable names
+inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$
+
+# Good variable names which should always be accepted, separated by a comma
+good-names=i,j,k,ex,Run,_,e,d1,d2,v,f,l,d
+
+# Bad variable names which should always be refused, separated by a comma
+bad-names=foo,bar,baz,toto,tutu,tata
+
+# List of builtins function names that should not be used, separated by a comma
+bad-functions=map,filter,apply,input
+
+
+# checks for sign of poor/misdesign:
+# * number of methods, attributes, local variables...
+# * size, complexity of functions, methods
+#
+[DESIGN]
+
+# Maximum number of arguments for function / method
+max-args=5
+
+# Maximum number of locals for function / method body
+max-locals=15
+
+# Maximum number of return / yield for function / method body
+max-returns=6
+
+# Maximum number of branch for function / method body
+max-branchs=12
+
+# Maximum number of statements in function / method body
+max-statements=50
+
+# Maximum number of parents for a class (see R0901).
+max-parents=7
+
+# Maximum number of attributes for a class (see R0902).
+max-attributes=20
+
+# Minimum number of public methods for a class (see R0903).
+min-public-methods=2
+
+# Maximum number of public methods for a class (see R0904).
+max-public-methods=30
+
+
+# checks for
+# * external modules dependencies
+# * relative / wildcard imports
+# * cyclic imports
+# * uses of deprecated modules
+#
+[IMPORTS]
+
+# Deprecated modules which should not be used, separated by a comma
+deprecated-modules=regsub,string,TERMIOS,Bastion,rexec
+
+# Create a graph of every (i.e. internal and external) dependencies in the
+# given file (report R0402 must not be disabled)
+import-graph=
+
+# Create a graph of external dependencies in the given file (report R0402 must
+# not be disabled)
+ext-import-graph=
+
+# Create a graph of internal dependencies in the given file (report R0402 must
+# not be disabled)
+int-import-graph=
+
+
+# checks for :
+# * methods without self as first argument
+# * overridden methods signature
+# * access only to existant members via self
+# * attributes not defined in the __init__ method
+# * supported interfaces implementation
+# * unreachable code
+#
+[CLASSES]
+
+# List of interface methods to ignore, separated by a comma. This is used for
+# instance to not check methods defines in Zope's Interface base class.
+ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by
+
+# List of method names used to declare (i.e. assign) instance attributes.
+defining-attr-methods=__init__,__new__,setUp
+
+
+# checks for similarities and duplicated code. This computation may be
+# memory / CPU intensive, so you should disable it if you experiments some
+# problems.
+#
+[SIMILARITIES]
+
+# Minimum lines number of a similarity.
+min-similarity-lines=4
+
+# Ignore comments when computing similarities.
+ignore-comments=yes
+
+# Ignore docstrings when computing similarities.
+ignore-docstrings=yes
+
+
+# checks for:
+# * warning notes in the code like FIXME, XXX
+# * PEP 263: source code with non ascii character but no encoding declaration
+#
+[MISCELLANEOUS]
+
+# List of note tags to take in consideration, separated by a comma.
+notes=FIXME,XXX,TODO
+
+
+# checks for :
+# * unauthorized constructions
+# * strict indentation
+# * line length
+# * use of <> instead of !=
+#
+[FORMAT]
+
+# Maximum number of characters on a single line.
+max-line-length=80
+
+# Maximum number of lines in a module
+max-module-lines=1000
+
+# String used as indentation unit. This is usually "    " (4 spaces) or "\t" (1
+# tab).  In repo it is 2 spaces.
+indent-string='  '
diff --git a/contrib/fake_ldap.pl b/contrib/fake_ldap.pl
new file mode 100644
index 0000000..5d423a78
--- /dev/null
+++ b/contrib/fake_ldap.pl
@@ -0,0 +1,333 @@
+#!/usr/bin/env perl
+
+# Fake LDAP server for Gerrit
+# Author: Olivier Croquette <ocroquette@free.fr>
+# Last change: 2012-11-12
+#
+# Abstract:
+# ====================================================================
+#
+# Gerrit currently supports several authentication schemes, but
+# unfortunately not the most basic one, e.g. local accounts with
+# local passwords.
+#
+# As a workaround, this script implements a minimal LDAP server
+# that can be used to authenticate against Gerrit. The information
+# required by Gerrit relative to users (user ID, password, display
+# name, email) is stored in a text file similar to /etc/passwd
+#
+#
+# Usage (see below for the setup)
+# ====================================================================
+#
+# To create a new file to store the user information:
+#   fake-ldap edituser --datafile /path/datafile --username maxpower \
+#     --displayname "Max Power" --email max.power@provider.com
+#
+# To modify an existing user (for instance the email):
+#   fake-ldap edituser --datafile /path/datafile --username ocroquette \
+#     --email max.power@provider2.com
+#
+# To set a new password for an existing user:
+#   fake-ldap edituser --datafile /path/datafile --username ocroquette \
+#     --password ""
+#
+# To start the server:
+#   fake-ldap start --datafile /path/datafile
+#
+# The server reads the user data file on each new connection. It's not
+# scalable but it should not be a problem for the intended usage
+# (small teams, testing,...)
+#
+#
+# Setup
+# ===================================================================
+#
+# Install the dependencies
+#
+#   Install the Perl module dependencies. On Debian and MacPorts,
+#   all modules are available as packages, except Net::LDAP::Server.
+#
+#   Debian: apt-get install libterm-readkey-perl
+#
+#   Since Net::LDAP::Server consists only of one file, you can put it
+#   along the script in Net/LDAP/Server.pm
+#
+# Create the data file with the first user (see above)
+#
+# Start as the script a server ("start" command, see above)
+#
+# Configure Gerrit with the following options:
+#
+#   gerrit.canonicalWebUrl = ... (workaround for a known Gerrit bug)
+#   auth.type = LDAP_BIND
+#   ldap.server = ldap://localhost:10389
+#   ldap.accountBase = ou=People,dc=nodomain
+#   ldap.groupBase = ou=Group,dc=nodomain
+#
+# Start Gerrit
+#
+# Log on in the Web interface
+#
+# If you want the fake LDAP server to start at boot time, add it to
+# /etc/inittab, with a line like:
+#
+# ld1:6:respawn:su someuser /path/fake-ldap start --datafile /path/datafile
+#
+# ===================================================================
+
+use strict;
+
+# Global var containing the options passed on the command line:
+my %cmdLineOptions;
+
+# Global var containing the user data read from the data file:
+my %userData;
+
+my $defaultport = 10389;
+
+package MyServer;
+
+use Data::Dumper;
+use Net::LDAP::Server;
+use Net::LDAP::Constant qw(LDAP_SUCCESS LDAP_INVALID_CREDENTIALS LDAP_OPERATIONS_ERROR);
+use IO::Socket;
+use IO::Select;
+use Term::ReadKey;
+
+use Getopt::Long;
+
+use base 'Net::LDAP::Server';
+
+sub bind {
+  my $self = shift;
+  my ($reqData, $fullRequest) = @_;
+
+  print "bind called\n" if $cmdLineOptions{verbose} >= 1;
+  print Dumper(\@_) if $cmdLineOptions{verbose} >= 2;
+  my $sha1 = undef;
+  my $uid = undef;
+  eval{
+    $uid = $reqData->{name};
+    $sha1 = main::encryptpwd($uid, $reqData->{authentication}->{simple})
+  };
+  if ($@) {
+    warn $@;
+    return({
+        'matchedDN' => '',
+        'errorMessage' => $@,
+        'resultCode' => LDAP_OPERATIONS_ERROR
+    });
+  }
+
+  print $sha1 . "\n" if $cmdLineOptions{verbose} >= 2;
+  print Dumper($userData{$uid}) . "\n" if $cmdLineOptions{verbose} >= 2;
+
+  if ( defined($sha1) && $sha1 && $userData{$uid} && ( $sha1 eq $userData{$uid}->{password} ) ) {
+    print "authentication of $uid succeeded\n" if $cmdLineOptions{verbose} >= 1;
+    return({
+      'matchedDN' => "dn=$uid,ou=People,dc=nodomain",
+      'errorMessage' => '',
+      'resultCode' => LDAP_SUCCESS
+    });
+  }
+  else {
+    print "authentication of $uid failed\n" if $cmdLineOptions{verbose} >= 1;
+    return({
+      'matchedDN' => '',
+      'errorMessage' => '',
+      'resultCode' => LDAP_INVALID_CREDENTIALS
+    });
+  }
+}
+
+sub search {
+    my $self = shift;
+    my ($reqData, $fullRequest) = @_;
+    print "search called\n" if $cmdLineOptions{verbose} >= 1;
+    print Dumper($reqData)  if $cmdLineOptions{verbose} >= 2;
+    my @entries;
+    if ( $reqData->{baseObject} eq 'ou=People,dc=nodomain' ) {
+        my $uid = $reqData->{filter}->{equalityMatch}->{assertionValue};
+        push @entries, Net::LDAP::Entry->new ( "dn=$uid,ou=People,dc=nodomain",
+       , 'objectName'=>"dn=uid,ou=People,dc=nodomain", 'uid'=>$uid, 'mail'=>$userData{$uid}->{email}, 'displayName'=>$userData{$uid}->{displayName});
+   }
+   elsif ( $reqData->{baseObject} eq 'ou=Group,dc=nodomain'  ) {
+        push @entries, Net::LDAP::Entry->new ( 'dn=Users,ou=Group,dc=nodomain',
+       , 'objectName'=>'dn=Users,ou=Group,dc=nodomain');
+   }
+
+    return {
+        'matchedDN' => '',
+        'errorMessage' => '',
+        'resultCode' => LDAP_SUCCESS
+    }, @entries;
+}
+
+
+package main;
+
+use Digest::SHA1  qw(sha1 sha1_hex sha1_base64);
+
+sub exitWithError {
+  my $msg = shift;
+  print STDERR $msg . "\n";
+  exit(1);
+}
+
+sub encryptpwd {
+  my ($uid, $passwd) = @_;
+  # Use the user id to compute the hash, to avoid rainbox table attacks
+  return sha1_hex($uid.$passwd);
+}
+
+my $result = Getopt::Long::GetOptions (
+  "port=i"        => \$cmdLineOptions{port},
+  "datafile=s"    => \$cmdLineOptions{datafile},
+  "email=s"       => \$cmdLineOptions{email},
+  "displayname=s" => \$cmdLineOptions{displayName},
+  "username=s"    => \$cmdLineOptions{userName},
+  "password=s"    => \$cmdLineOptions{password},
+  "verbose=i"     => \$cmdLineOptions{verbose},
+);
+exitWithError("Failed to parse command line arguments") if ! $result;
+exitWithError("Please provide a valid path for the datafile") if ! $cmdLineOptions{datafile};
+
+my @commands = qw(start edituser);
+if ( @ARGV != 1 || ! grep {$_ eq $ARGV[0]} @commands ) {
+  exitWithError("Please provide a valid command among: " . join(",", @commands));
+}
+
+my $command = $ARGV[0];
+if ( $command eq "start") {
+  startServer();
+}
+elsif ( $command eq "edituser") {
+  editUser();
+}
+
+
+sub startServer() {
+
+  my $port = $cmdLineOptions{port} || $defaultport;
+
+  print "starting on port $port\n" if $cmdLineOptions{verbose} >= 1;
+
+  my $sock = IO::Socket::INET->new(
+    Listen => 5,
+    Proto => 'tcp',
+    Reuse => 1,
+    LocalAddr => "localhost", # Comment this line if Gerrit doesn't run on this host
+    LocalPort => $port
+  );
+
+  my $sel = IO::Select->new($sock);
+  my %Handlers;
+  while (my @ready = $sel->can_read) {
+    foreach my $fh (@ready) {
+      if ($fh == $sock) {
+        # Make sure the data is up to date on new every connection
+        readUserData();
+
+        # let's create a new socket
+        my $psock = $sock->accept;
+        $sel->add($psock);
+        $Handlers{*$psock} = MyServer->new($psock);
+      } else {
+        my $result = $Handlers{*$fh}->handle;
+        if ($result) {
+          # we have finished with the socket
+          $sel->remove($fh);
+          $fh->close;
+          delete $Handlers{*$fh};
+        }
+      }
+    }
+  }
+}
+
+sub readUserData {
+  %userData = ();
+  open (MYFILE, "<$cmdLineOptions{datafile}") || exitWithError("Could not open \"$cmdLineOptions{datafile}\" for reading");
+  while (<MYFILE>) {
+    chomp;
+    my @fields = split(/:/, $_);
+    $userData{$fields[0]} = { password=>$fields[1], displayName=>$fields[2], email=>$fields[3] };
+  }
+  close (MYFILE);
+}
+
+sub writeUserData {
+  open (MYFILE, ">$cmdLineOptions{datafile}") || exitWithError("Could not open \"$cmdLineOptions{datafile}\" for writing");
+  foreach my $userid (sort(keys(%userData))) {
+    my $userInfo = $userData{$userid};
+    print MYFILE join(":",
+      $userid,
+      $userInfo->{password},
+      $userInfo->{displayName},
+      $userInfo->{email}
+      ). "\n";
+  }
+  close (MYFILE);
+}
+
+sub readPassword {
+  Term::ReadKey::ReadMode('noecho');
+  my $password = Term::ReadKey::ReadLine(0);
+  Term::ReadKey::ReadMode('normal');
+  print "\n";
+  return $password;
+}
+
+sub readAndConfirmPassword {
+  print "Please enter the password: ";
+  my $pwd = readPassword();
+  print "Please re-enter the password: ";
+  my $pwdCheck = readPassword();
+  exitWithError("The passwords are different") if $pwd ne $pwdCheck;
+  return $pwd;
+}
+
+sub editUser {
+  exitWithError("Please provide a valid user name") if ! $cmdLineOptions{userName};
+  my $userName = $cmdLineOptions{userName};
+
+  readUserData() if -r $cmdLineOptions{datafile};
+
+  my $encryptedPassword = undef;
+  if ( ! defined($userData{$userName}) ) {
+    # New user
+
+    exitWithError("Please provide a valid display name") if ! $cmdLineOptions{displayName};
+    exitWithError("Please provide a valid email") if ! $cmdLineOptions{email};
+
+    $userData{$userName} = { };
+
+    if ( ! defined($cmdLineOptions{password}) ) {
+      # No password provided on the command line. Force reading from terminal.
+      $cmdLineOptions{password} = "";
+    }
+  }
+
+  if ( defined($cmdLineOptions{password}) && ! $cmdLineOptions{password} ) {
+    $cmdLineOptions{password} = readAndConfirmPassword();
+    exitWithError("Please provide a non empty password") if ! $cmdLineOptions{password};
+  }
+
+
+  if ( $cmdLineOptions{password} ) {
+    $encryptedPassword = encryptpwd($userName, $cmdLineOptions{password});
+  }
+
+
+  $userData{$userName}->{password}    = $encryptedPassword if $encryptedPassword;
+  $userData{$userName}->{displayName} = $cmdLineOptions{displayName} if $cmdLineOptions{displayName};
+  $userData{$userName}->{email}       = $cmdLineOptions{email} if $cmdLineOptions{email};
+  # print Data::Dumper::Dumper(\%userData);
+
+  print "New user data for $cmdLineOptions{userName}:\n";
+  foreach ( sort(keys(%{$userData{$userName}}))) {
+    printf "  %-15s : %s\n", $_, $userData{$userName}->{$_}
+  }
+  writeUserData();
+}
\ No newline at end of file
diff --git a/contrib/themes/diffy/etc/GerritSite.css b/contrib/themes/diffy/etc/GerritSite.css
new file mode 100644
index 0000000..d476957
--- /dev/null
+++ b/contrib/themes/diffy/etc/GerritSite.css
@@ -0,0 +1,29 @@
+/* Copyright (C) 2013 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT 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_topmenu {
+  left: 60px;
+  margin-right: 60px;
+  padding-right: 10px;
+  position: relative;
+}
+
+#diffy_logo {
+  display: block !important;
+  margin-bottom: -55px;
+  padding-left: 20px;
+  position: relative;
+  top: -45px;
+  width: 60px;
+}
diff --git a/contrib/themes/diffy/etc/GerritSiteHeader.html b/contrib/themes/diffy/etc/GerritSiteHeader.html
new file mode 100644
index 0000000..89b4db5
--- /dev/null
+++ b/contrib/themes/diffy/etc/GerritSiteHeader.html
@@ -0,0 +1,5 @@
+<div>
+  <div id="diffy_logo">
+    <img width="50" height="46" src="static/logo.png"/>
+  </div>
+</div>
diff --git a/contrib/themes/diffy/static/logo.png b/contrib/themes/diffy/static/logo.png
new file mode 100644
index 0000000..989c303
--- /dev/null
+++ b/contrib/themes/diffy/static/logo.png
Binary files differ
diff --git a/contrib/themes/spotify/etc/GerritSite.css b/contrib/themes/spotify/etc/GerritSite.css
new file mode 100644
index 0000000..489452a
--- /dev/null
+++ b/contrib/themes/spotify/etc/GerritSite.css
@@ -0,0 +1,113 @@
+#gerrit_topmenu {
+	font-size: 9pt !important;
+	padding-top: 5px !important;
+	padding-left: 15px !important;
+	padding-right: 15px !important;
+	background: url(static/background-spotigreen.jpg) !important;
+	float: left !important;
+	margin-left: 250px !important;
+	margin-right: 150px !important;
+	border-radius: 0 0 10px 10px !important;
+	padding-bottom: 10px !important;
+	border-bottom: 1px solid #abc506 !important;
+	border-right: 1px solid #abc506 !important;
+	border-left: 1px solid #abc506 !important;
+}
+
+body, .gwt-DialogBox .dialogMiddleCenter {
+	background: #FFF url(static/background-gradient.png) no-repeat !important;
+}
+
+#logo-spotify {
+	background: url(static/logo.png) no-repeat;
+	height: 98px;
+	width: 228px;
+	margin-left: 2px;
+}
+
+#gerrit_header {
+	background: #FCFEEF;
+	padding-bottom: 10px;
+	border-bottom: dashed 1px rgba(0, 0, 0, 0.05);
+}
+
+#gerrit_topmenu .gwt-TextBox {
+	margin-top: 15px;
+	width: 240px;
+}
+
+#gerrit_topmenu > table {
+    float: left;
+}
+
+#gerrit_topmenu > table > tbody > tr > td:nth-child(3) a {
+	margin-left: 5px;
+	background-color: #FFC;
+	border-radius: 3px;
+	padding: 5px 5px;
+	border-right: none !important;
+}
+
+.gwt-TabPanelBottom {
+	background: #FFC !important;
+	border-radius: 0 0 3px 3px !important;
+	padding-bottom: 6px !important;
+	padding-top: 6px !important;
+}
+
+.gwt-TabBar .gwt-TabBarItem, .gwt-TabBar .gwt-TabBarRest, .gwt-TabBar .gwt-TabPanelBottom {
+	background: transparent !important;
+}
+
+.gwt-TabBarItem-wrapper {
+	background: rgba(255, 255, 255, 0.2) !important;
+}
+
+#gerrit_topmenu .gwt-TabBar .gwt-TabBarItem-selected {
+	background: #FFC !important;
+	border-radius: 3px 3px 0 0 !important;
+}
+
+#gerrit_topmenu .gwt-TabBar .gwt-TabBarItem-selected:hover {
+	background: #FFC !important;
+}
+
+#gerrit_topmenu .gwt-TabBar .gwt-TabBarItem-selected:focus {
+	outline: none !important;
+}
+
+#gerrit_topmenu > table > tbody > tr > td > table {
+	border: none !important;
+}
+
+.gwt-TabBar {
+	border-bottom: 1px solid #E2E2AD !important;
+}
+
+.gwt-TabBarItem {
+	border-right: 1px solid rgba(79, 58, 0, 0.3) !important;
+	background: rgba(255, 255, 255, 0.2) !important;
+}
+
+.gwt-TabBarItem:hover {
+	background: rgba(255, 255, 255, 0.1) !important;
+}
+
+a, a:visited {
+	text-decoration: none !important;
+	color: #3F4D00 !important;
+}
+
+a:hover {
+	color: #5d7200 !important;
+	text-decoration: underline !important;
+}
+
+.gwt-Label {
+height: 30px !important;
+line-height: 30px !important;
+}
+
+#gerrit_btmmenu > div {
+	color: rgba(0, 0, 0, 0.3) !important;
+}
\ No newline at end of file
diff --git a/contrib/themes/spotify/etc/GerritSiteHeader.html b/contrib/themes/spotify/etc/GerritSiteHeader.html
new file mode 100644
index 0000000..8af84be
--- /dev/null
+++ b/contrib/themes/spotify/etc/GerritSiteHeader.html
@@ -0,0 +1 @@
+<div id="logo-spotify"></div>
\ No newline at end of file
diff --git a/contrib/themes/spotify/static/background-gradient.png b/contrib/themes/spotify/static/background-gradient.png
new file mode 100644
index 0000000..b40f35b
--- /dev/null
+++ b/contrib/themes/spotify/static/background-gradient.png
Binary files differ
diff --git a/contrib/themes/spotify/static/background-spotigreen.jpg b/contrib/themes/spotify/static/background-spotigreen.jpg
new file mode 100644
index 0000000..fcc7983
--- /dev/null
+++ b/contrib/themes/spotify/static/background-spotigreen.jpg
Binary files differ
diff --git a/contrib/themes/spotify/static/logo.png b/contrib/themes/spotify/static/logo.png
new file mode 100644
index 0000000..bfe1fce
--- /dev/null
+++ b/contrib/themes/spotify/static/logo.png
Binary files differ
diff --git a/contrib/trivial_rebase.py b/contrib/trivial_rebase.py
index a514b4c..7764470 100755
--- a/contrib/trivial_rebase.py
+++ b/contrib/trivial_rebase.py
@@ -1,4 +1,4 @@
-#!/usr/bin/env python2.6
+#!/usr/bin/env python
 
 # Copyright (c) 2010, Code Aurora Forum. All rights reserved.
 #
@@ -27,196 +27,224 @@
 # OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN
 # IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 
-# This script is designed to detect when a patchset uploaded to Gerrit is
-# 'identical' (determined via git-patch-id) and reapply reviews onto the new
-# patchset from the previous patchset.
+""" This script is designed to detect when a patchset uploaded to Gerrit is
+'identical' (determined via git-patch-id) and reapply reviews onto the new
+patchset from the previous patchset.
 
-# Get usage and help info by running: ./trivial_rebase.py --help
-# Documentation is available here: https://www.codeaurora.org/xwiki/bin/QAEP/Gerrit
+Get usage and help info by running: ./trivial_rebase.py --help
+Documentation is available here: https://www.codeaurora.org/xwiki/bin/QAEP/Gerrit
 
+"""
+
+import argparse
 import json
-from optparse import OptionParser
+import re
 import subprocess
-from sys import exit
+import sys
 
-class CheckCallError(OSError):
-  """CheckCall() returned non-0."""
-  def __init__(self, command, cwd, retcode, stdout, stderr=None):
-    OSError.__init__(self, command, cwd, retcode, stdout, stderr)
-    self.command = command
-    self.cwd = cwd
-    self.retcode = retcode
-    self.stdout = stdout
-    self.stderr = stderr
+class TrivialRebase:
+  def __init__(self):
+    usage = "%(prog)s <required options> [--server-port=PORT]"
+    parser = argparse.ArgumentParser(usage=usage)
+    parser.add_argument("--change-url", dest="changeUrl", help="Change URL")
+    parser.add_argument("--project", help="Project path in Gerrit")
+    parser.add_argument("--commit", help="Git commit-ish for this patchset")
+    parser.add_argument("--patchset", type=int, help="The patchset number")
+    parser.add_argument("--private-key-path", dest="private_key_path",
+                        help="Full path to Gerrit SSH daemon's private host key")
+    parser.add_argument("--server", default='localhost',
+                        help="Gerrit SSH server [default: %(default)s]")
+    parser.add_argument("--server-port", dest="port", default='29418',
+                        help="Port to connect to Gerrit's SSH daemon "
+                             "[default: %(default)s]")
+    parser.add_argument("--ssh", default="ssh", help="SSH executable")
+    parser.add_argument("--ssh-port-flag", dest="ssh_port_flag", default="-p", help="SSH port flag")
 
-def CheckCall(command, cwd=None):
-  """Like subprocess.check_call() but returns stdout.
+    args = parser.parse_known_args()[0]
+    if None in [args.changeUrl, args.project, args.commit, args.patchset]:
+      parser.error("Incomplete arguments")
+    try:
+      self.changeId = re.search(r'\d+$', args.changeUrl).group()
+    except AttributeError:
+      parser.error("Invalid changeId")
+    self.project = args.project
+    self.commit = args.commit
+    self.patchset = args.patchset
+    self.private_key_path = args.private_key_path
+    self.server = args.server
+    self.port = args.port
+    self.ssh = args.ssh
+    self.ssh_port_flag = args.ssh_port_flag
 
-  Works on python 2.4
-  """
-  try:
-    process = subprocess.Popen(command, cwd=cwd, stdout=subprocess.PIPE)
-    std_out, std_err = process.communicate()
-  except OSError, e:
-    raise CheckCallError(command, cwd, e.errno, None)
-  if process.returncode:
-    raise CheckCallError(command, cwd, process.returncode, std_out, std_err)
-  return std_out, std_err
+  class CheckCallError(OSError):
+    """CheckCall() returned non-0."""
+    def __init__(self, command, cwd, retcode, stdout, stderr=None):
+      OSError.__init__(self, command, cwd, retcode, stdout, stderr)
+      self.command = command
+      self.cwd = cwd
+      self.retcode = retcode
+      self.stdout = stdout
+      self.stderr = stderr
 
-def GsqlQuery(sql_query, server, port):
-  """Runs a gerrit gsql query and returns the result"""
-  gsql_cmd = ['ssh', '-p', port, server, 'gerrit', 'gsql', '--format',
-              'JSON', '-c', sql_query]
-  try:
-    (gsql_out, gsql_stderr) = CheckCall(gsql_cmd)
-  except CheckCallError, e:
-    print "return code is %s" % e.retcode
-    print "stdout and stderr is\n%s%s" % (e.stdout, e.stderr)
-    raise
+  def CheckCall(self, command, cwd=None):
+    """Like subprocess.check_call() but returns stdout.
 
-  new_out = gsql_out.replace('}}\n', '}}\nsplit here\n')
-  return new_out.split('split here\n')
+    Works on python 2.4
 
-def FindPrevRev(changeId, patchset, server, port):
-  """Finds the revision of the previous patch set on the change"""
-  sql_query = ("\"SELECT revision FROM patch_sets,changes WHERE "
-               "patch_sets.change_id = changes.change_id AND "
-               "patch_sets.patch_set_id = %s AND "
-               "changes.change_key = \'%s\'\"" % ((patchset - 1), changeId))
-  revisions = GsqlQuery(sql_query, server, port)
+    """
+    try:
+      process = subprocess.Popen(command, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.PIPE)
+      std_out, std_err = process.communicate()
+    except OSError, e:
+      raise self.CheckCallError(command, cwd, e.errno, None)
+    if process.returncode:
+      raise self.CheckCallError(command, cwd, process.returncode, std_out, std_err)
+    return std_out, std_err
 
-  json_dict = json.loads(revisions[0], strict=False)
-  return json_dict["columns"]["revision"]
+  def GsqlQuery(self, sql_query):
+    """Run a gerrit gsql query and return the result."""
+    gsql_cmd = [self.ssh, self.ssh_port_flag, self.port, self.server, 'gerrit', 'gsql',
+                '--format', 'JSON', '-c', sql_query]
+    try:
+      (gsql_out, _gsql_stderr) = self.CheckCall(gsql_cmd)
+    except self.CheckCallError, e:
+      print "return code is %s" % e.retcode
+      print "stdout and stderr is\n%s%s" % (e.stdout, e.stderr)
+      raise
 
-def GetApprovals(changeId, patchset, server, port):
-  """Get all the approvals on a specific patch set
+    new_out = gsql_out.replace('}}\n', '}}\nsplit here\n')
+    return new_out.split('split here\n')
 
-  Returns a list of approval dicts"""
-  sql_query = ("\"SELECT value,account_id,category_id FROM patch_set_approvals "
-               "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 = []
-  for json_str in gsql_out:
-    dict = json.loads(json_str, strict=False)
-    if dict["type"] == "row":
-      approvals.append(dict["columns"])
-  return approvals
+  def FindPrevRev(self):
+    """Find the revision of the previous patch set on the change."""
+    sql_query = ("\"SELECT revision FROM patch_sets WHERE "
+                 "change_id = %s AND patch_set_id = %s\"" %
+                 (self.changeId, (self.patchset - 1)))
+    revisions = self.GsqlQuery(sql_query)
 
-def GetEmailFromAcctId(account_id, server, port):
-  """Returns the preferred email address associated with the account_id"""
-  sql_query = ("\"SELECT preferred_email FROM accounts WHERE account_id = %s\""
-               % account_id)
-  email_addr = GsqlQuery(sql_query, server, port)
+    json_dict = json.loads(revisions[0], strict=False)
+    return json_dict["columns"]["revision"]
 
-  json_dict = json.loads(email_addr[0], strict=False)
-  return json_dict["columns"]["preferred_email"]
+  def GetApprovals(self):
+    """Get all the approvals on a specific patch set.
 
-def GetPatchId(revision):
-  git_show_cmd = ['git', 'show', revision]
-  patch_id_cmd = ['git', 'patch-id']
-  patch_id_process = subprocess.Popen(patch_id_cmd, stdout=subprocess.PIPE,
-                                      stdin=subprocess.PIPE)
-  git_show_process = subprocess.Popen(git_show_cmd, stdout=subprocess.PIPE)
-  return patch_id_process.communicate(git_show_process.communicate()[0])[0]
+    Returns a list of approval dicts.
 
-def SuExec(server, port, private_key, as_user, cmd):
-  suexec_cmd = ['ssh', '-l', "Gerrit Code Review", '-p', port, server, '-i',
-                private_key, 'suexec', '--as', as_user, '--', cmd]
-  CheckCall(suexec_cmd)
+    """
+    sql_query = ("\"SELECT value,account_id,category_id FROM patch_set_approvals "
+                 "WHERE change_id = %s AND patch_set_id = %s AND value != 0\""
+                 % (self.changeId, (self.patchset - 1)))
+    gsql_out = self.GsqlQuery(sql_query)
+    approvals = []
+    for json_str in gsql_out:
+      data = json.loads(json_str, strict=False)
+      if data["type"] == "row":
+        approvals.append(data["columns"])
+    return approvals
 
-def DiffCommitMessages(commit1, commit2):
-  log_cmd1 = ['git', 'log', '--pretty=format:"%an %ae%n%s%n%b"',
-              commit1 + '^!']
-  commit1_log = CheckCall(log_cmd1)
-  log_cmd2 = ['git', 'log', '--pretty=format:"%an %ae%n%s%n%b"',
-              commit2 + '^!']
-  commit2_log = CheckCall(log_cmd2)
-  if commit1_log != commit2_log:
-    return True
-  return False
+  def AppendAcctApproval(self, account_id, value):
+    try:
+      newval = self.acct_approvals[account_id] + ' ' + value
+    except KeyError:
+      newval = value
+    self.acct_approvals[account_id] = newval
 
-def Main():
-  server = 'localhost'
-  usage = "usage: %prog <required options> [--server-port=PORT]"
-  parser = OptionParser(usage=usage)
-  parser.add_option("--change", dest="changeId", help="Change identifier")
-  parser.add_option("--project", help="Project path in Gerrit")
-  parser.add_option("--commit", help="Git commit-ish for this patchset")
-  parser.add_option("--patchset", type="int", help="The patchset number")
-  parser.add_option("--private-key-path", dest="private_key_path",
-                    help="Full path to Gerrit SSH daemon's private host key")
-  parser.add_option("--server-port", dest="port", default='29418',
-                    help="Port to connect to Gerrit's SSH daemon "
-                         "[default: %default]")
+  def GetEmailFromAcctId(self, account_id):
+    """Return the preferred email address associated with the account_id."""
+    sql_query = ("\"SELECT preferred_email FROM accounts WHERE account_id = %s\""
+                 % account_id)
+    email_addr = self.GsqlQuery(sql_query)
 
-  (options, args) = parser.parse_args()
+    json_dict = json.loads(email_addr[0], strict=False)
+    return json_dict["columns"]["preferred_email"]
 
-  if not options.changeId:
-    parser.print_help()
-    exit(0)
+  def GetPatchId(self, revision):
+    git_show_cmd = ['git', 'show', revision]
+    patch_id_cmd = ['git', 'patch-id']
+    git_show_process = subprocess.Popen(git_show_cmd, stdout=subprocess.PIPE)
+    patch_id_process = subprocess.Popen(patch_id_cmd, stdout=subprocess.PIPE,
+                                        stdin=git_show_process.stdout)
+    res = patch_id_process.communicate()[0] or '0'
+    return res.split()[0]
 
-  if options.patchset == 1:
-    # Nothing to detect on first patchset
-    exit(0)
-  prev_revision = None
-  prev_revision = FindPrevRev(options.changeId, options.patchset, server,
-                              options.port)
-  if not prev_revision:
-    # Couldn't find a previous revision
-    exit(0)
-  prev_patch_id = GetPatchId(prev_revision)
-  cur_patch_id = GetPatchId(options.commit)
-  if cur_patch_id.split()[0] != prev_patch_id.split()[0]:
-    # patch-ids don't match
-    exit(0)
-  # Patch ids match. This is a trivial rebase.
-  # In addition to patch-id we should check if the commit message changed. Most
-  # approvers would want to re-review changes when the commit message changes.
-  changed = DiffCommitMessages(prev_revision, options.commit)
-  if changed:
-    # Insert a comment into the change letting the approvers know only the
-    # commit message changed
-    comment_msg = ("\'--message=New patchset patch-id matches previous patchset"
-                   ", but commit message has changed.'")
-    comment_cmd = ['ssh', '-p', options.port, server, 'gerrit', 'approve',
-                   '--project', options.project, comment_msg, options.commit]
-    CheckCall(comment_cmd)
-    exit(0)
+  def SuExec(self, as_user, cmd):
+    suexec_cmd = [self.ssh, '-l', "Gerrit Code Review", self.ssh_port_flag, self.port, self.server]
+    if self.private_key_path:
+      suexec_cmd += ['-i', self.private_key_path]
+    suexec_cmd += ['suexec', '--as', as_user, '--', cmd]
+    self.CheckCall(suexec_cmd)
 
-  # Need to get all approvals on prior patch set, then suexec them onto
-  # this patchset.
-  approvals = GetApprovals(options.changeId, options.patchset, server,
-                           options.port)
-  gerrit_approve_msg = ("\'Automatically re-added by Gerrit trivial rebase "
-                        "detection script.\'")
-  for approval in approvals:
-    # Note: Sites with different 'copy_min_score' values in the
-    # approval_categories DB table might want different behavior here.
-    # Additional categories should also be added if desired.
-    if approval["category_id"] == "CRVW":
-      approve_category = '--code-review'
-    elif approval["category_id"] == "VRIF":
-      # Don't re-add verifies
-      #approve_category = '--verified'
-      continue
-    elif approval["category_id"] == "SUBM":
-      # We don't care about previous submit attempts
-      continue
-    else:
-      print "Unsupported category: %s" % approval
-      exit(0)
+  def DiffCommitMessages(self, prev_commit):
+    log_cmd1 = ['git', 'log', '--pretty=format:"%an %ae%n%s%n%b"',
+                prev_commit + '^!']
+    commit1_log = self.CheckCall(log_cmd1)
+    log_cmd2 = ['git', 'log', '--pretty=format:"%an %ae%n%s%n%b"',
+                self.commit + '^!']
+    commit2_log = self.CheckCall(log_cmd2)
+    if commit1_log != commit2_log:
+      return True
+    return False
 
-    score = approval["value"]
-    gerrit_approve_cmd = ['gerrit', 'approve', '--project', options.project,
-                          '--message', gerrit_approve_msg, approve_category,
-                          score, options.commit]
-    email_addr = GetEmailFromAcctId(approval["account_id"], server,
-                                    options.port)
-    SuExec(server, options.port, options.private_key_path, email_addr,
-           ' '.join(gerrit_approve_cmd))
-  exit(0)
+  def Run(self):
+    if self.patchset == 1:
+      # Nothing to detect on first patchset
+      return
+    prev_revision = self.FindPrevRev()
+    assert prev_revision, "Previous revision not found"
+    prev_patch_id = self.GetPatchId(prev_revision)
+    cur_patch_id = self.GetPatchId(self.commit)
+    if prev_patch_id == '0' and cur_patch_id == '0':
+      print "commits %s and %s are both empty or merge commits" % (prev_revision, self.commit)
+      return
+    if cur_patch_id != prev_patch_id:
+      # patch-ids don't match
+      return
+    # Patch ids match. This is a trivial rebase.
+    # In addition to patch-id we should check if the commit message changed. Most
+    # approvers would want to re-review changes when the commit message changes.
+    changed = self.DiffCommitMessages(prev_revision)
+    if changed:
+      # Insert a comment into the change letting the approvers know only the
+      # commit message changed
+      comment_msg = ("\'--message=New patchset patch-id matches previous patchset"
+                     ", but commit message has changed.'")
+      comment_cmd = [self.ssh, self.ssh_port_flag, self.port, self.server, 'gerrit',
+                     'review', '--project', self.project, comment_msg, self.commit]
+      self.CheckCall(comment_cmd)
+      return
+
+    # Need to get all approvals on prior patch set, then suexec them onto
+    # this patchset.
+    approvals = self.GetApprovals()
+    self.acct_approvals = dict()
+    for approval in approvals:
+      # Note: Sites with different 'copy_min_score' values in the
+      # approval_categories DB table might want different behavior here.
+      # Additional categories should also be added if desired.
+      if approval["category_id"] == "Code-Review" and approval['value'] != '-2':
+        self.AppendAcctApproval(approval['account_id'], '--code-review %s' % approval['value'])
+      elif approval["category_id"] == "Verified":
+        # Don't re-add verifies
+        # self.AppendAcctApproval(approval['account_id'], '--verified %s' % approval['value'])
+        continue
+      elif approval["category_id"] == "SUBM":
+        # We don't care about previous submit attempts
+        continue
+      else:
+        self.AppendAcctApproval(approval['account_id'], '--%s %s' %
+                                (approval['category_id'].lower().replace(' ', '-'),
+                                 approval['value']))
+
+    gerrit_review_msg = ("\'Automatically re-added by Gerrit trivial rebase "
+                          "detection script.\'")
+    for acct, flags in self.acct_approvals.items():
+      gerrit_review_cmd = ['gerrit', 'review', '--project', self.project,
+                            '--message', gerrit_review_msg, flags, self.commit]
+      email_addr = self.GetEmailFromAcctId(acct)
+      self.SuExec(email_addr, ' '.join(gerrit_review_cmd))
 
 if __name__ == "__main__":
-  Main()
+  try:
+    TrivialRebase().Run()
+  except AssertionError, e:
+    print >> sys.stderr, e
diff --git a/gerrit-acceptance-tests/.gitignore b/gerrit-acceptance-tests/.gitignore
new file mode 100644
index 0000000..e1914aa
--- /dev/null
+++ b/gerrit-acceptance-tests/.gitignore
@@ -0,0 +1,7 @@
+/bin
+/target
+/.classpath
+/.project
+/.settings/org.maven.ide.eclipse.prefs
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-common.iml
diff --git a/gerrit-acceptance-tests/.settings/org.eclipse.core.resources.prefs b/gerrit-acceptance-tests/.settings/org.eclipse.core.resources.prefs
new file mode 100644
index 0000000..8dd9b1d
--- /dev/null
+++ b/gerrit-acceptance-tests/.settings/org.eclipse.core.resources.prefs
@@ -0,0 +1,3 @@
+eclipse.preferences.version=1
+encoding//src/test/java=UTF-8
+encoding/<project>=UTF-8
diff --git a/gerrit-acceptance-tests/.settings/org.eclipse.core.runtime.prefs b/gerrit-acceptance-tests/.settings/org.eclipse.core.runtime.prefs
new file mode 100644
index 0000000..5a0ad22
--- /dev/null
+++ b/gerrit-acceptance-tests/.settings/org.eclipse.core.runtime.prefs
@@ -0,0 +1,2 @@
+eclipse.preferences.version=1
+line.separator=\n
diff --git a/gerrit-acceptance-tests/.settings/org.eclipse.jdt.core.prefs b/gerrit-acceptance-tests/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 0000000..a6765b0
--- /dev/null
+++ b/gerrit-acceptance-tests/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,263 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
+org.eclipse.jdt.core.compiler.compliance=1.6
+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.join_lines_in_comments=true
+org.eclipse.jdt.core.formatter.join_wrapped_lines=true
+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-acceptance-tests/.settings/org.eclipse.jdt.ui.prefs b/gerrit-acceptance-tests/.settings/org.eclipse.jdt.ui.prefs
new file mode 100644
index 0000000..c6dd34f
--- /dev/null
+++ b/gerrit-acceptance-tests/.settings/org.eclipse.jdt.ui.prefs
@@ -0,0 +1,60 @@
+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-acceptance-tests/pom.xml b/gerrit-acceptance-tests/pom.xml
new file mode 100644
index 0000000..e79f8a7
--- /dev/null
+++ b/gerrit-acceptance-tests/pom.xml
@@ -0,0 +1,148 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2013 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT 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.6-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>gerrit-acceptance-tests</artifactId>
+
+  <name>Gerrit Code Review - Acceptance Tests</name>
+
+  <description>
+    Gerrit Acceptance Tests
+  </description>
+
+  <dependencies>
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <scope>test</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>com.google.gerrit</groupId>
+      <artifactId>gerrit-reviewdb</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>com.google.gerrit</groupId>
+      <artifactId>gerrit-main</artifactId>
+      <version>${project.version}</version>
+      <scope>runtime</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>com.google.gerrit</groupId>
+      <artifactId>gerrit-server</artifactId>
+      <version>${project.version}</version>
+      <exclusions>
+        <exclusion>
+          <groupId>org.apache.tomcat</groupId>
+          <artifactId>servlet-api</artifactId>
+        </exclusion>
+      </exclusions>
+    </dependency>
+
+    <dependency>
+      <groupId>org.slf4j</groupId>
+      <artifactId>slf4j-log4j12</artifactId>
+    </dependency>
+
+    <dependency>
+      <groupId>log4j</groupId>
+      <artifactId>log4j</artifactId>
+    </dependency>
+
+    <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>
+
+    <dependency>
+      <groupId>com.google.gerrit</groupId>
+      <artifactId>gerrit-httpd</artifactId>
+      <version>${project.version}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>com.google.gerrit</groupId>
+      <artifactId>gerrit-pgm</artifactId>
+      <version>${project.version}</version>
+      <exclusions>
+        <exclusion>
+          <groupId>org.eclipse.jetty</groupId>
+          <artifactId>jetty-servlet</artifactId>
+        </exclusion>
+      </exclusions>
+    </dependency>
+
+    <dependency>
+      <groupId>org.eclipse.jetty</groupId>
+      <artifactId>jetty-servlet</artifactId>
+      <scope>provided</scope>
+    </dependency>
+  </dependencies>
+
+  <profiles>
+    <profile>
+      <id>acceptance</id>
+      <activation>
+        <property>
+          <name>!gerrit.acceptance-tests.skip</name>
+        </property>
+      </activation>
+      <build>
+        <plugins>
+          <plugin>
+            <groupId>org.apache.maven.plugins</groupId>
+            <artifactId>maven-failsafe-plugin</artifactId>
+            <version>2.5</version>
+            <executions>
+              <execution>
+                <id>integration-test</id>
+                <goals>
+                  <goal>integration-test</goal>
+                </goals>
+              </execution>
+              <execution>
+                <id>verify</id>
+                <goals>
+                  <goal>verify</goal>
+                </goals>
+              </execution>
+            </executions>
+          </plugin>
+        </plugins>
+      </build>
+    </profile>
+  </profiles>
+</project>
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
new file mode 100644
index 0000000..51c7b3d
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AbstractDaemonTest.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance;
+
+import org.junit.After;
+import org.junit.Before;
+
+
+public abstract class AbstractDaemonTest {
+
+  private GerritServer server;
+
+  @Before
+  public final void beforeTest() throws Exception {
+    server = GerritServer.start();
+    server.getTestInjector().injectMembers(this);
+  }
+
+  @After
+  public final void afterTest() throws Exception {
+    server.stop();
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AccountCreator.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AccountCreator.java
new file mode 100644
index 0000000..f56ef07
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/AccountCreator.java
@@ -0,0 +1,132 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance;
+
+import java.io.ByteArrayOutputStream;
+import java.io.UnsupportedEncodingException;
+import java.util.Collections;
+
+import javax.inject.Inject;
+
+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.AccountSshKey;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.account.AccountByEmailCache;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.ssh.SshKeyCache;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
+import com.jcraft.jsch.JSch;
+import com.jcraft.jsch.JSchException;
+import com.jcraft.jsch.KeyPair;
+
+public class AccountCreator {
+
+  private SchemaFactory<ReviewDb> reviewDbProvider;
+  private GroupCache groupCache;
+  private SshKeyCache sshKeyCache;
+  private AccountCache accountCache;
+  private AccountByEmailCache byEmailCache;
+
+  @Inject
+  AccountCreator(SchemaFactory<ReviewDb> schema, GroupCache groupCache,
+      SshKeyCache sshKeyCache, AccountCache accountCache,
+      AccountByEmailCache byEmailCache) {
+    reviewDbProvider = schema;
+    this.groupCache = groupCache;
+    this.sshKeyCache = sshKeyCache;
+    this.accountCache = accountCache;
+    this.byEmailCache = byEmailCache;
+  }
+
+  public TestAccount create(String username, String email, String fullName,
+      String... groups)
+      throws OrmException, UnsupportedEncodingException, JSchException {
+    ReviewDb db = reviewDbProvider.open();
+    try {
+      Account.Id id = new Account.Id(db.nextAccountId());
+      KeyPair sshKey = genSshKey();
+      AccountSshKey key =
+          new AccountSshKey(new AccountSshKey.Id(id, 1), publicKey(sshKey, email));
+      AccountExternalId extUser =
+          new AccountExternalId(id, new AccountExternalId.Key(
+              AccountExternalId.SCHEME_USERNAME, username));
+      String httpPass = "http-pass";
+      extUser.setPassword(httpPass);
+      db.accountExternalIds().insert(Collections.singleton(extUser));
+
+      if (email != null) {
+        AccountExternalId extMailto = new AccountExternalId(id, getEmailKey(email));
+        extMailto.setEmailAddress(email);
+        db.accountExternalIds().insert(Collections.singleton(extMailto));
+      }
+
+      Account a = new Account(id);
+      a.setFullName(fullName);
+      a.setPreferredEmail(email);
+      db.accounts().insert(Collections.singleton(a));
+
+      db.accountSshKeys().insert(Collections.singleton(key));
+
+      if (groups != null) {
+        for (String n : groups) {
+          AccountGroup.NameKey k = new AccountGroup.NameKey(n);
+          AccountGroup g = groupCache.get(k);
+          AccountGroupMember m =
+              new AccountGroupMember(new AccountGroupMember.Key(id, g.getId()));
+          db.accountGroupMembers().insert(Collections.singleton(m));
+        }
+      }
+
+      sshKeyCache.evict(username);
+      accountCache.evictByUsername(username);
+      byEmailCache.evict(email);
+
+      return new TestAccount(id, username, email, fullName, sshKey, httpPass);
+    } finally {
+      db.close();
+    }
+  }
+
+  public TestAccount create(String username, String group)
+      throws OrmException, UnsupportedEncodingException, JSchException {
+    return create(username, null, username, group);
+  }
+
+  public TestAccount create(String username)
+      throws UnsupportedEncodingException, OrmException, JSchException {
+    return create(username, null, username, (String[]) null);
+  }
+
+  private AccountExternalId.Key getEmailKey(String email) {
+    return new AccountExternalId.Key(AccountExternalId.SCHEME_MAILTO, email);
+  }
+
+  private static KeyPair genSshKey() throws JSchException {
+    JSch jsch = new JSch();
+    return KeyPair.genKeyPair(jsch, KeyPair.RSA);
+  }
+
+  private static String publicKey(KeyPair sshKey, String comment)
+      throws UnsupportedEncodingException {
+    ByteArrayOutputStream out = new ByteArrayOutputStream();
+    sshKey.writePublicKey(out, comment);
+    return out.toString("ASCII");
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GcAssert.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GcAssert.java
new file mode 100644
index 0000000..4c7289b2
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GcAssert.java
@@ -0,0 +1,70 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance;
+
+import static org.junit.Assert.assertTrue;
+
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Repository;
+
+import java.io.File;
+import java.io.FilenameFilter;
+import java.io.IOException;
+
+public class GcAssert {
+
+  private final GitRepositoryManager repoManager;
+
+  @Inject
+  public GcAssert(GitRepositoryManager repoManager) {
+    this.repoManager = repoManager;
+  }
+
+  public void assertHasPackFile(Project.NameKey... projects)
+      throws RepositoryNotFoundException, IOException {
+    for (Project.NameKey p : projects) {
+      assertTrue("Project " + p.get() + " has no pack files.",
+          getPackFiles(p).length > 0);
+    }
+  }
+
+  public void assertHasNoPackFile(Project.NameKey... projects)
+      throws RepositoryNotFoundException, IOException {
+    for (Project.NameKey p : projects) {
+      assertTrue("Project " + p.get() + " has pack files.",
+          getPackFiles(p).length == 0);
+    }
+  }
+
+  private String[] getPackFiles(Project.NameKey p)
+      throws RepositoryNotFoundException, IOException {
+    Repository repo = repoManager.openRepository(p);
+    try {
+      File packDir = new File(repo.getDirectory(), "objects/pack");
+      return packDir.list(new FilenameFilter() {
+        @Override
+        public boolean accept(File dir, String name) {
+          return name.endsWith(".pack");
+        }
+      });
+    } finally {
+      repo.close();
+    }
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GerritServer.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GerritServer.java
new file mode 100644
index 0000000..65bab6a
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/GerritServer.java
@@ -0,0 +1,128 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance;
+
+import java.lang.reflect.Field;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.concurrent.BrokenBarrierException;
+import java.util.concurrent.Callable;
+import java.util.concurrent.CyclicBarrier;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.TimeUnit;
+
+import com.google.gerrit.lifecycle.LifecycleManager;
+import com.google.gerrit.pgm.Daemon;
+import com.google.gerrit.pgm.Init;
+import com.google.gerrit.server.config.FactoryModule;
+import com.google.inject.Injector;
+import com.google.inject.Module;
+
+class GerritServer {
+
+  /** Returns fully started Gerrit server */
+  static GerritServer start() throws Exception {
+
+    final String sitePath = initSite();
+
+    final CyclicBarrier serverStarted = new CyclicBarrier(2);
+
+    final Daemon daemon = new Daemon(new Runnable() {
+      public void run() {
+        try {
+          serverStarted.await();
+        } catch (InterruptedException e) {
+          throw new RuntimeException(e);
+        } catch (BrokenBarrierException e) {
+          throw new RuntimeException(e);
+        }
+      }
+    });
+
+    ExecutorService daemonService = Executors.newSingleThreadExecutor();
+    daemonService.submit(new Callable<Void>() {
+      public Void call() throws Exception {
+        int rc = daemon.main(new String[] {"-d", sitePath, "--headless" });
+        if (rc != 0) {
+          System.out.println("Failed to start Gerrit daemon. Check "
+              + sitePath + "/logs/error_log");
+          serverStarted.reset();
+        }
+        return null;
+      };
+    });
+
+    serverStarted.await();
+    System.out.println("Gerrit Server Started");
+
+    Injector i = createTestInjector(daemon);
+    return new GerritServer(i, daemon, daemonService);
+  }
+
+  private static String initSite() throws Exception {
+    DateFormat df = new SimpleDateFormat("yyyyMMddHHmmss");
+    String path = "target/test_site_" + df.format(new Date());
+    Init init = new Init();
+    int rc = init.main(new String[] {"-d", path, "--batch", "--no-auto-start"});
+    if (rc != 0) {
+      throw new RuntimeException("Couldn't initialize site");
+    }
+    return path;
+  }
+
+  private static Injector createTestInjector(Daemon daemon) throws Exception {
+    Injector sysInjector = get(daemon, "sysInjector");
+    Module module = new FactoryModule() {
+      @Override
+      protected void configure() {
+        bind(AccountCreator.class);
+      }
+    };
+    return sysInjector.createChildInjector(module);
+  }
+
+  @SuppressWarnings("unchecked")
+  private static <T> T get(Object obj, String field) throws SecurityException,
+      NoSuchFieldException, IllegalArgumentException, IllegalAccessException {
+    Field f = obj.getClass().getDeclaredField(field);
+    f.setAccessible(true);
+    return (T) f.get(obj);
+  }
+
+  private Daemon daemon;
+  private ExecutorService daemonService;
+  private Injector testInjector;
+
+  private GerritServer(Injector testInjector,
+      Daemon daemon, ExecutorService daemonService) {
+    this.testInjector = testInjector;
+    this.daemon = daemon;
+    this.daemonService = daemonService;
+  }
+
+  Injector getTestInjector() {
+    return testInjector;
+  }
+
+  void stop() throws Exception {
+    LifecycleManager manager = get(daemon, "manager");
+    System.out.println("Gerrit Server Shutdown");
+    manager.stop();
+    daemonService.shutdownNow();
+    daemonService.awaitTermination(Long.MAX_VALUE, TimeUnit.SECONDS);
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/RestResponse.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/RestResponse.java
new file mode 100644
index 0000000..9f93489
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/RestResponse.java
@@ -0,0 +1,52 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance;
+
+import static com.google.gerrit.httpd.restapi.RestApiServlet.JSON_MAGIC;
+
+import org.apache.http.HttpResponse;
+
+import java.io.IOException;
+import java.io.InputStreamReader;
+import java.io.Reader;
+
+public class RestResponse {
+
+  private HttpResponse response;
+  private Reader reader;
+
+  RestResponse(HttpResponse response) {
+    this.response = response;
+  }
+
+  public Reader getReader() throws IllegalStateException, IOException {
+    if (reader == null && response.getEntity() != null) {
+      reader = new InputStreamReader(response.getEntity().getContent());
+      reader.skip(JSON_MAGIC.length);
+    }
+    return reader;
+  }
+
+  public void consume() throws IllegalStateException, IOException {
+    Reader reader = getReader();
+    if (reader != null) {
+      while (reader.read() != -1);
+    }
+  }
+
+  public int getStatusCode() {
+    return response.getStatusLine().getStatusCode();
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/RestSession.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/RestSession.java
new file mode 100644
index 0000000..2bf6523
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/RestSession.java
@@ -0,0 +1,86 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance;
+
+import com.google.gson.Gson;
+
+import org.apache.http.auth.AuthScope;
+import org.apache.http.auth.UsernamePasswordCredentials;
+import org.apache.http.client.methods.HttpDelete;
+import org.apache.http.client.methods.HttpGet;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.client.methods.HttpPut;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.http.message.BasicHeader;
+import org.apache.http.protocol.HTTP;
+
+import java.io.IOException;
+
+public class RestSession {
+
+  private final TestAccount account;
+  DefaultHttpClient client;
+
+  public RestSession(TestAccount account) {
+    this.account = account;
+  }
+
+  public RestResponse get(String endPoint) throws IOException {
+    HttpGet get = new HttpGet("http://localhost:8080/a" + endPoint);
+    return new RestResponse(getClient().execute(get));
+  }
+
+  public RestResponse put(String endPoint) throws IOException {
+    return put(endPoint, null);
+  }
+
+  public RestResponse put(String endPoint, Object content) throws IOException {
+    HttpPut put = new HttpPut("http://localhost:8080/a" + endPoint);
+    if (content != null) {
+      put.addHeader(new BasicHeader("Content-Type", "application/json"));
+      put.setEntity(new StringEntity((new Gson()).toJson(content), HTTP.UTF_8));
+    }
+    return new RestResponse(getClient().execute(put));
+  }
+
+  public RestResponse post(String endPoint) throws IOException {
+    return post(endPoint, null);
+  }
+
+  public RestResponse post(String endPoint, Object content) throws IOException {
+    HttpPost post = new HttpPost("http://localhost:8080/a" + endPoint);
+    if (content != null) {
+      post.addHeader(new BasicHeader("Content-Type", "application/json"));
+      post.setEntity(new StringEntity((new Gson()).toJson(content), HTTP.UTF_8));
+    }
+    return new RestResponse(getClient().execute(post));
+  }
+
+  public RestResponse delete(String endPoint) throws IOException {
+    HttpDelete delete = new HttpDelete("http://localhost:8080/a" + endPoint);
+    return new RestResponse(getClient().execute(delete));
+  }
+
+  private DefaultHttpClient getClient() {
+    if (client == null) {
+      client = new DefaultHttpClient();
+      client.getCredentialsProvider().setCredentials(
+          new AuthScope("localhost", 8080),
+          new UsernamePasswordCredentials(account.username, account.httpPassword));
+    }
+    return client;
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/SshSession.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/SshSession.java
new file mode 100644
index 0000000..aae9236
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/SshSession.java
@@ -0,0 +1,91 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.Scanner;
+
+import com.jcraft.jsch.ChannelExec;
+import com.jcraft.jsch.JSch;
+import com.jcraft.jsch.JSchException;
+import com.jcraft.jsch.Session;
+
+public class SshSession {
+
+  private final TestAccount account;
+  private Session session;
+  private String error;
+
+  public SshSession(TestAccount account) {
+    this.account = account;
+  }
+
+  public String exec(String command) throws JSchException, IOException {
+    ChannelExec channel = (ChannelExec) getSession().openChannel("exec");
+    try {
+      channel.setCommand(command);
+      channel.setInputStream(null);
+      InputStream in = channel.getInputStream();
+      channel.connect();
+
+      Scanner s = new Scanner(channel.getErrStream()).useDelimiter("\\A");
+      error =  s.hasNext() ? s.next() : null;
+
+      s = new Scanner(in).useDelimiter("\\A");
+      return s.hasNext() ? s.next() : "";
+    } finally {
+      channel.disconnect();
+    }
+  }
+
+  public boolean hasError() {
+    return error != null;
+  }
+
+  public String getError() {
+    return error;
+  }
+
+  public void close() {
+    if (session != null) {
+      session.disconnect();
+      session = null;
+    }
+  }
+
+  private Session getSession() throws JSchException {
+    if (session == null) {
+      JSch jsch = new JSch();
+      jsch.addIdentity("KeyPair",
+          account.privateKey(), account.sshKey.getPublicKeyBlob(), null);
+      session = jsch.getSession(account.username, "localhost", 29418);
+      session.setConfig("StrictHostKeyChecking", "no");
+      session.connect();
+    }
+    return session;
+  }
+
+  public String getUrl() {
+    StringBuilder b = new StringBuilder();
+    b.append("ssh://");
+    b.append(session.getUserName());
+    b.append("@");
+    b.append(session.getHost());
+    b.append(":");
+    b.append(session.getPort());
+    return b.toString();
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/SystemGroupsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/SystemGroupsIT.java
new file mode 100644
index 0000000..6ee2045
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/SystemGroupsIT.java
@@ -0,0 +1,105 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance;
+
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.collect.Sets;
+import com.google.gerrit.acceptance.rest.group.GroupInfo;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Inject;
+
+import com.jcraft.jsch.JSchException;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * An example test that tests presence of system groups in a newly initialized
+ * review site.
+ *
+ * The test shows how to perform these checks via SSH, REST or using Gerrit
+ * internals.
+ */
+public class SystemGroupsIT extends AbstractDaemonTest {
+
+  @Inject
+  private SchemaFactory<ReviewDb> reviewDbProvider;
+
+  @Inject
+  private AccountCreator accounts;
+
+  protected TestAccount admin;
+
+  @Before
+  public void setUp() throws Exception {
+    admin = accounts.create("admin", "admin@sap.com", "Administrator",
+            "Administrators");
+  }
+
+  @Test
+  public void systemGroupsCreated_ssh() throws JSchException, IOException {
+    SshSession session = new SshSession(admin);
+    String result = session.exec("gerrit ls-groups");
+    assertTrue(result.contains("Administrators"));
+    assertTrue(result.contains("Anonymous Users"));
+    assertTrue(result.contains("Non-Interactive Users"));
+    assertTrue(result.contains("Project Owners"));
+    assertTrue(result.contains("Registered Users"));
+    session.close();
+  }
+
+  @Test
+  public void systemGroupsCreated_rest() throws IOException {
+    RestSession session = new RestSession(admin);
+    RestResponse r = session.get("/groups/");
+    Gson gson = new Gson();
+    Map<String, GroupInfo> result =
+        gson.fromJson(r.getReader(), new TypeToken<Map<String, GroupInfo>>() {}.getType());
+    Set<String> names = result.keySet();
+    assertTrue(names.contains("Administrators"));
+    assertTrue(names.contains("Anonymous Users"));
+    assertTrue(names.contains("Non-Interactive Users"));
+    assertTrue(names.contains("Project Owners"));
+    assertTrue(names.contains("Registered Users"));
+  }
+
+  @Test
+  public void systemGroupsCreated_internals() throws OrmException {
+    ReviewDb db = reviewDbProvider.open();
+    try {
+      Set<String> names = Sets.newHashSet();
+      for (AccountGroup g : db.accountGroups().all()) {
+        names.add(g.getName());
+      }
+      assertTrue(names.contains("Administrators"));
+      assertTrue(names.contains("Anonymous Users"));
+      assertTrue(names.contains("Non-Interactive Users"));
+      assertTrue(names.contains("Project Owners"));
+      assertTrue(names.contains("Registered Users"));
+    } finally {
+      db.close();
+    }
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/TempFileUtil.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/TempFileUtil.java
new file mode 100644
index 0000000..adee361
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/TempFileUtil.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.acceptance;
+
+import java.io.File;
+import java.text.DateFormat;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+
+public class TempFileUtil {
+
+  private static int testCount;
+  private static DateFormat df = new SimpleDateFormat("yyyyMMddHHmmss");
+  private static final File temp = new File(new File("target"), "temp");
+
+  private static String createUniqueTestFolderName() {
+    return "test_" + (df.format(new Date()) + "_" + (testCount++));
+  }
+
+  public static File createTempDirectory() {
+    final String name = createUniqueTestFolderName();
+    final File directory = new File(temp, name);
+    if (!directory.mkdirs()) {
+      throw new RuntimeException("failed to create folder '"
+          + directory.getAbsolutePath() + "'");
+    }
+    return directory;
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/TestAccount.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/TestAccount.java
new file mode 100644
index 0000000..358680f
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/TestAccount.java
@@ -0,0 +1,63 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance;
+
+import com.google.gerrit.reviewdb.client.Account;
+
+import java.io.ByteArrayOutputStream;
+
+import com.jcraft.jsch.KeyPair;
+
+import org.eclipse.jgit.lib.PersonIdent;
+
+
+public class TestAccount {
+  public final Account.Id id;
+  public final String username;
+  public final String email;
+  public final String fullName;
+  public final KeyPair sshKey;
+  public final String httpPassword;
+
+  TestAccount(Account.Id id, String username, String email, String fullName,
+      KeyPair sshKey, String httpPassword) {
+    this.id = id;
+    this.username = username;
+    this.email = email;
+    this.fullName = fullName;
+    this.sshKey = sshKey;
+    this.httpPassword = httpPassword;
+  }
+
+  public byte[] privateKey() {
+    ByteArrayOutputStream out = new ByteArrayOutputStream();
+    sshKey.writePrivateKey(out);
+    return out.toByteArray();
+  }
+
+  public PersonIdent getIdent() {
+    return new PersonIdent(username, email);
+  }
+
+  public String getHttpUrl() {
+    StringBuilder b = new StringBuilder();
+    b.append("http://");
+    b.append(username);
+    b.append(":");
+    b.append(httpPassword);
+    b.append("@localhost:8080");
+    return b.toString();
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/GitUtil.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/GitUtil.java
new file mode 100644
index 0000000..9faf32a
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/GitUtil.java
@@ -0,0 +1,148 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.git;
+
+import com.google.common.collect.Iterables;
+import com.google.gerrit.acceptance.SshSession;
+import com.google.gerrit.acceptance.TempFileUtil;
+import com.google.gerrit.acceptance.TestAccount;
+
+import com.jcraft.jsch.JSch;
+import com.jcraft.jsch.JSchException;
+import com.jcraft.jsch.Session;
+
+import org.eclipse.jgit.api.AddCommand;
+import org.eclipse.jgit.api.CloneCommand;
+import org.eclipse.jgit.api.CommitCommand;
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.PushCommand;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.JschConfigSessionFactory;
+import org.eclipse.jgit.transport.OpenSshConfig.Host;
+import org.eclipse.jgit.transport.PushResult;
+import org.eclipse.jgit.transport.RefSpec;
+import org.eclipse.jgit.transport.SshSessionFactory;
+import org.eclipse.jgit.util.ChangeIdUtil;
+import org.eclipse.jgit.util.FS;
+
+import java.io.BufferedWriter;
+import java.io.File;
+import java.io.FileWriter;
+import java.io.IOException;
+import java.util.Properties;
+
+public class GitUtil {
+
+  public static void initSsh(final TestAccount a) {
+    final Properties config = new Properties();
+    config.put("StrictHostKeyChecking", "no");
+    JSch.setConfig(config);
+
+    // register a JschConfigSessionFactory that adds the private key as identity
+    // to the JSch instance of JGit so that SSH communication via JGit can
+    // succeed
+    SshSessionFactory.setInstance(new JschConfigSessionFactory() {
+      @Override
+      protected void configure(Host hc, Session session) {
+        try {
+          final JSch jsch = getJSch(hc, FS.DETECTED);
+          jsch.addIdentity("KeyPair", a.privateKey(),
+              a.sshKey.getPublicKeyBlob(), null);
+        } catch (JSchException e) {
+          throw new RuntimeException(e);
+        }
+      }
+    });
+  }
+
+  public static void createProject(SshSession s, String name)
+      throws JSchException, IOException {
+    s.exec("gerrit create-project --empty-commit --name \"" + name + "\"");
+  }
+
+  public static Git cloneProject(String url) throws GitAPIException {
+    final File gitDir = TempFileUtil.createTempDirectory();
+    final CloneCommand cloneCmd = Git.cloneRepository();
+    cloneCmd.setURI(url);
+    cloneCmd.setDirectory(gitDir);
+    return cloneCmd.call();
+  }
+
+  public static void add(Git git, String path, String content)
+      throws GitAPIException, IOException {
+    File f = new File(git.getRepository().getDirectory().getParentFile(), path);
+    File p = f.getParentFile();
+    if (!p.exists() && !p.mkdirs()) {
+      throw new IOException("failed to create dir: " + p.getAbsolutePath());
+    }
+    FileWriter w = new FileWriter(f);
+    BufferedWriter out = new BufferedWriter(w);
+    try {
+      out.write(content);
+    } finally {
+      out.close();
+    }
+
+    final AddCommand addCmd = git.add();
+    addCmd.addFilepattern(path);
+    addCmd.call();
+  }
+
+  public static String createCommit(Git git, PersonIdent i, String msg)
+      throws GitAPIException, IOException {
+    return createCommit(git, i, msg, true);
+  }
+
+  public static String createCommit(Git git, PersonIdent i, String msg,
+      boolean insertChangeId) throws GitAPIException, IOException {
+    ObjectId changeId = null;
+    if (insertChangeId) {
+      changeId = computeChangeId(git, i, msg);
+      msg = ChangeIdUtil.insertId(msg, changeId);
+    }
+
+    final CommitCommand commitCmd = git.commit();
+    commitCmd.setAuthor(i);
+    commitCmd.setCommitter(i);
+    commitCmd.setMessage(msg);
+    commitCmd.call();
+
+    return changeId != null ? "I" + changeId.getName() : null;
+  }
+
+  private static ObjectId computeChangeId(Git git, PersonIdent i, String msg)
+      throws IOException {
+    RevWalk rw = new RevWalk(git.getRepository());
+    try {
+      RevCommit parent =
+          rw.lookupCommit(git.getRepository().getRef(Constants.HEAD).getObjectId());
+      return ChangeIdUtil.computeChangeId(parent.getTree(), parent.getId(), i, i, msg);
+    } finally {
+      rw.release();
+    }
+  }
+
+  public static PushResult pushHead(Git git, String ref) throws GitAPIException {
+    PushCommand pushCmd = git.push();
+    pushCmd.setRefSpecs(new RefSpec("HEAD:" + ref));
+    Iterable<PushResult> r = pushCmd.call();
+    return Iterables.getOnlyElement(r);
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/PushForReviewIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/PushForReviewIT.java
new file mode 100644
index 0000000..f905350
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/git/PushForReviewIT.java
@@ -0,0 +1,309 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.git;
+
+import static com.google.gerrit.acceptance.git.GitUtil.add;
+import static com.google.gerrit.acceptance.git.GitUtil.cloneProject;
+import static com.google.gerrit.acceptance.git.GitUtil.createCommit;
+import static com.google.gerrit.acceptance.git.GitUtil.createProject;
+import static com.google.gerrit.acceptance.git.GitUtil.initSsh;
+import static com.google.gerrit.acceptance.git.GitUtil.pushHead;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.base.Function;
+import com.google.common.base.Strings;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.AccountCreator;
+import com.google.gerrit.acceptance.SshSession;
+import com.google.gerrit.acceptance.TestAccount;
+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.Project;
+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.jcraft.jsch.JSchException;
+
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.transport.PushResult;
+import org.eclipse.jgit.transport.RemoteRefUpdate;
+import org.eclipse.jgit.transport.RemoteRefUpdate.Status;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.Parameterized;
+import org.junit.runners.Parameterized.Parameters;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Set;
+
+@RunWith(Parameterized.class)
+public class PushForReviewIT extends AbstractDaemonTest {
+
+  private enum Protocol {
+    SSH, HTTP
+  }
+
+  @Parameters(name="{0}")
+  public static List<Object[]> getParam() {
+    List<Object[]> params = Lists.newArrayList();
+    for(Protocol p : Protocol.values()) {
+      params.add(new Object[] {p});
+    }
+    return params;
+  }
+
+  @Inject
+  private AccountCreator accounts;
+
+  @Inject
+  private SchemaFactory<ReviewDb> reviewDbProvider;
+
+  private TestAccount admin;
+  private Project.NameKey project;
+  private Git git;
+  private ReviewDb db;
+  private Protocol protocol;
+
+  public PushForReviewIT(Protocol p) {
+    this.protocol = p;
+  }
+
+  @Before
+  public void setUp() throws Exception {
+    admin =
+        accounts.create("admin", "admin@example.com", "Administrator",
+            "Administrators");
+
+    project = new Project.NameKey("p");
+    initSsh(admin);
+    SshSession sshSession = new SshSession(admin);
+    createProject(sshSession, project.get());
+    String url;
+    switch (protocol) {
+      case SSH:
+        url = sshSession.getUrl();
+        break;
+      case HTTP:
+        url = admin.getHttpUrl();
+        break;
+      default:
+        throw new IllegalStateException("unexpected protocol: " + protocol);
+    }
+    git = cloneProject(url + "/" + project.get());
+    sshSession.close();
+
+    db = reviewDbProvider.open();
+  }
+
+  @After
+  public void cleanup() {
+    db.close();
+  }
+
+  @Test
+  public void testPushForMaster() throws GitAPIException, OrmException,
+      IOException {
+    PushOneCommit push = new PushOneCommit();
+    String ref = "refs/for/master";
+    PushResult r = push.to(ref);
+    assertOkStatus(r, ref);
+    assertChange(push.changeId, Change.Status.NEW, PushOneCommit.SUBJECT, null);
+  }
+
+  @Test
+  public void testPushForMasterWithTopic() throws GitAPIException,
+      OrmException, IOException {
+    // specify topic in ref
+    PushOneCommit push = new PushOneCommit();
+    String topic = "my/topic";
+    String ref = "refs/for/master/" + topic;
+    PushResult r = push.to(ref);
+    assertOkStatus(r, ref);
+    assertChange(push.changeId, Change.Status.NEW, PushOneCommit.SUBJECT, topic);
+
+    // specify topic as option
+    push = new PushOneCommit();
+    ref = "refs/for/master%topic=" + topic;
+    r = push.to(ref);
+    assertOkStatus(r, ref);
+    assertChange(push.changeId, Change.Status.NEW, PushOneCommit.SUBJECT, topic);
+  }
+
+  @Test
+  public void testPushForMasterWithCc() throws GitAPIException, OrmException,
+      IOException, JSchException {
+    // cc one user
+    TestAccount user = accounts.create("user", "user@example.com", "User");
+    PushOneCommit push = new PushOneCommit();
+    String topic = "my/topic";
+    String ref = "refs/for/master/" + topic + "%cc=" + user.email;
+    PushResult r = push.to(ref);
+    assertOkStatus(r, ref);
+    assertChange(push.changeId, Change.Status.NEW, PushOneCommit.SUBJECT, topic);
+
+    // cc several users
+    TestAccount user2 =
+        accounts.create("another-user", "another.user@example.com", "Another User");
+    push = new PushOneCommit();
+    ref = "refs/for/master/" + topic + "%cc=" + admin.email + ",cc=" + user.email
+        + ",cc=" + user2.email;
+    r = push.to(ref);
+    assertOkStatus(r, ref);
+    assertChange(push.changeId, Change.Status.NEW, PushOneCommit.SUBJECT, topic);
+
+    // cc non-existing user
+    String nonExistingEmail = "non.existing@example.com";
+    push = new PushOneCommit();
+    ref = "refs/for/master/" + topic + "%cc=" + admin.email + ",cc="
+        + nonExistingEmail + ",cc=" + user.email;
+    r = push.to(ref);
+    assertErrorStatus(r, "user \"" + nonExistingEmail + "\" not found", ref);
+  }
+
+  @Test
+  public void testPushForMasterWithReviewer() throws GitAPIException,
+      OrmException, IOException, JSchException {
+    // add one reviewer
+    TestAccount user = accounts.create("user", "user@example.com", "User");
+    PushOneCommit push = new PushOneCommit();
+    String topic = "my/topic";
+    String ref = "refs/for/master/" + topic + "%r=" + user.email;
+    PushResult r = push.to(ref);
+    assertOkStatus(r, ref);
+    assertChange(push.changeId, Change.Status.NEW, PushOneCommit.SUBJECT,
+        topic, user);
+
+    // add several reviewers
+    TestAccount user2 =
+        accounts.create("another-user", "another.user@example.com", "Another User");
+    push = new PushOneCommit();
+    ref = "refs/for/master/" + topic + "%r=" + admin.email + ",r=" + user.email
+        + ",r=" + user2.email;
+    r = push.to(ref);
+    assertOkStatus(r, ref);
+    // admin is the owner of the change and should not appear as reviewer
+    assertChange(push.changeId, Change.Status.NEW, PushOneCommit.SUBJECT,
+        topic, user, user2);
+
+    // add non-existing user as reviewer
+    String nonExistingEmail = "non.existing@example.com";
+    push = new PushOneCommit();
+    ref = "refs/for/master/" + topic + "%r=" + admin.email + ",r="
+        + nonExistingEmail + ",r=" + user.email;
+    r = push.to(ref);
+    assertErrorStatus(r, "user \"" + nonExistingEmail + "\" not found", ref);
+  }
+
+  @Test
+  public void testPushForMasterAsDraft() throws GitAPIException, OrmException,
+      IOException {
+    // create draft by pushing to 'refs/drafts/'
+    PushOneCommit push = new PushOneCommit();
+    String ref = "refs/drafts/master";
+    PushResult r = push.to(ref);
+    assertOkStatus(r, ref);
+    assertChange(push.changeId, Change.Status.DRAFT, PushOneCommit.SUBJECT, null);
+
+    // create draft by using 'draft' option
+    push = new PushOneCommit();
+    ref = "refs/for/master%draft";
+    r = push.to(ref);
+    assertOkStatus(r, ref);
+    assertChange(push.changeId, Change.Status.DRAFT, PushOneCommit.SUBJECT, null);
+  }
+
+  @Test
+  public void testPushForNonExistingBranch() throws GitAPIException,
+      OrmException, IOException {
+    PushOneCommit push = new PushOneCommit();
+    String branchName = "non-existing";
+    String ref = "refs/for/" + branchName;
+    PushResult r = push.to(ref);
+    assertErrorStatus(r, "branch " + branchName + " not found", ref);
+  }
+
+  private void assertChange(String changeId, Change.Status expectedStatus,
+      String expectedSubject, String expectedTopic,
+      TestAccount... expectedReviewers) throws OrmException {
+    Change c =
+        Iterables.getOnlyElement(db.changes().byKey(new Change.Key(changeId)).toList());
+    assertEquals(expectedSubject, c.getSubject());
+    assertEquals(expectedStatus, c.getStatus());
+    assertEquals(expectedTopic, Strings.emptyToNull(c.getTopic()));
+    assertReviewers(c, expectedReviewers);
+  }
+
+  private void assertReviewers(Change c, TestAccount... expectedReviewers)
+      throws OrmException {
+    Set<Account.Id> expectedReviewerIds =
+        Sets.newHashSet(Lists.transform(Arrays.asList(expectedReviewers),
+            new Function<TestAccount, Account.Id>() {
+              @Override
+              public Account.Id apply(TestAccount a) {
+                return a.id;
+              }
+            }));
+
+    for (PatchSetApproval psa : db.patchSetApprovals().byPatchSet(
+        c.currentPatchSetId())) {
+      assertTrue("unexpected reviewer " + psa.getAccountId(),
+          expectedReviewerIds.remove(psa.getAccountId()));
+    }
+    assertTrue("missing reviewers: " + expectedReviewerIds,
+        expectedReviewerIds.isEmpty());
+  }
+
+  private static void assertOkStatus(PushResult result, String ref) {
+    assertStatus(Status.OK, null, result, ref);
+  }
+
+  private static void assertErrorStatus(PushResult result,
+      String expectedMessage, String ref) {
+    assertStatus(Status.REJECTED_OTHER_REASON, expectedMessage, result, ref);
+  }
+
+  private static void assertStatus(Status expectedStatus,
+      String expectedMessage, PushResult result, String ref) {
+    RemoteRefUpdate refUpdate = result.getRemoteUpdate(ref);
+    assertEquals(refUpdate.getMessage() + "\n" + result.getMessages(),
+        expectedStatus, refUpdate.getStatus());
+    assertEquals(expectedMessage, refUpdate.getMessage());
+  }
+
+  private class PushOneCommit {
+    final static String FILE_NAME = "a.txt";
+    final static String FILE_CONTENT = "some content";
+    final static String SUBJECT = "test commit";
+    String changeId;
+
+    public PushResult to(String ref) throws GitAPIException, IOException {
+      add(git, FILE_NAME, FILE_CONTENT);
+      changeId = createCommit(git, admin.getIdent(), SUBJECT);
+      return pushHead(git, ref);
+    }
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/AccountAssert.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/AccountAssert.java
new file mode 100644
index 0000000..fb26669
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/AccountAssert.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.account;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.google.gerrit.acceptance.TestAccount;
+
+public class AccountAssert {
+
+  public static void assertAccountInfo(TestAccount a, AccountInfo ai) {
+    assertTrue(a.id.get() == ai._account_id);
+    assertEquals(a.fullName, ai.name);
+    assertEquals(a.email, ai.email);
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/AccountInfo.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/AccountInfo.java
new file mode 100644
index 0000000..cf88bc6
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/AccountInfo.java
@@ -0,0 +1,21 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.account;
+
+public class AccountInfo {
+  public Integer _account_id;
+  public String name;
+  public String email;
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/GetAccountIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/GetAccountIT.java
new file mode 100644
index 0000000..8d75487
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/account/GetAccountIT.java
@@ -0,0 +1,88 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.account;
+
+import static com.google.gerrit.acceptance.rest.account.AccountAssert.assertAccountInfo;
+import static org.junit.Assert.assertEquals;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.AccountCreator;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.RestSession;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.extensions.restapi.Url;
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import com.google.inject.Inject;
+
+import org.apache.http.HttpStatus;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+
+public class GetAccountIT extends AbstractDaemonTest {
+
+  @Inject
+  private AccountCreator accounts;
+
+  private TestAccount admin;
+  private RestSession session;
+
+  @Before
+  public void setUp() throws Exception {
+    admin = accounts.create("admin", "admin@example.com", "Administrator",
+            "Administrators");
+    session = new RestSession(admin);
+  }
+
+  @Test
+  public void getNonExistingAccount_NotFound() throws IOException {
+    assertEquals(HttpStatus.SC_NOT_FOUND, session.get("/accounts/non-existing")
+        .getStatusCode());
+  }
+
+  @Test
+  public void getAccount() throws IOException {
+    // by formatted string
+    testGetAccount("/accounts/"
+        + Url.encode(admin.fullName + " <" + admin.email + ">"), admin);
+
+    // by email
+    testGetAccount("/accounts/" + admin.email, admin);
+
+    // by full name
+    testGetAccount("/accounts/" + admin.fullName, admin);
+
+    // by account ID
+    testGetAccount("/accounts/" + admin.id.get(), admin);
+
+    // by user name
+    testGetAccount("/accounts/" + admin.username, admin);
+
+    // by 'self'
+    testGetAccount("/accounts/self", admin);
+  }
+
+  private void testGetAccount(String url, TestAccount expectedAccount)
+      throws IOException {
+    RestResponse r = session.get(url);
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    AccountInfo account =
+        (new Gson()).fromJson(r.getReader(),
+            new TypeToken<AccountInfo>() {}.getType());
+    assertAccountInfo(expectedAccount, account);
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/AddRemoveGroupMembersIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/AddRemoveGroupMembersIT.java
new file mode 100644
index 0000000..a254f15
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/AddRemoveGroupMembersIT.java
@@ -0,0 +1,259 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.group;
+
+import static com.google.gerrit.acceptance.rest.account.AccountAssert.assertAccountInfo;
+import static com.google.gerrit.acceptance.rest.group.GroupAssert.assertGroupInfo;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.AccountCreator;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.RestSession;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.rest.account.AccountInfo;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuid;
+import com.google.gerrit.reviewdb.client.AccountGroupMember;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.account.GroupCache;
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.ResultSet;
+import com.google.gwtorm.server.SchemaFactory;
+import com.google.inject.Inject;
+
+import org.apache.http.HttpStatus;
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class AddRemoveGroupMembersIT extends AbstractDaemonTest {
+
+  @Inject
+  private AccountCreator accounts;
+
+  @Inject
+  private SchemaFactory<ReviewDb> reviewDbProvider;
+
+  @Inject
+  private GroupCache groupCache;
+
+  private RestSession session;
+  private TestAccount admin;
+  private ReviewDb db;
+
+  @Before
+  public void setUp() throws Exception {
+    admin = accounts.create("admin", "Administrators");
+    session = new RestSession(admin);
+    db = reviewDbProvider.open();
+  }
+
+  @After
+  public void tearDown() {
+    db.close();
+  }
+
+  @Test
+  public void addToNonExistingGroup_NotFound() throws IOException {
+    assertEquals(HttpStatus.SC_NOT_FOUND,
+        PUT("/groups/non-existing/members/admin").getStatusCode());
+  }
+
+  @Test
+  public void removeFromNonExistingGroup_NotFound() throws IOException {
+    assertEquals(HttpStatus.SC_NOT_FOUND,
+        DELETE("/groups/non-existing/members/admin"));
+  }
+
+  @Test
+  public void addRemoveMember() throws Exception {
+    TestAccount u = accounts.create("user", "user@example.com", "Full Name");
+    RestResponse r = PUT("/groups/Administrators/members/user");
+    assertEquals(HttpStatus.SC_CREATED, r.getStatusCode());
+    AccountInfo ai = (new Gson()).fromJson(r.getReader(),
+        new TypeToken<AccountInfo>() {}.getType());
+    assertAccountInfo(u, ai);
+    assertMembers("Administrators", admin, u);
+    r.consume();
+
+    assertEquals(HttpStatus.SC_NO_CONTENT,
+        DELETE("/groups/Administrators/members/user"));
+    assertMembers("Administrators", admin);
+  }
+
+  @Test
+  public void addExistingMember_OK() throws IOException {
+    assertEquals(HttpStatus.SC_OK,
+        PUT("/groups/Administrators/members/admin").getStatusCode());
+  }
+
+  @Test
+  public void addMultipleMembers() throws Exception {
+    group("users");
+    TestAccount u1 = accounts.create("u1", "u1@example.com", "Full Name 1");
+    TestAccount u2 = accounts.create("u2", "u2@example.com", "Full Name 2");
+    MembersInput input = new MembersInput();
+    input.members = Lists.newLinkedList();
+    input.members.add(u1.username);
+    input.members.add(u2.username);
+    RestResponse r = POST("/groups/users/members", input);
+    List<AccountInfo> ai = (new Gson()).fromJson(r.getReader(),
+        new TypeToken<List<AccountInfo>>() {}.getType());
+    assertMembers(ai, u1, u2);
+  }
+
+  @Test
+  public void includeRemoveGroup() throws Exception {
+    group("newGroup");
+    RestResponse r = PUT("/groups/Administrators/groups/newGroup");
+    assertEquals(HttpStatus.SC_CREATED, r.getStatusCode());
+    GroupInfo i = (new Gson()).fromJson(r.getReader(), new TypeToken<GroupInfo>() {}.getType());
+    r.consume();
+    assertGroupInfo(groupCache.get(new AccountGroup.NameKey("newGroup")), i);
+    assertIncludes("Administrators", "newGroup");
+
+    assertEquals(HttpStatus.SC_NO_CONTENT,
+        DELETE("/groups/Administrators/groups/newGroup"));
+    assertNoIncludes("Administrators");
+  }
+
+  @Test
+  public void includeExistingGroup_OK() throws Exception {
+    group("newGroup");
+    PUT("/groups/Administrators/groups/newGroup").consume();
+    assertEquals(HttpStatus.SC_OK,
+        PUT("/groups/Administrators/groups/newGroup").getStatusCode());
+  }
+
+  @Test
+  public void addMultipleIncludes() throws Exception {
+    group("newGroup1");
+    group("newGroup2");
+    GroupsInput input = new GroupsInput();
+    input.groups = Lists.newLinkedList();
+    input.groups.add("newGroup1");
+    input.groups.add("newGroup2");
+    RestResponse r = POST("/groups/Administrators/groups", input);
+    List<GroupInfo> gi = (new Gson()).fromJson(r.getReader(),
+        new TypeToken<List<GroupInfo>>() {}.getType());
+    assertIncludes(gi, "newGroup1", "newGroup2");
+  }
+
+  private RestResponse PUT(String endpoint) throws IOException {
+    return session.put(endpoint);
+  }
+
+  private int DELETE(String endpoint) throws IOException {
+    RestResponse r = session.delete(endpoint);
+    r.consume();
+    return r.getStatusCode();
+  }
+
+  private RestResponse POST(String endPoint, MembersInput mi) throws IOException {
+    return session.post(endPoint, mi);
+  }
+
+  private RestResponse POST(String endPoint, GroupsInput gi) throws IOException {
+    return session.post(endPoint, gi);
+  }
+
+  private void group(String name) throws IOException {
+    GroupInput in = new GroupInput();
+    session.put("/groups/" + name, in).consume();
+  }
+
+  private void assertMembers(String group, TestAccount... members)
+      throws OrmException {
+    AccountGroup g = groupCache.get(new AccountGroup.NameKey(group));
+    Set<Account.Id> ids = Sets.newHashSet();
+    ResultSet<AccountGroupMember> all =
+        db.accountGroupMembers().byGroup(g.getId());
+    for (AccountGroupMember m : all) {
+      ids.add(m.getAccountId());
+    }
+    assertTrue(ids.size() == members.length);
+    for (TestAccount a : members) {
+      assertTrue(ids.contains(a.id));
+    }
+  }
+
+  private void assertMembers(List<AccountInfo> ai, TestAccount... members) {
+    Map<Integer, AccountInfo> infoById = Maps.newHashMap();
+    for (AccountInfo i : ai) {
+      infoById.put(i._account_id, i);
+    }
+
+    for (TestAccount a : members) {
+      AccountInfo i = infoById.get(a.id.get());
+      assertNotNull(i);
+      assertAccountInfo(a, i);
+    }
+    assertEquals(ai.size(), members.length);
+  }
+
+  private void assertIncludes(String group, String... includes)
+      throws OrmException {
+    AccountGroup g = groupCache.get(new AccountGroup.NameKey(group));
+    Set<AccountGroup.UUID> ids = Sets.newHashSet();
+    ResultSet<AccountGroupIncludeByUuid> all =
+        db.accountGroupIncludesByUuid().byGroup(g.getId());
+    for (AccountGroupIncludeByUuid m : all) {
+      ids.add(m.getIncludeUUID());
+    }
+    assertTrue(ids.size() == includes.length);
+    for (String i : includes) {
+      AccountGroup.UUID id = groupCache.get(
+          new AccountGroup.NameKey(i)).getGroupUUID();
+      assertTrue(ids.contains(id));
+    }
+  }
+
+  private void assertIncludes(List<GroupInfo> gi, String... includes) {
+    Map<String, GroupInfo> groupsByName = Maps.newHashMap();
+    for (GroupInfo i : gi) {
+      groupsByName.put(i.name, i);
+    }
+
+    for (String name : includes) {
+      GroupInfo i = groupsByName.get(name);
+      assertNotNull(i);
+      assertGroupInfo(groupCache.get(new AccountGroup.NameKey(name)), i);
+    }
+    assertEquals(gi.size(), includes.length);
+  }
+
+  private void assertNoIncludes(String group) throws OrmException {
+    AccountGroup g = groupCache.get(new AccountGroup.NameKey(group));
+    Iterator<AccountGroupIncludeByUuid> it =
+        db.accountGroupIncludesByUuid().byGroup(g.getId()).iterator();
+    assertFalse(it.hasNext());
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/CreateGroupIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/CreateGroupIT.java
new file mode 100644
index 0000000..4641f09
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/CreateGroupIT.java
@@ -0,0 +1,101 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.group;
+
+import static com.google.gerrit.acceptance.rest.group.GroupAssert.assertGroupInfo;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.AccountCreator;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.RestSession;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.account.GroupCache;
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+
+import com.jcraft.jsch.JSchException;
+
+import org.apache.http.HttpStatus;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+
+public class CreateGroupIT extends AbstractDaemonTest {
+
+  @Inject
+  private AccountCreator accounts;
+
+  @Inject
+  private GroupCache groupCache;
+
+  private TestAccount admin;
+  private RestSession session;
+
+  @Before
+  public void setUp() throws Exception {
+    admin = accounts.create("admin", "admin@example.com", "Administrator",
+            "Administrators");
+    session = new RestSession(admin);
+  }
+
+  @Test
+  public void testCreateGroup() throws IOException {
+    final String newGroupName = "newGroup";
+    RestResponse r = session.put("/groups/" + newGroupName);
+    GroupInfo g = (new Gson()).fromJson(r.getReader(), new TypeToken<GroupInfo>() {}.getType());
+    assertEquals(newGroupName, g.name);
+    AccountGroup group = groupCache.get(new AccountGroup.NameKey(newGroupName));
+    assertNotNull(group);
+    assertGroupInfo(group, g);
+  }
+
+  @Test
+  public void testCreateGroupWithProperties() throws IOException {
+    final String newGroupName = "newGroup";
+    GroupInput in = new GroupInput();
+    in.description = "Test description";
+    in.visible_to_all = true;
+    in.owner_id = groupCache.get(new AccountGroup.NameKey("Administrators")).getGroupUUID().get();
+    RestResponse r = session.put("/groups/" + newGroupName, in);
+    GroupInfo g = (new Gson()).fromJson(r.getReader(), new TypeToken<GroupInfo>() {}.getType());
+    assertEquals(newGroupName, g.name);
+    AccountGroup group = groupCache.get(new AccountGroup.NameKey(newGroupName));
+    assertEquals(in.description, group.getDescription());
+    assertEquals(in.visible_to_all, group.isVisibleToAll());
+    assertEquals(in.owner_id, group.getOwnerGroupUUID().get());
+  }
+
+  @Test
+  public void testCreateGroupWithoutCapability_Forbidden() throws OrmException,
+      JSchException, IOException {
+    TestAccount user = accounts.create("user", "user@example.com", "User");
+    RestResponse r = (new RestSession(user)).put("/groups/newGroup");
+    assertEquals(HttpStatus.SC_FORBIDDEN, r.getStatusCode());
+  }
+
+  @Test
+  public void testCreateGroupWhenGroupAlreadyExists_Conflict()
+      throws OrmException, JSchException, IOException {
+    RestResponse r = session.put("/groups/Administrators");
+    assertEquals(HttpStatus.SC_CONFLICT, r.getStatusCode());
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/GetGroupIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/GetGroupIT.java
new file mode 100644
index 0000000..5fe386d
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/GetGroupIT.java
@@ -0,0 +1,72 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.group;
+
+import static com.google.gerrit.acceptance.rest.group.GroupAssert.assertGroupInfo;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.AccountCreator;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.RestSession;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.account.GroupCache;
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import com.google.inject.Inject;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+
+public class GetGroupIT extends AbstractDaemonTest {
+
+  @Inject
+  private AccountCreator accounts;
+
+  @Inject
+  private GroupCache groupCache;
+
+  private TestAccount admin;
+  private RestSession session;
+
+  @Before
+  public void setUp() throws Exception {
+    admin = accounts.create("admin", "admin@example.com", "Administrator",
+            "Administrators");
+    session = new RestSession(admin);
+  }
+
+  @Test
+  public void testGetGroup() throws IOException {
+    AccountGroup adminGroup = groupCache.get(new AccountGroup.NameKey("Administrators"));
+
+    // by UUID
+    testGetGroup("/groups/" + adminGroup.getGroupUUID().get(), adminGroup);
+
+    // by name
+    testGetGroup("/groups/" + adminGroup.getName(), adminGroup);
+
+    // by legacy numeric ID
+    testGetGroup("/groups/" + adminGroup.getId().get(), adminGroup);
+  }
+
+  private void testGetGroup(String url, AccountGroup expectedGroup) throws IOException {
+    RestResponse r = session.get(url);
+    GroupInfo group = (new Gson()).fromJson(r.getReader(), new TypeToken<GroupInfo>() {}.getType());
+    assertGroupInfo(expectedGroup, group);
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/GroupAssert.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/GroupAssert.java
new file mode 100644
index 0000000..792d8c6
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/GroupAssert.java
@@ -0,0 +1,53 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.group;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.google.gerrit.extensions.restapi.Url;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+
+import java.util.Set;
+
+public class GroupAssert {
+
+  public static void assertGroups(Iterable<String> expected, Set<String> actual) {
+    for (String g : expected) {
+      assertTrue("missing group " + g, actual.remove(g));
+    }
+    assertTrue("unexpected groups: " + actual, actual.isEmpty());
+  }
+
+  public static void assertGroupInfo(AccountGroup group, GroupInfo info) {
+    if (info.name != null) {
+      // 'name' is not set if returned in a map
+      assertEquals(group.getName(), info.name);
+    }
+    assertEquals(group.getGroupUUID().get(), Url.decode(info.id));
+    assertEquals(Integer.valueOf(group.getId().get()), info.group_id);
+    assertEquals("#/admin/groups/uuid-" + Url.encode(group.getGroupUUID().get()), info.url);
+    assertEquals(group.isVisibleToAll(), toBoolean(info.options.visible_to_all));
+    assertEquals(group.getDescription(), info.description);
+    assertEquals(group.getOwnerGroupUUID().get(), Url.decode(info.owner_id));
+  }
+
+  public static boolean toBoolean(Boolean b) {
+    if (b == null) {
+      return false;
+    }
+    return b.booleanValue();
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/GroupInfo.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/GroupInfo.java
new file mode 100644
index 0000000..c48f968
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/GroupInfo.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.group;
+
+public class GroupInfo {
+  public String id;
+  public String name;
+  public String url;
+  public GroupOptionsInfo options;
+  public String description;
+  public Integer group_id;
+  public String owner_id;
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/GroupInput.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/GroupInput.java
new file mode 100644
index 0000000..86864839
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/GroupInput.java
@@ -0,0 +1,21 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.group;
+
+public class GroupInput {
+  String description;
+  Boolean visible_to_all;
+  String owner_id;
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/GroupOptionsInfo.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/GroupOptionsInfo.java
new file mode 100644
index 0000000..4457fb6
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/GroupOptionsInfo.java
@@ -0,0 +1,19 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.group;
+
+public class GroupOptionsInfo {
+  public Boolean visible_to_all;
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/GroupPropertiesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/GroupPropertiesIT.java
new file mode 100644
index 0000000..ab22367
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/GroupPropertiesIT.java
@@ -0,0 +1,217 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.group;
+
+import static com.google.gerrit.acceptance.rest.group.GroupAssert.toBoolean;
+import static com.google.gerrit.acceptance.rest.group.GroupAssert.assertGroupInfo;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNotNull;
+import static org.junit.Assert.assertNull;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.AccountCreator;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.RestSession;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.account.GroupCache;
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import com.google.inject.Inject;
+
+import org.apache.http.HttpStatus;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+
+public class GroupPropertiesIT extends AbstractDaemonTest {
+
+  @Inject
+  private AccountCreator accounts;
+
+  @Inject
+  private GroupCache groupCache;
+
+  private TestAccount admin;
+  private RestSession session;
+
+  @Before
+  public void setUp() throws Exception {
+    admin = accounts.create("admin", "admin@example.com", "Administrator",
+            "Administrators");
+    session = new RestSession(admin);
+  }
+
+  @Test
+  public void testGroupName() throws IOException {
+    AccountGroup.NameKey adminGroupName = new AccountGroup.NameKey("Administrators");
+    String url = "/groups/" + groupCache.get(adminGroupName).getGroupUUID().get() + "/name";
+
+    // get name
+    RestResponse r = session.get(url);
+    String name = (new Gson()).fromJson(r.getReader(), new TypeToken<String>() {}.getType());
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    assertEquals("Administrators", name);
+    r.consume();
+
+    // set name with name conflict
+    GroupNameInput in = new GroupNameInput();
+    in.name = "Registered Users";
+    r = session.put(url, in);
+    assertEquals(HttpStatus.SC_CONFLICT, r.getStatusCode());
+    r.consume();
+
+    // set name to same name
+    in = new GroupNameInput();
+    in.name = "Administrators";
+    r = session.put(url, in);
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    r.consume();
+
+    // rename
+    in = new GroupNameInput();
+    in.name = "Admins";
+    r = session.put(url, in);
+    String newName = (new Gson()).fromJson(r.getReader(), new TypeToken<String>() {}.getType());
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    assertNotNull(groupCache.get(new AccountGroup.NameKey(in.name)));
+    assertNull(groupCache.get(adminGroupName));
+    assertEquals(in.name, newName);
+    r.consume();
+  }
+
+  @Test
+  public void testGroupDescription() throws IOException {
+    AccountGroup.NameKey adminGroupName = new AccountGroup.NameKey("Administrators");
+    AccountGroup adminGroup = groupCache.get(adminGroupName);
+    String url = "/groups/" + adminGroup.getGroupUUID().get() + "/description";
+
+    // get description
+    RestResponse r = session.get(url);
+    String description = (new Gson()).fromJson(r.getReader(), new TypeToken<String>() {}.getType());
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    assertEquals(adminGroup.getDescription(), description);
+    r.consume();
+
+    // set description
+    GroupDescriptionInput in = new GroupDescriptionInput();
+    in.description = "All users that can administrate the Gerrit Server.";
+    r = session.put(url, in);
+    String newDescription = (new Gson()).fromJson(r.getReader(), new TypeToken<String>() {}.getType());
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    assertEquals(in.description, newDescription);
+    adminGroup = groupCache.get(adminGroupName);
+    assertEquals(in.description, adminGroup.getDescription());
+    r.consume();
+
+    // delete description
+    r = session.delete(url);
+    assertEquals(HttpStatus.SC_NO_CONTENT, r.getStatusCode());
+    adminGroup = groupCache.get(adminGroupName);
+    assertNull(adminGroup.getDescription());
+
+    // set description to empty string
+    in = new GroupDescriptionInput();
+    in.description = "";
+    r = session.put(url, in);
+    assertEquals(HttpStatus.SC_NO_CONTENT, r.getStatusCode());
+    adminGroup = groupCache.get(adminGroupName);
+    assertNull(adminGroup.getDescription());
+  }
+
+  @Test
+  public void testGroupOptions() throws IOException {
+    AccountGroup.NameKey adminGroupName = new AccountGroup.NameKey("Administrators");
+    AccountGroup adminGroup = groupCache.get(adminGroupName);
+    String url = "/groups/" + adminGroup.getGroupUUID().get() + "/options";
+
+    // get options
+    RestResponse r = session.get(url);
+    GroupOptionsInfo options = (new Gson()).fromJson(r.getReader(), new TypeToken<GroupOptionsInfo>() {}.getType());
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    assertEquals(adminGroup.isVisibleToAll(), toBoolean(options.visible_to_all));
+    r.consume();
+
+    // set options
+    GroupOptionsInput in = new GroupOptionsInput();
+    in.visible_to_all = !adminGroup.isVisibleToAll();
+    r = session.put(url, in);
+    GroupOptionsInfo newOptions = (new Gson()).fromJson(r.getReader(), new TypeToken<GroupOptionsInfo>() {}.getType());
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    assertEquals(in.visible_to_all, toBoolean(newOptions.visible_to_all));
+    adminGroup = groupCache.get(adminGroupName);
+    assertEquals(in.visible_to_all, adminGroup.isVisibleToAll());
+    r.consume();
+  }
+
+  @Test
+  public void testGroupOwner() throws IOException {
+    AccountGroup.NameKey adminGroupName = new AccountGroup.NameKey("Administrators");
+    AccountGroup adminGroup = groupCache.get(adminGroupName);
+    String url = "/groups/" + adminGroup.getGroupUUID().get() + "/owner";
+
+    // get owner
+    RestResponse r = session.get(url);
+    GroupInfo options = (new Gson()).fromJson(r.getReader(), new TypeToken<GroupInfo>() {}.getType());
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    assertGroupInfo(groupCache.get(adminGroup.getOwnerGroupUUID()), options);
+    r.consume();
+
+    // set owner by name
+    GroupOwnerInput in = new GroupOwnerInput();
+    in.owner = "Registered Users";
+    r = session.put(url, in);
+    GroupInfo newOwner = (new Gson()).fromJson(r.getReader(), new TypeToken<GroupInfo>() {}.getType());
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    assertEquals(in.owner, newOwner.name);
+    adminGroup = groupCache.get(adminGroupName);
+    assertGroupInfo(groupCache.get(adminGroup.getOwnerGroupUUID()), newOwner);
+    r.consume();
+
+    // set owner by UUID
+    in = new GroupOwnerInput();
+    in.owner = adminGroup.getGroupUUID().get();
+    r = session.put(url, in);
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    adminGroup = groupCache.get(adminGroupName);
+    assertEquals(in.owner, groupCache.get(adminGroup.getOwnerGroupUUID()).getGroupUUID().get());
+    r.consume();
+
+    // set non existing owner
+    in = new GroupOwnerInput();
+    in.owner = "Non-Existing Group";
+    r = session.put(url, in);
+    assertEquals(HttpStatus.SC_UNPROCESSABLE_ENTITY, r.getStatusCode());
+    r.consume();
+  }
+
+  private static class GroupNameInput {
+    String name;
+  }
+
+  private static class GroupDescriptionInput {
+    String description;
+  }
+
+  private static class GroupOptionsInput {
+    Boolean visible_to_all;
+  }
+
+  private static class GroupOwnerInput {
+    String owner;
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/GroupsInput.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/GroupsInput.java
new file mode 100644
index 0000000..1eceda9
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/GroupsInput.java
@@ -0,0 +1,21 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.group;
+
+import java.util.List;
+
+public class GroupsInput {
+  List<String> groups;
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/ListGroupIncludesIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/ListGroupIncludesIT.java
new file mode 100644
index 0000000..997f476
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/ListGroupIncludesIT.java
@@ -0,0 +1,124 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.group;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Collections2;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.AccountCreator;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.RestSession;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import com.google.inject.Inject;
+
+import org.apache.http.HttpStatus;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+public class ListGroupIncludesIT extends AbstractDaemonTest {
+
+  @Inject
+  private AccountCreator accounts;
+
+  private RestSession session;
+
+  @Before
+  public void setUp() throws Exception {
+    TestAccount admin = accounts.create("admin", "Administrators");
+    session = new RestSession(admin);
+  }
+
+  @Test
+  public void listNonExistingGroupIncludes_NotFound() throws Exception {
+    assertEquals(HttpStatus.SC_NOT_FOUND,
+      session.get("/groups/non-existing/groups/").getStatusCode());
+  }
+
+  @Test
+  public void listEmptyGroupIncludes() throws Exception {
+    assertTrue(GET("/groups/Administrators/groups/").isEmpty());
+  }
+
+  @Test
+  public void listNonEmptyGroupIncludes() throws Exception {
+    group("gx", "Administrators");
+    group("gy", "Administrators");
+    PUT("/groups/Administrators/groups/gx");
+    PUT("/groups/Administrators/groups/gy");
+
+    assertIncludes(GET("/groups/Administrators/groups/"), "gx", "gy");
+  }
+
+  @Test
+  public void listOneIncludeMember() throws Exception {
+    group("gx", "Administrators");
+    group("gy", "Administrators");
+    PUT("/groups/Administrators/groups/gx");
+    PUT("/groups/Administrators/groups/gy");
+
+    assertEquals(GET_ONE("/groups/Administrators/groups/gx").name, "gx");
+  }
+
+  private List<GroupInfo> GET(String endpoint) throws IOException {
+    RestResponse r = session.get(endpoint);
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    return (new Gson()).fromJson(r.getReader(),
+        new TypeToken<List<GroupInfo>>() {}.getType());
+  }
+
+  private GroupInfo GET_ONE(String endpoint) throws IOException {
+    RestResponse r = session.get(endpoint);
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    return (new Gson()).fromJson(r.getReader(),
+        new TypeToken<GroupInfo>() {}.getType());
+  }
+
+  private void PUT(String endpoint) throws IOException {
+    session.put(endpoint).consume();
+  }
+
+  private void group(String name, String ownerGroup) throws IOException {
+    GroupInput in = new GroupInput();
+    in.owner_id = ownerGroup;
+    session.put("/groups/" + name, in).consume();
+  }
+
+  private void assertIncludes(List<GroupInfo> includes, String name,
+      String... names) {
+    Collection<String> includeNames = Collections2.transform(includes,
+        new Function<GroupInfo, String>() {
+          @Override
+          public String apply(@Nullable GroupInfo info) {
+            return info.name;
+          }
+        });
+    assertTrue(includeNames.contains(name));
+    for (String n : names) {
+      assertTrue(includeNames.contains(n));
+    }
+    assertEquals(includes.size(), names.length + 1);
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/ListGroupMembersIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/ListGroupMembersIT.java
new file mode 100644
index 0000000..a5aa3dd
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/ListGroupMembersIT.java
@@ -0,0 +1,136 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.group;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Collections2;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.AccountCreator;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.RestSession;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.acceptance.rest.account.AccountInfo;
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import com.google.inject.Inject;
+
+import org.apache.http.HttpStatus;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.Collection;
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+public class ListGroupMembersIT extends AbstractDaemonTest {
+
+  @Inject
+  private AccountCreator accounts;
+
+  private RestSession session;
+
+  @Before
+  public void setUp() throws Exception {
+    TestAccount admin = accounts.create("admin", "Administrators");
+    session = new RestSession(admin);
+  }
+
+  @Test
+  public void listNonExistingGroupMembers_NotFound() throws Exception {
+    assertEquals(HttpStatus.SC_NOT_FOUND,
+        session.get("/groups/non-existing/members/").getStatusCode());
+  }
+
+  @Test
+  public void listEmptyGroupMembers() throws Exception {
+    group("empty", "Administrators");
+    assertTrue(GET("/groups/empty/members/").isEmpty());
+  }
+
+  @Test
+  public void listNonEmptyGroupMembers() throws Exception {
+    assertMembers(GET("/groups/Administrators/members/"), "admin");
+
+    accounts.create("admin2", "Administrators");
+    assertMembers(GET("/groups/Administrators/members/"), "admin", "admin2");
+  }
+
+  @Test
+  public void listOneGroupMember() throws IOException {
+    assertEquals(GET_ONE("/groups/Administrators/members/admin").name,
+        "admin");
+  }
+
+  @Test
+  public void listGroupMembersRecursively() throws Exception {
+    group("gx", "Administrators");
+    accounts.create("ux", "gx");
+
+    group("gy", "Administrators");
+    accounts.create("uy", "gy");
+
+    PUT("/groups/Administrators/groups/gx");
+    PUT("/groups/gx/groups/gy");
+    assertMembers(GET("/groups/Administrators/members/?recursive"),
+        "admin", "ux", "uy");
+  }
+
+  private List<AccountInfo> GET(String endpoint) throws IOException {
+    RestResponse r = session.get(endpoint);
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    return (new Gson()).fromJson(r.getReader(),
+        new TypeToken<List<AccountInfo>>() {}.getType());
+  }
+
+  private AccountInfo GET_ONE(String endpoint) throws IOException {
+    RestResponse r = session.get(endpoint);
+    assertEquals(HttpStatus.SC_OK, r.getStatusCode());
+    return (new Gson()).fromJson(r.getReader(),
+        new TypeToken<AccountInfo>() {}.getType());
+  }
+
+  private void PUT(String endpoint) throws IOException {
+    session.put(endpoint).consume();
+  }
+
+  private void group(String name, String ownerGroup)
+      throws IOException {
+    GroupInput in = new GroupInput();
+    in.owner_id = ownerGroup;
+    session.put("/groups/" + name, in).consume();
+  }
+
+  private void assertMembers(List<AccountInfo> members, String name,
+      String... names) {
+    Collection<String> memberNames = Collections2.transform(members,
+        new Function<AccountInfo, String>() {
+          @Override
+          public String apply(@Nullable AccountInfo info) {
+            return info.name;
+          }
+        });
+
+    assertTrue(memberNames.contains(name));
+    for (String n : names) {
+      assertTrue(memberNames.contains(n));
+    }
+    assertEquals(members.size(), names.length + 1);
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/ListGroupsIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/ListGroupsIT.java
new file mode 100644
index 0000000..07c7c93
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/ListGroupsIT.java
@@ -0,0 +1,103 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.group;
+
+import static com.google.gerrit.acceptance.rest.group.GroupAssert.assertGroupInfo;
+import static com.google.gerrit.acceptance.rest.group.GroupAssert.assertGroups;
+
+import com.google.common.base.Function;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Sets;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.AccountCreator;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.RestSession;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.account.GroupCache;
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+
+import com.jcraft.jsch.JSchException;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+public class ListGroupsIT extends AbstractDaemonTest {
+
+  @Inject
+  private AccountCreator accounts;
+
+  @Inject
+  private GroupCache groupCache;
+
+  private TestAccount admin;
+  private RestSession session;
+
+  @Before
+  public void setUp() throws Exception {
+    admin = accounts.create("admin", "admin@example.com", "Administrator",
+            "Administrators");
+    session = new RestSession(admin);
+  }
+
+  @Test
+  public void testListAllGroups() throws IOException, OrmException {
+    Iterable<String> expectedGroups = Iterables.transform(groupCache.all(),
+        new Function<AccountGroup, String>() {
+          @Override
+          @Nullable
+          public String apply(@Nullable AccountGroup group) {
+            return group.getName();
+          }
+        });
+    RestResponse r = session.get("/groups/");
+    Map<String, GroupInfo> result =
+        (new Gson()).fromJson(r.getReader(), new TypeToken<Map<String, GroupInfo>>() {}.getType());
+    assertGroups(expectedGroups, result.keySet());
+  }
+
+  @Test
+  public void testOnlyVisibleGroupsReturned() throws OrmException,
+      JSchException, IOException {
+    Set<String> expectedGroups = Sets.newHashSet();
+    expectedGroups.add("Anonymous Users");
+    expectedGroups.add("Registered Users");
+    TestAccount user = accounts.create("user", "user@example.com", "User");
+    RestResponse r = (new RestSession(user)).get("/groups/");
+    Map<String, GroupInfo> result =
+        (new Gson()).fromJson(r.getReader(), new TypeToken<Map<String, GroupInfo>>() {}.getType());
+    assertGroups(expectedGroups, result.keySet());
+  }
+
+  @Test
+  public void testAllGroupInfoFieldsSetCorrectly() throws IOException,
+      OrmException {
+    AccountGroup adminGroup = groupCache.get(new AccountGroup.NameKey("Administrators"));
+    RestResponse r = session.get("/groups/?q=" + adminGroup.getName());
+    Map<String, GroupInfo> result =
+        (new Gson()).fromJson(r.getReader(), new TypeToken<Map<String, GroupInfo>>() {}.getType());
+    GroupInfo adminGroupInfo = result.get(adminGroup.getName());
+    assertGroupInfo(adminGroup, adminGroupInfo);
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/MembersInput.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/MembersInput.java
new file mode 100644
index 0000000..11779f4
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/group/MembersInput.java
@@ -0,0 +1,21 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.group;
+
+import java.util.List;
+
+public class MembersInput {
+  List<String> members;
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
new file mode 100644
index 0000000..6afe8e6
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/CreateProjectIT.java
@@ -0,0 +1,274 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.project;
+
+import static com.google.gerrit.acceptance.rest.project.ProjectAssert.assertProjectInfo;
+import static com.google.gerrit.acceptance.rest.project.ProjectAssert.assertProjectOwners;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertNotNull;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.AccountCreator;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.RestSession;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.Project.InheritableBoolean;
+import com.google.gerrit.reviewdb.client.Project.SubmitType;
+import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.gson.Gson;
+import com.google.gson.reflect.TypeToken;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+
+import com.jcraft.jsch.JSchException;
+
+import org.apache.http.HttpStatus;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Constants;
+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.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+public class CreateProjectIT extends AbstractDaemonTest {
+
+  @Inject
+  private AccountCreator accounts;
+
+  @Inject
+  private ProjectCache projectCache;
+
+  @Inject
+  private GroupCache groupCache;
+
+  @Inject
+  private GitRepositoryManager git;
+
+  private TestAccount admin;
+  private RestSession session;
+
+  @Before
+  public void setUp() throws Exception {
+    admin = accounts.create("admin", "admin@example.com", "Administrator",
+            "Administrators");
+    session = new RestSession(admin);
+  }
+
+  @Test
+  public void testCreateProject() throws IOException {
+    final String newProjectName = "newProject";
+    RestResponse r = session.put("/projects/" + newProjectName);
+    assertEquals(HttpStatus.SC_CREATED, r.getStatusCode());
+    ProjectInfo p = (new Gson()).fromJson(r.getReader(), new TypeToken<ProjectInfo>() {}.getType());
+    assertEquals(newProjectName, p.name);
+    ProjectState projectState = projectCache.get(new Project.NameKey(newProjectName));
+    assertNotNull(projectState);
+    assertProjectInfo(projectState.getProject(), p);
+    assertHead(newProjectName, "refs/heads/master");
+  }
+
+  @Test
+  public void testCreateProjectWithNameMismatch_BadRequest() throws IOException {
+    ProjectInput in = new ProjectInput();
+    in.name = "otherName";
+    RestResponse r = session.put("/projects/someName", in);
+    assertEquals(HttpStatus.SC_BAD_REQUEST, r.getStatusCode());
+  }
+
+  @Test
+  public void testCreateProjectWithProperties() throws IOException {
+    final String newProjectName = "newProject";
+    ProjectInput in = new ProjectInput();
+    in.description = "Test description";
+    in.submit_type = SubmitType.CHERRY_PICK;
+    in.use_contributor_agreements = InheritableBoolean.TRUE;
+    in.use_signed_off_by = InheritableBoolean.TRUE;
+    in.use_content_merge = InheritableBoolean.TRUE;
+    in.require_change_id = InheritableBoolean.TRUE;
+    RestResponse r = session.put("/projects/" + newProjectName, in);
+    ProjectInfo p = (new Gson()).fromJson(r.getReader(), new TypeToken<ProjectInfo>() {}.getType());
+    assertEquals(newProjectName, p.name);
+    Project project = projectCache.get(new Project.NameKey(newProjectName)).getProject();
+    assertProjectInfo(project, p);
+    assertEquals(in.description, project.getDescription());
+    assertEquals(in.submit_type, project.getSubmitType());
+    assertEquals(in.use_contributor_agreements, project.getUseContributorAgreements());
+    assertEquals(in.use_signed_off_by, project.getUseSignedOffBy());
+    assertEquals(in.use_content_merge, project.getUseContentMerge());
+    assertEquals(in.require_change_id, project.getRequireChangeID());
+  }
+
+  @Test
+  public void testCreateChildProject() throws IOException {
+    final String parentName = "parent";
+    RestResponse r = session.put("/projects/" + parentName);
+    r.consume();
+    final String childName = "child";
+    ProjectInput in = new ProjectInput();
+    in.parent = parentName;
+    r = session.put("/projects/" + childName, in);
+    Project project = projectCache.get(new Project.NameKey(childName)).getProject();
+    assertEquals(in.parent, project.getParentName());
+  }
+
+  public void testCreateChildProjectUnderNonExistingParent_UnprocessableEntity()
+      throws IOException {
+    ProjectInput in = new ProjectInput();
+    in.parent = "non-existing-project";
+    RestResponse r = session.put("/projects/child", in);
+    assertEquals(HttpStatus.SC_UNPROCESSABLE_ENTITY, r.getStatusCode());
+  }
+
+  @Test
+  public void testCreateProjectWithOwner() throws IOException {
+    final String newProjectName = "newProject";
+    ProjectInput in = new ProjectInput();
+    in.owners = Lists.newArrayListWithCapacity(3);
+    in.owners.add("Administrators"); // by name
+    in.owners.add(groupUuid("Registered Users").get()); // by group UUID
+    in.owners.add(Integer.toString(groupCache.get(new AccountGroup.NameKey("Anonymous Users"))
+        .getId().get())); // by legacy group ID
+    session.put("/projects/" + newProjectName, in);
+    ProjectState projectState = projectCache.get(new Project.NameKey(newProjectName));
+    Set<AccountGroup.UUID> expectedOwnerIds = Sets.newHashSetWithExpectedSize(3);
+    expectedOwnerIds.add(groupUuid("Administrators"));
+    expectedOwnerIds.add(groupUuid("Registered Users"));
+    expectedOwnerIds.add(groupUuid("Anonymous Users"));
+    assertProjectOwners(expectedOwnerIds, projectState);
+  }
+
+  public void testCreateProjectWithNonExistingOwner_UnprocessableEntity()
+      throws IOException {
+    ProjectInput in = new ProjectInput();
+    in.owners = Collections.singletonList("non-existing-group");
+    RestResponse r = session.put("/projects/newProject", in);
+    assertEquals(HttpStatus.SC_UNPROCESSABLE_ENTITY, r.getStatusCode());
+  }
+
+  @Test
+  public void testCreatePermissionOnlyProject() throws IOException {
+    final String newProjectName = "newProject";
+    ProjectInput in = new ProjectInput();
+    in.permissions_only = true;
+    session.put("/projects/" + newProjectName, in);
+    assertHead(newProjectName, GitRepositoryManager.REF_CONFIG);
+  }
+
+  @Test
+  public void testCreateProjectWithEmptyCommit() throws IOException {
+    final String newProjectName = "newProject";
+    ProjectInput in = new ProjectInput();
+    in.create_empty_commit = true;
+    session.put("/projects/" + newProjectName, in);
+    assertEmptyCommit(newProjectName, "refs/heads/master");
+  }
+
+  @Test
+  public void testCreateProjectWithBranches() throws IOException {
+    final String newProjectName = "newProject";
+    ProjectInput in = new ProjectInput();
+    in.create_empty_commit = true;
+    in.branches = Lists.newArrayListWithCapacity(3);
+    in.branches.add("refs/heads/test");
+    in.branches.add("refs/heads/master");
+    in.branches.add("release"); // without 'refs/heads' prefix
+    session.put("/projects/" + newProjectName, in);
+    assertHead(newProjectName, "refs/heads/test");
+    assertEmptyCommit(newProjectName, "refs/heads/test", "refs/heads/master",
+        "refs/heads/release");
+  }
+
+  @Test
+  public void testCreateProjectWithoutCapability_Forbidden() throws OrmException,
+      JSchException, IOException {
+    TestAccount user = accounts.create("user", "user@example.com", "User");
+    RestResponse r = (new RestSession(user)).put("/projects/newProject");
+    assertEquals(HttpStatus.SC_FORBIDDEN, r.getStatusCode());
+  }
+
+  @Test
+  public void testCreateProjectWhenProjectAlreadyExists_Conflict()
+      throws OrmException, JSchException, IOException {
+    RestResponse r = session.put("/projects/All-Projects");
+    assertEquals(HttpStatus.SC_CONFLICT, r.getStatusCode());
+  }
+
+  private AccountGroup.UUID groupUuid(String groupName) {
+    return groupCache.get(new AccountGroup.NameKey(groupName)).getGroupUUID();
+  }
+
+  private void assertHead(String projectName, String expectedRef)
+      throws RepositoryNotFoundException, IOException {
+    Repository repo = git.openRepository(new Project.NameKey(projectName));
+    try {
+      assertEquals(expectedRef, repo.getRef(Constants.HEAD).getTarget()
+          .getName());
+    } finally {
+      repo.close();
+    }
+  }
+
+  private void assertEmptyCommit(String projectName, String... refs)
+      throws RepositoryNotFoundException, IOException {
+    Repository repo = git.openRepository(new Project.NameKey(projectName));
+    RevWalk rw = new RevWalk(repo);
+    TreeWalk tw = new TreeWalk(repo);
+    try {
+      for (String ref : refs) {
+        RevCommit commit = rw.lookupCommit(repo.getRef(ref).getObjectId());
+        rw.parseBody(commit);
+        tw.addTree(commit.getTree());
+        assertFalse("ref " + ref + " has non empty commit", tw.next());
+        tw.reset();
+      }
+    } finally {
+      tw.release();
+      rw.release();
+      repo.close();
+    }
+  }
+
+  @SuppressWarnings("unused")
+  private static class ProjectInput {
+    String name;
+    String parent;
+    String description;
+    boolean permissions_only;
+    boolean create_empty_commit;
+    SubmitType submit_type;
+    List<String> branches;
+    List<String> owners;
+    InheritableBoolean use_contributor_agreements;
+    InheritableBoolean use_signed_off_by;
+    InheritableBoolean use_content_merge;
+    InheritableBoolean require_change_id;
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GarbageCollectionIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GarbageCollectionIT.java
new file mode 100644
index 0000000..e55c52a
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/GarbageCollectionIT.java
@@ -0,0 +1,102 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.project;
+
+import static com.google.gerrit.acceptance.git.GitUtil.createProject;
+import static org.junit.Assert.assertEquals;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.AccountCreator;
+import com.google.gerrit.acceptance.GcAssert;
+import com.google.gerrit.acceptance.RestResponse;
+import com.google.gerrit.acceptance.RestSession;
+import com.google.gerrit.acceptance.SshSession;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.git.GarbageCollectionQueue;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+
+import com.jcraft.jsch.JSchException;
+
+import org.apache.http.HttpStatus;
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+
+public class GarbageCollectionIT extends AbstractDaemonTest {
+
+  @Inject
+  private AccountCreator accounts;
+
+  @Inject
+  private AllProjectsName allProjects;
+
+  @Inject
+  private GarbageCollectionQueue gcQueue;
+
+  @Inject
+  private GcAssert gcAssert;
+
+  private TestAccount admin;
+  private RestSession session;
+  private Project.NameKey project1;
+  private Project.NameKey project2;
+
+  @Before
+  public void setUp() throws Exception {
+    admin =
+        accounts.create("admin", "admin@example.com", "Administrator",
+            "Administrators");
+
+    SshSession sshSession = new SshSession(admin);
+
+    project1 = new Project.NameKey("p1");
+    createProject(sshSession, project1.get());
+
+    project2 = new Project.NameKey("p2");
+    createProject(sshSession, project2.get());
+
+    session = new RestSession(admin);
+  }
+
+  @Test
+  public void testGcNonExistingProject_NotFound() throws IOException {
+    assertEquals(HttpStatus.SC_NOT_FOUND, POST("/projects/non-existing/gc"));
+  }
+
+  @Test
+  public void testGcNotAllowed_Forbidden() throws IOException, OrmException, JSchException {
+    assertEquals(HttpStatus.SC_FORBIDDEN,
+        new RestSession(accounts.create("user", "user@example.com", "User"))
+            .post("/projects/" + allProjects.get() + "/gc").getStatusCode());
+  }
+
+  @Test
+  public void testGcOneProject() throws JSchException, IOException {
+
+    assertEquals(HttpStatus.SC_OK, POST("/projects/" + allProjects.get() + "/gc"));
+    gcAssert.assertHasPackFile(allProjects);
+    gcAssert.assertHasNoPackFile(project1, project2);
+  }
+
+  private int POST(String endPoint) throws IOException {
+    RestResponse r = session.post(endPoint);
+    r.consume();
+    return r.getStatusCode();
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ProjectAssert.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ProjectAssert.java
new file mode 100644
index 0000000..25ccbee
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ProjectAssert.java
@@ -0,0 +1,49 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.project;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertTrue;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.extensions.restapi.Url;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.project.ProjectState;
+
+import java.util.Set;
+
+public class ProjectAssert {
+
+  public static void assertProjectInfo(Project project, ProjectInfo info) {
+    if (info.name != null) {
+      // 'name' is not set if returned in a map
+      assertEquals(project.getName(), info.name);
+    }
+    assertEquals(project.getName(), Url.decode(info.id));
+    Project.NameKey parentName = project.getParent(new Project.NameKey("All-Projects"));
+    assertEquals(parentName != null ? parentName.get() : null, info.parent);
+    assertEquals(project.getDescription(), Strings.nullToEmpty(info.description));
+  }
+
+  public static void assertProjectOwners(Set<AccountGroup.UUID> expectedOwners,
+      ProjectState state) {
+    for (AccountGroup.UUID g : state.getOwners()) {
+      assertTrue("unexpected owner group " + g, expectedOwners.remove(g));
+    }
+    assertTrue("missing owner groups: " + expectedOwners,
+        expectedOwners.isEmpty());
+  }
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ProjectInfo.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ProjectInfo.java
new file mode 100644
index 0000000..72dd2d6
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/rest/project/ProjectInfo.java
@@ -0,0 +1,22 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.rest.project;
+
+public class ProjectInfo {
+  public String id;
+  public String name;
+  public String parent;
+  public String description;
+}
diff --git a/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/GarbageCollectionIT.java b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/GarbageCollectionIT.java
new file mode 100644
index 0000000..9c1e7d0
--- /dev/null
+++ b/gerrit-acceptance-tests/src/test/java/com/google/gerrit/acceptance/ssh/GarbageCollectionIT.java
@@ -0,0 +1,131 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.acceptance.ssh;
+
+import static com.google.gerrit.acceptance.git.GitUtil.createProject;
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
+
+import com.google.gerrit.acceptance.AbstractDaemonTest;
+import com.google.gerrit.acceptance.AccountCreator;
+import com.google.gerrit.acceptance.GcAssert;
+import com.google.gerrit.acceptance.SshSession;
+import com.google.gerrit.acceptance.TestAccount;
+import com.google.gerrit.common.data.GarbageCollectionResult;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.git.GarbageCollection;
+import com.google.gerrit.server.git.GarbageCollectionQueue;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+
+import com.jcraft.jsch.JSchException;
+
+import org.junit.Before;
+import org.junit.Test;
+
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Locale;
+
+public class GarbageCollectionIT extends AbstractDaemonTest {
+
+  @Inject
+  private AccountCreator accounts;
+
+  @Inject
+  private AllProjectsName allProjects;
+
+  @Inject
+  private GarbageCollection.Factory garbageCollectionFactory;
+
+  @Inject
+  private GarbageCollectionQueue gcQueue;
+
+  @Inject
+  private GcAssert gcAssert;
+
+  private TestAccount admin;
+  private SshSession sshSession;
+  private Project.NameKey project1;
+  private Project.NameKey project2;
+  private Project.NameKey project3;
+
+  @Before
+  public void setUp() throws Exception {
+    admin =
+        accounts.create("admin", "admin@example.com", "Administrator",
+            "Administrators");
+
+    sshSession = new SshSession(admin);
+
+    project1 = new Project.NameKey("p1");
+    createProject(sshSession, project1.get());
+
+    project2 = new Project.NameKey("p2");
+    createProject(sshSession, project2.get());
+
+    project3 = new Project.NameKey("p3");
+    createProject(sshSession, project3.get());
+  }
+
+  @Test
+  public void testGc() throws JSchException, IOException {
+    String response =
+        sshSession.exec("gerrit gc \"" + project1.get() + "\" \""
+            + project2.get() + "\"");
+    assertFalse(sshSession.hasError());
+    assertNoError(response);
+    gcAssert.assertHasPackFile(project1, project2);
+    gcAssert.assertHasNoPackFile(allProjects, project3);
+  }
+
+  @Test
+  public void testGcAll() throws JSchException, IOException {
+    String response = sshSession.exec("gerrit gc --all");
+    assertFalse(sshSession.hasError());
+    assertNoError(response);
+    gcAssert.assertHasPackFile(allProjects, project1, project2, project3);
+  }
+
+  @Test
+  public void testGcWithoutCapability_Error() throws IOException, OrmException,
+      JSchException {
+    SshSession s = new SshSession(accounts.create("user", "user@example.com", "User"));
+    s.exec("gerrit gc --all");
+    assertError("fatal: user does not have \"runGC\" capability.", s.getError());
+  }
+
+  @Test
+  public void testGcAlreadyScheduled() {
+    gcQueue.addAll(Arrays.asList(project1));
+    GarbageCollectionResult result = garbageCollectionFactory.create().run(
+        Arrays.asList(allProjects, project1, project2, project3));
+    assertTrue(result.hasErrors());
+    assertEquals(1, result.getErrors().size());
+    GarbageCollectionResult.Error error = result.getErrors().get(0);
+    assertEquals(GarbageCollectionResult.Error.Type.GC_ALREADY_SCHEDULED, error.getType());
+    assertEquals(project1, error.getProjectName());
+  }
+
+  private void assertError(String expectedError, String response) {
+    assertTrue(response, response.contains(expectedError));
+  }
+
+  private void assertNoError(String response) {
+    assertFalse(response, response.toLowerCase(Locale.US).contains("error"));
+  }
+}
diff --git a/gerrit-antlr/pom.xml b/gerrit-antlr/pom.xml
index 34cb46f..314d8d2 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.5-SNAPSHOT</version>
+    <version>2.6-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-antlr</artifactId>
diff --git a/gerrit-antlr/src/main/antlr3/com/google/gerrit/server/query/Query.g b/gerrit-antlr/src/main/antlr3/com/google/gerrit/server/query/Query.g
index 74b7851..423acb4 100644
--- a/gerrit-antlr/src/main/antlr3/com/google/gerrit/server/query/Query.g
+++ b/gerrit-antlr/src/main/antlr3/com/google/gerrit/server/query/Query.g
@@ -155,6 +155,10 @@
       String s = $text;
       setText(s.substring(1, s.length() - 1));
     }
+  | '{' ( ~('{'|'}') )* '}' {
+      String s = $text;
+      setText(s.substring(1, s.length() - 1));
+    }
   ;
 
 SINGLE_WORD
diff --git a/gerrit-cache-h2/pom.xml b/gerrit-cache-h2/pom.xml
index 4d4303c..1a26b21 100644
--- a/gerrit-cache-h2/pom.xml
+++ b/gerrit-cache-h2/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.5-SNAPSHOT</version>
+    <version>2.6-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-cache-h2</artifactId>
diff --git a/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java b/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
index 27da20f..85a051b 100644
--- a/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
+++ b/gerrit-cache-h2/src/main/java/com/google/gerrit/server/cache/h2/H2CacheFactory.java
@@ -146,7 +146,7 @@
     }
   }
 
-  @SuppressWarnings({"unchecked", "rawtypes", "cast"})
+  @SuppressWarnings({"unchecked", "cast"})
   @Override
   public <K, V> Cache<K, V> build(CacheBinding<K, V> def) {
     Preconditions.checkState(!started, "cache must be built before start");
diff --git a/gerrit-common/pom.xml b/gerrit-common/pom.xml
index 9b3fe5f..1db3549 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.5-SNAPSHOT</version>
+    <version>2.6-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-common</artifactId>
@@ -61,6 +61,11 @@
       <artifactId>gerrit-patch-jgit</artifactId>
       <version>${project.version}</version>
     </dependency>
+
+    <dependency>
+      <groupId>com.google.code.findbugs</groupId>
+      <artifactId>jsr305</artifactId>
+    </dependency>
   </dependencies>
 
   <build>
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 2b9b72a..beab1d9 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
@@ -15,6 +15,7 @@
 package com.google.gerrit.common;
 
 import com.google.gerrit.common.data.ChangeInfo;
+import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Change.Status;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -37,6 +38,8 @@
   public static final String TOP = "n,z";
 
   public static final String MINE = "/";
+  public static final String PROJECTS = "/projects/";
+  public static final String DASHBOARDS = ",dashboards/";
   public static final String ADMIN_GROUPS = "/admin/groups/";
   public static final String ADMIN_CREATE_GROUP = "/admin/create-group/";
   public static final String ADMIN_PROJECTS = "/admin/projects/";
@@ -55,18 +58,26 @@
     return "/c/" + ps.getParentKey() + "/" + ps.get();
   }
 
+  public static String toProject(final Project.NameKey p) {
+    return ADMIN_PROJECTS + p.get();
+  }
+
   public static String toProjectAcceess(final Project.NameKey p) {
     return "/admin/projects/" + p.get() + ",access";
   }
 
-  public static String toAccountQuery(final String fullname) {
-    return toAccountQuery(fullname, Status.NEW);
-  }
-
   public static String toAccountQuery(String fullname, Status status) {
     return toChangeQuery(op("owner", fullname) + " " + status(status), TOP);
   }
 
+  public static String toCustomDashboard(final String params) {
+    return "/dashboard/?" + params;
+  }
+
+  public static String toProjectDashboards(Project.NameKey proj) {
+    return ADMIN_PROJECTS + proj.get() + ",dashboards";
+  }
+
   public static String toChangeQuery(final String query) {
     return toChangeQuery(query, TOP);
   }
@@ -75,10 +86,22 @@
     return "/q/" + KeyUtil.encode(query) + "," + page;
   }
 
+  public static String toProjectDashboard(Project.NameKey name, String id) {
+    return PROJECTS + name.get() + DASHBOARDS + id;
+  }
+
+  public static String projectQuery(Project.NameKey proj) {
+    return op("project", proj.get());
+  }
+
   public static String projectQuery(Project.NameKey proj, Status status) {
       return status(status) + " " + op("project", proj.get());
   }
 
+  public static String toGroup(AccountGroup.UUID uuid) {
+    return ADMIN_GROUPS + "uuid-" + uuid;
+  }
+
   private static String status(Status status) {
     switch (status) {
       case ABANDONED:
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/ProjectAccessUtil.java b/gerrit-common/src/main/java/com/google/gerrit/common/ProjectAccessUtil.java
new file mode 100644
index 0000000..12e47b1
--- /dev/null
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/ProjectAccessUtil.java
@@ -0,0 +1,68 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.common.data.AccessSection;
+import com.google.gerrit.common.data.Permission;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+public class ProjectAccessUtil {
+  public static List<AccessSection> mergeSections(final List<AccessSection> src) {
+    final Map<String, AccessSection> map =
+        new LinkedHashMap<String, AccessSection>();
+    for (final AccessSection section : src) {
+      if (section.getPermissions().isEmpty()) {
+        continue;
+      }
+
+      final AccessSection prior = map.get(section.getName());
+      if (prior != null) {
+        prior.mergeFrom(section);
+      } else {
+        map.put(section.getName(), section);
+      }
+    }
+    return new ArrayList<AccessSection>(map.values());
+  }
+
+  public static List<AccessSection> removeEmptyPermissionsAndSections(
+      final List<AccessSection> src) {
+    final Set<AccessSection> sectionsToRemove = new HashSet<AccessSection>();
+    for (final AccessSection section : src) {
+      final Set<Permission> permissionsToRemove = new HashSet<Permission>();
+      for (final Permission permission : section.getPermissions()) {
+        if (permission.getRules().isEmpty()) {
+          permissionsToRemove.add(permission);
+        }
+      }
+      for (final Permission permissionToRemove : permissionsToRemove) {
+        section.remove(permissionToRemove);
+      }
+      if (section.getPermissions().isEmpty()) {
+        sectionsToRemove.add(section);
+      }
+    }
+    for (final AccessSection sectionToRemove : sectionsToRemove) {
+      src.remove(sectionToRemove);
+    }
+    return src;
+  }
+}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/ProjectUtil.java b/gerrit-common/src/main/java/com/google/gerrit/common/ProjectUtil.java
new file mode 100644
index 0000000..0fba41e
--- /dev/null
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/ProjectUtil.java
@@ -0,0 +1,33 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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;
+
+public class ProjectUtil {
+  public static String stripGitSuffix(String name) {
+    if (name.endsWith(".git")) {
+      // Be nice and drop the trailing ".git" suffix, which we never keep
+      // in our database, but clients might mistakenly provide anyway.
+      //
+      name = name.substring(0, name.length() - 4);
+      while (name.endsWith("/")) {
+        name = name.substring(0, name.length() - 1);
+      }
+    }
+    return name;
+  }
+
+  private ProjectUtil() {
+  }
+}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/auth/SignInMode.java b/gerrit-common/src/main/java/com/google/gerrit/common/auth/SignInMode.java
deleted file mode 100644
index 867ed56..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/auth/SignInMode.java
+++ /dev/null
@@ -1,19 +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.auth;
-
-public enum SignInMode {
-  SIGN_IN, LINK_IDENTIY, REGISTER;
-}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/auth/openid/DiscoveryResult.java b/gerrit-common/src/main/java/com/google/gerrit/common/auth/openid/DiscoveryResult.java
deleted file mode 100644
index aed26fa..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/auth/openid/DiscoveryResult.java
+++ /dev/null
@@ -1,50 +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.auth.openid;
-
-import java.util.Map;
-
-public final class DiscoveryResult {
-  public static enum Status {
-    /** Provider was discovered and {@code providerUrl} is valid. */
-    VALID,
-
-    /** The identifier is not allowed to be used, by site configuration. */
-    NOT_ALLOWED,
-
-    /** Identifier isn't for an OpenID provider. */
-    NO_PROVIDER,
-
-    /** The provider was discovered, but something else failed. */
-    ERROR;
-  }
-
-  public Status status;
-  public String providerUrl;
-  public Map<String, String> providerArgs;
-
-  protected DiscoveryResult() {
-  }
-
-  public DiscoveryResult(final String redirect, final Map<String, String> args) {
-    status = Status.VALID;
-    providerUrl = redirect;
-    providerArgs = args;
-  }
-
-  public DiscoveryResult(final Status s) {
-    status = s;
-  }
-}
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
deleted file mode 100644
index c80d3eb..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/auth/openid/OpenIdProviderPattern.java
+++ /dev/null
@@ -1,45 +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.auth.openid;
-
-import com.google.gerrit.reviewdb.client.AccountExternalId;
-
-public class OpenIdProviderPattern {
-  public static OpenIdProviderPattern create(String pattern) {
-    OpenIdProviderPattern r = new OpenIdProviderPattern();
-    r.regex = pattern.startsWith("^") && pattern.endsWith("$");
-    r.pattern = pattern;
-    return r;
-  }
-
-  protected boolean regex;
-  protected String pattern;
-
-  protected OpenIdProviderPattern() {
-  }
-
-  public boolean matches(String id) {
-    return regex ? id.matches(pattern) : id.startsWith(pattern);
-  }
-
-  public boolean matches(AccountExternalId id) {
-    return matches(id.getExternalId());
-  }
-
-  @Override
-  public String toString() {
-    return 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
deleted file mode 100644
index 0deba34..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/auth/openid/OpenIdService.java
+++ /dev/null
@@ -1,30 +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.auth.openid;
-
-import com.google.gerrit.common.auth.SignInMode;
-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 {
-  @AllowCrossSiteRequest
-  void discover(String openidIdentifier, SignInMode mode,
-      boolean remember, String returnToken,
-      AsyncCallback<DiscoveryResult> callback);
-}
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
deleted file mode 100644
index e89cdd2..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/auth/userpass/LoginResult.java
+++ /dev/null
@@ -1,53 +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.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
deleted file mode 100644
index 0936d23..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/auth/userpass/UserPassAuthService.java
+++ /dev/null
@@ -1,30 +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.auth.userpass;
-
-import com.google.gerrit.common.audit.Audit;
-import com.google.gwtjsonrpc.common.AsyncCallback;
-import com.google.gwtjsonrpc.common.AllowCrossSiteRequest;
-import com.google.gwtjsonrpc.common.RemoteJsonService;
-import com.google.gwtjsonrpc.common.RpcImpl;
-import com.google.gwtjsonrpc.common.RpcImpl.Version;
-
-@RpcImpl(version = Version.V2_0)
-public interface UserPassAuthService extends RemoteJsonService {
-  @Audit(action = "sign in", obfuscate={1})
-  @AllowCrossSiteRequest
-  void authenticate(String username, String password,
-      AsyncCallback<LoginResult> callback);
-}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/changes/ListChangesOption.java b/gerrit-common/src/main/java/com/google/gerrit/common/changes/ListChangesOption.java
index a5ab851..3d08d06 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/changes/ListChangesOption.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/changes/ListChangesOption.java
@@ -19,6 +19,7 @@
 /** Output options available when using {@code /changes/} RPCs. */
 public enum ListChangesOption {
   LABELS(0),
+  DETAILED_LABELS(8),
 
   /** Return information on the current patch set of the change. */
   CURRENT_REVISION(1),
@@ -30,7 +31,10 @@
 
   /** If a patch set is included, include the files of the patch set. */
   CURRENT_FILES(5),
-  ALL_FILES(6);
+  ALL_FILES(6),
+
+  /** If accounts are included, include detailed account info. */
+  DETAILED_ACCOUNTS(7);
 
   private final int value;
 
@@ -42,10 +46,6 @@
     return value;
   }
 
-  public static ListChangesOption fromValue(int value) {
-    return ListChangesOption.values()[value];
-  }
-
   public static EnumSet<ListChangesOption> fromBits(int v) {
     EnumSet<ListChangesOption> r = EnumSet.noneOf(ListChangesOption.class);
     for (ListChangesOption o : ListChangesOption.values()) {
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
deleted file mode 100644
index e24900b..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/AccountDashboardInfo.java
+++ /dev/null
@@ -1,71 +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.client.Account;
-
-import java.util.List;
-
-/** Summary information needed to display an account dashboard. */
-public class AccountDashboardInfo {
-  protected AccountInfoCache accounts;
-  protected Account.Id owner;
-  protected List<ChangeInfo> byOwner;
-  protected List<ChangeInfo> forReview;
-  protected List<ChangeInfo> closed;
-
-  protected AccountDashboardInfo() {
-  }
-
-  public AccountDashboardInfo(final Account.Id forUser) {
-    owner = forUser;
-  }
-
-  public AccountInfoCache getAccounts() {
-    return accounts;
-  }
-
-  public void setAccounts(final AccountInfoCache ac) {
-    accounts = ac;
-  }
-
-  public Account.Id getOwner() {
-    return owner;
-  }
-
-  public List<ChangeInfo> getByOwner() {
-    return byOwner;
-  }
-
-  public void setByOwner(List<ChangeInfo> c) {
-    byOwner = c;
-  }
-
-  public List<ChangeInfo> getForReview() {
-    return forReview;
-  }
-
-  public void setForReview(List<ChangeInfo> c) {
-    forReview = c;
-  }
-
-  public List<ChangeInfo> getClosed() {
-    return closed;
-  }
-
-  public void setClosed(List<ChangeInfo> c) {
-    closed = c;
-  }
-}
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 aa212f9..c2d03ab 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
@@ -18,7 +18,6 @@
 import com.google.gerrit.common.auth.SignInRequired;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
-import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.AccountSshKey;
 import com.google.gerrit.reviewdb.client.ContactInformation;
 import com.google.gwtjsonrpc.common.AsyncCallback;
@@ -61,9 +60,6 @@
   @SignInRequired
   void myExternalIds(AsyncCallback<List<AccountExternalId>> callback);
 
-  @SignInRequired
-  void myGroups(AsyncCallback<List<AccountGroup>> callback);
-
   @Audit
   @SignInRequired
   void deleteExternalIds(Set<AccountExternalId.Key> keys,
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/AddBranchResult.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/AddBranchResult.java
new file mode 100644
index 0000000..24e527d
--- /dev/null
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/AddBranchResult.java
@@ -0,0 +1,96 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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 AddBranchResult {
+  protected ListBranchesResult listBranchesResult;
+  protected Error error;
+
+  protected AddBranchResult() {
+  }
+
+  public AddBranchResult(final Error error) {
+    this.error = error;
+  }
+
+  public AddBranchResult(final ListBranchesResult listBranchesResult) {
+    this.listBranchesResult = listBranchesResult;
+  }
+
+  public ListBranchesResult getListBranchesResult() {
+    return listBranchesResult;
+  }
+
+  public boolean hasError() {
+    return error != null;
+  }
+
+  public Error getError() {
+    return error;
+  }
+
+  public static class Error {
+    public static enum Type {
+      /** The branch cannot be created because the given branch name is invalid. */
+      INVALID_NAME,
+
+      /** The branch cannot be created because the given revision is invalid. */
+      INVALID_REVISION,
+
+      /**
+       * The branch cannot be created under the given refname prefix (e.g
+       * branches cannot be created under magic refname prefixes).
+       */
+      BRANCH_CREATION_NOT_ALLOWED_UNDER_REFNAME_PREFIX,
+
+      /** The branch that should be created exists already. */
+      BRANCH_ALREADY_EXISTS,
+
+      /**
+       * The branch cannot be created because it conflicts with an existing
+       * branch (branches cannot be nested).
+       */
+      BRANCH_CREATION_CONFLICT
+    }
+
+    protected Type type;
+    protected String refname;
+
+    protected Error() {
+    }
+
+    public Error(final Type type) {
+      this(type, null);
+    }
+
+    public Error(final Type type, final String refname) {
+      this.type = type;
+      this.refname = refname;
+    }
+
+    public Type getType() {
+      return type;
+    }
+
+    public String getRefname() {
+      return refname;
+    }
+
+    @Override
+    public String toString() {
+      return type + " " + refname;
+    }
+  }
+}
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 3d438f2..36a9814 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
@@ -15,38 +15,41 @@
 package com.google.gerrit.common.data;
 
 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.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;
 
 public class ApprovalDetail {
-  public static final Comparator<ApprovalDetail> SORT =
-      new Comparator<ApprovalDetail>() {
-        public int compare(final ApprovalDetail o1, final ApprovalDetail o2) {
-          int cmp;
-          cmp = o2.hasNonZero - o1.hasNonZero;
-          if (cmp != 0) return cmp;
-          return o1.sortOrder.compareTo(o2.sortOrder);
-        }
-      };
-
-  static final Timestamp EG_0 = new Timestamp(0);
-  static final Timestamp EG_D = new Timestamp(Long.MAX_VALUE);
+  public static List<ApprovalDetail> sort(Collection<ApprovalDetail> ads,
+      final int owner) {
+    List<ApprovalDetail> sorted = new ArrayList<ApprovalDetail>(ads);
+    Collections.sort(sorted, new Comparator<ApprovalDetail>() {
+      public int compare(ApprovalDetail o1, ApprovalDetail o2) {
+        int byOwner = (o2.account.get() == owner ? 1 : 0)
+            - (o1.account.get() == owner ? 1 : 0);
+        return byOwner != 0 ? byOwner : (o1.hasNonZero - o2.hasNonZero);
+      }
+    });
+    return sorted;
+  }
 
   protected Account.Id account;
   protected List<PatchSetApproval> approvals;
   protected boolean canRemove;
+  private Set<String> votable;
 
   private transient Set<String> approved;
   private transient Set<String> rejected;
+  private transient Map<String, Integer> values;
   private transient int hasNonZero;
-  private transient Timestamp sortOrder = EG_D;
 
   protected ApprovalDetail() {
   }
@@ -68,41 +71,12 @@
     canRemove = removeable;
   }
 
-  public List<PatchSetApproval> getPatchSetApprovals() {
-    return approvals;
-  }
-
-  public PatchSetApproval getPatchSetApproval(ApprovalCategory.Id category) {
-    for (PatchSetApproval psa : approvals) {
-      if (psa.getCategoryId().equals(category)) {
-        return psa;
-      }
-    }
-    return null;
-  }
-
-  public void sortFirst() {
-    hasNonZero = 1;
-    sortOrder = ApprovalDetail.EG_0;
-  }
-
-  public void add(final PatchSetApproval ca) {
-    approvals.add(ca);
-
-    final Timestamp g = ca.getGranted();
-    if (g != null && g.compareTo(sortOrder) < 0) {
-      sortOrder = g;
-    }
-    if (ca.getValue() != 0) {
-      hasNonZero = 1;
-    }
-  }
-
   public void approved(String label) {
     if (approved == null) {
       approved = new HashSet<String>();
     }
     approved.add(label);
+    hasNonZero = 1;
   }
 
   public void rejected(String label) {
@@ -110,6 +84,24 @@
       rejected = new HashSet<String>();
     }
     rejected.add(label);
+    hasNonZero = 1;
+  }
+
+  public void votable(String label) {
+    if (votable == null) {
+      votable = new HashSet<String>();
+    }
+    votable.add(label);
+  }
+
+  public void value(String label, int value) {
+    if (values == null) {
+      values = new HashMap<String, Integer>();
+    }
+    values.put(label, value);
+    if (value != 0) {
+      hasNonZero = 1;
+    }
   }
 
   public boolean isApproved(String label) {
@@ -119,4 +111,16 @@
   public boolean isRejected(String label) {
     return rejected != null && rejected.contains(label);
   }
+
+  public boolean canVote(String label) {
+    return votable != null && votable.contains(label);
+  }
+
+  public int getValue(String label) {
+    if (values == null) {
+      return 0;
+    }
+    Integer v = values.get(label);
+    return v != null ? v : 0;
+  }
 }
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
deleted file mode 100644
index 19c9d67..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ApprovalSummary.java
+++ /dev/null
@@ -1,44 +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 com.google.gerrit.reviewdb.client.ApprovalCategory;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
-
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
-
-/** Summarizes the approvals (or negative approvals) for a patch set.
- * This will typically contain zero or one approvals for each
- * category, with all of the approvals coming from a single patch set.
- */
-public class ApprovalSummary {
-  protected Map<ApprovalCategory.Id, PatchSetApproval> approvals;
-
-  protected ApprovalSummary() {
-  }
-
-  public ApprovalSummary(final Iterable<PatchSetApproval> list) {
-    approvals = new HashMap<ApprovalCategory.Id, PatchSetApproval>();
-    for (final PatchSetApproval a : list) {
-      approvals.put(a.getCategoryId(), a);
-    }
-  }
-
-  public Map<ApprovalCategory.Id, PatchSetApproval> getApprovalMap() {
-    return Collections.unmodifiableMap(approvals);
-  }
-}
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
deleted file mode 100644
index 2b11085..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ApprovalSummarySet.java
+++ /dev/null
@@ -1,49 +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 com.google.gerrit.reviewdb.client.Change;
-
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
-
-/** Contains a set of ApprovalSummary objects, keyed by the change id
- * from which they were derived.
- */
-public class ApprovalSummarySet {
-  protected AccountInfoCache accounts;
-
-  protected Map<Change.Id, ApprovalSummary> summaries;
-
-  protected ApprovalSummarySet() {
-  }
-
-  public ApprovalSummarySet(final AccountInfoCache accts,
-      final Map<Change.Id, ApprovalSummary> map) {
-    accounts = accts;
-
-    summaries = new HashMap<Change.Id, ApprovalSummary>();
-    summaries.putAll(map);
-  }
-
-  public AccountInfoCache getAccountInfoCache() {
-    return accounts;
-  }
-
-  public Map<Change.Id, ApprovalSummary> getSummaryMap() {
-    return Collections.unmodifiableMap(summaries);
-  }
-}
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
deleted file mode 100644
index 333b91c..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ApprovalType.java
+++ /dev/null
@@ -1,126 +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.client.ApprovalCategory;
-import com.google.gerrit.reviewdb.client.ApprovalCategoryValue;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-public class ApprovalType {
-  protected ApprovalCategory category;
-  protected List<ApprovalCategoryValue> values;
-  protected short maxNegative;
-  protected short maxPositive;
-
-  private transient List<Integer> intList;
-  private transient Map<Short, ApprovalCategoryValue> byValue;
-
-  protected ApprovalType() {
-  }
-
-  public ApprovalType(final ApprovalCategory ac,
-      final List<ApprovalCategoryValue> valueList) {
-    category = ac;
-    values = new ArrayList<ApprovalCategoryValue>(valueList);
-    Collections.sort(values, new Comparator<ApprovalCategoryValue>() {
-      public int compare(ApprovalCategoryValue o1, ApprovalCategoryValue o2) {
-        return o1.getValue() - o2.getValue();
-      }
-    });
-
-    maxNegative = Short.MIN_VALUE;
-    maxPositive = Short.MAX_VALUE;
-    if (values.size() > 0) {
-      if (values.get(0).getValue() < 0) {
-        maxNegative = values.get(0).getValue();
-      }
-      if (values.get(values.size() - 1).getValue() > 0) {
-        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() {
-    return category;
-  }
-
-  public List<ApprovalCategoryValue> getValues() {
-    return values;
-  }
-
-  public ApprovalCategoryValue getMin() {
-    if (values.isEmpty()) {
-      return null;
-    }
-    return values.get(0);
-  }
-
-  public ApprovalCategoryValue getMax() {
-    if (values.isEmpty()) {
-      return null;
-    }
-    final ApprovalCategoryValue v = values.get(values.size() - 1);
-    return v.getValue() > 0 ? v : null;
-  }
-
-  public boolean isMaxNegative(final PatchSetApproval ca) {
-    return maxNegative == ca.getValue();
-  }
-
-  public boolean isMaxPositive(final PatchSetApproval ca) {
-    return maxPositive == ca.getValue();
-  }
-
-  public ApprovalCategoryValue getValue(final short value) {
-    initByValue();
-    return byValue.get(value);
-  }
-
-  public ApprovalCategoryValue getValue(final PatchSetApproval ca) {
-    initByValue();
-    return byValue.get(ca.getValue());
-  }
-
-  private void initByValue() {
-    if (byValue == null) {
-      byValue = new HashMap<Short, ApprovalCategoryValue>();
-      for (final ApprovalCategoryValue acv : values) {
-        byValue.put(acv.getValue(), acv);
-      }
-    }
-  }
-
-  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
deleted file mode 100644
index b1e32d1..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ApprovalTypes.java
+++ /dev/null
@@ -1,71 +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 com.google.gerrit.reviewdb.client.ApprovalCategory;
-
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-public class ApprovalTypes {
-  protected List<ApprovalType> approvalTypes;
-  private transient Map<ApprovalCategory.Id, ApprovalType> byId;
-  private transient Map<String, ApprovalType> byLabel;
-
-  protected ApprovalTypes() {
-  }
-
-  public ApprovalTypes(final List<ApprovalType> approvals) {
-    approvalTypes = approvals;
-    byCategory();
-  }
-
-  public List<ApprovalType> getApprovalTypes() {
-    return approvalTypes;
-  }
-
-  public ApprovalType byId(final ApprovalCategory.Id id) {
-    return byCategory().get(id);
-  }
-
-  private Map<ApprovalCategory.Id, ApprovalType> byCategory() {
-    if (byId == null) {
-      byId = new HashMap<ApprovalCategory.Id, ApprovalType>();
-      if (approvalTypes != null) {
-        for (final ApprovalType t : approvalTypes) {
-          byId.put(t.getCategory().getId(), t);
-        }
-      }
-    }
-    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-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 74d1962..7ac21db 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
@@ -17,10 +17,8 @@
 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.Project;
 
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
 import java.util.List;
 
 /** Detail necessary to display a change. */
@@ -28,6 +26,7 @@
   protected AccountInfoCache accounts;
   protected boolean allowsAnonymous;
   protected boolean canAbandon;
+  protected boolean canEditCommitMessage;
   protected boolean canPublish;
   protected boolean canRebase;
   protected boolean canRestore;
@@ -38,13 +37,15 @@
   protected List<ChangeInfo> dependsOn;
   protected List<ChangeInfo> neededBy;
   protected List<PatchSet> patchSets;
-  protected List<ApprovalDetail> approvals;
   protected List<SubmitRecord> submitRecords;
+  protected Project.SubmitType submitType;
+  protected SubmitTypeRecord submitTypeRecord;
   protected boolean canSubmit;
   protected List<ChangeMessage> messages;
   protected PatchSet.Id currentPatchSetId;
   protected PatchSetDetail currentDetail;
   protected boolean canEdit;
+  protected boolean canEditTopicName;
 
   public ChangeDetail() {
   }
@@ -73,6 +74,14 @@
     canAbandon = a;
   }
 
+  public boolean canEditCommitMessage() {
+    return canEditCommitMessage;
+  }
+
+  public void setCanEditCommitMessage(final boolean a) {
+    canEditCommitMessage = a;
+  }
+
   public boolean canPublish() {
     return canPublish;
   }
@@ -121,6 +130,14 @@
     canDeleteDraft = a;
   }
 
+  public boolean canEditTopicName() {
+    return canEditTopicName;
+  }
+
+  public void setCanEditTopicName(boolean a) {
+    canEditTopicName = a;
+  }
+
   public Change getChange() {
     return change;
   }
@@ -170,15 +187,6 @@
     patchSets = s;
   }
 
-  public List<ApprovalDetail> getApprovals() {
-    return approvals;
-  }
-
-  public void setApprovals(Collection<ApprovalDetail> list) {
-    approvals = new ArrayList<ApprovalDetail>(list);
-    Collections.sort(approvals, ApprovalDetail.SORT);
-  }
-
   public void setSubmitRecords(List<SubmitRecord> all) {
     submitRecords = all;
   }
@@ -187,6 +195,14 @@
     return submitRecords;
   }
 
+  public void setSubmitTypeRecord(SubmitTypeRecord submitTypeRecord) {
+    this.submitTypeRecord = submitTypeRecord;
+  }
+
+  public SubmitTypeRecord getSubmitTypeRecord() {
+    return submitTypeRecord;
+  }
+
   public boolean isCurrentPatchSet(final PatchSetDetail detail) {
     return currentPatchSetId != null
         && detail.getPatchSet().getId().equals(currentPatchSetId);
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 391d615..f2e833f 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
@@ -50,7 +50,7 @@
     lastUpdatedOn = c.getLastUpdatedOn();
     sortKey = c.getSortKey();
     patchSetId = patchId;
-    latest = patchSetId == null || c.currPatchSetId().equals(patchSetId);
+    latest = patchSetId == null || patchSetId.equals(c.currentPatchSetId());
   }
 
   public ChangeInfo(final Change c) {
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 4ef6b3e..d0e5154 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
@@ -27,22 +27,8 @@
 public interface ChangeManageService extends RemoteJsonService {
   @Audit
   @SignInRequired
-  void submit(PatchSet.Id patchSetId, AsyncCallback<ChangeDetail> callback);
-
-  @Audit
-  @SignInRequired
-  void abandonChange(PatchSet.Id patchSetId, String message,
-      AsyncCallback<ChangeDetail> callback);
-
-  @Audit
-  @SignInRequired
-  void revertChange(PatchSet.Id patchSetId, String message,
-      AsyncCallback<ChangeDetail> callback);
-
-  @Audit
-  @SignInRequired
-  void restoreChange(PatchSet.Id patchSetId, String message,
-      AsyncCallback<ChangeDetail> callback);
+  void createNewPatchSet(final PatchSet.Id patchSetId, final String newCommitMessage,
+      final AsyncCallback<ChangeDetail> callback);
 
   @Audit
   @SignInRequired
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 0e079b3..3cb3f05 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
@@ -89,9 +89,6 @@
   }
 
   public List<PatchLineComment> getForA(final int lineNbr) {
-    if (lineNbr == 0) {
-      return Collections.emptyList();
-    }
     if (forA == null) {
       forA = index(a);
     }
@@ -99,9 +96,6 @@
   }
 
   public List<PatchLineComment> getForB(final int lineNbr) {
-    if (lineNbr == 0) {
-      return Collections.emptyList();
-    }
     if (forB == null) {
       forB = index(b);
     }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GarbageCollectionResult.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GarbageCollectionResult.java
new file mode 100644
index 0000000..93f283b
--- /dev/null
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GarbageCollectionResult.java
@@ -0,0 +1,82 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.common.data;
+
+import com.google.common.collect.Lists;
+import com.google.gerrit.reviewdb.client.Project;
+
+import java.util.List;
+
+public class GarbageCollectionResult {
+  protected List<Error> errors;
+
+  public GarbageCollectionResult() {
+    errors = Lists.newArrayList();
+  }
+
+  public void addError(Error e) {
+    errors.add(e);
+  }
+
+  public List<Error> getErrors() {
+    return errors;
+  }
+
+  public boolean hasErrors() {
+    return !errors.isEmpty();
+  }
+
+  public static class Error {
+    public static enum Type {
+      /** Git garbage collection was already scheduled for this project */
+      GC_ALREADY_SCHEDULED,
+
+      /** The repository was not found. */
+      REPOSITORY_NOT_FOUND,
+
+      /** The Git garbage collection failed. */
+      GC_FAILED
+    }
+
+    protected Type type;
+    protected Project.NameKey projectName;
+
+    protected Error() {
+    }
+
+    public Error(Type type, Project.NameKey projectName) {
+      this.type = type;
+      this.projectName = projectName;
+    }
+
+    public Type getType() {
+      return type;
+    }
+
+    public Project.NameKey getProjectName() {
+      return projectName;
+    }
+
+    @Override
+    public String toString() {
+      StringBuilder b = new StringBuilder();
+      b.append(type);
+      if (projectName != null) {
+        b.append(" ").append(projectName);
+      }
+      return b.toString();
+    }
+  }
+}
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 7c16129..16b99dc 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
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.common.data;
 
-import com.google.gerrit.common.auth.openid.OpenIdProviderPattern;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Account.FieldName;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadCommand;
@@ -28,10 +27,9 @@
 
 public class GerritConfig implements Cloneable {
   protected String registerUrl;
+  protected String registerText;
   protected String httpPasswordUrl;
   protected String reportBugUrl;
-  protected String openIdSsoUrl;
-  protected List<OpenIdProviderPattern> allowedOpenIDs;
 
   protected GitwebConfig gitweb;
   protected boolean useContributorAgreements;
@@ -45,12 +43,12 @@
   protected String sshdAddress;
   protected String editFullNameUrl;
   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;
+  protected int suggestFrom;
 
   public String getRegisterUrl() {
     return registerUrl;
@@ -60,6 +58,14 @@
     registerUrl = u;
   }
 
+  public String getRegisterText() {
+    return registerText;
+  }
+
+  public void setRegisterText(final String t) {
+    registerText = t;
+  }
+
   public String getReportBugUrl() {
     return reportBugUrl;
   }
@@ -84,22 +90,6 @@
     httpPasswordUrl = url;
   }
 
-  public String getOpenIdSsoUrl() {
-      return openIdSsoUrl;
-  }
-
-  public void setOpenIdSsoUrl(final String u) {
-    openIdSsoUrl = u;
-  }
-
-  public List<OpenIdProviderPattern> getAllowedOpenIDs() {
-    return allowedOpenIDs;
-  }
-
-  public void setAllowedOpenIDs(List<OpenIdProviderPattern> l) {
-    allowedOpenIDs = l;
-  }
-
   public AuthType getAuthType() {
     return authType;
   }
@@ -186,14 +176,6 @@
     wildProject = wp;
   }
 
-  public ApprovalTypes getApprovalTypes() {
-    return approvalTypes;
-  }
-
-  public void setApprovalTypes(final ApprovalTypes at) {
-    approvalTypes = at;
-  }
-
   public boolean canEdit(final Account.FieldName f) {
     return editableAccountFields.contains(f);
   }
@@ -238,6 +220,14 @@
     this.anonymousCowardName = anonymousCowardName;
   }
 
+  public int getSuggestFrom() {
+    return suggestFrom;
+  }
+
+  public void setSuggestFrom(final int suggestFrom) {
+    this.suggestFrom = suggestFrom;
+  }
+
   public boolean siteHasUsernames() {
     if (getAuthType() == AuthType.CUSTOM_EXTENSION
         && getHttpPasswordUrl() != null
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 0e2e50d..3580774 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
@@ -36,10 +36,10 @@
     } 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}");
+      type.setProject("${project}.git/summary");
+      type.setRevision("${project}.git/commit/?id=${commit}");
+      type.setBranch("${project}.git/log/?h=${branch}");
+      type.setFileHistory("${project}.git/log/${file}?h=${branch}");
 
     } else if (name.equalsIgnoreCase("custom")) {
       type = new GitWebType();
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java
index 81d4fc9..7db691d 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GlobalCapability.java
@@ -21,6 +21,9 @@
 
 /** Server wide capabilities. Represented as {@link Permission} objects. */
 public class GlobalCapability {
+  /** Ability to access the database (with gsql). */
+  public static final String ACCESS_DATABASE = "accessDatabase";
+
   /**
    * Denotes the server's administrators.
    * <p>
@@ -28,7 +31,7 @@
    * 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".
+   * "OR user has capability ADMINISTRATE_SERVER".
    */
   public static final String ADMINISTRATE_SERVER = "administrateServer";
 
@@ -64,6 +67,9 @@
   /** Maximum result limit per executed query. */
   public static final String QUERY_LIMIT = "queryLimit";
 
+  /** Can run the Git garbage collection. */
+  public static final String RUN_GC = "runGC";
+
   /** Forcefully restart replication to any configured destination. */
   public static final String START_REPLICATION = "startReplication";
 
@@ -81,6 +87,7 @@
 
   static {
     NAMES_ALL = new ArrayList<String>();
+    NAMES_ALL.add(ACCESS_DATABASE);
     NAMES_ALL.add(ADMINISTRATE_SERVER);
     NAMES_ALL.add(CREATE_ACCOUNT);
     NAMES_ALL.add(CREATE_GROUP);
@@ -90,6 +97,7 @@
     NAMES_ALL.add(KILL_TASK);
     NAMES_ALL.add(PRIORITY);
     NAMES_ALL.add(QUERY_LIMIT);
+    NAMES_ALL.add(RUN_GC);
     NAMES_ALL.add(START_REPLICATION);
     NAMES_ALL.add(VIEW_CACHES);
     NAMES_ALL.add(VIEW_CONNECTIONS);
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
deleted file mode 100644
index 5cb7fa2..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupAdminService.java
+++ /dev/null
@@ -1,89 +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.common.audit.Audit;
-import com.google.gerrit.common.auth.SignInRequired;
-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.Set;
-
-@RpcImpl(version = Version.V2_0)
-public interface GroupAdminService extends RemoteJsonService {
-  @Audit
-  @SignInRequired
-  void visibleGroups(AsyncCallback<GroupList> callback);
-
-  @Audit
-  @SignInRequired
-  void createGroup(String newName, AsyncCallback<AccountGroup.Id> callback);
-
-  @Audit
-  @SignInRequired
-  void groupDetail(AccountGroup.Id groupId, AccountGroup.UUID uuid,
-      AsyncCallback<GroupDetail> callback);
-
-  @Audit
-  @SignInRequired
-  void changeGroupDescription(AccountGroup.Id groupId, String description,
-      AsyncCallback<VoidResult> callback);
-
-  @Audit
-  @SignInRequired
-  void changeGroupOptions(AccountGroup.Id groupId, GroupOptions groupOptions,
-      AsyncCallback<VoidResult> callback);
-
-  @Audit
-  @SignInRequired
-  void changeGroupOwner(AccountGroup.Id groupId, String newOwnerName,
-      AsyncCallback<VoidResult> callback);
-
-  @Audit
-  @SignInRequired
-  void renameGroup(AccountGroup.Id groupId, String newName,
-      AsyncCallback<GroupDetail> callback);
-
-  @Audit
-  @SignInRequired
-  void changeGroupType(AccountGroup.Id groupId, AccountGroup.Type newType,
-      AsyncCallback<VoidResult> callback);
-
-  @Audit
-  @SignInRequired
-  void addGroupMember(AccountGroup.Id groupId, String nameOrEmail,
-      AsyncCallback<GroupDetail> callback);
-
-  @Audit
-  @SignInRequired
-  void addGroupInclude(AccountGroup.Id groupId, String groupName,
-      AsyncCallback<GroupDetail> callback);
-
-  @Audit
-  @SignInRequired
-  void deleteGroupMembers(AccountGroup.Id groupId,
-      Set<AccountGroupMember.Key> keys, AsyncCallback<VoidResult> callback);
-
-  @Audit
-  @SignInRequired
-  void deleteGroupIncludes(AccountGroup.Id groupId,
-      Set<AccountGroupInclude.Key> keys, AsyncCallback<VoidResult> callback);
-}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDescription.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDescription.java
index 828bf24..ccd50fc 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDescription.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDescription.java
@@ -16,6 +16,8 @@
 
 import com.google.gerrit.reviewdb.client.AccountGroup;
 
+import javax.annotation.Nullable;
+
 /**
  * Group methods exposed by the GroupBackend.
  */
@@ -30,8 +32,21 @@
     /** @return the non-null name of the group. */
     String getName();
 
-    /** @return whether the group is visible to all accounts. */
-    boolean isVisibleToAll();
+    /**
+     * @return optional email address to send to the group's members. If
+     *         provided, Gerrit will use this email address to send
+     *         change notifications to the group.
+     */
+    @Nullable
+    String getEmailAddress();
+
+    /**
+     * @return optional URL to information about the group. Typically a URL to a
+     *         web page that permits users to apply to join the group, or manage
+     *         their membership.
+     */
+    @Nullable
+    String getUrl();
   }
 
   /**
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDescriptions.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDescriptions.java
index e0bc7d8..f986600 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDescriptions.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupDescriptions.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.common.data;
 
+import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 
 import javax.annotation.Nullable;
@@ -44,13 +45,20 @@
       }
 
       @Override
-      public boolean isVisibleToAll() {
-        return group.isVisibleToAll();
+      public AccountGroup getAccountGroup() {
+        return group;
       }
 
       @Override
-      public AccountGroup getAccountGroup() {
-        return group;
+      @Nullable
+      public String getEmailAddress() {
+        return null;
+      }
+
+      @Override
+      @Nullable
+      public String getUrl() {
+        return "#" + PageLinks.toGroup(getGroupUUID());
       }
     };
   }
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 01c7985..0d2be51 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
@@ -15,17 +15,16 @@
 package com.google.gerrit.common.data;
 
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupInclude;
+import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuid;
 import com.google.gerrit.reviewdb.client.AccountGroupMember;
 
 import java.util.List;
 
 public class GroupDetail {
   public AccountInfoCache accounts;
-  public GroupInfoCache groups;
   public AccountGroup group;
   public List<AccountGroupMember> members;
-  public List<AccountGroupInclude> includes;
+  public List<AccountGroupIncludeByUuid> includes;
   public GroupReference ownerGroup;
   public boolean canModify;
 
@@ -36,10 +35,6 @@
     accounts = c;
   }
 
-  public void setGroups(GroupInfoCache c) {
-    groups = c;
-  }
-
   public void setGroup(AccountGroup g) {
     group = g;
   }
@@ -48,7 +43,7 @@
     members = m;
   }
 
-  public void setIncludes(List<AccountGroupInclude> i) {
+  public void setIncludes(List<AccountGroupIncludeByUuid> i) {
     includes = i;
   }
 
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 547a5f4..1acdd9a 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
@@ -18,9 +18,10 @@
 
 /** Summary information about an {@link AccountGroup}, for simple tabular displays. */
 public class GroupInfo {
-  protected AccountGroup.Id id;
+  protected AccountGroup.UUID uuid;
   protected String name;
   protected String description;
+  protected String url;
 
   protected GroupInfo() {
   }
@@ -32,8 +33,8 @@
    * lookup has failed and a stale group id has been discovered in the data
    * store.
    */
-  public GroupInfo(final AccountGroup.Id id) {
-    this.id = id;
+  public GroupInfo(final AccountGroup.UUID uuid) {
+    this.uuid = uuid;
   }
 
   /**
@@ -41,15 +42,20 @@
    *
    * @param a the data store record holding the specific group details.
    */
-  public GroupInfo(final AccountGroup a) {
-    id = a.getId();
+  public GroupInfo(GroupDescription.Basic a) {
+    uuid = a.getGroupUUID();
     name = a.getName();
-    description = a.getDescription();
+    url = a.getUrl();
+
+    if (a instanceof GroupDescription.Internal) {
+      AccountGroup group = ((GroupDescription.Internal) a).getAccountGroup();
+      description = group.getDescription();
+    }
   }
 
   /** @return the unique local id of the group */
-  public AccountGroup.Id getId() {
-    return id;
+  public AccountGroup.UUID getId() {
+    return uuid;
   }
 
   /** @return the name of the group; null if not supplied */
@@ -61,4 +67,8 @@
   public String getDescription() {
     return description;
   }
+
+  public String getUrl() {
+    return url;
+  }
 }
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 6a5dd5c..085973c 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
@@ -33,13 +33,13 @@
     return EMPTY;
   }
 
-  protected Map<AccountGroup.Id, GroupInfo> groups;
+  protected Map<AccountGroup.UUID, GroupInfo> groups;
 
   protected GroupInfoCache() {
   }
 
   public GroupInfoCache(final Iterable<GroupInfo> list) {
-    groups = new HashMap<AccountGroup.Id, GroupInfo>();
+    groups = new HashMap<AccountGroup.UUID, GroupInfo>();
     for (final GroupInfo gi : list) {
       groups.put(gi.getId(), gi);
     }
@@ -58,15 +58,15 @@
    * @param id the id desired.
    * @return info block for the group.
    */
-  public GroupInfo get(final AccountGroup.Id id) {
-    if (id == null) {
+  public GroupInfo get(final AccountGroup.UUID uuid) {
+    if (uuid == null) {
       return null;
     }
 
-    GroupInfo r = groups.get(id);
+    GroupInfo r = groups.get(uuid);
     if (r == null) {
-      r = new GroupInfo(id);
-      groups.put(id, r);
+      r = new GroupInfo(uuid);
+      groups.put(uuid, r);
     }
     return r;
   }
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
deleted file mode 100644
index b3095cd..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupList.java
+++ /dev/null
@@ -1,48 +0,0 @@
-// Copyright (C) 2011 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.common.data;
-
-import com.google.gerrit.reviewdb.client.AccountGroup;
-
-import java.util.List;
-
-public class GroupList {
-  protected List<AccountGroup> groups;
-  protected boolean canCreateGroup;
-
-  protected GroupList() {
-  }
-
-  public GroupList(final List<AccountGroup> groups, final boolean canCreateGroup) {
-    this.groups = groups;
-    this.canCreateGroup = canCreateGroup;
-  }
-
-  public List<AccountGroup> getGroups() {
-    return groups;
-  }
-
-  public void setGroups(List<AccountGroup> 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
deleted file mode 100644
index c6ed781..0000000
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/GroupOptions.java
+++ /dev/null
@@ -1,36 +0,0 @@
-// Copyright (C) 2011 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.common.data;
-
-import com.google.gerrit.reviewdb.client.AccountGroup;
-
-/**
- * Options for an {@link AccountGroup}.
- */
-public class GroupOptions {
-
-  private boolean visibleToAll;
-
-  protected GroupOptions() {
-  }
-
-  public GroupOptions(final boolean visibleToAll) {
-    this.visibleToAll = visibleToAll;
-  }
-
-  public boolean isVisibleToAll() {
-    return visibleToAll;
-  }
-}
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 f991f4c..f014f5f 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
@@ -17,13 +17,16 @@
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountDiffPreference;
 
+import java.util.List;
+
 /** 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 String xGerritAuth;
   public GerritConfig config;
   public Theme theme;
+  public List<String> plugins;
 
   public static class Theme {
     public String backgroundColor;
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/LabelType.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/LabelType.java
new file mode 100644
index 0000000..2049887
--- /dev/null
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/LabelType.java
@@ -0,0 +1,249 @@
+// 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.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.PatchSetApproval.LabelId;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class LabelType {
+  public static LabelType withDefaultValues(String name) {
+    checkName(name);
+    List<LabelValue> values = new ArrayList<LabelValue>(2);
+    values.add(new LabelValue((short) 0, "Rejected"));
+    values.add(new LabelValue((short) 1, "Approved"));
+    return new LabelType(name, values);
+  }
+
+  private static String checkName(String name) {
+    if ("SUBM".equals(name)) {
+      throw new IllegalArgumentException(
+          "Reserved label name \"" + name + "\"");
+    }
+    for (int i = 0; i < name.length(); i++) {
+      char c = name.charAt(i);
+      if (!((c >= 'a' && c <= 'z') ||
+            (c >= 'A' && c <= 'Z') ||
+            (c >= '0' && c <= '9') ||
+            c == '-')) {
+        throw new IllegalArgumentException(
+            "Illegal label name \"" + name + "\"");
+      }
+    }
+    return name;
+  }
+
+  public static String defaultAbbreviation(String name) {
+    StringBuilder abbr = new StringBuilder();
+    for (int i = 0; i < name.length(); i++) {
+      char c = name.charAt(i);
+      if (c >= 'A' && c <= 'Z') {
+        abbr.append(c);
+      }
+    }
+    if (abbr.length() == 0) {
+      abbr.append(Character.toUpperCase(name.charAt(0)));
+    }
+    return abbr.toString();
+  }
+
+  private static List<LabelValue> sortValues(List<LabelValue> values) {
+    values = new ArrayList<LabelValue>(values);
+    if (values.size() <= 1) {
+      return Collections.unmodifiableList(values);
+    }
+    Collections.sort(values, new Comparator<LabelValue>() {
+      public int compare(LabelValue o1, LabelValue o2) {
+        return o1.getValue() - o2.getValue();
+      }
+    });
+    short min = values.get(0).getValue();
+    short max = values.get(values.size() - 1).getValue();
+    short v = min;
+    short i = 0;
+    List<LabelValue> result = new ArrayList<LabelValue>(max - min + 1);
+    // Fill in any missing values with empty text.
+    while (i < values.size()) {
+      while (v < values.get(i).getValue()) {
+        result.add(new LabelValue(v++, ""));
+      }
+      v++;
+      result.add(values.get(i++));
+    }
+    return Collections.unmodifiableList(result);
+  }
+
+  protected String name;
+
+  protected String abbreviatedName;
+  protected String functionName;
+  protected boolean copyMinScore;
+
+  protected List<LabelValue> values;
+  protected short maxNegative;
+  protected short maxPositive;
+
+  private transient boolean canOverride;
+  private transient List<Integer> intList;
+  private transient Map<Short, LabelValue> byValue;
+
+  protected LabelType() {
+  }
+
+  public LabelType(String name, List<LabelValue> valueList) {
+    this.name = checkName(name);
+    canOverride = true;
+    values = sortValues(valueList);
+
+    abbreviatedName = defaultAbbreviation(name);
+    functionName = "MaxWithBlock";
+
+    maxNegative = Short.MIN_VALUE;
+    maxPositive = Short.MAX_VALUE;
+    if (values.size() > 0) {
+      if (values.get(0).getValue() < 0) {
+        maxNegative = values.get(0).getValue();
+      }
+      if (values.get(values.size() - 1).getValue() > 0) {
+        maxPositive = values.get(values.size() - 1).getValue();
+      }
+    }
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  public boolean matches(PatchSetApproval psa) {
+    return psa.getLabelId().get().equalsIgnoreCase(name);
+  }
+
+  public String getAbbreviatedName() {
+    return abbreviatedName;
+  }
+
+  public void setAbbreviatedName(String abbreviatedName) {
+    this.abbreviatedName = abbreviatedName;
+  }
+
+  public String getFunctionName() {
+    return functionName;
+  }
+
+  public void setFunctionName(String functionName) {
+    this.functionName = functionName;
+  }
+
+  public boolean canOverride() {
+    return canOverride;
+  }
+
+  public void setCanOverride(boolean canOverride) {
+    this.canOverride = canOverride;
+  }
+
+  public List<LabelValue> getValues() {
+    return values;
+  }
+
+  public LabelValue getMin() {
+    if (values.isEmpty()) {
+      return null;
+    }
+    return values.get(0);
+  }
+
+  public LabelValue getMax() {
+    if (values.isEmpty()) {
+      return null;
+    }
+    final LabelValue v = values.get(values.size() - 1);
+    return v.getValue() > 0 ? v : null;
+  }
+
+  public boolean isCopyMinScore() {
+    return copyMinScore;
+  }
+
+  public void setCopyMinScore(boolean copyMinScore) {
+    this.copyMinScore = copyMinScore;
+  }
+
+  public boolean isMaxNegative(PatchSetApproval ca) {
+    return maxNegative == ca.getValue();
+  }
+
+  public boolean isMaxPositive(PatchSetApproval ca) {
+    return maxPositive == ca.getValue();
+  }
+
+  public LabelValue getValue(short value) {
+    initByValue();
+    return byValue.get(value);
+  }
+
+  public LabelValue getValue(final PatchSetApproval ca) {
+    initByValue();
+    return byValue.get(ca.getValue());
+  }
+
+  private void initByValue() {
+    if (byValue == null) {
+      byValue = new HashMap<Short, LabelValue>();
+      for (final LabelValue v : values) {
+        byValue.put(v.getValue(), v);
+      }
+    }
+  }
+
+  public List<Integer> getValuesAsList() {
+    if (intList == null) {
+      intList = new ArrayList<Integer>(values.size());
+      for (LabelValue v : values) {
+        intList.add(Integer.valueOf(v.getValue()));
+      }
+      Collections.sort(intList);
+      Collections.reverse(intList);
+    }
+    return intList;
+  }
+
+  public LabelId getLabelId() {
+    return new LabelId(name);
+  }
+
+  @Override
+  public String toString() {
+    StringBuilder sb = new StringBuilder(name).append('[');
+    LabelValue min = getMin();
+    LabelValue max = getMax();
+    if (min != null && max != null) {
+      sb.append(new PermissionRange(Permission.forLabel(name), min.getValue(),
+          max.getValue()).toString().trim());
+    } else if (min != null) {
+      sb.append(min.formatValue().trim());
+    } else if (max != null) {
+      sb.append(max.formatValue().trim());
+    }
+    sb.append(']');
+    return sb.toString();
+  }
+}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/LabelTypes.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/LabelTypes.java
new file mode 100644
index 0000000..34a45a4
--- /dev/null
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/LabelTypes.java
@@ -0,0 +1,101 @@
+// 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 com.google.gerrit.reviewdb.client.PatchSetApproval.LabelId;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class LabelTypes {
+  protected List<LabelType> labelTypes;
+  private transient Map<String, LabelType> byLabel;
+  private transient Map<String, Integer> positions;
+
+  protected LabelTypes() {
+  }
+
+  public LabelTypes(final List<? extends LabelType> approvals) {
+    labelTypes =
+        Collections.unmodifiableList(new ArrayList<LabelType>(approvals));
+  }
+
+  public List<LabelType> getLabelTypes() {
+    return labelTypes;
+  }
+
+  public LabelType byLabel(LabelId labelId) {
+    return byLabel().get(labelId.get().toLowerCase());
+  }
+
+  public LabelType byLabel(String labelName) {
+    return byLabel().get(labelName.toLowerCase());
+  }
+
+  private Map<String, LabelType> byLabel() {
+    if (byLabel == null) {
+      byLabel = new HashMap<String, LabelType>();
+      if (labelTypes != null) {
+        for (LabelType t : labelTypes) {
+          byLabel.put(t.getName().toLowerCase(), t);
+        }
+      }
+    }
+    return byLabel;
+  }
+
+  @Override
+  public String toString() {
+    return labelTypes.toString();
+  }
+
+  public Comparator<String> nameComparator() {
+    final Map<String, Integer> positions = positions();
+    return new Comparator<String>() {
+      @Override
+      public int compare(String left, String right) {
+        int lp = position(left);
+        int rp = position(right);
+        int cmp = lp - rp;
+        if (cmp == 0) {
+          cmp = left.compareTo(right);
+        }
+        return cmp;
+      }
+
+      private int position(String name) {
+        Integer p = positions.get(name);
+        return p != null ? p : positions.size();
+      }
+    };
+  }
+
+  private Map<String, Integer> positions() {
+    if (positions == null) {
+      positions = new HashMap<String, Integer>();
+      if (labelTypes != null) {
+        int i = 0;
+        for (LabelType t : labelTypes) {
+          positions.put(t.getName(), i++);
+        }
+      }
+    }
+    return positions;
+  }
+}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/LabelValue.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/LabelValue.java
new file mode 100644
index 0000000..cd29e05
--- /dev/null
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/LabelValue.java
@@ -0,0 +1,63 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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 LabelValue {
+  public static String formatValue(short value) {
+    if (value < 0) {
+      return Short.toString(value);
+    } else if (value == 0) {
+      return " 0";
+    } else {
+      return "+" + Short.toString(value);
+    }
+  }
+
+  protected short value;
+  protected String text;
+
+  public LabelValue(short value, String text) {
+    this.value = value;
+    this.text = text;
+  }
+
+  protected LabelValue() {
+  }
+
+  public short getValue() {
+    return value;
+  }
+
+  public String getText() {
+    return text;
+  }
+
+  public String formatValue() {
+    return formatValue(value);
+  }
+
+  public String format() {
+    StringBuilder sb = new StringBuilder(formatValue());
+    if (!text.isEmpty()) {
+      sb.append(' ').append(text);
+    }
+    return sb.toString();
+  }
+
+  @Override
+  public String toString() {
+    return format();
+  }
+}
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 91ecb92..5b234c71 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
@@ -16,22 +16,15 @@
 
 import com.google.gerrit.common.audit.Audit;
 import com.google.gerrit.common.auth.SignInRequired;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountDiffPreference;
-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;
+import com.google.gwtjsonrpc.common.VoidResult;
 
 @RpcImpl(version = Version.V2_0)
 public interface PatchDetailService extends RemoteJsonService {
@@ -64,33 +57,4 @@
   @Audit
   @SignInRequired
   void deleteDraftPatchSet(PatchSet.Id psid, AsyncCallback<ChangeDetail> callback);
-
-  @Audit
-  @SignInRequired
-  void publishComments(PatchSet.Id psid, String message,
-      Set<ApprovalCategoryValue.Id> approvals,
-      AsyncCallback<VoidResult> callback);
-
-  @Audit
-  @SignInRequired
-  void addReviewers(Change.Id id, List<String> reviewers, boolean confirmed,
-      AsyncCallback<ReviewerResult> callback);
-
-  @Audit
-  @SignInRequired
-  void removeReviewer(Change.Id id, Account.Id reviewerId,
-      AsyncCallback<ReviewerResult> callback);
-
-  void userApprovals(Set<Change.Id> cids, Account.Id aid,
-      AsyncCallback<ApprovalSummarySet> callback);
-
-  void strongestApprovals(Set<Change.Id> cids,
-      AsyncCallback<ApprovalSummarySet> callback);
-
-  /**
-   * Update the reviewed status for the patch.
-   */
-  @Audit
-  @SignInRequired
-  void setReviewedByCurrentUser(Key patchKey, boolean reviewed, AsyncCallback<VoidResult> callback);
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/PatchScript.java
index 2308b77..fecbb76 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
@@ -53,6 +53,7 @@
   protected boolean hugeFile;
   protected boolean intralineDifference;
   protected boolean intralineFailure;
+  protected boolean intralineTimeout;
 
   public PatchScript(final Change.Key ck, final ChangeType ct, final String on,
       final String nn, final FileMode om, final FileMode nm,
@@ -60,7 +61,7 @@
       final SparseFileContent ca, final SparseFileContent cb,
       final List<Edit> e, final DisplayMethod ma, final DisplayMethod mb,
       final CommentDetail cd, final List<Patch> hist, final boolean hf,
-      final boolean id, final boolean idf) {
+      final boolean id, final boolean idf, final boolean idt) {
     changeId = ck;
     changeType = ct;
     oldName = on;
@@ -79,6 +80,7 @@
     hugeFile = hf;
     intralineDifference = id;
     intralineFailure = idf;
+    intralineTimeout = idt;
   }
 
   protected PatchScript() {
@@ -152,6 +154,10 @@
     return intralineFailure;
   }
 
+  public boolean hasIntralineTimeout() {
+    return intralineTimeout;
+  }
+
   public boolean isExpandAllComments() {
     return diffPrefs.isExpandAllComments();
   }
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 3c5c688..a9b6335 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,15 +14,10 @@
 
 package com.google.gerrit.common.data;
 
-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.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
 import java.util.List;
 
 public class PatchSetPublishDetail {
@@ -30,43 +25,16 @@
   protected PatchSetInfo patchSetInfo;
   protected Change change;
   protected List<PatchLineComment> drafts;
-  protected List<PermissionRange> labels;
-  protected List<ApprovalDetail> approvals;
   protected List<SubmitRecord> submitRecords;
-  protected List<PatchSetApproval> given;
+  protected SubmitTypeRecord submitTypeRecord;
   protected boolean canSubmit;
 
-  public List<PermissionRange> getLabels() {
-    return labels;
+  public void setSubmitTypeRecord(SubmitTypeRecord submitTypeRecord) {
+    this.submitTypeRecord = submitTypeRecord;
   }
 
-  public void setLabels(List<PermissionRange> labels) {
-    this.labels = labels;
-  }
-
-  public List<ApprovalDetail> getApprovals() {
-    return approvals;
-  }
-
-  public void setApprovals(Collection<ApprovalDetail> list) {
-    approvals = new ArrayList<ApprovalDetail>(list);
-    Collections.sort(approvals, ApprovalDetail.SORT);
-  }
-
-  public void setSubmitRecords(List<SubmitRecord> all) {
-    submitRecords = all;
-  }
-
-  public List<SubmitRecord> getSubmitRecords() {
-    return submitRecords;
-  }
-
-  public List<PatchSetApproval> getGiven() {
-    return given;
-  }
-
-  public void setGiven(List<PatchSetApproval> given) {
-    this.given = given;
+  public SubmitTypeRecord getSubmitTypeRecord() {
+    return submitTypeRecord;
   }
 
   public void setAccounts(AccountInfoCache accounts) {
@@ -105,24 +73,6 @@
     return drafts;
   }
 
-  public PermissionRange getRange(final String permissionName) {
-    for (PermissionRange s : labels) {
-      if (s.getName().equals(permissionName)) {
-        return s;
-      }
-    }
-    return null;
-  }
-
-  public PatchSetApproval getChangeApproval(ApprovalCategory.Id id) {
-    for (PatchSetApproval a : given) {
-      if (a.getCategoryId().equals(id)) {
-        return a;
-      }
-    }
-    return null;
-  }
-
   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
index fd40888..0585651 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/Permission.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/Permission.java
@@ -23,17 +23,23 @@
 public class Permission implements Comparable<Permission> {
   public static final String ABANDON = "abandon";
   public static final String CREATE = "create";
+  public static final String DELETE_DRAFTS = "deleteDrafts";
+  public static final String EDIT_TOPIC_NAME = "editTopicName";
   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 PUBLISH_DRAFTS = "publishDrafts";
   public static final String PUSH = "push";
   public static final String PUSH_MERGE = "pushMerge";
   public static final String PUSH_TAG = "pushTag";
+  public static final String PUSH_SIGNED_TAG = "pushSignedTag";
   public static final String READ = "read";
   public static final String REBASE = "rebase";
+  public static final String REMOVE_REVIEWER = "removeReviewer";
   public static final String SUBMIT = "submit";
+  public static final String VIEW_DRAFTS = "viewDrafts";
 
   private static final List<String> NAMES_LC;
   private static final int labelIndex;
@@ -50,9 +56,15 @@
     NAMES_LC.add(PUSH.toLowerCase());
     NAMES_LC.add(PUSH_MERGE.toLowerCase());
     NAMES_LC.add(PUSH_TAG.toLowerCase());
+    NAMES_LC.add(PUSH_SIGNED_TAG.toLowerCase());
     NAMES_LC.add(LABEL.toLowerCase());
     NAMES_LC.add(REBASE.toLowerCase());
+    NAMES_LC.add(REMOVE_REVIEWER.toLowerCase());
     NAMES_LC.add(SUBMIT.toLowerCase());
+    NAMES_LC.add(VIEW_DRAFTS.toLowerCase());
+    NAMES_LC.add(EDIT_TOPIC_NAME.toLowerCase());
+    NAMES_LC.add(DELETE_DRAFTS.toLowerCase());
+    NAMES_LC.add(PUBLISH_DRAFTS.toLowerCase());
 
     labelIndex = NAMES_LC.indexOf(Permission.LABEL);
   }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/PermissionRule.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/PermissionRule.java
index 5960165..a7a66b1 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/PermissionRule.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/PermissionRule.java
@@ -16,6 +16,7 @@
 
 public class PermissionRule implements Comparable<PermissionRule> {
   public static final String FORCE_PUSH = "Force Push";
+  public static final String FORCE_EDIT = "Force Edit";
   public static enum Action {
     ALLOW, DENY, BLOCK,
 
@@ -251,7 +252,7 @@
     return rule;
   }
 
-  private static int parseInt(String value) {
+  public static int parseInt(String value) {
     if (value.startsWith("+")) {
       value = value.substring(1);
     }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectAccess.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectAccess.java
index 1893843..904c5c7 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectAccess.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ProjectAccess.java
@@ -27,6 +27,7 @@
   protected Set<String> ownerOf;
   protected boolean isConfigVisible;
   protected boolean canUpload;
+  protected LabelTypes labelTypes;
 
   public ProjectAccess() {
   }
@@ -103,4 +104,12 @@
   public void setCanUpload(boolean canUpload) {
     this.canUpload = canUpload;
   }
+
+  public LabelTypes getLabelTypes() {
+    return labelTypes;
+  }
+
+  public void setLabelTypes(LabelTypes labelTypes) {
+    this.labelTypes = labelTypes;
+  }
 }
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 13c0a48..0638906 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
@@ -22,7 +22,6 @@
 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;
@@ -35,12 +34,6 @@
   void projectDetail(Project.NameKey projectName,
       AsyncCallback<ProjectDetail> callback);
 
-  @Audit
-  @SignInRequired
-  void createNewProject(String projectName, String parentName,
-      boolean emptyCommit, boolean permissionsOnly,
-      AsyncCallback<VoidResult> callback);
-
   void projectAccess(Project.NameKey projectName,
       AsyncCallback<ProjectAccess> callback);
 
@@ -66,7 +59,7 @@
   @Audit
   @SignInRequired
   void addBranch(Project.NameKey projectName, String branchName,
-      String startingRevision, AsyncCallback<ListBranchesResult> callback);
+      String startingRevision, AsyncCallback<AddBranchResult> callback);
 
   @Audit
   @SignInRequired
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 1d88cf8..1e7589b 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,6 +14,7 @@
 
 package com.google.gerrit.common.data;
 
+import com.google.gerrit.reviewdb.client.InheritedBoolean;
 import com.google.gerrit.reviewdb.client.Project;
 
 public class ProjectDetail {
@@ -24,6 +25,10 @@
   public boolean canModifyAccess;
   public boolean canModifyState;
   public boolean isPermissionOnly;
+  public InheritedBoolean useContributorAgreements;
+  public InheritedBoolean useSignedOffBy;
+  public InheritedBoolean useContentMerge;
+  public InheritedBoolean requireChangeID;
 
   public ProjectDetail() {
   }
@@ -55,4 +60,20 @@
   public void setPermissionOnly(final boolean ipo) {
     isPermissionOnly = ipo;
   }
+
+  public void setUseContributorAgreements(final InheritedBoolean uca) {
+    useContributorAgreements = uca;
+  }
+
+  public void setUseSignedOffBy(final InheritedBoolean usob) {
+    useSignedOffBy = usob;
+  }
+
+  public void setUseContentMerge(final InheritedBoolean ucm) {
+    useContentMerge = ucm;
+  }
+
+  public void setRequireChangeID(final InheritedBoolean rcid) {
+    requireChangeID = rcid;
+  }
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/RefConfigSection.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/RefConfigSection.java
index 810e906..f740464 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/RefConfigSection.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/RefConfigSection.java
@@ -21,9 +21,6 @@
   /** Pattern that matches all branches in a project. */
   public static final String HEADS = "refs/heads/*";
 
-  /** Configuration settings for a project {@code refs/meta/config} */
-  public static final String REF_CONFIG = "refs/meta/config";
-
   /** Prefix that triggers a regular expression pattern. */
   public static final String REGEX_PREFIX = "^";
 
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/ReviewResult.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/ReviewResult.java
index c0bf818..1c87c71 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/data/ReviewResult.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/ReviewResult.java
@@ -82,7 +82,10 @@
       GIT_ERROR,
 
       /** The destination branch does not exist */
-      DEST_BRANCH_NOT_FOUND
+      DEST_BRANCH_NOT_FOUND,
+
+      /** Not permitted to edit the topic name */
+      EDIT_TOPIC_NAME_NOT_PERMITTED
     }
 
     protected Type type;
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/data/SubmitTypeRecord.java b/gerrit-common/src/main/java/com/google/gerrit/common/data/SubmitTypeRecord.java
new file mode 100644
index 0000000..4eea798
--- /dev/null
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/data/SubmitTypeRecord.java
@@ -0,0 +1,58 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.common.data;
+
+import com.google.gerrit.reviewdb.client.Project;
+
+/**
+ * Describes the submit type for a change.
+ */
+public class SubmitTypeRecord {
+  public static enum Status {
+    /** The type was computed successfully */
+    OK,
+
+    /** An internal server error occurred preventing computation.
+     * <p>
+     * Additional detail may be available in {@link SubmitTypeRecord#errorMessage}
+     */
+    RULE_ERROR
+  }
+
+  public static SubmitTypeRecord OK(Project.SubmitType type) {
+    SubmitTypeRecord r = new SubmitTypeRecord();
+    r.status = Status.OK;
+    r.type = type;
+    return r;
+  }
+
+  public Status status;
+  public Project.SubmitType type;
+  public String errorMessage;
+
+  public String toString() {
+    StringBuilder sb = new StringBuilder();
+    sb.append(status);
+    if (status == Status.RULE_ERROR && errorMessage != null) {
+      sb.append('(').append(errorMessage).append(")");
+    }
+    if (type != null) {
+      sb.append('[');
+      sb.append(type.name());
+      sb.append(']');
+    }
+    return sb.toString();
+  }
+}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/errors/EmailException.java b/gerrit-common/src/main/java/com/google/gerrit/common/errors/EmailException.java
new file mode 100644
index 0000000..635335d
--- /dev/null
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/errors/EmailException.java
@@ -0,0 +1,29 @@
+// 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.errors;
+
+public class EmailException extends Exception {
+  private static final long serialVersionUID = 1L;
+
+  public static final String MESSAGE = "Mail Error: ";
+
+  public EmailException(String msg) {
+    super(MESSAGE + msg);
+  }
+
+  public EmailException(String msg, Throwable why) {
+    super(MESSAGE + msg, why);
+  }
+}
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/errors/NameAlreadyUsedException.java b/gerrit-common/src/main/java/com/google/gerrit/common/errors/NameAlreadyUsedException.java
index a2d487b..ea20e2e 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/errors/NameAlreadyUsedException.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/errors/NameAlreadyUsedException.java
@@ -18,9 +18,9 @@
 public class NameAlreadyUsedException extends Exception {
   private static final long serialVersionUID = 1L;
 
-  public static final String MESSAGE = "Name Already Used";
+  public static final String MESSAGE = "Name Already Used: ";
 
-  public NameAlreadyUsedException() {
-    super(MESSAGE);
+  public NameAlreadyUsedException(String name) {
+    super(MESSAGE + name);
   }
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/errors/NoSuchEntityException.java b/gerrit-common/src/main/java/com/google/gerrit/common/errors/NoSuchEntityException.java
index c47cf07..1829c8b 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/errors/NoSuchEntityException.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/errors/NoSuchEntityException.java
@@ -23,4 +23,8 @@
   public NoSuchEntityException() {
     super(MESSAGE);
   }
+
+  public NoSuchEntityException(String message) {
+    super(message);
+  }
 }
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/errors/PermissionDeniedException.java b/gerrit-common/src/main/java/com/google/gerrit/common/errors/PermissionDeniedException.java
index 76520aa..0faf498 100644
--- a/gerrit-common/src/main/java/com/google/gerrit/common/errors/PermissionDeniedException.java
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/errors/PermissionDeniedException.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.common.errors;
 
-/** Indicats the user cannot perform this task. */
+/** Indicates the user cannot perform this task. */
 public class PermissionDeniedException extends Exception {
   private static final long serialVersionUID = 1L;
 
diff --git a/gerrit-common/src/main/java/com/google/gerrit/common/groups/ListGroupsOption.java b/gerrit-common/src/main/java/com/google/gerrit/common/groups/ListGroupsOption.java
new file mode 100644
index 0000000..9da7fd5
--- /dev/null
+++ b/gerrit-common/src/main/java/com/google/gerrit/common/groups/ListGroupsOption.java
@@ -0,0 +1,62 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.groups;
+
+import java.util.EnumSet;
+
+
+/** Output options available when using {@code /groups/} RPCs. */
+public enum ListGroupsOption {
+  /** Return information on the direct group members. */
+  MEMBERS(0),
+
+  /** Return information on the directly included groups. */
+  INCLUDES(1);
+
+  private final int value;
+
+  private ListGroupsOption(int v) {
+    this.value = v;
+  }
+
+  public int getValue() {
+    return value;
+  }
+
+  public static EnumSet<ListGroupsOption> fromBits(int v) {
+    EnumSet<ListGroupsOption> r = EnumSet.noneOf(ListGroupsOption.class);
+    for (ListGroupsOption o : ListGroupsOption.values()) {
+      if ((v & (1 << o.value)) != 0) {
+        r.add(o);
+        v &= ~(1 << o.value);
+      }
+      if (v == 0) {
+        return r;
+      }
+    }
+    if (v != 0) {
+      throw new IllegalArgumentException("unknown " + Integer.toHexString(v));
+    }
+    return r;
+  }
+
+  public static int toBits(EnumSet<ListGroupsOption> set) {
+    int r = 0;
+    for (ListGroupsOption o : set) {
+      r |= 1 << o.value;
+    }
+    return r;
+  }
+}
diff --git a/gerrit-extension-api/.settings/org.eclipse.jdt.core.prefs b/gerrit-extension-api/.settings/org.eclipse.jdt.core.prefs
index 470942d..941fb31 100644
--- a/gerrit-extension-api/.settings/org.eclipse.jdt.core.prefs
+++ b/gerrit-extension-api/.settings/org.eclipse.jdt.core.prefs
@@ -7,6 +7,7 @@
 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.annotationSuperInterface=ignore
 org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
 org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
 org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
diff --git a/gerrit-extension-api/pom.xml b/gerrit-extension-api/pom.xml
index ff672d5..1080ed1 100644
--- a/gerrit-extension-api/pom.xml
+++ b/gerrit-extension-api/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.5-SNAPSHOT</version>
+    <version>2.6-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-extension-api</artifactId>
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/GitReferenceUpdatedListener.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/GitReferenceUpdatedListener.java
index 438500d..2911ded 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/GitReferenceUpdatedListener.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/events/GitReferenceUpdatedListener.java
@@ -23,6 +23,8 @@
 public interface GitReferenceUpdatedListener {
   public interface Update {
     String getRefName();
+    String getOldObjectId();
+    String getNewObjectId();
   }
 
   public interface Event {
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicItem.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicItem.java
new file mode 100644
index 0000000..aa1dc76
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicItem.java
@@ -0,0 +1,220 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.registration;
+
+import com.google.inject.Binder;
+import com.google.inject.Key;
+import com.google.inject.Provider;
+import com.google.inject.ProvisionException;
+import com.google.inject.Scopes;
+import com.google.inject.TypeLiteral;
+import com.google.inject.binder.LinkedBindingBuilder;
+import com.google.inject.util.Providers;
+import com.google.inject.util.Types;
+
+import java.util.concurrent.atomic.AtomicReference;
+
+/**
+ * A single item that can be modified as plugins reload.
+ * <p>
+ * DynamicItems are always mapped as singletons in Guice. Items store a Provider
+ * internally, and resolve the provider to an instance on demand. This enables
+ * registrations to decide between singleton and non-singleton members. If
+ * multiple plugins try to provide the same Provider, an exception is thrown.
+ */
+public class DynamicItem<T> {
+  /** Pair of provider implementation and plugin providing it. */
+  static class NamedProvider<T> {
+    final Provider<T> impl;
+    final String pluginName;
+
+    NamedProvider(Provider<T> provider, String pluginName) {
+      this.impl = provider;
+      this.pluginName = pluginName;
+    }
+  }
+
+  /**
+   * Declare a singleton {@code DynamicItem<T>} with a binder.
+   * <p>
+   * Items must be defined in a Guice module before they can be bound:
+   * <pre>
+   *   DynamicItem.itemOf(binder(), Interface.class);
+   *   DynamicItem.bind(binder(), Interface.class).to(Impl.class);
+   * </pre>
+   *
+   * @param binder a new binder created in the module.
+   * @param member type of entry to store.
+   */
+  public static <T> void itemOf(Binder binder, Class<T> member) {
+    itemOf(binder, TypeLiteral.get(member));
+  }
+
+  /**
+   * Declare a singleton {@code DynamicItem<T>} with a binder.
+   * <p>
+   * Items must be defined in a Guice module before they can be bound:
+   * <pre>
+   *   DynamicSet.itemOf(binder(), new TypeLiteral<Thing<Foo>>() {});
+   * </pre>
+   *
+   * @param binder a new binder created in the module.
+   * @param member type of entry to store.
+   */
+  public static <T> void itemOf(Binder binder, TypeLiteral<T> member) {
+    @SuppressWarnings("unchecked")
+    Key<DynamicItem<T>> key = (Key<DynamicItem<T>>) Key.get(
+        Types.newParameterizedType(DynamicItem.class, member.getType()));
+    binder.bind(key)
+      .toProvider(new DynamicItemProvider<T>(member, key))
+      .in(Scopes.SINGLETON);
+  }
+
+  /**
+   * Bind one implementation as the item using a unique annotation.
+   *
+   * @param binder a new binder created in the module.
+   * @param type type of entry to store.
+   * @return a binder to continue configuring the new item.
+   */
+  public static <T> LinkedBindingBuilder<T> bind(Binder binder, Class<T> type) {
+    return bind(binder, TypeLiteral.get(type));
+  }
+
+  /**
+   * Bind one implementation as the item.
+   *
+   * @param binder a new binder created in the module.
+   * @param type type of entry to store.
+   * @return a binder to continue configuring the new item.
+   */
+  public static <T> LinkedBindingBuilder<T> bind(Binder binder,
+      TypeLiteral<T> type) {
+    return binder.bind(type);
+  }
+
+  private final Key<DynamicItem<T>> key;
+  private final AtomicReference<NamedProvider<T>> ref;
+
+  DynamicItem(Key<DynamicItem<T>> key, Provider<T> provider, String pluginName) {
+    NamedProvider<T> in = null;
+    if (provider != null) {
+      in = new NamedProvider<T>(provider, pluginName);
+    }
+    this.key = key;
+    this.ref = new AtomicReference<NamedProvider<T>>(in);
+  }
+
+  /**
+   * Get the configured item, or null.
+   *
+   * @return the configured item instance; null if no implementation has been
+   *         bound to the item. This is common if no plugin registered an
+   *         implementation for the type.
+   */
+  public T get() {
+    NamedProvider<T> item = ref.get();
+    return item != null ? item.impl.get() : null;
+  }
+
+  /**
+   * Set the element to provide.
+   *
+   * @param item the item to use. Must not be null.
+   * @param pluginName the name of the plugin providing the item.
+   * @return handle to remove the item at a later point in time.
+   */
+  public RegistrationHandle set(T item, String pluginName) {
+    return set(Providers.of(item), pluginName);
+  }
+
+  /**
+   * Set the element to provide.
+   *
+   * @param impl the item to add to the collection. Must not be null.
+   * @param pluginName name of the source providing the implementation.
+   * @return handle to remove the item at a later point in time.
+   */
+  public RegistrationHandle set(Provider<T> impl, String pluginName) {
+    final NamedProvider<T> item = new NamedProvider<T>(impl, pluginName);
+    while (!ref.compareAndSet(null, item)) {
+      NamedProvider<T> old = ref.get();
+      if (old != null) {
+        throw new ProvisionException(String.format(
+            "%s already provided by %s, ignoring plugin %s",
+            key.getTypeLiteral(), old.pluginName, pluginName));
+      }
+    }
+    return new RegistrationHandle() {
+      @Override
+      public void remove() {
+        ref.compareAndSet(item, null);
+      }
+    };
+  }
+
+  /**
+   * Set the element that may be hot-replaceable in the future.
+   *
+   * @param key unique description from the item's Guice binding. This can be
+   *        later obtained from the registration handle to facilitate matching
+   *        with the new equivalent instance during a hot reload.
+   * @param impl the item to set as our value right now. Must not be null.
+   * @param pluginName the name of the plugin providing the item.
+   * @return a handle that can remove this item later, or hot-swap the item.
+   */
+  public ReloadableRegistrationHandle<T> set(Key<T> key, Provider<T> impl,
+      String pluginName) {
+    final NamedProvider<T> item = new NamedProvider<T>(impl, pluginName);
+    while (!ref.compareAndSet(null, item)) {
+      NamedProvider<T> old = ref.get();
+      if (old != null) {
+        throw new ProvisionException(String.format(
+            "%s already provided by %s, ignoring plugin %s",
+            this.key.getTypeLiteral(), old.pluginName, pluginName));
+      }
+    }
+    return new ReloadableHandle(key, item);
+  }
+
+  private class ReloadableHandle implements ReloadableRegistrationHandle<T> {
+    private final Key<T> key;
+    private final NamedProvider<T> item;
+
+    ReloadableHandle(Key<T> key, NamedProvider<T> item) {
+      this.key = key;
+      this.item = item;
+    }
+
+    @Override
+    public Key<T> getKey() {
+      return key;
+    }
+
+    @Override
+    public void remove() {
+      ref.compareAndSet(item, null);
+    }
+
+    @Override
+    public ReloadableHandle replace(Key<T> newKey, Provider<T> newItem) {
+      NamedProvider<T> n = new NamedProvider<T>(newItem, item.pluginName);
+      if (ref.compareAndSet(item, n)) {
+        return new ReloadableHandle(newKey, n);
+      }
+      return null;
+    }
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicItemProvider.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicItemProvider.java
new file mode 100644
index 0000000..1074ee5
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicItemProvider.java
@@ -0,0 +1,56 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.registration;
+
+import com.google.inject.Binding;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.Key;
+import com.google.inject.Provider;
+import com.google.inject.ProvisionException;
+import com.google.inject.TypeLiteral;
+
+import java.util.List;
+
+class DynamicItemProvider<T> implements Provider<DynamicItem<T>> {
+  private final TypeLiteral<T> type;
+  private final Key<DynamicItem<T>> key;
+
+  @Inject
+  private Injector injector;
+
+  DynamicItemProvider(TypeLiteral<T> type, Key<DynamicItem<T>> key) {
+    this.type = type;
+    this.key = key;
+  }
+
+  public DynamicItem<T> get() {
+    return new DynamicItem<T>(key, find(injector, type), "gerrit");
+  }
+
+  private static <T> Provider<T> find(Injector src, TypeLiteral<T> type) {
+    List<Binding<T>> bindings = src.findBindingsByType(type);
+    if (bindings != null && bindings.size() == 1) {
+      return bindings.get(0).getProvider();
+    } else if (bindings != null && bindings.size() > 1) {
+      throw new ProvisionException(String.format(
+        "Multiple providers bound for DynamicItem<%s>\n"
+        + "This is not allowed; check the server configuration.",
+        type));
+    } else {
+      return null;
+    }
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSetProvider.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSetProvider.java
index 6c21553..21fa1b8 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSetProvider.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/DynamicSetProvider.java
@@ -19,7 +19,6 @@
 import com.google.inject.Injector;
 import com.google.inject.Provider;
 import com.google.inject.TypeLiteral;
-import com.google.inject.internal.UniqueAnnotations;
 
 import java.util.ArrayList;
 import java.util.Collections;
@@ -27,8 +26,6 @@
 import java.util.concurrent.atomic.AtomicReference;
 
 class DynamicSetProvider<T> implements Provider<DynamicSet<T>> {
-  private static final Class<?> UNIQUE_ANNOTATION =
-      UniqueAnnotations.create().getClass();
   private final TypeLiteral<T> type;
 
   @Inject
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicTypes.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicTypes.java
index 66dd45d..8bc57ab 100644
--- a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicTypes.java
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/registration/PrivateInternals_DynamicTypes.java
@@ -30,6 +30,22 @@
 
 /** <b>DO NOT USE</b> */
 public class PrivateInternals_DynamicTypes {
+  public static Map<TypeLiteral<?>, DynamicItem<?>> dynamicItemsOf(Injector src) {
+    Map<TypeLiteral<?>, DynamicItem<?>> m = newHashMap();
+    for (Map.Entry<Key<?>, Binding<?>> e : src.getBindings().entrySet()) {
+      TypeLiteral<?> type = e.getKey().getTypeLiteral();
+      if (type.getRawType() == DynamicItem.class) {
+        ParameterizedType p = (ParameterizedType) type.getType();
+        m.put(TypeLiteral.get(p.getActualTypeArguments()[0]),
+            (DynamicItem<?>) e.getValue().getProvider().get());
+      }
+    }
+    if (m.isEmpty()) {
+      return Collections.emptyMap();
+    }
+    return Collections.unmodifiableMap(m);
+  }
+
   public static Map<TypeLiteral<?>, DynamicSet<?>> dynamicSetsOf(Injector src) {
     Map<TypeLiteral<?>, DynamicSet<?>> m = newHashMap();
     for (Map.Entry<Key<?>, Binding<?>> e : src.getBindings().entrySet()) {
@@ -62,6 +78,38 @@
     return Collections.unmodifiableMap(m);
   }
 
+  public static List<RegistrationHandle> attachItems(
+      Injector src,
+      Map<TypeLiteral<?>, DynamicItem<?>> items, String pluginName) {
+    if (src == null || items == null || items.isEmpty()) {
+      return Collections.emptyList();
+    }
+
+    List<RegistrationHandle> handles = new ArrayList<RegistrationHandle>(4);
+    try {
+      for (Map.Entry<TypeLiteral<?>, DynamicItem<?>> e : items.entrySet()) {
+        @SuppressWarnings("unchecked")
+        TypeLiteral<Object> type = (TypeLiteral<Object>) e.getKey();
+
+        @SuppressWarnings("unchecked")
+        DynamicItem<Object> item = (DynamicItem<Object>) e.getValue();
+
+        for (Binding<Object> b : bindings(src, type)) {
+          if (b.getKey().getAnnotation() != null) {
+            handles.add(item.set(b.getKey(), b.getProvider(), pluginName));
+          }
+        }
+      }
+    } catch (RuntimeException e) {
+      remove(handles);
+      throw e;
+    } catch (Error e) {
+      remove(handles);
+      throw e;
+    }
+    return handles;
+  }
+
   public static List<RegistrationHandle> attachSets(
       Injector src,
       Map<TypeLiteral<?>, DynamicSet<?>> sets) {
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/AcceptsCreate.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/AcceptsCreate.java
new file mode 100644
index 0000000..506f281
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/AcceptsCreate.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.restapi;
+
+/**
+ * Optional interface for {@link RestCollection}.
+ * <p>
+ * Collections that implement this interface can accept a {@code PUT} or
+ * {@code POST} when the parse method throws {@link ResourceNotFoundException}.
+ */
+public interface AcceptsCreate<P extends RestResource> {
+  /**
+   * Handle creation of a child resource.
+   *
+   * @param parent parent collection handle.
+   * @param id id of the resource being created.
+   * @return a view to perform the creation. The create method must embed the id
+   *         into the newly returned view object, as it will not be passed.
+   * @throws RestApiException the view cannot be constructed.
+   */
+  <I> RestModifyView<P, I> create(P parent, IdString id) throws RestApiException;
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/AcceptsPost.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/AcceptsPost.java
new file mode 100644
index 0000000..470ea83
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/AcceptsPost.java
@@ -0,0 +1,36 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.restapi;
+
+/**
+ * Optional interface for {@link RestCollection}.
+ * <p>
+ * Collections that implement this interface can accept a {@code POST} directly
+ * on the collection itself when no id was given in the path. This interface is
+ * intended to be used with TopLevelResource collections. Nested collections
+ * often bind POST on the parent collection to the view implementation handling
+ * the insertion of a new member.
+ */
+public interface AcceptsPost<P extends RestResource> {
+  /**
+   * Handle creation of a child resource by POST on the collection.
+   *
+   * @param parent parent collection handle.
+   * @return a view to perform the creation. The id of the newly created
+   *         resource should be determined from the input body.
+   * @throws RestApiException the view cannot be constructed.
+   */
+  <I> RestModifyView<P, I> post(P parent) throws RestApiException;
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/AuthException.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/AuthException.java
new file mode 100644
index 0000000..1d4cda7
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/AuthException.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.restapi;
+
+/** Caller cannot perform the request operation (HTTP 403 Forbidden). */
+public class AuthException extends RestApiException {
+  private static final long serialVersionUID = 1L;
+
+  /** @param msg message to return to the client. */
+  public AuthException(String msg) {
+    super(msg);
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/BadRequestException.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/BadRequestException.java
new file mode 100644
index 0000000..d5a9c1f
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/BadRequestException.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.restapi;
+
+/** Request could not be parsed as sent (HTTP 400 Bad Request). */
+public class BadRequestException extends RestApiException {
+  private static final long serialVersionUID = 1L;
+
+  /** @param msg error text for client describing how request is bad. */
+  public BadRequestException(String msg) {
+    super(msg);
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/BinaryResult.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/BinaryResult.java
new file mode 100644
index 0000000..188011c
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/BinaryResult.java
@@ -0,0 +1,174 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.restapi;
+
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.UnsupportedEncodingException;
+
+/**
+ * Wrapper around a non-JSON result from a {@link RestView}.
+ * <p>
+ * Views may return this type to signal they want the server glue to write raw
+ * data to the client, instead of attempting automatic conversion to JSON. The
+ * create form is overloaded to handle plain text from a String, or binary data
+ * from a {@code byte[]} or {@code InputSteam}.
+ */
+public abstract class BinaryResult implements Closeable {
+  /** Default MIME type for unknown binary data. */
+  static final String OCTET_STREAM = "application/octet-stream";
+
+  /** Produce a UTF-8 encoded result from a string. */
+  public static BinaryResult create(String data) {
+    try {
+      return create(data.getBytes("UTF-8"))
+        .setContentType("text/plain")
+        .setCharacterEncoding("UTF-8");
+    } catch (UnsupportedEncodingException e) {
+      throw new RuntimeException("JVM does not support UTF-8", e);
+    }
+  }
+
+  /** Produce an {@code application/octet-stream} result from a byte array. */
+  public static BinaryResult create(byte[] data) {
+    return new Array(data);
+  }
+
+  /**
+   * Produce an {@code application/octet-stream} of unknown length by copying
+   * the InputStream until EOF. The server glue will automatically close this
+   * stream when copying is complete.
+   */
+  public static BinaryResult create(InputStream data) {
+    return new Stream(data);
+  }
+
+  private String contentType = OCTET_STREAM;
+  private String characterEncoding;
+  private long contentLength = -1;
+  private boolean gzip = true;
+
+  /** @return the MIME type of the result, for HTTP clients. */
+  public String getContentType() {
+    String enc = getCharacterEncoding();
+    if (enc != null) {
+      return contentType + "; charset=" + enc;
+    }
+    return contentType;
+  }
+
+  /** Set the MIME type of the result, and return {@code this}. */
+  public BinaryResult setContentType(String contentType) {
+    this.contentType = contentType != null ? contentType : OCTET_STREAM;
+    return this;
+  }
+
+  /** Get the character encoding; null if not known. */
+  public String getCharacterEncoding() {
+    return characterEncoding;
+  }
+
+  /** Set the character set used to encode text data and return {@code this}. */
+  public BinaryResult setCharacterEncoding(String encoding) {
+    characterEncoding = encoding;
+    return this;
+  }
+
+  /** @return length in bytes of the result; -1 if not known. */
+  public long getContentLength() {
+    return contentLength;
+  }
+
+  /** Set the content length of the result; -1 if not known. */
+  public BinaryResult setContentLength(long len) {
+    this.contentLength = len;
+    return this;
+  }
+
+  /** @return true if this result can be gzip compressed to clients. */
+  public boolean canGzip() {
+    return gzip;
+  }
+
+  /** Disable gzip compression for already compressed responses. */
+  public BinaryResult disableGzip() {
+    this.gzip = false;
+    return this;
+  }
+
+  /**
+   * Write or copy the result onto the specified output stream.
+   *
+   * @param os stream to write result data onto. This stream will be closed by
+   *        the caller after this method returns.
+   * @throws IOException if the data cannot be produced, or the OutputStream
+   *         {@code os} throws any IOException during a write or flush call.
+   */
+  public abstract void writeTo(OutputStream os) throws IOException;
+
+  /** Close the result and release any resources it holds. */
+  public void close() throws IOException {
+  }
+
+  @Override
+  public String toString() {
+    if (getContentLength() >= 0) {
+      return String.format(
+          "BinaryResult[Content-Type: %s, Content-Length: %d]",
+          getContentType(), getContentLength());
+    }
+    return String.format(
+        "BinaryResult[Content-Type: %s, Content-Length: unknown]",
+        getContentType());
+  }
+
+  private static class Array extends BinaryResult {
+    private final byte[] data;
+
+    Array(byte[] data) {
+      this.data = data;
+      setContentLength(data.length);
+    }
+
+    @Override
+    public void writeTo(OutputStream os) throws IOException {
+      os.write(data);
+    }
+  }
+
+  private static class Stream extends BinaryResult {
+    private final InputStream src;
+
+    Stream(InputStream src) {
+      this.src = src;
+    }
+
+    @Override
+    public void writeTo(OutputStream dst) throws IOException {
+      byte[] tmp = new byte[4096];
+      int n;
+      while (0 < (n = src.read(tmp))) {
+        dst.write(tmp, 0, n);
+      }
+    }
+
+    @Override
+    public void close() throws IOException {
+      src.close();
+    }
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/ChildCollection.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/ChildCollection.java
new file mode 100644
index 0000000..3bad9f8
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/ChildCollection.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.restapi;
+
+
+/**
+ * Nested collection of {@link RestResource}s below a parent.
+ *
+ * @param <P> type of the parent resource.
+ * @param <C> type of resource operated on by each view.
+ */
+public interface ChildCollection<P extends RestResource, C extends RestResource>
+    extends RestView<P>, RestCollection<P, C> {
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/DefaultInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/DefaultInput.java
new file mode 100644
index 0000000..83342d2
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/DefaultInput.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.restapi;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/** Applied to a String field to indicate the default input parameter. */
+@Target({ElementType.FIELD})
+@Retention(RUNTIME)
+public @interface DefaultInput {
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/IdString.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/IdString.java
new file mode 100644
index 0000000..f987425
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/IdString.java
@@ -0,0 +1,68 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.restapi;
+
+/**
+ * Resource identifier split out from a URL.
+ * <p>
+ * Identifiers are URL encoded and usually need to be decoded.
+ */
+public class IdString {
+  /** Construct an identifier from an already encoded string. */
+  public static IdString fromUrl(String id) {
+    return new IdString(id);
+  }
+
+  private final String urlEncoded;
+
+  private IdString(String s) {
+    urlEncoded = s;
+  }
+
+  /** @return the decoded value of the string. */
+  public String get() {
+    return Url.decode(urlEncoded);
+  }
+
+  /** @return true if the string is the empty string. */
+  public boolean isEmpty() {
+    return urlEncoded.isEmpty();
+  }
+
+  /** @return the original URL encoding supplied by the client. */
+  public String encoded() {
+    return urlEncoded;
+  }
+
+  @Override
+  public int hashCode() {
+    return urlEncoded.hashCode();
+  }
+
+  @Override
+  public boolean equals(Object other) {
+    if (other instanceof IdString) {
+      return urlEncoded.equals(((IdString) other).urlEncoded);
+    } else if (other instanceof String) {
+      return urlEncoded.equals(other);
+    }
+    return false;
+  }
+
+  @Override
+  public String toString() {
+    return encoded();
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/MethodNotAllowedException.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/MethodNotAllowedException.java
new file mode 100644
index 0000000..8b0fdd3
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/MethodNotAllowedException.java
@@ -0,0 +1,20 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.restapi;
+
+/** Method is not acceptable on the resource (HTTP 405 Method Not Allowed). */
+public class MethodNotAllowedException extends RestApiException {
+  private static final long serialVersionUID = 1L;
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/PreconditionFailedException.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/PreconditionFailedException.java
new file mode 100644
index 0000000..86204c8
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/PreconditionFailedException.java
@@ -0,0 +1,30 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.restapi;
+
+/**
+ * Resource state does not match request state (HTTP 412 Precondition failed).
+ */
+public class PreconditionFailedException extends RestApiException {
+  private static final long serialVersionUID = 1L;
+
+  public PreconditionFailedException() {
+  }
+
+  /** @param msg message to return to the client describing the error. */
+  public PreconditionFailedException(String msg) {
+    super(msg);
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/PutInput.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/PutInput.java
new file mode 100644
index 0000000..b2d62b3
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/PutInput.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.restapi;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+
+/** Raw data stream supplied by the body of a PUT. */
+public interface PutInput {
+  String getContentType();
+  long getContentLength();
+  InputStream getInputStream() throws IOException;
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/ResourceConflictException.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/ResourceConflictException.java
new file mode 100644
index 0000000..eb6d811
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/ResourceConflictException.java
@@ -0,0 +1,32 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.restapi;
+
+/**
+ * Resource state does not permit requested operation (HTTP 409 Conflict).
+ * <p>
+ * {@link RestModifyView} implementations may fail with this exception when the
+ * named resource does not permit the modification to take place at this time.
+ * An example use is trying to abandon a change that is already merged. The
+ * change cannot be abandoned once merged so an operation would throw.
+ */
+public class ResourceConflictException extends RestApiException {
+  private static final long serialVersionUID = 1L;
+
+  /** @param msg message to return to the client describing the error. */
+  public ResourceConflictException(String msg) {
+    super(msg);
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/ResourceNotFoundException.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/ResourceNotFoundException.java
new file mode 100644
index 0000000..aa891c9
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/ResourceNotFoundException.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.restapi;
+
+/** Named resource does not exist (HTTP 404 Not Found). */
+public class ResourceNotFoundException extends RestApiException {
+  private static final long serialVersionUID = 1L;
+
+  /** Requested resource is not found, failing portion not specified. */
+  public ResourceNotFoundException() {
+  }
+
+  /** @param id portion of the resource URI that does not exist. */
+  public ResourceNotFoundException(String id) {
+    super(id);
+  }
+
+  /** @param id portion of the resource URI that does not exist. */
+  public ResourceNotFoundException(IdString id) {
+    super(id.get());
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/Response.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/Response.java
new file mode 100644
index 0000000..97c4cbc
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/Response.java
@@ -0,0 +1,127 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.restapi;
+
+/** Special return value to mean specific HTTP status codes in a REST API. */
+public abstract class Response<T> {
+  @SuppressWarnings({"rawtypes"})
+  private static final Response NONE = new None();
+
+  /** HTTP 200 OK: pointless wrapper for type safety. */
+  public static <T> Response<T> ok(T value) {
+    return new Impl<T>(200, value);
+  }
+
+  /** HTTP 201 Created: typically used when a new resource is made. */
+  public static <T> Response<T> created(T value) {
+    return new Impl<T>(201, value);
+  }
+
+  /** HTTP 204 No Content: typically used when the resource is deleted. */
+  @SuppressWarnings("unchecked")
+  public static <T> Response<T> none() {
+    return NONE;
+  }
+
+  /** HTTP 302 Found: temporary redirect to another URL. */
+  public static Redirect redirect(String location) {
+    return new Redirect(location);
+  }
+
+  @SuppressWarnings({"unchecked", "rawtypes"})
+  public static <T> T unwrap(T obj) {
+    while (obj instanceof Response) {
+      obj = (T) ((Response) obj).value();
+    }
+    return obj;
+  }
+
+  public abstract int statusCode();
+  public abstract T value();
+  public abstract String toString();
+
+  private static final class Impl<T> extends Response<T> {
+    private final int statusCode;
+    private final T value;
+
+    private Impl(int sc, T val) {
+      statusCode = sc;
+      value = val;
+    }
+
+    @Override
+    public int statusCode() {
+      return statusCode;
+    }
+
+    @Override
+    public T value() {
+      return value;
+    }
+
+    @Override
+    public String toString() {
+      return "[" + statusCode() + "] " + value();
+    }
+  }
+
+  private static final class None extends Response<Object> {
+    private None() {
+    }
+
+    @Override
+    public int statusCode() {
+      return 204;
+    }
+
+    public Object value() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public String toString() {
+      return "[204 No Content] None";
+    }
+  }
+
+  /** An HTTP redirect to another location. */
+  public static final class Redirect {
+    private final String location;
+
+    private Redirect(String url) {
+      this.location = url;
+    }
+
+    public String location() {
+      return location;
+    }
+
+    @Override
+    public int hashCode() {
+      return location.hashCode();
+    }
+
+    @Override
+    public boolean equals(Object o) {
+      return o instanceof Redirect
+        && ((Redirect) o).location.equals(location);
+    }
+
+    @Override
+    public String toString() {
+      return String.format("[302 Redirect] %s", location);
+    }
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RestApiException.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RestApiException.java
new file mode 100644
index 0000000..a6d27cd
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RestApiException.java
@@ -0,0 +1,31 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.restapi;
+
+/** Root exception type for JSON API failures. */
+public abstract class RestApiException extends Exception {
+  private static final long serialVersionUID = 1L;
+
+  public RestApiException() {
+  }
+
+  public RestApiException(String msg) {
+    super(msg);
+  }
+
+  public RestApiException(String msg, Throwable cause) {
+    super(msg, cause);
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RestApiModule.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RestApiModule.java
new file mode 100644
index 0000000..b3a0e18
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RestApiModule.java
@@ -0,0 +1,178 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.restapi;
+
+import com.google.gerrit.extensions.annotations.Export;
+import com.google.gerrit.extensions.annotations.Exports;
+import com.google.inject.AbstractModule;
+import com.google.inject.Provider;
+import com.google.inject.TypeLiteral;
+import com.google.inject.binder.LinkedBindingBuilder;
+import com.google.inject.binder.ScopedBindingBuilder;
+
+/** Guice DSL for binding {@link RestView} implementations. */
+public abstract class RestApiModule extends AbstractModule {
+  protected static final String GET = "GET";
+  protected static final String PUT = "PUT";
+  protected static final String DELETE = "DELETE";
+  protected static final String POST = "POST";
+
+  protected <R extends RestResource>
+  ReadViewBinder<R> get(TypeLiteral<RestView<R>> viewType) {
+    return new ReadViewBinder<R>(view(viewType, GET, "/"));
+  }
+
+  protected <R extends RestResource>
+  ModifyViewBinder<R> put(TypeLiteral<RestView<R>> viewType) {
+    return new ModifyViewBinder<R>(view(viewType, PUT, "/"));
+  }
+
+  protected <R extends RestResource>
+  ModifyViewBinder<R> post(TypeLiteral<RestView<R>> viewType) {
+    return new ModifyViewBinder<R>(view(viewType, POST, "/"));
+  }
+
+  protected <R extends RestResource>
+  ModifyViewBinder<R> delete(TypeLiteral<RestView<R>> viewType) {
+    return new ModifyViewBinder<R>(view(viewType, DELETE, "/"));
+  }
+
+  protected <R extends RestResource>
+  ReadViewBinder<R> get(TypeLiteral<RestView<R>> viewType, String name) {
+    return new ReadViewBinder<R>(view(viewType, GET, name));
+  }
+
+  protected <R extends RestResource>
+  ModifyViewBinder<R> put(TypeLiteral<RestView<R>> viewType, String name) {
+    return new ModifyViewBinder<R>(view(viewType, PUT, name));
+  }
+
+  protected <R extends RestResource>
+  ModifyViewBinder<R> post(TypeLiteral<RestView<R>> viewType, String name) {
+    return new ModifyViewBinder<R>(view(viewType, POST, name));
+  }
+
+  protected <R extends RestResource>
+  ModifyViewBinder<R> delete(TypeLiteral<RestView<R>> viewType, String name) {
+    return new ModifyViewBinder<R>(view(viewType, DELETE, name));
+  }
+
+  protected <P extends RestResource>
+  ChildCollectionBinder<P> child(TypeLiteral<RestView<P>> type, String name) {
+    return new ChildCollectionBinder<P>(view(type, GET, name));
+  }
+
+  protected <R extends RestResource>
+  LinkedBindingBuilder<RestView<R>> view(
+      TypeLiteral<RestView<R>> viewType,
+      String method,
+      String name) {
+    return bind(viewType).annotatedWith(export(method, name));
+  }
+
+  private static Export export(String method, String name) {
+    if (name.length() > 1 && name.startsWith("/")) {
+      // Views may be bound as "/" to mean the resource itself, or
+      // as "status" as in "/type/{id}/status". Don't bind "/status"
+      // if the caller asked for that, bind what the server expects.
+      name = name.substring(1);
+    }
+    return Exports.named(method + "." + name);
+  }
+
+  public static class ReadViewBinder<P extends RestResource> {
+    private final LinkedBindingBuilder<RestView<P>> binder;
+
+    private ReadViewBinder(LinkedBindingBuilder<RestView<P>> binder) {
+      this.binder = binder;
+    }
+
+    public <T extends RestReadView<P>>
+    ScopedBindingBuilder to(Class<T> impl) {
+      return binder.to(impl);
+    }
+
+    public <T extends RestReadView<P>>
+    void toInstance(T impl) {
+      binder.toInstance(impl);
+    }
+
+    public <T extends RestReadView<P>>
+    ScopedBindingBuilder toProvider(Class<? extends Provider<? extends T>> providerType) {
+      return binder.toProvider(providerType);
+    }
+
+    public <T extends RestReadView<P>>
+    ScopedBindingBuilder toProvider(Provider<? extends T> provider) {
+      return binder.toProvider(provider);
+    }
+  }
+
+  public static class ModifyViewBinder<P extends RestResource> {
+    private final LinkedBindingBuilder<RestView<P>> binder;
+
+    private ModifyViewBinder(LinkedBindingBuilder<RestView<P>> binder) {
+      this.binder = binder;
+    }
+
+    public <T extends RestModifyView<P, ?>>
+    ScopedBindingBuilder to(Class<T> impl) {
+      return binder.to(impl);
+    }
+
+    public <T extends RestModifyView<P, ?>>
+    void toInstance(T impl) {
+      binder.toInstance(impl);
+    }
+
+    public <T extends RestModifyView<P, ?>>
+    ScopedBindingBuilder toProvider(Class<? extends Provider<? extends T>> providerType) {
+      return binder.toProvider(providerType);
+    }
+
+    public <T extends RestModifyView<P, ?>>
+    ScopedBindingBuilder toProvider(Provider<? extends T> provider) {
+      return binder.toProvider(provider);
+    }
+  }
+
+  public static class ChildCollectionBinder<P extends RestResource> {
+    private final LinkedBindingBuilder<RestView<P>> binder;
+
+    private ChildCollectionBinder(LinkedBindingBuilder<RestView<P>> binder) {
+      this.binder = binder;
+    }
+
+    public <C extends RestResource, T extends ChildCollection<P, C>>
+    ScopedBindingBuilder to(Class<T> impl) {
+      return binder.to(impl);
+    }
+
+    public <C extends RestResource, T extends ChildCollection<P, C>>
+    void toInstance(T impl) {
+      binder.toInstance(impl);
+    }
+
+    public <C extends RestResource, T extends ChildCollection<P, C>>
+    ScopedBindingBuilder toProvider(Class<? extends Provider<? extends T>> providerType) {
+      return binder.toProvider(providerType);
+    }
+
+    public <C extends RestResource, T extends ChildCollection<P, C>>
+    ScopedBindingBuilder toProvider(Provider<? extends T> provider) {
+      return binder.toProvider(provider);
+    }
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RestCollection.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RestCollection.java
new file mode 100644
index 0000000..96d0dbf
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RestCollection.java
@@ -0,0 +1,96 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.restapi;
+
+import com.google.gerrit.extensions.registration.DynamicMap;
+
+/**
+ * A collection of resources accessible through a REST API.
+ * <p>
+ * To build a collection declare a resource, the map in a module, and the
+ * collection itself accepting the map:
+ *
+ * <pre>
+ * public class MyResource implements RestResource {
+ *   public static final TypeLiteral&lt;RestView&lt;MyResource&gt;&gt; MY_KIND =
+ *       new TypeLiteral&lt;RestView&lt;MyResource&gt;&gt;() {};
+ * }
+ *
+ * public class MyModule extends AbstractModule {
+ *   &#064;Override
+ *   protected void configure() {
+ *     DynamicMap.mapOf(binder(), MyResource.MY_KIND);
+ *
+ *     get(MyResource.MY_KIND, &quot;action&quot;).to(MyAction.class);
+ *   }
+ * }
+ *
+ * public class MyCollection extends RestCollection&lt;TopLevelResource, MyResource&gt; {
+ *   private final DynamicMap&lt;RestView&lt;MyResource&gt;&gt; views;
+ *
+ *   &#064;Inject
+ *   MyCollection(DynamicMap&lt;RestView&lt;MyResource&gt;&gt; views) {
+ *     this.views = views;
+ *   }
+ *
+ *   public DynamicMap&lt;RestView&lt;MyResource&gt;&gt; views() {
+ *     return views;
+ *   }
+ * }
+ * </pre>
+ *
+ * <p>
+ * To build a nested collection, implement {@link ChildCollection}.
+ *
+ * @param <P> type of the parent resource. For a top level collection this
+ *        should always be {@link TopLevelResource}.
+ * @param <R> type of resource operated on by each view.
+ */
+public interface RestCollection<P extends RestResource, R extends RestResource> {
+  /**
+   * Create a view to list the contents of the collection.
+   * <p>
+   * The returned view should accept the parent type to scope the search, and
+   * may want to take a "q" parameter option to narrow the results.
+   *
+   * @return view to list the collection.
+   * @throws ResourceNotFoundException if the collection cannot be listed.
+   * @throws AuthException if the collection requires authentication.
+   */
+  RestView<P> list() throws ResourceNotFoundException, AuthException;
+
+  /**
+   * Parse a path component into a resource handle.
+   *
+   * @param parent the handle to the collection.
+   * @param id string identifier supplied by the client. In a URL such as
+   *        {@code /changes/1234/abandon} this string is {@code "1234"}.
+   * @return a resource handle for the identified object.
+   * @throws ResourceNotFoundException the object does not exist, or the caller
+   *         is not permitted to know if the resource exists.
+   * @throws Exception if the implementation had any errors converting to a
+   *         resource handle. This results in an HTTP 500 Internal Server Error.
+   */
+  R parse(P parent, IdString id) throws ResourceNotFoundException, Exception;
+
+  /**
+   * Get the views that support this collection.
+   * <p>
+   * Within a resource the views are accessed as {@code RESOURCE/plugin~view}.
+   *
+   * @return map of views.
+   */
+  DynamicMap<RestView<R>> views();
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RestModifyView.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RestModifyView.java
new file mode 100644
index 0000000..2fa0278
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RestModifyView.java
@@ -0,0 +1,46 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.restapi;
+
+/**
+ * RestView that supports accepting input and changing a resource.
+ * <p>
+ * The input must be supplied as JSON as the body of the HTTP request. Modify
+ * views can be invoked by any HTTP method that is not {@code GET}, which
+ * includes {@code POST}, {@code PUT}, {@code DELETE}.
+ *
+ * @param <R> type of the resource the view modifies.
+ * @param <I> type of input the JSON parser will parse the input into.
+ */
+public interface RestModifyView<R extends RestResource, I> extends RestView<R> {
+  /**
+   * Process the view operation by altering the resource.
+   *
+   * @param resource resource to modify.
+   * @param input input after parsing from request.
+   * @return result to return to the client. Use {@link BinaryResult} to avoid
+   *         automatic conversion to JSON.
+   * @throws AuthException the client is not permitted to access this view.
+   * @throws BadRequestException the request was incorrectly specified and
+   *         cannot be handled by this view.
+   * @throws ResourceConflictException the resource state does not permit this
+   *         view to make the changes at this time.
+   * @throws Exception the implementation of the view failed. The exception will
+   *         be logged and HTTP 500 Internal Server Error will be returned to
+   *         the client.
+   */
+  Object apply(R resource, I input) throws AuthException, BadRequestException,
+      ResourceConflictException, Exception;
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RestReadView.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RestReadView.java
new file mode 100644
index 0000000..32c250d
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RestReadView.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.extensions.restapi;
+
+/**
+ * RestView to read a resource without modification.
+ *
+ * @param <R> type of resource the view reads.
+ */
+public interface RestReadView<R extends RestResource> extends RestView<R> {
+  /**
+   * Process the view operation by reading from the resource.
+   *
+   * @param resource resource to modify.
+   * @return result to return to the client. Use {@link BinaryResult} to avoid
+   *         automatic conversion to JSON.
+   * @throws AuthException the client is not permitted to access this view.
+   * @throws BadRequestException the request was incorrectly specified and
+   *         cannot be handled by this view.
+   * @throws ResourceConflictException the resource state does not permit this
+   *         view to make the changes at this time.
+   * @throws Exception the implementation of the view failed. The exception will
+   *         be logged and HTTP 500 Internal Server Error will be returned to
+   *         the client.
+   */
+  Object apply(R resource) throws AuthException, BadRequestException,
+      ResourceConflictException, Exception;
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RestResource.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RestResource.java
new file mode 100644
index 0000000..063a570
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RestResource.java
@@ -0,0 +1,24 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.restapi;
+
+/**
+ * Generic resource handle defining arguments to views.
+ * <p>
+ * Resource handle returned by {@link RestCollection} and passed to a
+ * {@link RestView} such as {@link RestReadView} or {@link RestModifyView}.
+ */
+public interface RestResource {
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RestView.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RestView.java
new file mode 100644
index 0000000..36adf34
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/RestView.java
@@ -0,0 +1,22 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.restapi;
+
+/**
+ * Any type of view, see {@link RestReadView} for reads, {@link RestModifyView}
+ * for updates, and {@link RestCollection} for nested collections.
+ */
+public interface RestView<R extends RestResource> {
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/StreamingResponse.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/StreamingResponse.java
new file mode 100644
index 0000000..b2fb901
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/StreamingResponse.java
@@ -0,0 +1,23 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.restapi;
+
+import java.io.IOException;
+import java.io.OutputStream;
+
+public interface StreamingResponse {
+  public String getContentType();
+  public void stream(OutputStream out) throws IOException;
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/TopLevelResource.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/TopLevelResource.java
new file mode 100644
index 0000000..8ddd207
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/TopLevelResource.java
@@ -0,0 +1,23 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (thte "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.restapi;
+
+/** Special marker resource naming the top-level of a REST space. */
+public class TopLevelResource implements RestResource {
+  public static final TopLevelResource INSTANCE = new TopLevelResource();
+
+  private TopLevelResource() {
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/UnprocessableEntityException.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/UnprocessableEntityException.java
new file mode 100644
index 0000000..b63697f
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/UnprocessableEntityException.java
@@ -0,0 +1,24 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.restapi;
+
+/** Resource referenced in the request body is not found (HTTP 422 Unprocessable Entity). */
+public class UnprocessableEntityException extends RestApiException {
+  private static final long serialVersionUID = 1L;
+
+  public UnprocessableEntityException(String msg)  {
+    super(msg);
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/Url.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/Url.java
new file mode 100644
index 0000000..8f4d909
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/restapi/Url.java
@@ -0,0 +1,65 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.restapi;
+
+import java.io.UnsupportedEncodingException;
+import java.net.URLDecoder;
+import java.net.URLEncoder;
+
+/** URL related utility functions. */
+public final class Url {
+  /**
+   * Encode a path segment, escaping characters not valid for a URL.
+   * <p>
+   * The following characters are not escaped:
+   * <ul>
+   * <li>{@code a..z, A..Z, 0..9}
+   * <li>{@code . - * _}
+   * </ul>
+   * <p>
+   * ' ' (space) is encoded as '+'.
+   * <p>
+   * All other characters (including '/') are converted to the triplet "%xy"
+   * where "xy" is the hex representation of the character in UTF-8.
+   *
+   * @param component a string containing text to encode.
+   * @return a string with all invalid URL characters escaped.
+   */
+  public static String encode(String component) {
+    if (component != null) {
+      try {
+        return URLEncoder.encode(component, "UTF-8");
+      } catch (UnsupportedEncodingException e) {
+        throw new RuntimeException("JVM must support UTF-8", e);
+      }
+    }
+    return null;
+  }
+
+  /** Decode a URL encoded string, e.g. from {@code "%2F"} to {@code "/"}. */
+  public static String decode(String str) {
+    if (str != null) {
+      try {
+        return URLDecoder.decode(str, "UTF-8");
+      } catch (UnsupportedEncodingException e) {
+        throw new RuntimeException("JVM must support UTF-8", e);
+      }
+    }
+    return null;
+  }
+
+  private Url() {
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/GwtPlugin.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/GwtPlugin.java
new file mode 100644
index 0000000..e741a3f
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/GwtPlugin.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.extensions.webui;
+
+/** Configures a web UI plugin compiled using GWT. */
+public class GwtPlugin extends WebUiPlugin {
+  private final String moduleName;
+
+  /**
+   * @param moduleName name of GWT module. The resource
+   *        {@code static/$MODULE/$MODULE.nocache.js} will be used.
+   */
+  public GwtPlugin(String moduleName) {
+    this.moduleName = moduleName;
+  }
+
+  @Override
+  public String getJavaScriptResourcePath() {
+    return String.format("static/%s/%s.nocache.js", moduleName, moduleName);
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/JavaScriptPlugin.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/JavaScriptPlugin.java
new file mode 100644
index 0000000..89a4f33
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/JavaScriptPlugin.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.extensions.webui;
+
+/** Configures a web UI plugin written using JavaScript. */
+public class JavaScriptPlugin extends WebUiPlugin {
+  private final String fileName;
+
+  /**
+   * @param fileName of JavaScript source file under {@code static/}
+   *        subdirectory within the plugin's JAR.
+   */
+  public JavaScriptPlugin(String fileName) {
+    this.fileName = fileName;
+  }
+
+  @Override
+  public String getJavaScriptResourcePath() {
+    return "static/" + fileName;
+  }
+}
diff --git a/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/WebUiPlugin.java b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/WebUiPlugin.java
new file mode 100644
index 0000000..5cd1981
--- /dev/null
+++ b/gerrit-extension-api/src/main/java/com/google/gerrit/extensions/webui/WebUiPlugin.java
@@ -0,0 +1,69 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.extensions.webui;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.inject.Inject;
+
+/**
+ * Specifies JavaScript to dynamically load into the web UI.
+ * <p>
+ * To automatically register (instead of writing a Guice module), declare the
+ * intention with {@code @Listen}, extend the correct class and define a
+ * constructor to configure the correct resource:
+ *
+ * <pre>
+ * &#064;Listen
+ * class MyJs extends JavaScriptPlugin {
+ *   MyJs() {
+ *     super(&quot;hello.js&quot;);
+ *   }
+ * }
+ * </pre>
+ *
+ * @see GwtPlugin
+ * @see JavaScriptPlugin
+ */
+@ExtensionPoint
+public abstract class WebUiPlugin {
+  public static final GwtPlugin gwt(String moduleName) {
+    return new GwtPlugin(moduleName);
+  }
+
+  public static final JavaScriptPlugin js(String scriptName) {
+    return new JavaScriptPlugin(scriptName);
+  }
+
+  private String pluginName;
+
+  /** @return installed name of the plugin that provides this UI feature.  */
+  public final String getPluginName() {
+    return pluginName;
+  }
+
+  @Inject
+  void setPluginName(@PluginName String pluginName) {
+    this.pluginName = pluginName;
+  }
+
+  /** @return path to initialization script within the plugin's JAR. */
+  public abstract String getJavaScriptResourcePath();
+
+  @Override
+  public String toString() {
+    return getJavaScriptResourcePath();
+  }
+}
diff --git a/gerrit-gwtdebug/pom.xml b/gerrit-gwtdebug/pom.xml
index 01b93a6..942f507 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.5-SNAPSHOT</version>
+    <version>2.6-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-gwtdebug</artifactId>
@@ -34,11 +34,6 @@
 
   <dependencies>
     <dependency>
-      <groupId>com.google.gwt</groupId>
-      <artifactId>gwt-dev</artifactId>
-    </dependency>
-
-    <dependency>
       <groupId>com.google.gerrit</groupId>
       <artifactId>gerrit-gwtui</artifactId>
       <version>${project.version}</version>
@@ -73,14 +68,12 @@
     <dependency>
       <groupId>bouncycastle</groupId>
       <artifactId>bcprov-jdk15</artifactId>
-      <version>140</version>
       <scope>provided</scope>
     </dependency>
 
     <dependency>
       <groupId>bouncycastle</groupId>
       <artifactId>bcpg-jdk15</artifactId>
-      <version>140</version>
       <scope>provided</scope>
     </dependency>
 
@@ -96,5 +89,12 @@
       <classifier>sources</classifier>
       <scope>provided</scope>
     </dependency>
+
+     <!-- Workaround for overwriting our dependencies (like args4j) by additional
+      classes put in gwt-dev.jar -->
+    <dependency>
+      <groupId>com.google.gwt</groupId>
+      <artifactId>gwt-dev</artifactId>
+    </dependency>
   </dependencies>
 </project>
diff --git a/gerrit-gwtui/pom.xml b/gerrit-gwtui/pom.xml
index b3291d1..8d807a5 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.5-SNAPSHOT</version>
+    <version>2.6-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-gwtui</artifactId>
@@ -202,16 +202,32 @@
       <plugin>
         <groupId>org.codehaus.mojo</groupId>
         <artifactId>gwt-maven-plugin</artifactId>
-        <configuration>
-          <module>${GerritGwtUI.browserType}</module>
-          <extraJvmArgs>-Xmx512m</extraJvmArgs>
-          <compileReport>${gwt.compileReport}</compileReport>
-          <disableClassMetadata>true</disableClassMetadata>
-          <disableCastChecking>true</disableCastChecking>
-          <draftCompile>${GerritGwtUI.draftCompile}</draftCompile>
-        </configuration>
         <executions>
           <execution>
+            <id>optimized</id>
+            <configuration>
+              <module>${GerritGwtUI.browserType}</module>
+              <extraJvmArgs>-Xmx512m</extraJvmArgs>
+              <compileReport>${gwt.compileReport}</compileReport>
+              <disableClassMetadata>true</disableClassMetadata>
+              <disableCastChecking>true</disableCastChecking>
+              <draftCompile>${GerritGwtUI.draftCompile}</draftCompile>
+            </configuration>
+            <goals>
+              <goal>compile</goal>
+            </goals>
+          </execution>
+          <execution>
+            <id>debug</id>
+            <configuration>
+              <style>PRETTY</style>
+              <module>${GerritGwtUI.browserType}</module>
+              <extraJvmArgs>-Xmx512m</extraJvmArgs>
+              <disableClassMetadata>true</disableClassMetadata>
+              <disableRunAsync>true</disableRunAsync>
+              <draftCompile>true</draftCompile>
+              <webappDirectory>${project.build.directory}/${project.build.finalName}_dbg</webappDirectory>
+            </configuration>
             <goals>
               <goal>compile</goal>
             </goals>
@@ -232,7 +248,8 @@
             <configuration>
               <target>
                 <property name="dst" location="${project.build.directory}/${project.build.finalName}"/>
-                <property name="app" location="${dst}/gerrit"/>
+                <property name="dbg" location="${project.build.directory}/${project.build.finalName}_dbg"/>
+                <property name="app" location="${dst}/gerrit_ui"/>
 
                 <mkdir dir="${app}"/>
                 <apply executable="gzip" addsourcefile="false">
@@ -247,6 +264,16 @@
                     <outputmapper type="glob" from="*" to="${app}/*.gz"/>
                   </redirector>
                 </apply>
+
+                <copy file="${dbg}/gerrit_ui/gerrit_ui.nocache.js"
+                      tofile="${app}/gerrit_dbg.nocache.js"/>
+                <copy todir="${app}" overwrite="false">
+                  <fileset dir="${dbg}/gerrit_ui">
+                    <include name="**/*.html"/>
+                    <include name="**/*.css"/>
+                    <include name="deferredjs/**/*.js"/>
+                  </fileset>
+                </copy>
               </target>
             </configuration>
           </execution>
@@ -257,7 +284,7 @@
         <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-war-plugin</artifactId>
         <configuration>
-          <packagingExcludes>WEB-INF/classes/**,WEB-INF/lib/**</packagingExcludes>
+          <packagingExcludes>WEB-INF/classes/**,WEB-INF/deploy/**,WEB-INF/lib/**</packagingExcludes>
           <attachClasses>true</attachClasses>
           <archive>
             <addMavenDescriptor>false</addMavenDescriptor>
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 8555c75..5fdc5bb 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
@@ -13,7 +13,7 @@
  See the License for the specific language governing permissions and
  limitations under the License.
 -->
-<module rename-to="gerrit">
+<module rename-to="gerrit_ui">
   <inherits name='com.google.gwt.editor.Editor'/>
   <inherits name='com.google.gwt.user.User'/>
   <inherits name='com.google.gwt.resources.Resources'/>
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUIgecko1_8.gwt.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUIgecko1_8.gwt.xml
index d1a9fe4..d7e835f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUIgecko1_8.gwt.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUIgecko1_8.gwt.xml
@@ -13,7 +13,7 @@
  See the License for the specific language governing permissions and
  limitations under the License.
 -->
-<module rename-to="gerrit">
+<module rename-to="gerrit_ui">
   <inherits name='com.google.gerrit.GerritGwtUI'/>
   <set-property name="user.agent" value="gecko1_8" />
   <set-property name="locale" value="default" />
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUIsafari.gwt.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUIsafari.gwt.xml
index 7655c1f..88bea84 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUIsafari.gwt.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/GerritGwtUIsafari.gwt.xml
@@ -13,7 +13,7 @@
  See the License for the specific language governing permissions and
  limitations under the License.
 -->
-<module rename-to="gerrit">
+<module rename-to="gerrit_ui">
   <inherits name='com.google.gerrit.GerritGwtUI'/>
   <set-property name="user.agent" value="safari" />
   <set-property name="locale" value="default" />
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/AvatarImage.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/AvatarImage.java
new file mode 100644
index 0000000..0059723
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/AvatarImage.java
@@ -0,0 +1,69 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.client.rpc.RestApi;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gwt.event.dom.client.ErrorEvent;
+import com.google.gwt.event.dom.client.ErrorHandler;
+import com.google.gwt.user.client.ui.Image;
+
+public class AvatarImage extends Image {
+
+  /** A default sized avatar image. */
+  public AvatarImage(Account.Id account) {
+    this(account, 0);
+  }
+
+  /**
+   * An avatar image for the given account using the requested size.
+   *
+   * @param account The account in which we are interested
+   * @param size A requested size. Note that the size can be ignored depending
+   *        on the avatar provider. A size <= 0 indicates to let the provider
+   *        decide a default size.
+   */
+  public AvatarImage(Account.Id account, int size) {
+    super(url(account, size));
+
+    if (size > 0) {
+      // If the provider does not resize the image, force it in the browser.
+      setSize(size + "px", size + "px");
+    }
+
+    addErrorHandler(new ErrorHandler() {
+      @Override
+      public void onError(ErrorEvent event) {
+        // We got a 404, don't bother showing the image. Either the user doesn't
+        // have an avatar or there is no avatar provider plugin installed.
+        setVisible(false);
+      }
+    });
+  }
+
+  private static String url(Account.Id id, int size) {
+    String u;
+    if (Gerrit.isSignedIn() && id.equals(Gerrit.getUserAccount().getId())) {
+      u = "self";
+    } else {
+      u = id.toString();
+    }
+    RestApi api = new RestApi("/accounts/").id(u).view("avatar");
+    if (size > 0) {
+      api.addParameter("s", size);
+    }
+    return api.url();
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/CurrentUserPopupPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/CurrentUserPopupPanel.java
new file mode 100644
index 0000000..7983f9c
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/CurrentUserPopupPanel.java
@@ -0,0 +1,67 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.PageLinks;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.uibinder.client.UiBinder;
+import com.google.gwt.uibinder.client.UiField;
+import com.google.gwt.user.client.ui.Anchor;
+import com.google.gwt.user.client.ui.Label;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwtexpui.user.client.PluginSafePopupPanel;
+
+public class CurrentUserPopupPanel extends PluginSafePopupPanel {
+  interface Binder extends UiBinder<Widget, CurrentUserPopupPanel> {
+  }
+
+  private static final Binder binder = GWT.create(Binder.class);
+
+  @UiField(provided = true)
+  AvatarImage avatar;
+  @UiField
+  Label userName;
+  @UiField
+  Label userEmail;
+  @UiField
+  Anchor logout;
+  @UiField
+  Anchor settings;
+
+  public CurrentUserPopupPanel(Account account, boolean canLogOut) {
+    super(/* auto hide */true, /* modal */false);
+    avatar = new AvatarImage(account.getId(), 100);
+    setWidget(binder.createAndBindUi(this));
+    // We must show and then hide this popup so that it is part of the DOM.
+    // Otherwise the image does not get any events.  Calling hide() would
+    // remove it from the DOM so we use setVisible(false) instead.
+    show();
+    setVisible(false);
+    setStyleName(Gerrit.RESOURCES.css().userInfoPopup());
+    if (account.getFullName() != null) {
+      userName.setText(account.getFullName());
+    }
+    if (account.getPreferredEmail() != null) {
+      userEmail.setText(account.getPreferredEmail());
+    }
+    if (canLogOut) {
+      logout.setHref(Gerrit.selfRedirect("/logout"));
+    } else {
+      logout.setVisible(false);
+    }
+    settings.setHref(Gerrit.selfRedirect(PageLinks.SETTINGS));
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/CurrentUserPopupPanel.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/CurrentUserPopupPanel.ui.xml
new file mode 100644
index 0000000..0db5788
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/CurrentUserPopupPanel.ui.xml
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2012 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<!DOCTYPE ui:UiBinder SYSTEM "http://dl.google.com/gwt/DTD/xhtml.ent">
+<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
+  xmlns:g='urn:import:com.google.gwt.user.client.ui'
+  xmlns:gerrit='urn:import:com.google.gerrit.client'>
+  <ui:with field='constants' type='com.google.gerrit.client.GerritConstants'/>
+
+  <ui:style>
+    .panel {
+      padding: 8px;
+    }
+    .avatar {
+      padding-right: 4px;
+      width: 100px;
+      height: 100px;
+    }
+    .infoCell {
+      vertical-align: top;
+    }
+    .userName {
+      font-weight: bold;
+    }
+    .email {
+      padding-bottom: 6px;
+    }
+    .logout {
+      padding-left: 16px;
+      float: right;
+    }
+  </ui:style>
+
+  <g:HTMLPanel styleName='{style.panel}'>
+    <table><tr><td>
+      <gerrit:AvatarImage ui:field='avatar' styleName='{style.avatar}' />
+    </td><td class='{style.infoCell}'>
+      <g:Label ui:field='userName' styleName="{style.userName}" />
+      <g:Label ui:field='userEmail' styleName="{style.email}" />
+    </td></tr></table>
+    <g:Anchor ui:field='settings'>
+      <ui:text from='{constants.menuSettings}' />
+    </g:Anchor>
+    <g:Anchor ui:field='logout' styleName="{style.logout}">
+      <ui:text from='{constants.menuSignOut}' />
+    </g:Anchor>
+  </g:HTMLPanel>
+</ui:UiBinder>
\ No newline at end of file
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 aefad27..c8b1e48 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/Dispatcher.java
@@ -17,9 +17,11 @@
 import static com.google.gerrit.common.PageLinks.ADMIN_CREATE_GROUP;
 import static com.google.gerrit.common.PageLinks.ADMIN_CREATE_PROJECT;
 import static com.google.gerrit.common.PageLinks.ADMIN_GROUPS;
-import static com.google.gerrit.common.PageLinks.ADMIN_PROJECTS;
 import static com.google.gerrit.common.PageLinks.ADMIN_PLUGINS;
+import static com.google.gerrit.common.PageLinks.ADMIN_PROJECTS;
+import static com.google.gerrit.common.PageLinks.DASHBOARDS;
 import static com.google.gerrit.common.PageLinks.MINE;
+import static com.google.gerrit.common.PageLinks.PROJECTS;
 import static com.google.gerrit.common.PageLinks.REGISTER;
 import static com.google.gerrit.common.PageLinks.SETTINGS;
 import static com.google.gerrit.common.PageLinks.SETTINGS_AGREEMENTS;
@@ -54,24 +56,26 @@
 import com.google.gerrit.client.admin.PluginListScreen;
 import com.google.gerrit.client.admin.ProjectAccessScreen;
 import com.google.gerrit.client.admin.ProjectBranchesScreen;
+import com.google.gerrit.client.admin.ProjectDashboardsScreen;
 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;
 import com.google.gerrit.client.changes.ChangeScreen;
 import com.google.gerrit.client.changes.CustomDashboardScreen;
 import com.google.gerrit.client.changes.PatchTable;
+import com.google.gerrit.client.changes.ProjectDashboardScreen;
 import com.google.gerrit.client.changes.PublishCommentScreen;
 import com.google.gerrit.client.changes.QueryScreen;
+import com.google.gerrit.client.dashboards.DashboardInfo;
+import com.google.gerrit.client.dashboards.DashboardList;
+import com.google.gerrit.client.groups.GroupApi;
+import com.google.gerrit.client.groups.GroupInfo;
 import com.google.gerrit.client.patches.PatchScreen;
 import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.RestApi;
 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.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
@@ -81,6 +85,7 @@
 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.http.client.URL;
 import com.google.gwt.user.client.Window;
 import com.google.gwtorm.client.KeyUtil;
 
@@ -137,16 +142,20 @@
     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) {
+    return PageLinks.toGroup(uuid);
   }
 
   public static String toGroup(AccountGroup.UUID uuid, String panel) {
-    return "/admin/groups/uuid-" + uuid.toString() + "," + panel;
+    return toGroup(uuid) + "," + panel;
+  }
+
+  public static String toProject(Project.NameKey n) {
+    return toProjectAdmin(n, ProjectScreen.getSavedPanel());
   }
 
   public static String toProjectAdmin(Project.NameKey n, String panel) {
-    if (ProjectScreen.INFO.equals(panel)) {
+    if (panel == null || ProjectScreen.INFO.equals(panel)) {
       return "/admin/projects/" + n.toString();
     }
     return "/admin/projects/" + n.toString() + "," + panel;
@@ -186,6 +195,9 @@
     } else if (matchPrefix("/dashboard/", token)) {
       dashboard(token);
 
+    } else if (matchPrefix(PROJECTS, token)) {
+      projects(token);
+
     } else if (matchExact(SETTINGS, token) //
         || matchPrefix("/settings/", token) //
         || matchExact("register", token) //
@@ -384,6 +396,52 @@
     Gerrit.display(token, new NotFoundScreen());
   }
 
+  private static void projects(final String token) {
+    String rest = skip(token);
+    int c = rest.indexOf(DASHBOARDS);
+    if (0 <= c) {
+      final String project = URL.decodePathSegment(rest.substring(0, c));
+      rest = rest.substring(c);
+      if (matchPrefix(DASHBOARDS, rest)) {
+        final String dashboardId = skip(rest);
+        GerritCallback<DashboardInfo> cb = new GerritCallback<DashboardInfo>() {
+          @Override
+          public void onSuccess(DashboardInfo result) {
+            if (matchPrefix("/dashboard/", result.url())) {
+              String params = skip(result.url()).substring(1);
+              ProjectDashboardScreen dash = new ProjectDashboardScreen(
+                  new Project.NameKey(project), params);
+              Gerrit.display(token, dash);
+            }
+          }
+
+          @Override
+          public void onFailure(Throwable caught) {
+            if ("default".equals(dashboardId) && RestApi.isNotFound(caught)) {
+              Gerrit.display(PageLinks.toChangeQuery(
+                  PageLinks.projectQuery(new Project.NameKey(project))));
+            } else {
+              super.onFailure(caught);
+            }
+          }
+        };
+        if ("default".equals(dashboardId)) {
+          DashboardList.getDefault(new Project.NameKey(project), cb);
+          return;
+        }
+        c = dashboardId.indexOf(":");
+        if (0 <= c) {
+          final String ref = URL.decodeQueryString(dashboardId.substring(0, c));
+          final String path = URL.decodeQueryString(dashboardId.substring(c + 1));
+          DashboardList.get(new Project.NameKey(project), ref + ":" + path, cb);
+          return;
+        }
+      }
+    }
+
+    Gerrit.display(token, new NotFoundScreen());
+  }
+
   private static void change(final String token) {
     String rest = skip(token);
     int c = rest.lastIndexOf(',');
@@ -572,30 +630,6 @@
         if (matchPrefix("/VE/", token) || matchPrefix("VE,", token))
           return new ValidateEmailScreen(skip(token));
 
-        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;
-          switch (Gerrit.getConfig().getAuthType()) {
-            case OPENID:
-              new OpenIdSignInDialog(mode, to, msg).center();
-              break;
-            case LDAP:
-            case LDAP_BIND:
-              new UserPassSignInDialog(to, msg).center();
-              break;
-            default:
-              return null;
-          }
-          switch (mode) {
-            case SIGN_IN:
-              return QueryScreen.forQuery("status:open");
-            case LINK_IDENTIY:
-              return new MyIdentitiesScreen();
-          }
-        }
-
         if (matchExact(SETTINGS_NEW_AGREEMENT, token))
           return new NewAgreementScreen();
 
@@ -616,14 +650,36 @@
           Gerrit.display(token, new GroupListScreen());
 
         } else if (matchPrefix("/admin/groups/", token)) {
-          group();
+          String rest = skip(token);
+          if (rest.startsWith("?")) {
+            Gerrit.display(token, new GroupListScreen(rest.substring(1)));
+          } else {
+            group();
+          }
+
+        } else if (matchPrefix("/admin/groups", token)) {
+          String rest = skip(token);
+          if (rest.startsWith("?")) {
+            Gerrit.display(token, new GroupListScreen(rest.substring(1)));
+          }
 
         } 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());
+            String rest = skip(token);
+            if (rest.startsWith("?")) {
+              Gerrit.display(token, new ProjectListScreen(rest.substring(1)));
+            } else {
+              Gerrit.display(token, selectProject());
+            }
+
+        } else if (matchPrefix("/admin/projects", token)) {
+          String rest = skip(token);
+          if (rest.startsWith("?")) {
+            Gerrit.display(token, new ProjectListScreen(rest.substring(1)));
+          }
 
         } else if (matchPrefix(ADMIN_PLUGINS, token)
             || matchExact("/admin/plugins", token)) {
@@ -644,27 +700,26 @@
 
       private void group() {
         final String panel;
-        AccountGroup.Id groupId = null;
-        AccountGroup.UUID groupUUID = null;
+        final String group;
 
         if (matchPrefix("/admin/groups/uuid-", token)) {
           String p = skip(token);
           int c = p.indexOf(',');
           if (c < 0) {
-            groupUUID = AccountGroup.UUID.parse(p);
+            group = p;
             panel = null;
           } else {
-            groupUUID = AccountGroup.UUID.parse(p.substring(0, c));
+            group = 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);
+            group = p;
             panel = null;
           } else {
-            groupId = AccountGroup.Id.parse(p.substring(0, c));
+            group = p.substring(0, c);
             panel = p.substring(c + 1);
           }
         } else {
@@ -672,37 +727,33 @@
           return;
         }
 
-        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());
-                }
+        GroupApi.getGroupDetail(group, new GerritCallback<GroupInfo>() {
+          @Override
+          public void onSuccess(GroupInfo group) {
+            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 (AccountGroup.isInternalGroup(group.getGroupUUID())
+                  && !AccountGroup.isSystemGroup(group.getGroupUUID())) {
+                Gerrit.display(toGroup(group.getGroupId(), AccountGroupScreen.MEMBERS),
+                    new AccountGroupMembersScreen(group, token));
+              } else {
+                Gerrit.display(toGroup(group.getGroupId(), AccountGroupScreen.INFO),
+                    new AccountGroupInfoScreen(group, token));
               }
-            });
+            } else if (AccountGroupScreen.INFO.equals(panel)) {
+              Gerrit.display(token, new AccountGroupInfoScreen(group, token));
+            } else if (AccountGroupScreen.MEMBERS.equals(panel)) {
+              Gerrit.display(token, new AccountGroupMembersScreen(group, token));
+            } else {
+              Gerrit.display(token, new NotFoundScreen());
+            }
+          }
+        });
       }
 
       private Screen selectProject() {
@@ -729,6 +780,10 @@
           if (ProjectScreen.ACCESS.equals(panel)) {
             return new ProjectAccessScreen(k);
           }
+
+          if (ProjectScreen.DASHBOARDS.equals(panel)) {
+            return new ProjectDashboardsScreen(k);
+          }
         }
         return new NotFoundScreen();
       }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ErrorDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ErrorDialog.java
index 13bba12..3adec8f 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ErrorDialog.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ErrorDialog.java
@@ -14,11 +14,13 @@
 
 package com.google.gerrit.client;
 
+import com.google.gerrit.client.rpc.RestApi;
 import com.google.gerrit.client.rpc.RpcConstants;
 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.dom.client.KeyPressHandler;
+import com.google.gwt.http.client.Response;
 import com.google.gwt.user.client.DOM;
 import com.google.gwt.user.client.Window;
 import com.google.gwt.user.client.rpc.StatusCodeException;
@@ -104,34 +106,54 @@
   public ErrorDialog(final Throwable what) {
     this();
 
-    String cn;
-    if (what instanceof RemoteJsonException) {
-      cn = RpcConstants.C.errorRemoteJsonException();
+    String hdr;
+    String msg;
 
-    } else if (what instanceof StatusCodeException) {
-      cn = RpcConstants.C.errorServerUnavailable();
+    if (what instanceof StatusCodeException) {
+      StatusCodeException sc = (StatusCodeException) what;
+      if (RestApi.isExpected(sc.getStatusCode())) {
+        hdr = null;
+        msg = sc.getEncodedResponse();
+      } else if (sc.getStatusCode() == Response.SC_INTERNAL_SERVER_ERROR) {
+        hdr = null;
+        msg = what.getMessage();
+      } else {
+        hdr = RpcConstants.C.errorServerUnavailable();
+        msg = what.getMessage();
+      }
+
+    } else if (what instanceof RemoteJsonException) {
+      // TODO Remove RemoteJsonException from Gerrit sources.
+      hdr = RpcConstants.C.errorRemoteJsonException();
+      msg = what.getMessage();
 
     } else {
-      cn = what.getClass().getName();
-      if (cn.startsWith("java.lang.")) {
-        cn = cn.substring("java.lang.".length());
-      } else if (cn.startsWith("com.google.gerrit.")) {
-        cn = cn.substring(cn.lastIndexOf('.') + 1);
+      // TODO Fix callers of ErrorDialog to stop passing random types.
+      hdr = what.getClass().getName();
+      if (hdr.startsWith("java.lang.")) {
+        hdr = hdr.substring("java.lang.".length());
+      } else if (hdr.startsWith("com.google.gerrit.")) {
+        hdr = hdr.substring(hdr.lastIndexOf('.') + 1);
       }
-      if (cn.endsWith("Exception")) {
-        cn = cn.substring(0, cn.length() - "Exception".length());
-      } else if (cn.endsWith("Error")) {
-        cn = cn.substring(0, cn.length() - "Error".length());
+      if (hdr.endsWith("Exception")) {
+        hdr = hdr.substring(0, hdr.length() - "Exception".length());
+      } else if (hdr.endsWith("Error")) {
+        hdr = hdr.substring(0, hdr.length() - "Error".length());
       }
+      msg = what.getMessage();
     }
 
-    final Label r = new Label(cn);
-    r.setStyleName(Gerrit.RESOURCES.css().errorDialogErrorType());
-    body.add(r);
+    if (hdr != null) {
+      final Label r = new Label(hdr);
+      r.setStyleName(Gerrit.RESOURCES.css().errorDialogErrorType());
+      body.add(r);
+    }
 
-    final Label m = new Label(what.getMessage());
-    DOM.setStyleAttribute(m.getElement(),"whiteSpace","pre");
-    body.add(m);
+    if (msg != null) {
+      final Label m = new Label(msg);
+      DOM.setStyleAttribute(m.getElement(), "whiteSpace", "pre");
+      body.add(m);
+    }
   }
 
   public void setText(final String t) {
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 f10762a..5448dc0 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
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.client;
 
-import com.google.gerrit.common.data.AccountInfo;
+import com.google.gerrit.client.account.AccountInfo;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
 import com.google.gwt.i18n.client.DateTimeFormat;
@@ -119,13 +119,8 @@
     }
   }
 
-  /** Format an account as a name and email address. */
-  public static String nameEmail(final Account acct) {
-    return nameEmail(new AccountInfo(acct));
-  }
-
   /**
-   * Formats an account as an name and an email address.
+   * Formats an account as a name and an email address.
    * <p>
    * Example output:
    * <ul>
@@ -135,22 +130,17 @@
    * <li><code>Anonymous Coward (12)</code>: missing name and email address</li>
    * </ul>
    */
-  public static String nameEmail(final AccountInfo acct) {
-    String name = acct.getFullName();
-    if (name == null) {
+  public static String nameEmail(AccountInfo info) {
+    String name = info.name();
+    if (name == null || name.trim().isEmpty()) {
       name = Gerrit.getConfig().getAnonymousCowardName();
     }
 
-    final StringBuilder b = new StringBuilder();
-    b.append(name);
-    if (acct.getPreferredEmail() != null) {
-      b.append(" <");
-      b.append(acct.getPreferredEmail());
-      b.append(">");
-    } else if (acct.getId() != null) {
-      b.append(" (");
-      b.append(acct.getId().get());
-      b.append(")");
+    StringBuilder b = new StringBuilder().append(name);
+    if (info.email() != null) {
+      b.append(" <").append(info.email()).append(">");
+    } else if (info._account_id() > 0) {
+      b.append(" (").append(info._account_id()).append(")");
     }
     return b.toString();
   }
@@ -161,13 +151,45 @@
    * If the account has a full name, it returns only the full name. Otherwise it
    * returns a longer form that includes the email address.
    */
-  public static String name(final AccountInfo ai) {
-    if (ai.getFullName() != null) {
-      return ai.getFullName();
+  public static String name(Account acct) {
+    return name(asInfo(acct));
+  }
+
+  /**
+   * Formats an account name.
+   * <p>
+   * If the account has a full name, it returns only the full name. Otherwise it
+   * returns a longer form that includes the email address.
+   */
+  public static String name(AccountInfo ai) {
+    if (ai.name() != null && !ai.name().trim().isEmpty()) {
+      return ai.name();
     }
-    if (ai.getPreferredEmail() != null) {
-      return ai.getPreferredEmail();
+    String email = ai.email();
+    if (email != null) {
+      int at = email.indexOf('@');
+      return 0 < at ? email.substring(0, at) : email;
     }
     return nameEmail(ai);
   }
+
+  private static AccountInfo asInfo(Account acct) {
+    if (acct == null) {
+      return AccountInfo.create(0, null, null);
+    }
+    return AccountInfo.create(
+        acct.getId() != null ? acct.getId().get() : 0,
+        acct.getFullName(),
+        acct.getPreferredEmail());
+  }
+
+  public static AccountInfo asInfo(com.google.gerrit.common.data.AccountInfo acct) {
+    if (acct == null) {
+      return AccountInfo.create(0, null, null);
+    }
+    return AccountInfo.create(
+        acct.getId() != null ? acct.getId().get() : 0,
+        acct.getFullName(),
+        acct.getPreferredEmail());
+  }
 }
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 8fe658b..d67a4ca 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
@@ -15,13 +15,11 @@
 package com.google.gerrit.client;
 
 import static com.google.gerrit.common.data.GlobalCapability.ADMINISTRATE_SERVER;
-import static com.google.gerrit.common.data.GlobalCapability.CREATE_PROJECT;
 import static com.google.gerrit.common.data.GlobalCapability.CREATE_GROUP;
+import static com.google.gerrit.common.data.GlobalCapability.CREATE_PROJECT;
 
 import com.google.gerrit.client.account.AccountCapabilities;
-import com.google.gerrit.client.auth.openid.OpenIdSignInDialog;
-import com.google.gerrit.client.auth.openid.OpenIdSsoPanel;
-import com.google.gerrit.client.auth.userpass.UserPassSignInDialog;
+import com.google.gerrit.client.admin.ProjectScreen;
 import com.google.gerrit.client.changes.ChangeConstants;
 import com.google.gerrit.client.changes.ChangeListScreen;
 import com.google.gerrit.client.patches.PatchScreen;
@@ -31,9 +29,9 @@
 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.client.ui.ScreenLoadEvent;
 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;
@@ -42,12 +40,24 @@
 import com.google.gerrit.reviewdb.client.AccountDiffPreference;
 import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
 import com.google.gerrit.reviewdb.client.AuthType;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gwt.aria.client.Roles;
+import com.google.gwt.core.client.Callback;
 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.core.client.ScriptInjector;
 import com.google.gwt.dom.client.AnchorElement;
+import com.google.gwt.dom.client.Document;
+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.KeyDownEvent;
+import com.google.gwt.event.dom.client.KeyDownHandler;
 import com.google.gwt.event.logical.shared.ValueChangeEvent;
 import com.google.gwt.event.logical.shared.ValueChangeHandler;
+import com.google.gwt.event.shared.EventBus;
+import com.google.gwt.event.shared.SimpleEventBus;
 import com.google.gwt.http.client.URL;
 import com.google.gwt.http.client.UrlBuilder;
 import com.google.gwt.user.client.Command;
@@ -55,9 +65,9 @@
 import com.google.gwt.user.client.History;
 import com.google.gwt.user.client.Window;
 import com.google.gwt.user.client.Window.Location;
-import com.google.gwt.user.client.ui.Accessibility;
 import com.google.gwt.user.client.ui.Anchor;
 import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.FocusPanel;
 import com.google.gwt.user.client.ui.Grid;
 import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
 import com.google.gwt.user.client.ui.InlineHTML;
@@ -67,9 +77,12 @@
 import com.google.gwtexpui.clippy.client.CopyableLabel;
 import com.google.gwtexpui.user.client.UserAgent;
 import com.google.gwtexpui.user.client.ViewSite;
+import com.google.gwtjsonrpc.client.CallbackHandle;
 import com.google.gwtjsonrpc.client.JsonDefTarget;
 import com.google.gwtjsonrpc.client.JsonUtil;
 import com.google.gwtjsonrpc.client.XsrfManager;
+import com.google.gwtjsonrpc.client.impl.ResultDeserializer;
+import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.gwtorm.client.KeyUtil;
 
 import java.util.ArrayList;
@@ -81,17 +94,19 @@
   public static final GerritResources RESOURCES =
       GWT.create(GerritResources.class);
   public static final SystemInfoService SYSTEM_SVC;
+  public static final EventBus EVENT_BUS = GWT.create(SimpleEventBus.class);
 
   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 String xGerritAuth;
 
   private static MorphingTabPanel menuLeft;
   private static LinkMenuBar menuRight;
   private static LinkMenuBar diffBar;
+  private static LinkMenuBar projectsBar;
   private static RootPanel siteHeader;
   private static RootPanel siteFooter;
   private static SearchPanel searchPanel;
@@ -233,6 +248,11 @@
     return myAccount;
   }
 
+  /** @return access token to prove user identity during REST API calls. */
+  public static String getXGerritAuth() {
+    return xGerritAuth;
+  }
+
   /** @return the currently signed in users's diff preferences; null if no diff preferences defined for the account */
   public static AccountDiffPreference getAccountDiffPreference() {
     return myAccountDiffPref;
@@ -249,34 +269,7 @@
 
   /** Sign the user into the application. */
   public static void doSignIn(String token) {
-    switch (myConfig.getAuthType()) {
-      case HTTP:
-      case HTTP_LDAP:
-      case CLIENT_SSL_CERT_LDAP:
-      case CUSTOM_EXTENSION:
-        Location.assign(loginRedirect(token));
-        break;
-
-      case DEVELOPMENT_BECOME_ANY_ACCOUNT:
-        Location.assign(selfRedirect("/become"));
-        break;
-
-      case OPENID_SSO:
-        final RootPanel gBody = RootPanel.get("gerrit_body");
-        OpenIdSsoPanel singleSignOnPanel = new OpenIdSsoPanel();
-        gBody.add(singleSignOnPanel);
-        singleSignOnPanel.authenticate(SignInMode.SIGN_IN, token);
-        break;
-
-      case OPENID:
-        new OpenIdSignInDialog(SignInMode.SIGN_IN, token, null).center();
-        break;
-
-      case LDAP:
-      case LDAP_BIND:
-        new UserPassSignInDialog(token, null).center();
-        break;
-    }
+    Location.assign(loginRedirect(token));
   }
 
   public static String loginRedirect(String token) {
@@ -327,7 +320,7 @@
   static void deleteSessionCookie() {
     myAccount = null;
     myAccountDiffPref = null;
-    xsrfToken = null;
+    xGerritAuth = null;
     refreshMenuBar();
 
     // If the cookie was HttpOnly, this request to delete it will
@@ -370,18 +363,20 @@
 
     final HostPageDataService hpd = GWT.create(HostPageDataService.class);
     hpd.load(new GerritCallback<HostPageData>() {
+      @Override
       public void onSuccess(final HostPageData result) {
+        Document.get().getElementById("gerrit_hostpagedata").removeFromParent();
         myConfig = result.config;
         myTheme = result.theme;
         if (result.account != null) {
           myAccount = result.account;
-          xsrfToken = result.xsrfToken;
+          xGerritAuth = result.xGerritAuth;
         }
         if (result.accountDiffPref != null) {
           myAccountDiffPref = result.accountDiffPref;
           applyUserPreferences();
         }
-        onModuleLoad2();
+        onModuleLoad2(result);
       }
     });
   }
@@ -463,7 +458,7 @@
     btmmenu.add(poweredBy);
   }
 
-  private void onModuleLoad2() {
+  private void onModuleLoad2(HostPageData hpd) {
     RESOURCES.gwt_override().ensureInjected();
     RESOURCES.css().ensureInjected();
 
@@ -484,8 +479,8 @@
     gTopMenu.add(menuLine);
     final FlowPanel menuRightPanel = new FlowPanel();
     menuRightPanel.setStyleName(RESOURCES.css().topmenuMenuRight());
-    menuRightPanel.add(menuRight);
     menuRightPanel.add(searchPanel);
+    menuRightPanel.add(menuRight);
     menuLine.setWidget(0, 0, menuLeft);
     menuLine.setWidget(0, 1, new FlowPanel());
     menuLine.setWidget(0, 2, menuRightPanel);
@@ -522,7 +517,7 @@
     JsonUtil.setDefaultXsrfManager(new XsrfManager() {
       @Override
       public String getToken(JsonDefTarget proxy) {
-        return xsrfToken;
+        return xGerritAuth;
       }
 
       @Override
@@ -541,6 +536,7 @@
     refreshMenuBar();
 
     History.addValueChangeHandler(new ValueChangeHandler<String>() {
+      @Override
       public void onValueChange(final ValueChangeEvent<String> event) {
         display(event.getValue());
       }
@@ -556,7 +552,50 @@
     if (signInAnchor != null) {
       signInAnchor.setHref(loginRedirect(token));
     }
-    display(token);
+    loadPlugins(hpd, token);
+  }
+
+  private void loadPlugins(HostPageData hpd, final String token) {
+    if (hpd.plugins != null) {
+      for (final String url : hpd.plugins) {
+        ScriptInjector.fromUrl(url)
+            .setWindow(ScriptInjector.TOP_WINDOW)
+            .setCallback(new Callback<Void, Exception>() {
+              @Override
+              public void onSuccess(Void result) {
+              }
+
+              @Override
+              public void onFailure(Exception reason) {
+                ErrorDialog d = new ErrorDialog(reason);
+                d.setTitle(M.pluginFailed(url));
+                d.center();
+              }
+            }).inject();
+      }
+    }
+
+    CallbackHandle<Void> cb = new CallbackHandle<Void>(
+        new ResultDeserializer<Void>() {
+          @Override
+          public Void fromResult(JavaScriptObject responseObject) {
+            return null;
+          }
+        },
+        new AsyncCallback<Void>() {
+          @Override
+          public void onFailure(Throwable caught) {
+          }
+
+          @Override
+          public void onSuccess(Void result) {
+            display(token);
+          }
+        });
+    cb.install();
+    ScriptInjector.fromString(cb.getFunctionName() + "();")
+        .setWindow(ScriptInjector.TOP_WINDOW)
+        .inject();
   }
 
   public static void refreshMenuBar() {
@@ -577,9 +616,9 @@
       m = new LinkMenuBar();
       addLink(m, C.menuMyChanges(), PageLinks.MINE);
       addLink(m, C.menuMyDrafts(), PageLinks.toChangeQuery("is:draft"));
+      addLink(m, C.menuMyDraftComments(), PageLinks.toChangeQuery("has:draft"));
       addLink(m, C.menuMyWatchedChanges(), PageLinks.toChangeQuery("is:watched status:open"));
       addLink(m, C.menuMyStarredChanges(), PageLinks.toChangeQuery("is:starred"));
-      addLink(m, C.menuMyDraftComments(), PageLinks.toChangeQuery("has:draft"));
       menuLeft.add(m, C.menuMine());
       menuLeft.selectTab(1);
     } else {
@@ -596,44 +635,43 @@
     addDiffLink(diffBar, C.menuDiffPatchSets(), PatchScreen.TopView.PATCH_SETS);
     addDiffLink(diffBar, C.menuDiffFiles(), PatchScreen.TopView.FILES);
 
-    final LinkMenuBar projectsBar = new LinkMenuBar();
+    projectsBar = new LinkMenuBar() {
+      @Override
+      public void onScreenLoad(ScreenLoadEvent event) {
+        if (event.getScreen() instanceof ProjectScreen) {
+          menuLeft.selectTab(menuLeft.getWidgetIndex(this));
+        }
+      }
+    };
     addLink(projectsBar, C.menuProjectsList(), PageLinks.ADMIN_PROJECTS);
-    if(signedIn) {
+    addProjectLink(projectsBar, C.menuProjectsInfo(), ProjectScreen.INFO);
+    addProjectLink(projectsBar, C.menuProjectsBranches(), ProjectScreen.BRANCH);
+    addProjectLink(projectsBar, C.menuProjectsAccess(), ProjectScreen.ACCESS);
+    addProjectLink(projectsBar, C.menuProjectsDashboards(), ProjectScreen.DASHBOARDS);
+    menuLeft.add(projectsBar, C.menuProjects());
+
+    if (signedIn) {
+      final LinkMenuBar peopleBar = new LinkMenuBar();
+      addLink(peopleBar, C.menuPeopleGroupsList(), PageLinks.ADMIN_GROUPS);
+      menuLeft.add(peopleBar, C.menuPeople());
+
+      final LinkMenuBar pluginsBar = new LinkMenuBar();
       AccountCapabilities.all(new GerritCallback<AccountCapabilities>() {
         @Override
         public void onSuccess(AccountCapabilities result) {
           if (result.canPerform(CREATE_PROJECT)) {
             addLink(projectsBar, C.menuProjectsCreate(), PageLinks.ADMIN_CREATE_PROJECT);
           }
-        }
-      }, CREATE_PROJECT);
-    }
-    menuLeft.add(projectsBar, C.menuProjects());
-
-    if (signedIn) {
-      final LinkMenuBar groupsBar = new LinkMenuBar();
-      addLink(groupsBar, C.menuGroupsList(), PageLinks.ADMIN_GROUPS);
-      AccountCapabilities.all(new GerritCallback<AccountCapabilities>() {
-        @Override
-        public void onSuccess(AccountCapabilities result) {
           if (result.canPerform(CREATE_GROUP)) {
-            addLink(groupsBar, C.menuGroupsCreate(), PageLinks.ADMIN_CREATE_GROUP);
+            addLink(peopleBar, C.menuPeopleGroupsCreate(), PageLinks.ADMIN_CREATE_GROUP);
           }
-        }
-      }, CREATE_GROUP);
-      menuLeft.add(groupsBar, C.menuGroups());
-
-      final LinkMenuBar pluginsBar = new LinkMenuBar();
-      AccountCapabilities.all(new GerritCallback<AccountCapabilities>() {
-        @Override
-        public void onSuccess(AccountCapabilities result) {
           if (result.canPerform(ADMINISTRATE_SERVER)) {
             addLink(pluginsBar, C.menuPluginsInstalled(), PageLinks.ADMIN_PLUGINS);
             menuLeft.insert(pluginsBar, C.menuPlugins(),
-                menuLeft.getWidgetIndex(groupsBar) + 1);
+                menuLeft.getWidgetIndex(peopleBar) + 1);
           }
         }
-      }, ADMINISTRATE_SERVER);
+      }, CREATE_PROJECT, CREATE_GROUP, ADMINISTRATE_SERVER);
     }
 
     if (getConfig().isDocumentationAvailable()) {
@@ -642,15 +680,12 @@
       addDocLink(m, C.menuDocumentationSearch(), "user-search.html");
       addDocLink(m, C.menuDocumentationUpload(), "user-upload.html");
       addDocLink(m, C.menuDocumentationAccess(), "access-control.html");
+      addDocLink(m, C.menuDocumentationAPI(), "rest-api.html");
       menuLeft.add(m, C.menuDocumentation());
     }
 
     if (signedIn) {
-      whoAmI();
-      addLink(menuRight, C.menuSettings(), PageLinks.SETTINGS);
-      if (cfg.getAuthType() != AuthType.CLIENT_SSL_CERT_LDAP) {
-        menuRight.add(anchor(C.menuSignOut(), selfRedirect("/logout")));
-      }
+      whoAmI(cfg.getAuthType() != AuthType.CLIENT_SSL_CERT_LDAP);
     } else {
       switch (cfg.getAuthType()) {
         case HTTP:
@@ -661,8 +696,11 @@
         case OPENID:
           menuRight.addItem(C.menuRegister(), new Command() {
             public void execute() {
-              final String to = History.getToken();
-              new OpenIdSignInDialog(SignInMode.REGISTER, to, null).center();
+              String t = History.getToken();
+              if (t == null) {
+                t = "";
+              }
+              doSignIn(PageLinks.REGISTER + t);
             }
           });
           menuRight.addItem(C.menuSignIn(), new Command() {
@@ -684,7 +722,8 @@
         case LDAP_BIND:
         case CUSTOM_EXTENSION:
           if (cfg.getRegisterUrl() != null) {
-            menuRight.add(anchor(C.menuRegister(), cfg.getRegisterUrl()));
+            final String registerText = cfg.getRegisterText() == null ? C.menuRegister() : cfg.getRegisterText();
+            menuRight.add(anchor(registerText, cfg.getRegisterUrl()));
           }
           menuRight.addItem(C.menuSignIn(), new Command() {
             public void execute() {
@@ -694,7 +733,7 @@
           break;
 
         case DEVELOPMENT_BECOME_ANY_ACCOUNT:
-          menuRight.add(anchor("Become", selfRedirect("/become")));
+          menuRight.add(anchor("Become", loginRedirect("")));
           break;
       }
     }
@@ -714,17 +753,54 @@
     }
   }
 
-  private static void whoAmI() {
-    final String name = FormatUtil.nameEmail(getUserAccount());
-    final InlineLabel l = new InlineLabel(name);
+  private static void whoAmI(boolean canLogOut) {
+    Account account = getUserAccount();
+    final CurrentUserPopupPanel userPopup =
+        new CurrentUserPopupPanel(account, canLogOut);
+    final FlowPanel userSummaryPanel = new FlowPanel();
+    class PopupHandler implements KeyDownHandler, ClickHandler {
+      private void showHidePopup() {
+        if (userPopup.isShowing() && userPopup.isVisible()) {
+          userPopup.hide();
+        } else {
+          userPopup.showRelativeTo(userSummaryPanel);
+        }
+      }
+
+      @Override
+      public void onClick(ClickEvent event) {
+        showHidePopup();
+      }
+
+      @Override
+      public void onKeyDown(KeyDownEvent event) {
+        if(event.getNativeKeyCode() == KeyCodes.KEY_ENTER) {
+          showHidePopup();
+          event.preventDefault();
+        }
+      }
+    }
+    final PopupHandler popupHandler = new PopupHandler();
+    final InlineLabel l = new InlineLabel(FormatUtil.name(account));
     l.setStyleName(RESOURCES.css().menuBarUserName());
-    menuRight.add(l);
+    final AvatarImage avatar = new AvatarImage(account.getId(), 26);
+    avatar.setStyleName(RESOURCES.css().menuBarUserNameAvatar());
+    userSummaryPanel.setStyleName(RESOURCES.css().menuBarUserNamePanel());
+    userSummaryPanel.add(l);
+    userSummaryPanel.add(avatar);
+    userSummaryPanel.add(new InlineLabel(" â–¾"));
+    userPopup.addAutoHidePartner(userSummaryPanel.getElement());
+    FocusPanel fp = new FocusPanel(userSummaryPanel);
+    fp.setStyleName(RESOURCES.css().menuBarUserNameFocusPanel());
+    fp.addKeyDownHandler(popupHandler);
+    fp.addClickHandler(popupHandler);
+    menuRight.add(fp);
   }
 
   private static Anchor anchor(final String text, final String to) {
     final Anchor a = new Anchor(text, to);
     a.setStyleName(RESOURCES.css().menuItem());
-    Accessibility.setRole(a.getElement(), Accessibility.ROLE_MENUITEM);
+    Roles.getMenuitemRole().set(a.getElement());
     return a;
   }
 
@@ -746,6 +822,30 @@
       });
   }
 
+  private static void addProjectLink(final LinkMenuBar m, final String text,
+      final String panel) {
+    m.addItem(new LinkMenuItem(text, "") {
+        @Override
+        public void onScreenLoad(ScreenLoadEvent event) {
+          Screen screen = event.getScreen();
+          Project.NameKey projectKey;
+          if (screen instanceof ProjectScreen) {
+            projectKey = ((ProjectScreen)screen).getProjectKey();
+          } else {
+            projectKey = ProjectScreen.getSavedKey();
+          }
+
+          if (projectKey != null) {
+            setVisible(true);
+            setTargetHistoryToken(Dispatcher.toProjectAdmin(projectKey, panel));
+          } else {
+            setVisible(false);
+          }
+          super.onScreenLoad(event);
+        }
+      });
+  }
+
   private static void addDiffLink(final LinkMenuBar m, final String text,
       final PatchScreen.Type type) {
     m.addItem(new LinkMenuItem(text, "") {
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 87c01cc..683f058 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
@@ -44,7 +44,6 @@
 
   String notFoundTitle();
   String notFoundBody();
-  String nameAlreadyUsedBody();
   String noSuchAccountTitle();
 
   String noSuchGroupTitle();
@@ -71,12 +70,16 @@
 
   String menuProjects();
   String menuProjectsList();
+  String menuProjectsInfo();
+  String menuProjectsBranches();
+  String menuProjectsAccess();
+  String menuProjectsDashboards();
   String menuProjectsCreate();
 
   String menuPeople();
-  String menuGroups();
-  String menuGroupsList();
-  String menuGroupsCreate();
+  String menuPeopleGroupsList();
+  String menuPeopleGroupsCreate();
+
   String menuPlugins();
   String menuPluginsInstalled();
 
@@ -85,6 +88,7 @@
   String menuDocumentationSearch();
   String menuDocumentationUpload();
   String menuDocumentationAccess();
+  String menuDocumentationAPI();
 
   String searchHint();
   String searchButton();
@@ -108,4 +112,6 @@
 
   String projectAccessError();
   String projectAccessProposeForReviewHint();
+
+  String userCannotVoteToolTip();
 }
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 596b3ad..defc7e4 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
@@ -11,7 +11,7 @@
 registerDialogTitle = Code Review - Register New Account
 loginTypeUnsupported = Sign in is not available.
 
-errorDialogTitle = Application Error
+errorDialogTitle = Code Review - Error
 errorDialogContinue = Continue
 
 confirmationDialogOk = OK
@@ -27,7 +27,6 @@
 
 notFoundTitle = 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
 
 noSuchGroupTitle = Code Review - Unknown Group
@@ -54,12 +53,15 @@
 
 menuProjects = Projects
 menuProjectsList = List
+menuProjectsInfo = General
+menuProjectsBranches = Branches
+menuProjectsAccess = Access
+menuProjectsDashboards = Dashboards
 menuProjectsCreate = Create New Project
 
 menuPeople = People
-menuGroups = Groups
-menuGroupsList = List
-menuGroupsCreate = Create New Group
+menuPeopleGroupsList = List Groups
+menuPeopleGroupsCreate = Create New Group
 
 menuPlugins = Plugins
 menuPluginsInstalled = Installed
@@ -69,8 +71,9 @@
 menuDocumentationSearch = Searching
 menuDocumentationUpload = Uploading
 menuDocumentationAccess = Access Controls
+menuDocumentationAPI = REST API
 
-searchHint = Change #, SHA-1, tr:id, owner:email or reviewer:email
+searchHint = Change #, SHA-1, tr:id or owner:email
 searchButton = Search
 
 rpcStatusWorking = Working ...
@@ -92,3 +95,5 @@
 
 projectAccessError = You don't have permissions to modify the access rights for the following refs:
 projectAccessProposeForReviewHint = You may propose these modifications to the project owners by clicking on 'Save for Review'.
+
+userCannotVoteToolTip = User cannot vote in this category
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 9cbf5cd..489ff00 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
@@ -17,22 +17,19 @@
 import com.google.gwt.resources.client.CssResource;
 
 public interface GerritCss extends CssResource {
-  String greenCheckClass();
   String accountContactOnFile();
   String accountContactPrivacyDetails();
   String accountDashboard();
   String accountInfoBlock();
   String accountName();
-  String accountUsername();
   String accountPassword();
+  String accountUsername();
   String activeRow();
+  String addBranch();
   String addMemberTextBox();
   String addReviewer();
-  String removeReviewer();
-  String removeReviewerCell();
   String addSshKeyPanel();
   String addWatchPanel();
-  String approvalCategoryList();
   String approvalTable();
   String approvalhint();
   String approvalrole();
@@ -40,12 +37,13 @@
   String blockHeader();
   String bottomheader();
   String cAPPROVAL();
-  String cID();
   String cLastUpdate();
-  String cPROJECT();
+  String cOWNER();
   String cSUBJECT();
+  String cellsNextToFileComment();
   String changeComments();
   String changeInfoBlock();
+  String changeInfoTopicPanel();
   String changeScreen();
   String changeScreenDescription();
   String changeScreenStarIcon();
@@ -54,23 +52,24 @@
   String changeTypeCell();
   String changeid();
   String closedstate();
-  String commentedActionDialog();
-  String commentedActionMessage();
   String commentCell();
   String commentEditorPanel();
   String commentHolder();
+  String commentHolderLeftmost();
   String commentPanel();
-  String commentPanelBorder();
   String commentPanelAuthorCell();
+  String commentPanelBorder();
   String commentPanelButtons();
   String commentPanelContent();
   String commentPanelDateCell();
   String commentPanelHeader();
   String commentPanelLast();
-  String commentPanelMessage();
   String commentPanelMenuBar();
+  String commentPanelMessage();
   String commentPanelSummary();
   String commentPanelSummaryCell();
+  String commentedActionDialog();
+  String commentedActionMessage();
   String complexHeader();
   String content();
   String contributorAgreementAlreadySubmitted();
@@ -87,25 +86,27 @@
   String diffTextCONTEXT();
   String diffTextDELETE();
   String diffTextFileHeader();
+  String diffTextForBinaryInSideBySide();
   String diffTextHunkHeader();
   String diffTextINSERT();
   String diffTextNoLF();
   String downloadLink();
-  String downloadLink_Active();
-  String downloadLinkListCell();
   String downloadLinkCopyLabel();
   String downloadLinkHeader();
   String downloadLinkHeaderGap();
   String downloadLinkList();
+  String downloadLinkListCell();
+  String downloadLink_Active();
   String drafts();
   String emptySection();
   String errorDialog();
-  String errorDialogGlass();
-  String errorDialogTitle();
   String errorDialogButtons();
   String errorDialogErrorType();
+  String errorDialogGlass();
   String errorDialogText();
+  String errorDialogTitle();
   String fileColumnHeader();
+  String fileCommentBorder();
   String fileLine();
   String fileLineCONTEXT();
   String fileLineDELETE();
@@ -113,8 +114,9 @@
   String fileLineMode();
   String fileLineNone();
   String filePathCell();
-  String gerritTopMenu();
   String gerritBody();
+  String gerritTopMenu();
+  String greenCheckClass();
   String groupDescriptionPanel();
   String groupExternalNameFilterTextBox();
   String groupIncludesTable();
@@ -125,24 +127,29 @@
   String groupOptionsPanel();
   String groupOwnerPanel();
   String groupOwnerTextBox();
-  String groupTypePanel();
   String groupTypeSelectListBox();
   String groupUUIDPanel();
   String header();
   String hyperlink();
   String iconCell();
+  String iconCellOfFileCommentRow();
   String iconHeader();
   String identityUntrustedExternalId();
   String infoBlock();
   String infoTable();
   String inputFieldTypeHint();
   String keyhelp();
+  String labelList();
   String leftMostCell();
   String lineHeader();
   String lineNumber();
+  String link();
   String linkMenuBar();
   String linkMenuItemNotLast();
   String menuBarUserName();
+  String menuBarUserNameAvatar();
+  String menuBarUserNameFocusPanel();
+  String menuBarUserNamePanel();
   String menuItem();
   String menuScreenMenuBar();
   String missingApproval();
@@ -152,10 +159,12 @@
   String negscore();
   String noLineLineNumber();
   String noborder();
+  String notVotable();
   String outdated();
   String parentsTable();
   String patchBrowserPopup();
   String patchBrowserPopupBody();
+  String patchCellReverseDiff();
   String patchComments();
   String patchContentTable();
   String patchHistoryTable();
@@ -170,12 +179,18 @@
   String patchSizeCell();
   String pluginsTable();
   String posscore();
-  String projectAdminApprovalCategoryRangeLine();
-  String projectAdminApprovalCategoryValue();
+  String projectAdminLabelRangeLine();
+  String projectAdminLabelValue();
+  String projectFilterLabel();
+  String projectFilterPanel();
   String publishCommentsScreen();
   String registerScreenExplain();
   String registerScreenNextLinks();
   String registerScreenSection();
+  String removeReviewer();
+  String removeReviewerCell();
+  String reviewedPanelBottom();
+  String rightBorder();
   String rightmost();
   String rpcStatus();
   String rpcStatusLoading();
@@ -185,8 +200,10 @@
   String screenNoHeader();
   String searchPanel();
   String sectionHeader();
+  String selectPatchSetOldVersion();
   String sideBySideScreenLinkTable();
   String sideBySideScreenSideBySideTable();
+  String sideBySideTableBinaryHeader();
   String singleLine();
   String skipLine();
   String smallHeading();
@@ -199,17 +216,18 @@
   String sshHostKeyPanelKnownHostEntry();
   String sshKeyPanelEncodedKey();
   String sshKeyPanelInvalid();
+  String topMostCell();
   String topmenu();
   String topmenuMenuLeft();
   String topmenuMenuRight();
   String topmenuTDglue();
   String topmenuTDmenu();
   String topmost();
-  String topMostCell();
+  String unifiedTable();
+  String unifiedTableHeader();
+  String userInfoPopup();
   String useridentity();
   String usernameField();
   String version();
   String watchedProjectFilter();
-  String selectPatchSetOldVersion();
-  String patchCellReverseDiff();
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritMessages.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritMessages.java
index aefa3a5..8fc196a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritMessages.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritMessages.java
@@ -22,6 +22,15 @@
   String poweredBy(String version);
 
   String noSuchAccountMessage(String who);
-
   String noSuchGroupMessage(String who);
+  String nameAlreadyUsedBody(String alreadyUsedName);
+
+  String branchCreationFailed(String branchName, String error);
+  String invalidBranchName(String branchName);
+  String invalidRevision(String revision);
+  String branchCreationNotAllowedUnderRefnamePrefix(String refnamePrefix);
+  String branchAlreadyExists(String branchName);
+  String branchCreationConflict(String branchName, String existingBranchName);
+
+  String pluginFailed(String scriptPath);
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritMessages.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritMessages.properties
index 79772bd..41caa44 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritMessages.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritMessages.properties
@@ -3,5 +3,14 @@
 poweredBy = Powered by <a href="http://code.google.com/p/gerrit/" target="_blank">Gerrit Code Review</a> ({0})
 
 noSuchAccountMessage = {0} is not a registered user.
-
 noSuchGroupMessage = Group {0} does not exist or is not visible to you.
+nameAlreadyUsedBody = The name {0} is already in use.
+
+branchCreationFailed = Creating branch {0} failed. Error: {1}
+invalidBranchName = The branch name {0} is not valid.
+invalidRevision = The revision {0} is not valid.
+branchCreationNotAllowedUnderRefnamePrefix = Branch creation is not allowed under {0}.
+branchAlreadyExists = A branch with the name {0} already exists.
+branchCreationConflict = Cannot create branch {0} since it conflicts with branch {1}.
+
+pluginFailed = Plugin JavaScript {0} failed to load
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritResources.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritResources.java
index d763ff1..fc7ea53 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritResources.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/GerritResources.java
@@ -28,6 +28,9 @@
   @Source("arrowRight.gif")
   public ImageResource arrowRight();
 
+  @Source("editText.png")
+  public ImageResource edit();
+
   @Source("starOpen.gif")
   public ImageResource starOpen();
 
@@ -42,4 +45,10 @@
 
   @Source("downloadIcon.png")
   public ImageResource downloadIcon();
+
+  @Source("queryIcon.png")
+  public ImageResource queryIcon();
+
+  @Source("addFileComment.png")
+  public ImageResource addFileComment();
 }
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 7e7b927..2ae6005 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
@@ -18,6 +18,9 @@
 import com.google.gerrit.client.ui.HintTextBox;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gwt.animation.client.Animation;
+import com.google.gwt.event.dom.client.BlurEvent;
+import com.google.gwt.event.dom.client.BlurHandler;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.event.dom.client.KeyCodes;
@@ -33,21 +36,51 @@
 import com.google.gwtexpui.globalkey.client.KeyCommand;
 
 class SearchPanel extends Composite {
+  private static final int FULL_SIZE = 70;
+  private static final int SMALL_SIZE = 45;
+
+  private class SizeAnimation extends Animation {
+    int targetSize;
+    int startSize;
+    public void run(boolean expand) {
+      if(expand) {
+        targetSize = FULL_SIZE;
+        startSize = SMALL_SIZE;
+      } else {
+        targetSize = SMALL_SIZE;
+        startSize = FULL_SIZE;
+      }
+      super.run(300);
+    }
+    @Override
+    protected void onUpdate(double progress) {
+      int size = (int) (targetSize * progress + startSize * (1-progress));
+      searchBox.setVisibleLength(size);
+    }
+
+    @Override
+    protected void onComplete() {
+      searchBox.setVisibleLength(targetSize);
+    }
+  }
   private final HintTextBox searchBox;
   private HandlerRegistration regFocus;
+  private final SizeAnimation sizeAnimation;
 
   SearchPanel() {
     final FlowPanel body = new FlowPanel();
+    sizeAnimation = new SizeAnimation();
     initWidget(body);
     setStyleName(Gerrit.RESOURCES.css().searchPanel());
 
     searchBox = new HintTextBox();
-    searchBox.setVisibleLength(70);
-    searchBox.setHintText(Gerrit.C.searchHint());
     final MySuggestionDisplay suggestionDisplay = new MySuggestionDisplay();
     searchBox.addKeyPressHandler(new KeyPressHandler() {
       @Override
       public void onKeyPress(final KeyPressEvent event) {
+        if (searchBox.getVisibleLength() == SMALL_SIZE) {
+          sizeAnimation.run(true);
+        }
         if (event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER) {
           if (!suggestionDisplay.isSuggestionSelected) {
             doSearch();
@@ -55,10 +88,20 @@
         }
       }
     });
+    searchBox.addBlurHandler(new BlurHandler() {
+      @Override
+      public void onBlur(BlurEvent event) {
+        if (searchBox.getVisibleLength() != SMALL_SIZE) {
+          sizeAnimation.run(false);
+        }
+      }
+    });
 
     final SuggestBox suggestBox =
         new SuggestBox(new SearchSuggestOracle(), searchBox, suggestionDisplay);
     searchBox.setStyleName("gwt-TextBox");
+    searchBox.setVisibleLength(SMALL_SIZE);
+    searchBox.setHintText(Gerrit.C.searchHint());
 
     final Button searchButton = new Button(Gerrit.C.searchButton());
     searchButton.addClickHandler(new ClickHandler() {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java
index 172a2af..dd4de33 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SearchSuggestOracle.java
@@ -14,13 +14,54 @@
 
 package com.google.gerrit.client;
 
+import com.google.gerrit.client.ui.AccountGroupSuggestOracle;
+import com.google.gerrit.client.ui.AccountSuggestOracle;
+import com.google.gerrit.client.ui.ProjectNameSuggestOracle;
 import com.google.gwt.user.client.ui.SuggestOracle;
 import com.google.gwtexpui.safehtml.client.HighlightSuggestOracle;
 
 import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
 import java.util.TreeSet;
 
 public class SearchSuggestOracle extends HighlightSuggestOracle {
+  private static final List<ParamSuggester> paramSuggester = Arrays.asList(
+      new ParamSuggester("project:", new ProjectNameSuggestOracle()),
+      new ParamSuggester(Arrays.asList("owner:", "reviewer:"),
+          new AccountSuggestOracle() {
+            @Override
+            public void onRequestSuggestions(final Request request, final Callback done) {
+              super.onRequestSuggestions(request, new Callback() {
+                @Override
+                public void onSuggestionsReady(final Request request,
+                    final Response response) {
+                  if ("self".startsWith(request.getQuery())) {
+                    final ArrayList<SuggestOracle.Suggestion> r =
+                        new ArrayList<SuggestOracle.Suggestion>(response
+                            .getSuggestions().size() + 1);
+                    r.addAll(response.getSuggestions());
+                    r.add(new SuggestOracle.Suggestion() {
+                      @Override
+                      public String getDisplayString() {
+                        return getReplacementString();
+                      }
+                      @Override
+                      public String getReplacementString() {
+                        return "self";
+                      }
+                    });
+                    response.setSuggestions(r);
+                  }
+                  done.onSuggestionsReady(request, response);
+                }
+              });
+            }
+          }),
+      new ParamSuggester(Arrays.asList("ownerin:", "reviewerin:"),
+          new AccountGroupSuggestOracle()));
+
   private static final TreeSet<String> suggestions = new TreeSet<String>();
 
   static {
@@ -93,17 +134,19 @@
   @Override
   protected void onRequestSuggestions(Request request, Callback done) {
     final String query = request.getQuery();
-    int lastSpace = query.lastIndexOf(' ');
-    final String lastWord;
-    // NOTE: this method is not called if the query is empty.
-    if (lastSpace == query.length() - 1) {
+
+    final String lastWord = getLastWord(query);
+    if (lastWord == null) {
       // Starting a new word - don't show suggestions yet.
       done.onSuggestionsReady(request, null);
       return;
-    } else if (lastSpace == -1) {
-      lastWord = query;
-    } else {
-      lastWord = query.substring(lastSpace + 1);
+    }
+
+    for (final ParamSuggester ps : paramSuggester) {
+      if (ps.applicable(lastWord)) {
+        ps.suggest(lastWord, request, done);
+        return;
+      }
     }
 
     final ArrayList<SearchSuggestion> r = new ArrayList<SearchSuggestOracle.SearchSuggestion>();
@@ -118,6 +161,27 @@
     done.onSuggestionsReady(request, new Response(r));
   }
 
+  private String getLastWord(final String query) {
+    final int lastSpace = query.lastIndexOf(' ');
+    if (lastSpace == query.length() - 1) {
+      return null;
+    }
+    if (lastSpace == -1) {
+      return query;
+    }
+    return query.substring(lastSpace + 1);
+  }
+
+  @Override
+  protected String getQueryPattern(final String query) {
+    return super.getQueryPattern(getLastWord(query));
+  }
+
+  @Override
+  protected boolean isHTML() {
+    return true;
+  }
+
   private static class SearchSuggestion implements SuggestOracle.Suggestion {
     private final String suggestion;
     private final String fullQuery;
@@ -137,4 +201,65 @@
       return fullQuery;
     }
   }
+
+  private static class ParamSuggester {
+    private final List<String> operators;
+    private final SuggestOracle parameterSuggestionOracle;
+
+    ParamSuggester(final String operator,
+        final SuggestOracle parameterSuggestionOracle) {
+      this(Collections.singletonList(operator), parameterSuggestionOracle);
+    }
+
+    ParamSuggester(final List<String> operators,
+        final SuggestOracle parameterSuggestionOracle) {
+      this.operators = operators;
+      this.parameterSuggestionOracle = parameterSuggestionOracle;
+    }
+
+    boolean applicable(final String query) {
+      final String operator = getApplicableOperator(query, operators);
+      return operator != null && query.length() > operator.length();
+    }
+
+    private String getApplicableOperator(final String lastWord,
+        final List<String> operators) {
+      for (final String operator : operators) {
+        if (lastWord.startsWith(operator)) {
+          return operator;
+        }
+      }
+      return null;
+    }
+
+    void suggest(final String lastWord, final Request request, final Callback done) {
+      final String operator = getApplicableOperator(lastWord, operators);
+      parameterSuggestionOracle.requestSuggestions(
+          new Request(lastWord.substring(operator.length()), request.getLimit()),
+          new Callback() {
+            @Override
+            public void onSuggestionsReady(final Request req,
+                final Response response) {
+              final String query = request.getQuery();
+              final List<SearchSuggestOracle.Suggestion> r =
+                  new ArrayList<SuggestOracle.Suggestion>(response
+                      .getSuggestions().size());
+              for (final SearchSuggestOracle.Suggestion s : response
+                  .getSuggestions()) {
+                r.add(new SearchSuggestion(s.getDisplayString(),
+                    query.substring(0, query.length() - lastWord.length()) +
+                    operator + quoteIfNeeded(s.getReplacementString())));
+              }
+              done.onSuggestionsReady(request, new Response(r));
+            }
+
+            private String quoteIfNeeded(final String s) {
+              if (!s.matches("^\\S*$")) {
+                return "\"" + s + "\"";
+              }
+              return s;
+            }
+          });
+    }
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SignInDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/SignInDialog.java
deleted file mode 100644
index 86fdb85..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/SignInDialog.java
+++ /dev/null
@@ -1,57 +0,0 @@
-// Copyright (C) 2008 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client;
-
-import com.google.gerrit.common.auth.SignInMode;
-import com.google.gwtexpui.globalkey.client.GlobalKey;
-import com.google.gwtexpui.user.client.AutoCenterDialogBox;
-
-/** Prompts the user to sign in to their account. */
-public abstract class SignInDialog extends AutoCenterDialogBox {
-  protected final SignInMode mode;
-  protected final String token;
-
-  /**
-   * Create a new dialog to handle user sign in.
-   *
-   * @param signInMode type of mode the login will perform.
-   * @param token the token to jump to after sign-in is complete.
-   */
-  protected SignInDialog(final SignInMode signInMode, final String token) {
-    super(/* auto hide */true, /* modal */true);
-    setGlassEnabled(true);
-
-    this.mode = signInMode;
-    this.token = token;
-
-    switch (signInMode) {
-      case LINK_IDENTIY:
-        setText(Gerrit.C.linkIdentityDialogTitle());
-        break;
-      case REGISTER:
-        setText(Gerrit.C.registerDialogTitle());
-        break;
-      default:
-        setText(Gerrit.C.signInDialogTitle());
-        break;
-    }
-  }
-
-  @Override
-  public void show() {
-    super.show();
-    GlobalKey.dialog(this);
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/VoidResult.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/VoidResult.java
new file mode 100644
index 0000000..d7bcd0c
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/VoidResult.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+public final class VoidResult extends JavaScriptObject {
+  protected VoidResult() {
+  }
+
+  public static VoidResult create() {
+    return createObject().cast();
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountCapabilities.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountCapabilities.java
index 0565d3e..42399ee 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountCapabilities.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountCapabilities.java
@@ -16,16 +16,14 @@
 
 import com.google.gerrit.client.rpc.RestApi;
 import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwtjsonrpc.common.AsyncCallback;
+import com.google.gwt.user.client.rpc.AsyncCallback;
 
 /** Capabilities the caller has from {@code /accounts/self/capabilities}.  */
 public class AccountCapabilities extends JavaScriptObject {
   public static void all(AsyncCallback<AccountCapabilities> cb, String... filter) {
-    RestApi api = new RestApi("/accounts/self/capabilities");
-    for (String name : filter) {
-      api.addParameter("q", name);
-    }
-    api.send(cb);
+    new RestApi("/accounts/self/capabilities")
+      .addParameter("q", filter)
+      .get(cb);
   }
 
   protected AccountCapabilities() {
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 d374d35..fa2c5fd 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
@@ -69,6 +69,7 @@
   String sshKeyStatus();
 
   String addSshKeyPanelHeader();
+  String addSshKeyHelpTitle();
   String addSshKeyHelp();
   String sshJavaAppletNotAvailable();
   String invalidSshKeyError();
@@ -95,8 +96,10 @@
   String watchedProjectFilter();
   String watchedProjectColumnEmailNotifications();
   String watchedProjectColumnNewChanges();
+  String watchedProjectColumnNewPatchSets();
   String watchedProjectColumnAllComments();
   String watchedProjectColumnSubmittedChanges();
+  String watchedProjectColumnAbandonedChanges();
 
   String contactFieldFullName();
   String contactFieldEmail();
@@ -110,6 +113,7 @@
   String buttonCancel();
   String titleRegisterNewEmail();
   String descRegisterNewEmail();
+  String errorDialogTitleRegisterNewEmail();
 
   String newAgreement();
   String agreementStatus();
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 8b32174..fd54363 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
@@ -60,7 +60,27 @@
 buttonLinkIdentity = Link Another Identity
 
 addSshKeyPanelHeader = Add SSH Public Key
-addSshKeyHelp = (<a href="http://help.github.com/key-setup-redirect" target="_blank">GitHub's Guide to SSH Keys</a>)
+addSshKeyHelpTitle = How to Generate an SSH Key
+addSshKeyHelp = \
+  <ol>\
+    <li>\
+      From the Terminal or Git Bash, run <em>ssh-keygen</em>\
+    </li>\
+    <li>\
+      Confirm the default path <em>.ssh/id_rsa</em>\
+    </li>\
+    <li>\
+      Enter a passphrase (recommended) or leave it blank.<br>\
+      Remember this passphrase, as you will need it to unlock the<br>\
+      key whenever you use it.\
+    </li>\
+    <li>\
+      Open <em>~/.ssh/id_rsa.pub</em> and copy & paste the contents into<br>\
+      the box below, then click on "Add".<br>\
+      Note that <em>id_rsa.pub</em> is your public key and can be shared,<br>\
+      while <em>id_rsa</em> is your private key and should be kept secret.\
+    </li>\
+  <\ol>
 invalidSshKeyError = Invalid SSH Key
 sshJavaAppletNotAvailable = Open Key Unavailable: Java not enabled
 
@@ -75,8 +95,10 @@
 watchedProjectFilter = Only If
 watchedProjectColumnEmailNotifications = Email Notifications
 watchedProjectColumnNewChanges = New Changes
+watchedProjectColumnNewPatchSets = New Patch Sets
 watchedProjectColumnAllComments = All Comments
 watchedProjectColumnSubmittedChanges = Submitted Changes
+watchedProjectColumnAbandonedChanges = Abandoned Changes
 
 contactFieldFullName = Full Name
 contactFieldEmail = Preferred Email
@@ -99,6 +121,7 @@
 descRegisterNewEmail = \
   <p>A confirmation link will be sent by email to this address.</p>\
   <p>You must click on the link to complete the registration and make the address available for selection.</p>
+errorDialogTitleRegisterNewEmail = Email Registration Failed
 
 
 newAgreement = New Contributor Agreement
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountInfo.java
new file mode 100644
index 0000000..601d807
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/account/AccountInfo.java
@@ -0,0 +1,31 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.account;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+public class AccountInfo extends JavaScriptObject {
+  public final native int _account_id() /*-{ return this._account_id || 0; }-*/;
+  public final native String name() /*-{ return this.name; }-*/;
+  public final native String email() /*-{ return this.email; }-*/;
+
+  public static native AccountInfo create(int id, String name,
+      String email) /*-{
+    return {'_account_id': id, 'name': name, 'email': email};
+  }-*/;
+
+  protected AccountInfo() {
+  }
+}
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 4fbe7a0..f35bd4b 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
@@ -19,6 +19,7 @@
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.ui.OnEditEnabler;
 import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Account.FieldName;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
@@ -300,7 +301,15 @@
           public void onFailure(final Throwable caught) {
             inEmail.setEnabled(true);
             register.setEnabled(true);
-            super.onFailure(caught);
+            if (caught.getMessage().startsWith(EmailException.MESSAGE)) {
+              final ErrorDialog d =
+                  new ErrorDialog(caught.getMessage().substring(
+                      EmailException.MESSAGE.length()));
+              d.setText(Util.C.errorDialogTitleRegisterNewEmail());
+              d.center();
+            } else {
+              super.onFailure(caught);
+            }
           }
         });
       }
@@ -339,7 +348,7 @@
 
   void doSave(final AsyncCallback<Account> onSave) {
     String newName = canEditFullName() ? nameTxt.getText() : null;
-    if ("".equals(newName)) {
+    if (newName != null && newName.trim().isEmpty()) {
       newName = null;
     }
 
@@ -383,6 +392,7 @@
     me.setFullName(result.getFullName());
     me.setPreferredEmail(result.getPreferredEmail());
     Gerrit.refreshMenuBar();
+    display(me);
   }
 
   ContactInformation toContactInformation() {
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 6cb749d..f83e9ec 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
@@ -15,10 +15,8 @@
 package com.google.gerrit.client.account;
 
 import com.google.gerrit.client.admin.GroupTable;
+import com.google.gerrit.client.groups.GroupList;
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-
-import java.util.List;
 
 public class MyGroupsScreen extends SettingsScreen {
   private GroupTable groups;
@@ -26,18 +24,18 @@
   @Override
   protected void onInitUI() {
     super.onInitUI();
-    groups = new GroupTable(true /* hyperlink to admin */);
+    groups = new GroupTable();
     add(groups);
   }
 
   @Override
   protected void onLoad() {
     super.onLoad();
-    Util.ACCOUNT_SEC.myGroups(new ScreenLoadCallback<List<AccountGroup>>(this) {
+    GroupList.my(new ScreenLoadCallback<GroupList>(this) {
       @Override
-      public void preDisplay(final List<AccountGroup> result) {
+      protected void preDisplay(GroupList result) {
         groups.display(result);
-      }
-    });
+        groups.finishDisplay();
+      }});
   }
 }
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 899aa03..ca872cc 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
@@ -15,19 +15,19 @@
 package com.google.gerrit.client.account;
 
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.auth.openid.OpenIdSignInDialog;
 import com.google.gerrit.client.auth.openid.OpenIdUtil;
 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.common.auth.SignInMode;
 import com.google.gerrit.common.auth.openid.OpenIdUrls;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
+import com.google.gerrit.reviewdb.client.AuthType;
 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.user.client.History;
+import com.google.gwt.user.client.Window.Location;
 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;
@@ -59,19 +59,15 @@
     });
     add(deleteIdentity);
 
-    switch (Gerrit.getConfig().getAuthType()) {
-      case OPENID: {
-        final Button linkIdentity = new Button(Util.C.buttonLinkIdentity());
-        linkIdentity.addClickHandler(new ClickHandler() {
-          @Override
-          public void onClick(final ClickEvent event) {
-            final String to = History.getToken();
-            new OpenIdSignInDialog(SignInMode.LINK_IDENTIY, to, null).center();
-          }
-        });
-        add(linkIdentity);
-        break;
-      }
+    if (Gerrit.getConfig().getAuthType() == AuthType.OPENID) {
+      Button linkIdentity = new Button(Util.C.buttonLinkIdentity());
+      linkIdentity.addClickHandler(new ClickHandler() {
+        @Override
+        public void onClick(final ClickEvent event) {
+          Location.assign(Gerrit.loginRedirect(History.getToken()) + "?link");
+        }
+      });
+      add(linkIdentity);
     }
   }
 
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 de6e666..c17a0aa 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
@@ -20,10 +20,9 @@
 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.OnEditEnabler;
 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;
 import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.i18n.client.DateTimeFormat;
@@ -52,39 +51,15 @@
   protected void onInitUI() {
     super.onInitUI();
 
-    final ClickHandler onClickSave = new ClickHandler() {
-      @Override
-      public void onClick(final ClickEvent event) {
-        save.setEnabled(true);
-      }
-    };
-    final ChangeHandler onChangeSave = new ChangeHandler() {
-      @Override
-      public void onChange(final ChangeEvent event) {
-        save.setEnabled(true);
-      }
-    };
-
     showSiteHeader = new CheckBox(Util.C.showSiteHeader());
-    showSiteHeader.addClickHandler(onClickSave);
-
     useFlashClipboard = new CheckBox(Util.C.useFlashClipboard());
-    useFlashClipboard.addClickHandler(onClickSave);
-
     copySelfOnEmails = new CheckBox(Util.C.copySelfOnEmails());
-    copySelfOnEmails.addClickHandler(onClickSave);
-
     reversePatchSetOrder = new CheckBox(Util.C.reversePatchSetOrder());
-    reversePatchSetOrder.addClickHandler(onClickSave);
-
     showUsernameInReviewCategory = new CheckBox(Util.C.showUsernameInReviewCategory());
-    showUsernameInReviewCategory.addClickHandler(onClickSave);
-
     maximumPageSize = new ListBox();
     for (final short v : PAGESIZE_CHOICES) {
       maximumPageSize.addItem(Util.M.rowsPerPage(v), String.valueOf(v));
     }
-    maximumPageSize.addChangeHandler(onChangeSave);
 
     Date now = new Date();
     dateFormat = new ListBox();
@@ -96,7 +71,6 @@
       r.append(DateTimeFormat.getFormat(fmt.getLongFormat()).format(now));
       dateFormat.addItem(r.toString(), fmt.name());
     }
-    dateFormat.addChangeHandler(onChangeSave);
 
     timeFormat = new ListBox();
     for (AccountGeneralPreferences.TimeFormat fmt : AccountGeneralPreferences.TimeFormat
@@ -105,7 +79,6 @@
       r.append(DateTimeFormat.getFormat(fmt.getFormat()).format(now));
       timeFormat.addItem(r.toString(), fmt.name());
     }
-    timeFormat.addChangeHandler(onChangeSave);
 
     FlowPanel dateTimePanel = new FlowPanel();
 
@@ -163,6 +136,16 @@
       }
     });
     add(save);
+
+    final OnEditEnabler e = new OnEditEnabler(save);
+    e.listenTo(showSiteHeader);
+    e.listenTo(useFlashClipboard);
+    e.listenTo(copySelfOnEmails);
+    e.listenTo(reversePatchSetOrder);
+    e.listenTo(showUsernameInReviewCategory);
+    e.listenTo(maximumPageSize);
+    e.listenTo(dateFormat);
+    e.listenTo(timeFormat);
   }
 
   @Override
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 d38015c..a528912 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
@@ -33,6 +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.HTMLTable.CellFormatter;
+import com.google.gwt.user.client.ui.HorizontalPanel;
 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;
@@ -60,7 +61,10 @@
     grid = new Grid(2, 2);
     grid.setStyleName(Gerrit.RESOURCES.css().infoBlock());
     grid.setText(0, 0, Util.C.watchedProjectName());
-    grid.setWidget(0, 1, nameTxt);
+    final HorizontalPanel hp = new HorizontalPanel();
+    hp.add(nameTxt);
+    hp.add(browse);
+    grid.setWidget(0, 1, hp);
 
     grid.setText(1, 0, Util.C.watchedProjectFilter());
     grid.setWidget(1, 1, filterTxt);
@@ -76,7 +80,6 @@
     fp.setStyleName(Gerrit.RESOURCES.css().addWatchPanel());
     fp.add(grid);
     fp.add(addNew);
-    fp.add(browse);
     add(fp);
 
 
@@ -90,7 +93,7 @@
       @Override
       protected void onMovePointerTo(String projectName) {
         // prevent user input from being overwritten by simply poping up
-        if (!projectsPopup.isPopingUp() || "".equals(nameBox.getText())) {
+        if (!projectsPopup.isPoppingUp() || "".equals(nameBox.getText())) {
           nameBox.setText(projectName);
         }
       }
@@ -193,7 +196,7 @@
   }
 
   protected void doAddNew() {
-    final String projectName = nameTxt.getText();
+    final String projectName = nameTxt.getText().trim();
     if ("".equals(projectName)) {
       return;
     }
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 c158c2c..e228647 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
@@ -51,13 +51,17 @@
     fmt.setRowSpan(0, 2, 2);
     DOM.setElementProperty(fmt.getElement(0, 3), "align", "center");
 
-    fmt.setColSpan(0, 3, 3);
+    fmt.setColSpan(0, 3, 5);
     table.setText(1, 0, Util.C.watchedProjectColumnNewChanges());
-    table.setText(1, 1, Util.C.watchedProjectColumnAllComments());
-    table.setText(1, 2, Util.C.watchedProjectColumnSubmittedChanges());
+    table.setText(1, 1, Util.C.watchedProjectColumnNewPatchSets());
+    table.setText(1, 2, Util.C.watchedProjectColumnAllComments());
+    table.setText(1, 3, Util.C.watchedProjectColumnSubmittedChanges());
+    table.setText(1, 4, Util.C.watchedProjectColumnAbandonedChanges());
     fmt.addStyleName(1, 0, Gerrit.RESOURCES.css().dataHeader());
     fmt.addStyleName(1, 1, Gerrit.RESOURCES.css().dataHeader());
     fmt.addStyleName(1, 2, Gerrit.RESOURCES.css().dataHeader());
+    fmt.addStyleName(1, 3, Gerrit.RESOURCES.css().dataHeader());
+    fmt.addStyleName(1, 4, Gerrit.RESOURCES.css().dataHeader());
   }
 
   public void deleteChecked() {
@@ -135,8 +139,10 @@
     table.setWidget(row, 2, fp);
 
     addNotifyButton(AccountProjectWatch.NotifyType.NEW_CHANGES, info, row, 3);
-    addNotifyButton(AccountProjectWatch.NotifyType.ALL_COMMENTS, info, row, 4);
-    addNotifyButton(AccountProjectWatch.NotifyType.SUBMITTED_CHANGES, info, row, 5);
+    addNotifyButton(AccountProjectWatch.NotifyType.NEW_PATCHSETS, info, row, 4);
+    addNotifyButton(AccountProjectWatch.NotifyType.ALL_COMMENTS, info, row, 5);
+    addNotifyButton(AccountProjectWatch.NotifyType.SUBMITTED_CHANGES, info, row, 6);
+    addNotifyButton(AccountProjectWatch.NotifyType.ABANDONED_CHANGES, info, row, 7);
 
     final FlexCellFormatter fmt = table.getFlexCellFormatter();
     fmt.addStyleName(row, 1, Gerrit.RESOURCES.css().iconCell());
@@ -144,6 +150,8 @@
     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, 7, Gerrit.RESOURCES.css().dataCell());
 
     setRowItem(row, info);
   }
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 73127f0..aaadc5e6 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
@@ -17,6 +17,7 @@
 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.ComplexDisclosurePanel;
 import com.google.gerrit.client.ui.FancyFlexTable;
 import com.google.gerrit.client.ui.SmallHeading;
 import com.google.gerrit.common.data.SshHostKey;
@@ -92,7 +93,11 @@
     addKeyBlock.setVisible(false);
     addKeyBlock.setStyleName(Gerrit.RESOURCES.css().addSshKeyPanel());
     addKeyBlock.add(new SmallHeading(Util.C.addSshKeyPanelHeader()));
-    addKeyBlock.add(new HTML(Util.C.addSshKeyHelp()));
+
+    final ComplexDisclosurePanel addSshKeyHelp =
+        new ComplexDisclosurePanel(Util.C.addSshKeyHelpTitle(), false);
+    addSshKeyHelp.setContent(new HTML(Util.C.addSshKeyHelp()));
+    addKeyBlock.add(addSshKeyHelp);
 
     addTxt = new NpTextArea();
     addTxt.setVisibleLines(12);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/addFileComment.png b/gerrit-gwtui/src/main/java/com/google/gerrit/client/addFileComment.png
new file mode 100644
index 0000000..4ae3ae8
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/addFileComment.png
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.java
index a9aa418..344104d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccessSectionEditor.java
@@ -16,7 +16,7 @@
 
 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.LabelType;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.ProjectAccess;
 import com.google.gerrit.common.data.RefConfigSection;
@@ -226,10 +226,8 @@
         addPermission(varName, perms);
       }
     } else if (RefConfigSection.isValid(value.getName())) {
-      for (ApprovalType t : Gerrit.getConfig().getApprovalTypes()
-          .getApprovalTypes()) {
-        String varName = Permission.LABEL + t.getCategory().getLabelName();
-        addPermission(varName, perms);
+      for (LabelType t : projectAccess.getLabelTypes().getLabelTypes()) {
+        addPermission(Permission.LABEL + t.getName(), perms);
       }
       for (String varName : Util.C.permissionNames().keySet()) {
         addPermission(varName, perms);
@@ -284,7 +282,8 @@
     @Override
     public PermissionEditor create(int index) {
       PermissionEditor subEditor =
-          new PermissionEditor(projectAccess.getProjectName(), readOnly, value);
+          new PermissionEditor(projectAccess.getProjectName(), readOnly, value,
+              projectAccess.getLabelTypes());
       permissionContainer.insert(subEditor, index);
       return subEditor;
     }
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
index 501c3fc..b31e02e 100644
--- 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
@@ -29,6 +29,7 @@
   @eval trimColor com.google.gerrit.client.Gerrit.getTheme().trimColor;
 
   .panel {
+    width: 50em;
     position: relative;
   }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupInfoScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupInfoScreen.java
index ea7c04b..ea836c6 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupInfoScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupInfoScreen.java
@@ -15,28 +15,24 @@
 package com.google.gerrit.client.admin;
 
 import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.VoidResult;
+import com.google.gerrit.client.groups.GroupApi;
+import com.google.gerrit.client.groups.GroupInfo;
 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.user.client.ui.Button;
 import com.google.gwt.user.client.ui.CheckBox;
-import com.google.gwt.user.client.ui.Label;
-import com.google.gwt.user.client.ui.ListBox;
 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;
 
 public class AccountGroupInfoScreen extends AccountGroupScreen {
   private CopyableLabel groupUUIDLabel;
@@ -51,14 +47,10 @@
   private NpTextArea descTxt;
   private Button saveDesc;
 
-  private Label typeSystem;
-  private ListBox typeSelect;
-  private Button saveType;
-
   private CheckBox visibleToAllCheckBox;
   private Button saveGroupOptions;
 
-  public AccountGroupInfoScreen(final GroupDetail toShow, final String token) {
+  public AccountGroupInfoScreen(final GroupInfo toShow, final String token) {
     super(toShow, token);
   }
 
@@ -70,14 +62,12 @@
     initOwner();
     initDescription();
     initGroupOptions();
-    initGroupType();
   }
 
   private void enableForm(final boolean canModify) {
     groupNameTxt.setEnabled(canModify);
     ownerTxtBox.setEnabled(canModify);
     descTxt.setEnabled(canModify);
-    typeSelect.setEnabled(canModify);
     visibleToAllCheckBox.setEnabled(canModify);
   }
 
@@ -104,12 +94,15 @@
       @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) {
+        GroupApi.renameGroup(getGroupUUID(), newName,
+            new GerritCallback<com.google.gerrit.client.VoidResult>() {
+              public void onSuccess(final com.google.gerrit.client.VoidResult result) {
                 saveName.setEnabled(false);
-                setPageTitle(Util.M.group(groupDetail.group.getName()));
-                display(groupDetail);
+                setPageTitle(Util.M.group(newName));
+                groupNameTxt.setText(newName);
+                if (getGroupUUID().equals(getOwnerGroupUUID())) {
+                  ownerTxt.setText(newName);
+                }
               }
             });
       }
@@ -139,9 +132,10 @@
       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) {
+          GroupApi.setGroupOwner(getGroupUUID(), newOwner,
+              new GerritCallback<GroupInfo>() {
+                public void onSuccess(final GroupInfo result) {
+                  updateOwnerGroup(result);
                   saveOwner.setEnabled(false);
                 }
               });
@@ -170,7 +164,7 @@
       @Override
       public void onClick(final ClickEvent event) {
         final String txt = descTxt.getText().trim();
-        Util.GROUP_SVC.changeGroupDescription(getGroupId(), txt,
+        GroupApi.setGroupDescription(getGroupUUID(), txt,
             new GerritCallback<VoidResult>() {
               public void onSuccess(final VoidResult result) {
                 saveDesc.setEnabled(false);
@@ -200,10 +194,8 @@
     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>() {
+        GroupApi.setGroupOptions(getGroupUUID(),
+            visibleToAllCheckBox.getValue(), new GerritCallback<VoidResult>() {
               public void onSuccess(final VoidResult result) {
                 saveGroupOptions.setEnabled(false);
               }
@@ -218,114 +210,20 @@
     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.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 setType(final AccountGroup.Type newType) {
-    final boolean system = newType == AccountGroup.Type.SYSTEM;
-
-    typeSystem.setVisible(system);
-    typeSelect.setVisible(!system);
-    saveType.setVisible(!system);
-
-    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);
-          }
-        });
-  }
-
   @Override
-  protected void display(final GroupDetail groupDetail) {
-    final AccountGroup group = groupDetail.group;
+  protected void display(final GroupInfo group, final boolean canModify) {
     groupUUIDLabel.setText(group.getGroupUUID().get());
-    groupNameTxt.setText(group.getName());
-    if (groupDetail.ownerGroup != null) {
-      ownerTxt.setText(groupDetail.ownerGroup.getName());
-    } else {
-      ownerTxt.setText(Util.M.deletedReference(group.getOwnerGroupUUID().get()));
-    }
-    descTxt.setText(group.getDescription());
+    groupNameTxt.setText(group.name());
+    ownerTxt.setText(group.owner() != null?group.owner():Util.M.deletedReference(group.getOwnerUUID().get()));
+    descTxt.setText(group.description());
+    visibleToAllCheckBox.setValue(group.options().isVisibleToAll());
+    setMembersTabVisible(AccountGroup.isInternalGroup(group.getGroupUUID())
+        && !AccountGroup.isSystemGroup(group.getGroupUUID()));
 
-    visibleToAllCheckBox.setValue(group.isVisibleToAll());
-
-    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);
+    enableForm(canModify);
+    saveName.setVisible(canModify);
+    saveOwner.setVisible(canModify);
+    saveDesc.setVisible(canModify);
+    saveGroupOptions.setVisible(canModify);
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java
index 4c0b1ba..c276a89 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/AccountGroupMembersScreen.java
@@ -16,37 +16,36 @@
 
 import com.google.gerrit.client.Dispatcher;
 import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.VoidResult;
+import com.google.gerrit.client.account.AccountInfo;
+import com.google.gerrit.client.groups.GroupApi;
+import com.google.gerrit.client.groups.GroupInfo;
 import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.ui.AccountGroupSuggestOracle;
 import com.google.gerrit.client.ui.AccountLink;
 import com.google.gerrit.client.ui.AddMemberBox;
 import com.google.gerrit.client.ui.FancyFlexTable;
 import com.google.gerrit.client.ui.Hyperlink;
 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.Anchor;
 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.Comparator;
 import java.util.HashSet;
 import java.util.List;
 
+import javax.annotation.Nullable;
+
 public class AccountGroupMembersScreen extends AccountGroupScreen {
 
-  private AccountInfoCache accounts = AccountInfoCache.empty();
-  private GroupInfoCache groups = GroupInfoCache.empty();
   private MemberTable members;
   private IncludeTable includes;
 
@@ -60,7 +59,7 @@
 
   private FlowPanel noMembersInfo;
 
-  public AccountGroupMembersScreen(final GroupDetail toShow, final String token) {
+  public AccountGroupMembersScreen(final GroupInfo toShow, final String token) {
     super(toShow, token);
   }
 
@@ -111,8 +110,8 @@
 
   private void initIncludeList() {
     addIncludeBox =
-      new AddMemberBox(Util.C.buttonAddIncludedGroup(),
-          Util.C.defaultAccountGroupName(), new AccountGroupSuggestOracle());
+        new AddMemberBox(Util.C.buttonAddIncludedGroup(),
+            Util.C.defaultAccountGroupName(), new AccountGroupSuggestOracle());
 
     addIncludeBox.addClickHandler(new ClickHandler() {
       @Override
@@ -148,24 +147,20 @@
   }
 
   @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;
+  protected void display(final GroupInfo group, final boolean canModify) {
+    if (AccountGroup.isInternalGroup(group.getGroupUUID())
+        && !AccountGroup.isSystemGroup(group.getGroupUUID())) {
+      members.display(Natives.asList(group.members()));
+      includes.display(Natives.asList(group.includes()));
+    } else {
+      memberPanel.setVisible(false);
+      includePanel.setVisible(false);
+      noMembersInfo.setVisible(true);
     }
 
-    enableForm(groupDetail.canModify);
-    delMember.setVisible(groupDetail.canModify);
-    delInclude.setVisible(groupDetail.canModify);
+    enableForm(canModify);
+    delMember.setVisible(canModify);
+    delInclude.setVisible(canModify);
   }
 
   void doAddNewMember() {
@@ -175,15 +170,12 @@
     }
 
     addMemberBox.setEnabled(false);
-    Util.GROUP_SVC.addGroupMember(getGroupId(), nameEmail,
-        new GerritCallback<GroupDetail>() {
-          public void onSuccess(final GroupDetail result) {
+    GroupApi.addMember(getGroupUUID(), nameEmail,
+        new GerritCallback<AccountInfo>() {
+          public void onSuccess(final AccountInfo memberInfo) {
             addMemberBox.setEnabled(true);
             addMemberBox.setText("");
-            if (result.accounts != null && result.members != null) {
-              accounts.merge(result.accounts);
-              members.display(result.members);
-            }
+            members.insert(memberInfo);
           }
 
           @Override
@@ -201,15 +193,12 @@
     }
 
     addIncludeBox.setEnabled(false);
-    Util.GROUP_SVC.addGroupInclude(getGroupId(), groupName,
-        new GerritCallback<GroupDetail>() {
-          public void onSuccess(final GroupDetail result) {
+    GroupApi.addIncludedGroup(getGroupUUID(), groupName,
+        new GerritCallback<GroupInfo>() {
+          public void onSuccess(final GroupInfo result) {
             addIncludeBox.setEnabled(true);
             addIncludeBox.setText("");
-            if (result.groups != null && result.includes != null) {
-              groups.merge(result.groups);
-              includes.display(result.includes);
-            }
+            includes.insert(result);
           }
 
           @Override
@@ -220,7 +209,7 @@
         });
   }
 
-  private class MemberTable extends FancyFlexTable<AccountGroupMember> {
+  private class MemberTable extends FancyFlexTable<AccountInfo> {
     private boolean enabled = true;
 
     MemberTable() {
@@ -236,29 +225,28 @@
     void setEnabled(final boolean enabled) {
       this.enabled = enabled;
       for (int row = 1; row < table.getRowCount(); row++) {
-        final AccountGroupMember k = getRowItem(row);
-        if (k != null) {
+        final AccountInfo i = getRowItem(row);
+        if (i != null) {
           ((CheckBox) table.getWidget(row, 1)).setEnabled(enabled);
         }
       }
     }
 
     void deleteChecked() {
-      final HashSet<AccountGroupMember.Key> ids =
-          new HashSet<AccountGroupMember.Key>();
+      final HashSet<Integer> ids = new HashSet<Integer>();
       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());
+        final AccountInfo i = getRowItem(row);
+        if (i != null && ((CheckBox) table.getWidget(row, 1)).getValue()) {
+          ids.add(i._account_id());
         }
       }
       if (!ids.isEmpty()) {
-        Util.GROUP_SVC.deleteGroupMembers(getGroupId(), ids,
+        GroupApi.removeMembers(getGroupUUID(), 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())) {
+                  final AccountInfo i = getRowItem(row);
+                  if (i != null && ids.contains(i._account_id())) {
                     table.removeRow(row);
                   } else {
                     row++;
@@ -269,36 +257,80 @@
       }
     }
 
-    void display(final List<AccountGroupMember> result) {
+    void display(final List<AccountInfo> result) {
       while (1 < table.getRowCount())
         table.removeRow(table.getRowCount() - 1);
 
-      for (final AccountGroupMember k : result) {
+      for (final AccountInfo i : result) {
         final int row = table.getRowCount();
         table.insertRow(row);
         applyDataRowStyle(row);
-        populate(row, k);
+        populate(row, i);
       }
     }
 
-    void populate(final int row, final AccountGroupMember k) {
-      final Account.Id accountId = k.getAccountId();
+    void insert(AccountInfo info) {
+      Comparator<AccountInfo> c = new Comparator<AccountInfo>() {
+        @Override
+        public int compare(AccountInfo a, AccountInfo b) {
+          int cmp = nullToEmpty(a.name()).compareTo(nullToEmpty(b.name()));
+          if (cmp != 0) {
+            return cmp;
+          }
+
+          cmp = nullToEmpty(a.email()).compareTo(nullToEmpty(b.email()));
+          if (cmp != 0) {
+            return cmp;
+          }
+
+          return a._account_id() - b._account_id();
+        }
+
+        public String nullToEmpty(String str) {
+          return str == null ? "" : str;
+        }
+      };
+      int insertPosition = table.getRowCount();
+      int left = 1;
+      int right = table.getRowCount() - 1;
+      while (left <= right) {
+        int middle = (left + right) >>> 1; // (left+right)/2
+        AccountInfo i = getRowItem(middle);
+        int cmp = c.compare(i, info);
+
+        if (cmp < 0) {
+          left = middle + 1;
+        } else if (cmp > 0) {
+          right = middle - 1;
+        } else {
+          // group is already contained in the table
+          return;
+        }
+      }
+      insertPosition = left;
+
+      table.insertRow(insertPosition);
+      applyDataRowStyle(insertPosition);
+      populate(insertPosition, info);
+    }
+
+    void populate(final int row, final AccountInfo i) {
       CheckBox checkBox = new CheckBox();
       table.setWidget(row, 1, checkBox);
       checkBox.setEnabled(enabled);
-      table.setWidget(row, 2, AccountLink.link(accounts, accountId));
-      table.setText(row, 3, accounts.get(accountId).getPreferredEmail());
+      table.setWidget(row, 2, new AccountLink(i));
+      table.setText(row, 3, i.email());
 
       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);
+      setRowItem(row, i);
     }
   }
 
-  private class IncludeTable extends FancyFlexTable<AccountGroupInclude> {
+  private class IncludeTable extends FancyFlexTable<GroupInfo> {
     private boolean enabled = true;
 
     IncludeTable() {
@@ -314,29 +346,28 @@
     void setEnabled(final boolean enabled) {
       this.enabled = enabled;
       for (int row = 1; row < table.getRowCount(); row++) {
-        final AccountGroupInclude k = getRowItem(row);
-        if (k != null) {
+        final GroupInfo i = getRowItem(row);
+        if (i != null) {
           ((CheckBox) table.getWidget(row, 1)).setEnabled(enabled);
         }
       }
     }
 
     void deleteChecked() {
-      final HashSet<AccountGroupInclude.Key> keys =
-          new HashSet<AccountGroupInclude.Key>();
+      final HashSet<AccountGroup.UUID> ids = new HashSet<AccountGroup.UUID>();
       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());
+        final GroupInfo i = getRowItem(row);
+        if (i != null && ((CheckBox) table.getWidget(row, 1)).getValue()) {
+          ids.add(i.getGroupUUID());
         }
       }
-      if (!keys.isEmpty()) {
-        Util.GROUP_SVC.deleteGroupIncludes(getGroupId(), keys,
+      if (!ids.isEmpty()) {
+        GroupApi.removeIncludedGroups(getGroupUUID(), ids,
             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())) {
+                  final GroupInfo i = getRowItem(row);
+                  if (i != null && ids.contains(i.getGroupUUID())) {
                     table.removeRow(row);
                   } else {
                     row++;
@@ -347,34 +378,87 @@
       }
     }
 
-    void display(final List<AccountGroupInclude> result) {
+    void display(List<GroupInfo> list) {
       while (1 < table.getRowCount())
         table.removeRow(table.getRowCount() - 1);
 
-      for (final AccountGroupInclude k : result) {
+      for (final GroupInfo i : list) {
         final int row = table.getRowCount();
         table.insertRow(row);
         applyDataRowStyle(row);
-        populate(row, k);
+        populate(row, i);
       }
     }
 
-    void populate(final int row, final AccountGroupInclude k) {
-      AccountGroup.Id id = k.getIncludeId();
-      GroupInfo group = groups.get(id);
+    void insert(GroupInfo info) {
+      Comparator<GroupInfo> c = new Comparator<GroupInfo>() {
+        @Override
+        public int compare(GroupInfo a, GroupInfo b) {
+          int cmp = nullToEmpty(a.name()).compareTo(nullToEmpty(b.name()));
+          if (cmp != 0) {
+            return cmp;
+          }
+          return a.getGroupUUID().compareTo(b.getGroupUUID());
+        }
+
+        private String nullToEmpty(@Nullable String str) {
+          return (str == null) ? "" : str;
+        }
+      };
+
+      int insertPosition = table.getRowCount();
+      int left = 1;
+      int right = table.getRowCount() - 1;
+      while (left <= right) {
+        int middle = (left + right) >>> 1; // (left+right)/2
+        GroupInfo i = getRowItem(middle);
+        int cmp = c.compare(i, info);
+
+        if (cmp < 0) {
+          left = middle + 1;
+        } else if (cmp > 0) {
+          right = middle - 1;
+        } else {
+          // group is already contained in the table
+          return;
+        }
+      }
+      insertPosition = left;
+
+      table.insertRow(insertPosition);
+      applyDataRowStyle(insertPosition);
+      populate(insertPosition, info);
+    }
+
+    void populate(final int row, final GroupInfo i) {
+      final FlexCellFormatter fmt = table.getFlexCellFormatter();
+
+      AccountGroup.UUID uuid = i.getGroupUUID();
       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());
+      if (AccountGroup.isInternalGroup(uuid)) {
+        table.setWidget(row, 2,
+            new Hyperlink(i.name(), Dispatcher.toGroup(uuid)));
+        fmt.getElement(row, 2).setTitle(null);
+        table.setText(row, 3, i.description());
+      } else if (i.url() != null) {
+        Anchor a = new Anchor();
+        a.setText(i.name());
+        a.setHref(i.url());
+        a.setTitle("UUID " + uuid.get());
+        table.setWidget(row, 2, a);
+        fmt.getElement(row, 2).setTitle(null);
+      } else {
+        table.setText(row, 2, i.name());
+        fmt.getElement(row, 2).setTitle("UUID " + uuid.get());
+      }
 
-      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);
+      setRowItem(row, i);
     }
   }
 }
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 9ac6c9a..ade00b9 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
@@ -16,48 +16,65 @@
 
 import static com.google.gerrit.client.Dispatcher.toGroup;
 
+import com.google.gerrit.client.groups.GroupApi;
+import com.google.gerrit.client.groups.GroupInfo;
+import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.ui.MenuScreen;
-import com.google.gerrit.common.data.GroupDetail;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 
 public abstract class AccountGroupScreen extends MenuScreen {
   public static final String INFO = "info";
   public static final String MEMBERS = "members";
 
-  private final GroupDetail groupDetail;
+  private final GroupInfo group;
   private final String membersTabToken;
 
-  public AccountGroupScreen(final GroupDetail toShow, final String token) {
+  public AccountGroupScreen(final GroupInfo toShow, final String token) {
     setRequiresSignIn(true);
 
-    this.groupDetail = toShow;
+    this.group = toShow;
     this.membersTabToken = getTabToken(token, MEMBERS);
 
     link(Util.C.groupTabGeneral(), getTabToken(token, INFO));
     link(Util.C.groupTabMembers(), membersTabToken,
-        groupDetail.group.getType() == AccountGroup.Type.INTERNAL);
+        AccountGroup.isInternalGroup(group.getGroupUUID())
+        && !AccountGroup.isSystemGroup(group.getGroupUUID()));
   }
 
   private String getTabToken(final String token, final String tab) {
     if (token.startsWith("/admin/groups/uuid-")) {
-      return toGroup(groupDetail.group.getGroupUUID(), tab);
+      return toGroup(group.getGroupUUID(), tab);
     } else {
-      return toGroup(groupDetail.group.getId(), tab);
+      return toGroup(group.getGroupId(), tab);
     }
   }
 
   @Override
   protected void onLoad() {
     super.onLoad();
-    setPageTitle(Util.M.group(groupDetail.group.getName()));
+    setPageTitle(Util.M.group(group.name()));
     display();
-    display(groupDetail);
+    GroupApi.isGroupOwner(group.name(), new GerritCallback<Boolean>() {
+      @Override
+      public void onSuccess(Boolean result) {
+        display(group, result);
+      }
+    });
   }
 
-  protected abstract void display(final GroupDetail groupDetail);
+  protected abstract void display(final GroupInfo group, final boolean canModify);
 
-  protected AccountGroup.Id getGroupId() {
-    return groupDetail.group.getId();
+  protected AccountGroup.UUID getGroupUUID() {
+    return group.getGroupUUID();
+  }
+
+  protected void updateOwnerGroup(GroupInfo ownerGroup) {
+    group.setOwnerUUID(ownerGroup.getGroupUUID());
+    group.owner(ownerGroup.name());
+  }
+
+  protected AccountGroup.UUID getOwnerGroupUUID() {
+    return group.getOwnerUUID();
   }
 
   protected void setMembersTabVisible(final boolean 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 0c18169..f4c0b55 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
@@ -55,7 +55,6 @@
   String headingOwner();
   String headingDescription();
   String headingProjectOptions();
-  String headingGroupType();
   String headingMembers();
   String headingIncludedGroups();
   String noMembersInfo();
@@ -65,19 +64,18 @@
   String columnProjectName();
   String headingAgreements();
 
+  String headingProjectSubmitType();
   String projectSubmitType_FAST_FORWARD_ONLY();
   String projectSubmitType_MERGE_ALWAYS();
   String projectSubmitType_MERGE_IF_NECESSARY();
+  String projectSubmitType_REBASE_IF_NECESSARY();
   String projectSubmitType_CHERRY_PICK();
 
+  String headingProjectState();
   String projectState_ACTIVE();
   String projectState_READ_ONLY();
   String projectState_HIDDEN();
 
-  String groupType_SYSTEM();
-  String groupType_INTERNAL();
-  String groupType_LDAP();
-
   String columnMember();
   String columnEmailAddress();
   String columnGroupName();
@@ -93,21 +91,20 @@
   String buttonDeleteBranch();
   String branchDeletionOpenChanges();
 
-  String groupListPrev();
-  String groupListNext();
-  String groupListOpen();
+  String groupItemHelp();
 
   String groupListTitle();
+  String groupFilter();
   String createGroupTitle();
   String groupTabGeneral();
   String groupTabMembers();
   String projectListTitle();
+  String projectFilter();
   String createProjectTitle();
-  String projectAdminTabGeneral();
-  String projectAdminTabBranches();
-  String projectAdminTabAccess();
+  String projectListQueryLink();
 
   String plugins();
+  String pluginEnabled();
   String pluginDisabled();
 
   String columnPluginName();
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 3ff3f43..1637919 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
@@ -22,8 +22,8 @@
 projectRepoBrowser = Repository Browser
 useContentMerge = Automatically resolve conflicts
 useContributorAgreements = Require a valid contributor agreement to upload
-useSignedOffBy = Require <a href="http://gerrit.googlecode.com/svn/documentation/2.0/user-signedoffby.html#Signed-off-by" target="_blank"><code>Signed-off-by</code></a> in commit message
-requireChangeID = Require <a href="http://gerrit.googlecode.com/svn/documentation/2.0/user-changeid.html" target="_blank"><code>Change-Id</code></a> in commit message
+useSignedOffBy = Require <a href="http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/user-signedoffby.html#Signed-off-by" target="_blank"><code>Signed-off-by</code></a> in commit message
+requireChangeID = Require <a href="http://gerrit-documentation.googlecode.com/svn/Documentation/2.5/user-changeid.html" target="_blank"><code>Change-Id</code></a> in commit message
 headingGroupOptions = Group Options
 isVisibleToAll = Make group visible to all registered users.
 buttonSaveGroupOptions = Save Group Options
@@ -36,7 +36,6 @@
 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.
@@ -44,19 +43,18 @@
 headingCreateGroup = Create New Group
 headingAgreements = Contributor Agreements
 
+headingProjectSubmitType = Submit Type
 projectSubmitType_FAST_FORWARD_ONLY = Fast Forward Only
-projectSubmitType_MERGE_IF_NECESSARY = Merge If Necessary
+projectSubmitType_MERGE_IF_NECESSARY = Merge if Necessary
+projectSubmitType_REBASE_IF_NECESSARY = Rebase if Necessary
 projectSubmitType_MERGE_ALWAYS = Always Merge
 projectSubmitType_CHERRY_PICK = Cherry Pick
 
+headingProjectState = State
 projectState_ACTIVE = Active
 projectState_READ_ONLY = Read Only
 projectState_HIDDEN = Hidden
 
-groupType_SYSTEM = System Group
-groupType_INTERNAL = Internal Group
-groupType_LDAP = LDAP Group
-
 columnMember = Member
 columnEmailAddress = Email Address
 columnGroupName = Group Name
@@ -73,21 +71,20 @@
 branchDeletionOpenChanges = The following branches were not deleted \
 because they have open changes:
 
-groupListPrev = Previous group
-groupListNext = Next group
-groupListOpen = Open group
+groupItemHelp = group
 
 groupListTitle = Groups
+groupFilter = Filter
 createGroupTitle = Create Group
 groupTabGeneral = General
 groupTabMembers = Members
 projectListTitle = Projects
+projectFilter = Filter
 createProjectTitle = Create Project
-projectAdminTabGeneral = General
-projectAdminTabBranches = Branches
-projectAdminTabAccess = Access
+projectListQueryLink = Search for changes on this project
 
 plugins = Plugins
+pluginEnabled = Enabled
 pluginDisabled = Disabled
 columnPluginName = Plugin Name
 columnPluginVersion = Version
@@ -104,28 +101,41 @@
 permissionNames = \
 	abandon, \
 	create, \
+	deleteDrafts, \
+	editTopicName, \
 	forgeAuthor, \
 	forgeCommitter, \
 	forgeServerAsCommitter, \
 	owner, \
+	publishDrafts, \
 	push, \
 	pushMerge, \
 	pushTag, \
+	pushSignedTag, \
 	read, \
 	rebase, \
-	submit
+	removeReviewer, \
+	submit, \
+	viewDrafts
+
 abandon = Abandon
 create = Create Reference
+deleteDrafts = Delete Drafts
+editTopicName = Edit Topic Name
 forgeAuthor = Forge Author Identity
 forgeCommitter = Forge Committer Identity
 forgeServerAsCommitter = Forge Server Identity
 owner = Owner
+publishDrafts = Publish Drafts
 push = Push
 pushMerge = Push Merge Commit
 pushTag = Push Annotated Tag
+pushSignedTag = Push Signed Tag
 read = Read
 rebase = Rebase
+removeReviewer = Remove Reviewer
 submit = Submit
+viewDrafts = View Drafts
 
 refErrorEmpty = Reference must be supplied
 refErrorBeginSlash = Reference must not start with '/'
@@ -136,6 +146,7 @@
 
 # Capability Names
 capabilityNames = \
+  accessDatabase, \
   administrateServer, \
   createAccount, \
   createGroup, \
@@ -145,11 +156,13 @@
   killTask, \
   priority, \
   queryLimit, \
+  runGC, \
   startReplication, \
   viewCaches, \
   viewConnections, \
   viewQueue
-administrateServer = Administrate Server 
+accessDatabase = Access Database
+administrateServer = Administrate Server
 createAccount = Create Account
 createGroup = Create Group
 createProject = Create Project
@@ -158,6 +171,7 @@
 killTask = Kill Task
 priority = Priority
 queryLimit = Query Limit
+runGC = Run Garbage Collection
 startReplication = Start Replication
 viewCaches = View Caches
 viewConnections = View Connections
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateGroupScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateGroupScreen.java
index 4574c6d..69dff5c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateGroupScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateGroupScreen.java
@@ -20,17 +20,21 @@
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.NotFoundScreen;
 import com.google.gerrit.client.account.AccountCapabilities;
+import com.google.gerrit.client.groups.GroupApi;
+import com.google.gerrit.client.groups.GroupInfo;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.ui.OnEditEnabler;
 import com.google.gerrit.client.ui.Screen;
 import com.google.gerrit.client.ui.SmallHeading;
 import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gwt.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.event.dom.client.KeyCodes;
 import com.google.gwt.event.dom.client.KeyPressEvent;
 import com.google.gwt.event.dom.client.KeyPressHandler;
+import com.google.gwt.user.client.Event;
 import com.google.gwt.user.client.History;
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.VerticalPanel;
@@ -73,7 +77,24 @@
     addPanel.setStyleName(Gerrit.RESOURCES.css().addSshKeyPanel());
     addPanel.add(new SmallHeading(Util.C.headingCreateGroup()));
 
-    addTxt = new NpTextBox();
+    addTxt = new NpTextBox() {
+      @Override
+      public void onBrowserEvent(Event event) {
+        super.onBrowserEvent(event);
+        if (event.getTypeInt() == Event.ONPASTE) {
+          Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+            @Override
+            public void execute() {
+              if (addTxt.getValue().trim().length() != 0) {
+                addNew.setEnabled(true);
+              }
+            }
+          });
+        }
+      }
+    };
+    addTxt.sinkEvents(Event.ONPASTE);
+
     addTxt.setVisibleLength(60);
     addTxt.addKeyPressHandler(new KeyPressHandler() {
       @Override
@@ -106,9 +127,10 @@
     }
 
     addNew.setEnabled(false);
-    Util.GROUP_SVC.createGroup(newName, new GerritCallback<AccountGroup.Id>() {
-      public void onSuccess(final AccountGroup.Id result) {
-        History.newItem(Dispatcher.toGroup(result, AccountGroupScreen.MEMBERS));
+    GroupApi.createGroup(newName, new GerritCallback<GroupInfo>() {
+      public void onSuccess(final GroupInfo result) {
+        History.newItem(Dispatcher.toGroup(result.getGroupId(),
+            AccountGroupScreen.MEMBERS));
       }
 
       @Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateProjectScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateProjectScreen.java
index b4950d8..7749e9c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateProjectScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/CreateProjectScreen.java
@@ -20,23 +20,31 @@
 import com.google.gerrit.client.ErrorDialog;
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.NotFoundScreen;
+import com.google.gerrit.client.VoidResult;
 import com.google.gerrit.client.account.AccountCapabilities;
+import com.google.gerrit.client.projects.ProjectApi;
 import com.google.gerrit.client.projects.ProjectInfo;
 import com.google.gerrit.client.projects.ProjectMap;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.ui.HintTextBox;
+import com.google.gerrit.client.ui.OnEditEnabler;
 import com.google.gerrit.client.ui.ProjectListPopup;
 import com.google.gerrit.client.ui.ProjectNameSuggestOracle;
 import com.google.gerrit.client.ui.ProjectsTable;
 import com.google.gerrit.client.ui.Screen;
 import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.common.ProjectUtil;
 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;
 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.Event;
 import com.google.gwt.user.client.History;
+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.CheckBox;
@@ -44,7 +52,6 @@
 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;
 
 public class CreateProjectScreen extends Screen {
   private Grid grid;
@@ -52,7 +59,7 @@
   private Button create;
   private Button browse;
   private HintTextBox parent;
-  private SuggestBox sugestParent;
+  private SuggestBox suggestParent;
   private CheckBox emptyCommit;
   private CheckBox permissionsOnly;
   private ProjectsTable suggestedParentsTab;
@@ -95,8 +102,8 @@
       @Override
       protected void onMovePointerTo(String projectName) {
         // prevent user input from being overwritten by simply poping up
-        if (!projectsPopup.isPopingUp() || "".equals(sugestParent.getText())) {
-          sugestParent.setText(projectName);
+        if (!projectsPopup.isPoppingUp() || "".equals(suggestParent.getText())) {
+          suggestParent.setText(projectName);
         }
       }
     };
@@ -107,8 +114,8 @@
     final VerticalPanel fp = new VerticalPanel();
     fp.setStyleName(Gerrit.RESOURCES.css().createProjectPanel());
 
-    initCreateTxt();
     initCreateButton();
+    initCreateTxt();
     initParentBox();
 
     addGrid(fp);
@@ -126,20 +133,38 @@
   }
 
   private void initCreateTxt() {
-    project = new NpTextBox();
+    project = new NpTextBox() {
+      @Override
+      public void onBrowserEvent(Event event) {
+        super.onBrowserEvent(event);
+        if (event.getTypeInt() == Event.ONPASTE) {
+          Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+            @Override
+            public void execute() {
+              if (project.getValue().trim().length() != 0) {
+                create.setEnabled(true);
+              }
+            }
+          });
+        }
+      }
+    };
+    project.sinkEvents(Event.ONPASTE);
     project.setVisibleLength(50);
     project.addKeyPressHandler(new KeyPressHandler() {
       @Override
       public void onKeyPress(KeyPressEvent event) {
-        if (event.getCharCode() == KeyCodes.KEY_ENTER) {
+        if (event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER) {
           doCreateProject();
         }
       }
     });
+    new OnEditEnabler(create, project);
   }
 
   private void initCreateButton() {
     create = new Button(Util.C.buttonCreateProject());
+    create.setEnabled(false);
     create.addClickHandler(new ClickHandler() {
       @Override
       public void onClick(final ClickEvent event) {
@@ -167,7 +192,7 @@
 
   private void initParentBox() {
     parent = new HintTextBox();
-    sugestParent =
+    suggestParent =
         new SuggestBox(new ProjectNameSuggestOracle(), parent);
     parent.setVisibleLength(50);
   }
@@ -185,7 +210,7 @@
 
           @Override
           public void onClick(ClickEvent event) {
-            sugestParent.setText(getRowItem(row).name());
+            suggestParent.setText(getRowItem(row).name());
           }
         });
 
@@ -215,14 +240,14 @@
     grid.setText(0, 0, Util.C.columnProjectName() + ":");
     grid.setWidget(0, 1, project);
     grid.setText(1, 0, Util.C.headingParentProjectName() + ":");
-    grid.setWidget(1, 1, sugestParent);
+    grid.setWidget(1, 1, suggestParent);
     grid.setWidget(1, 2, browse);
     fp.add(grid);
   }
 
   private void doCreateProject() {
     final String projectName = project.getText().trim();
-    final String parentName = sugestParent.getText().trim();
+    final String parentName = suggestParent.getText().trim();
 
     if ("".equals(projectName)) {
       project.setFocus(true);
@@ -230,13 +255,13 @@
     }
 
     enableForm(false);
-    Util.PROJECT_SVC.createNewProject(projectName, parentName,
-        emptyCommit.getValue(), permissionsOnly.getValue(),
-        new GerritCallback<VoidResult>() {
+    ProjectApi.createProject(projectName, parentName, emptyCommit.getValue(),
+        permissionsOnly.getValue(), new AsyncCallback<VoidResult>() {
           @Override
           public void onSuccess(VoidResult result) {
+            String nameWithoutSuffix = ProjectUtil.stripGitSuffix(projectName);
             History.newItem(Dispatcher.toProjectAdmin(new Project.NameKey(
-                projectName), ProjectScreen.INFO));
+                nameWithoutSuffix), ProjectScreen.INFO));
           }
 
           @Override
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 3157d97..dac0b6a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/GroupListScreen.java
@@ -14,36 +14,107 @@
 
 package com.google.gerrit.client.admin;
 
-import com.google.gerrit.client.rpc.ScreenLoadCallback;
-import com.google.gerrit.client.ui.AccountScreen;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.common.data.GroupList;
+import static com.google.gerrit.common.PageLinks.ADMIN_GROUPS;
 
-public class GroupListScreen extends AccountScreen {
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.groups.GroupMap;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.ui.AccountScreen;
+import com.google.gerrit.client.ui.FilteredUserInterface;
+import com.google.gerrit.client.ui.IgnoreOutdatedFilterResultsCallbackWrapper;
+import com.google.gerrit.common.PageLinks;
+import com.google.gwt.event.dom.client.KeyUpEvent;
+import com.google.gwt.event.dom.client.KeyUpHandler;
+import com.google.gwt.http.client.URL;
+import com.google.gwt.user.client.ui.HorizontalPanel;
+import com.google.gwt.user.client.ui.Label;
+import com.google.gwtexpui.globalkey.client.NpTextBox;
+
+public class GroupListScreen extends AccountScreen implements FilteredUserInterface {
   private GroupTable groups;
+  private NpTextBox filterTxt;
+  private String subname;
+
+  public GroupListScreen() {
+  }
+
+  public GroupListScreen(String params) {
+    for (String kvPair : params.split("[,;&]")) {
+      String[] kv = kvPair.split("=", 2);
+      if (kv.length != 2 || kv[0].isEmpty()) {
+        continue;
+      }
+
+      if ("filter".equals(kv[0])) {
+        subname = URL.decodeQueryString(kv[1]);
+      }
+    }
+  }
 
   @Override
   protected void onLoad() {
     super.onLoad();
-    Util.GROUP_SVC
-        .visibleGroups(new ScreenLoadCallback<GroupList>(this) {
-          @Override
-          protected void preDisplay(GroupList result) {
-            groups.display(result.getGroups());
-            groups.finishDisplay();
-          }
-        });
+    display();
+    refresh();
+  }
+
+  private void refresh() {
+    setToken(subname == null || "".equals(subname) ? ADMIN_GROUPS
+        : ADMIN_GROUPS + "?filter=" + URL.encodeQueryString(subname));
+    GroupMap.match(subname,
+        new IgnoreOutdatedFilterResultsCallbackWrapper<GroupMap>(this,
+            new GerritCallback<GroupMap>() {
+              @Override
+              public void onSuccess(GroupMap result) {
+                groups.display(result, subname);
+                groups.finishDisplay();
+              }
+            }));
+  }
+
+  @Override
+  public String getCurrentFilter() {
+    return subname;
   }
 
   @Override
   protected void onInitUI() {
     super.onInitUI();
     setPageTitle(Util.C.groupListTitle());
+    initPageHeader();
 
-    groups = new GroupTable(true /* hyperlink to admin */, PageLinks.ADMIN_GROUPS);
+    groups = new GroupTable(PageLinks.ADMIN_GROUPS);
     add(groups);
   }
 
+  private void initPageHeader() {
+    final HorizontalPanel hp = new HorizontalPanel();
+    hp.setStyleName(Gerrit.RESOURCES.css().projectFilterPanel());
+    final Label filterLabel = new Label(Util.C.projectFilter());
+    filterLabel.setStyleName(Gerrit.RESOURCES.css().projectFilterLabel());
+    hp.add(filterLabel);
+    filterTxt = new NpTextBox();
+    filterTxt.setValue(subname);
+    filterTxt.addKeyUpHandler(new KeyUpHandler() {
+      @Override
+      public void onKeyUp(KeyUpEvent event) {
+        subname = filterTxt.getValue();
+        refresh();
+      }
+    });
+    hp.add(filterTxt);
+    add(hp);
+  }
+
+  @Override
+  public void onShowView() {
+    super.onShowView();
+    if (subname != null) {
+      filterTxt.setCursorPos(subname.length());
+    }
+    filterTxt.setFocus(true);
+  }
+
   @Override
   public void registerKeys() {
     super.registerKeys();
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 6818841..76a0cfb 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
@@ -14,44 +14,45 @@
 
 package com.google.gerrit.client.admin;
 
+import static com.google.gerrit.client.admin.Util.C;
+
 import com.google.gerrit.client.Dispatcher;
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.ui.Hyperlink;
+import com.google.gerrit.client.groups.GroupInfo;
+import com.google.gerrit.client.groups.GroupList;
+import com.google.gerrit.client.groups.GroupMap;
+import com.google.gerrit.client.rpc.Natives;
+import com.google.gerrit.client.ui.HighlightingInlineHyperlink;
 import com.google.gerrit.client.ui.NavigationTable;
-import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.client.ui.Util;
+import com.google.gerrit.common.PageLinks;
 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.Anchor;
 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.Collections;
+import java.util.Comparator;
 import java.util.List;
 
 
-public class GroupTable extends NavigationTable<AccountGroup> {
+public class GroupTable extends NavigationTable<GroupInfo> {
   private static final int NUM_COLS = 3;
 
-  private final boolean enableLink;
-
-  public GroupTable(final boolean enableLink) {
-    this(enableLink, null);
+  public GroupTable() {
+    this(null);
   }
 
-  public GroupTable(final boolean enableLink, final String pointerId) {
-    this.enableLink = enableLink;
-
+  public GroupTable(final String pointerId) {
+    super(C.groupItemHelp());
     setSavePointerId(pointerId);
-    keysNavigation.add(new PrevKeyCommand(0, 'k', Util.C.groupListPrev()));
-    keysNavigation.add(new NextKeyCommand(0, 'j', Util.C.groupListNext()));
-    keysNavigation.add(new OpenKeyCommand(0, 'o', Util.C.groupListOpen()));
-    keysNavigation.add(new OpenKeyCommand(0, KeyCodes.KEY_ENTER, Util.C
-        .groupListOpen()));
 
-    table.setText(0, 1, Util.C.columnGroupName());
-    table.setText(0, 2, Util.C.columnGroupDescription());
-    table.setText(0, 3, Util.C.columnGroupVisibleToAll());
+    table.setText(0, 1, C.columnGroupName());
+    table.setText(0, 2, C.columnGroupDescription());
+    table.setText(0, 3, C.columnGroupVisibleToAll());
     table.addClickHandler(new ClickHandler() {
       @Override
       public void onClick(ClickEvent event) {
@@ -70,36 +71,57 @@
   }
 
   @Override
-  protected Object getRowItemKey(final AccountGroup item) {
-    return item.getId();
+  protected Object getRowItemKey(final GroupInfo item) {
+    return item.getGroupId();
   }
 
   @Override
   protected void onOpenRow(final int row) {
-    History.newItem(Dispatcher.toGroup(getRowItem(row).getId()));
+    History.newItem(Dispatcher.toGroup(getRowItem(row).getGroupId()));
   }
 
-  public void display(final List<AccountGroup> result) {
+  public void display(GroupMap groups, String toHighlight) {
+    display(Natives.asList(groups.values()), toHighlight);
+  }
+
+  public void display(GroupList groups) {
+    display(Natives.asList(groups), null);
+  }
+
+  public void display(List<GroupInfo> list, String toHighlight) {
     while (1 < table.getRowCount())
       table.removeRow(table.getRowCount() - 1);
 
-    for(AccountGroup group : result) {
+    Collections.sort(list, new Comparator<GroupInfo>() {
+      @Override
+      public int compare(GroupInfo a, GroupInfo b) {
+        return a.name().compareTo(b.name());
+      }
+    });
+    for(GroupInfo group : list) {
       final int row = table.getRowCount();
       table.insertRow(row);
       applyDataRowStyle(row);
-      populate(row, group);
+      populate(row, group, toHighlight);
     }
   }
 
-  void populate(final int row, final AccountGroup k) {
-    if (enableLink) {
-      table.setWidget(row, 1, new Hyperlink(k.getName(),
-          Dispatcher.toGroup(k.getId())));
+  void populate(final int row, final GroupInfo k, final String toHighlight) {
+    if (k.url() != null) {
+      if (k.url().startsWith("#" + PageLinks.ADMIN_GROUPS)) {
+        table.setWidget(row, 1, new HighlightingInlineHyperlink(k.name(),
+            Dispatcher.toGroup(k.getGroupId()), toHighlight));
+      } else {
+        Anchor link = new Anchor();
+        link.setHTML(Util.highlight(k.name(), toHighlight));
+        link.setHref(k.url());
+        table.setWidget(row, 1, link);
+      }
     } else {
-      table.setText(row, 1, k.getName());
+      table.setHTML(row, 1, Util.highlight(k.name(), toHighlight));
     }
-    table.setText(row, 2, k.getDescription());
-    if (k.isVisibleToAll()) {
+    table.setText(row, 2, k.description());
+    if (k.options().isVisibleToAll()) {
       table.setWidget(row, 3, new Image(Gerrit.RESOURCES.greenCheck()));
     }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.java
index 2c43233..9848c18 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionEditor.java
@@ -14,13 +14,13 @@
 
 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.LabelType;
+import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRange;
 import com.google.gerrit.common.data.PermissionRule;
@@ -102,16 +102,19 @@
   private final Project.NameKey projectName;
   private final boolean readOnly;
   private final AccessSection section;
+  private final LabelTypes labelTypes;
   private Permission value;
   private PermissionRange.WithDefaults validRange;
   private boolean isDeleted;
 
   public PermissionEditor(Project.NameKey projectName,
       boolean readOnly,
-      AccessSection section) {
+      AccessSection section,
+      LabelTypes labelTypes) {
     this.readOnly = readOnly;
     this.section = section;
     this.projectName = projectName;
+    this.labelTypes = labelTypes;
 
     normalName = new ValueLabel<String>(PermissionNameRenderer.INSTANCE);
     deletedName = new ValueLabel<String>(PermissionNameRenderer.INSTANCE);
@@ -260,12 +263,12 @@
     this.value = value;
 
     if (value.isLabel()) {
-      ApprovalType at = Gerrit.getConfig().getApprovalTypes().byLabel(value.getLabel());
-      if (at != null) {
+      LabelType lt = labelTypes.byLabel(value.getLabel());
+      if (lt != null) {
         validRange = new PermissionRange.WithDefaults(
             value.getName(),
-            at.getMin().getValue(), at.getMax().getValue(),
-            at.getMin().getValue(), at.getMax().getValue());
+            lt.getMin().getValue(), lt.getMax().getValue(),
+            lt.getMin().getValue(), lt.getMax().getValue());
       }
     } else if (GlobalCapability.isCapability(value.getName())) {
       validRange = GlobalCapability.getRange(value.getName());
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionRuleEditor.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionRuleEditor.java
index 5dd8b3c..0c2629c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionRuleEditor.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PermissionRuleEditor.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.client.admin;
 
+import static com.google.gerrit.common.data.Permission.EDIT_TOPIC_NAME;
 import static com.google.gerrit.common.data.Permission.PUSH;
 import static com.google.gerrit.common.data.Permission.PUSH_TAG;
 
@@ -137,8 +138,11 @@
     if (canForce) {
       String ref = section.getName();
       canForce = !ref.startsWith("refs/for/") && !ref.startsWith("^refs/for/");
+      force.setText(PermissionRule.FORCE_PUSH);
+    } else {
+      canForce = EDIT_TOPIC_NAME.equals(name);
+      force.setText(PermissionRule.FORCE_EDIT);
     }
-    force.setText(PermissionRule.FORCE_PUSH);
     force.setVisible(canForce);
     force.setEnabled(!readOnly);
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PluginListScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PluginListScreen.java
index 3948b35..9e4b81b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PluginListScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/PluginListScreen.java
@@ -17,6 +17,7 @@
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.plugins.PluginInfo;
 import com.google.gerrit.client.plugins.PluginMap;
+import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
 import com.google.gerrit.client.ui.FancyFlexTable;
 import com.google.gwt.user.client.ui.Anchor;
@@ -73,7 +74,7 @@
         table.removeRow(table.getRowCount() - 1);
       }
 
-      for (final PluginInfo p : plugins.values().asList()) {
+      for (final PluginInfo p : Natives.asList(plugins.values())) {
         final int row = table.getRowCount();
         table.insertRow(row);
         applyDataRowStyle(row);
@@ -92,9 +93,8 @@
                 + plugin.name() + "/")));
       }
       table.setText(row, 2, plugin.version());
-      if (plugin.isDisabled()) {
-        table.setText(row, 3, Util.C.pluginDisabled());
-      }
+      table.setText(row, 3, plugin.isDisabled() ? Util.C.pluginDisabled()
+          : Util.C.pluginEnabled());
 
       final FlexCellFormatter fmt = table.getFlexCellFormatter();
       fmt.addStyleName(row, 1, Gerrit.RESOURCES.css().dataCell());
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 4403ce6..96824f3 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
@@ -14,6 +14,9 @@
 
 package com.google.gerrit.client.admin;
 
+import static com.google.gerrit.common.ProjectAccessUtil.mergeSections;
+import static com.google.gerrit.common.ProjectAccessUtil.removeEmptyPermissionsAndSections;
+
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
@@ -39,6 +42,7 @@
 
 import java.util.Collections;
 import java.util.HashSet;
+import java.util.List;
 import java.util.Set;
 
 public class ProjectAccessScreen extends ProjectScreen {
@@ -110,6 +114,7 @@
             displayReadOnly(access);
           }
         });
+    savedPanel = ACCESS;
   }
 
   private void displayReadOnly(ProjectAccess access) {
@@ -194,12 +199,14 @@
 
           private Set<String> getDiffs(ProjectAccess wantedAccess,
               ProjectAccess newAccess) {
+            final List<AccessSection> wantedSections =
+                mergeSections(removeEmptyPermissionsAndSections(wantedAccess.getLocal()));
             final HashSet<AccessSection> same =
-                new HashSet<AccessSection>(wantedAccess.getLocal());
+                new HashSet<AccessSection>(wantedSections);
             final HashSet<AccessSection> different =
-                new HashSet<AccessSection>(wantedAccess.getLocal().size()
+                new HashSet<AccessSection>(wantedSections.size()
                     + newAccess.getLocal().size());
-            different.addAll(wantedAccess.getLocal());
+            different.addAll(wantedSections);
             different.addAll(newAccess.getLocal());
             same.retainAll(newAccess.getLocal());
             different.removeAll(same);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.ui.xml
index a664191..ab26ba8 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectAccessScreen.ui.xml
@@ -83,6 +83,5 @@
       <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 56d9417..a7c6a23 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
@@ -24,9 +24,8 @@
 import com.google.gerrit.client.ui.BranchLink;
 import com.google.gerrit.client.ui.FancyFlexTable;
 import com.google.gerrit.client.ui.HintTextBox;
+import com.google.gerrit.common.data.AddBranchResult;
 import com.google.gerrit.common.data.ListBranchesResult;
-import com.google.gerrit.common.errors.InvalidNameException;
-import com.google.gerrit.common.errors.InvalidRevisionException;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
@@ -44,9 +43,9 @@
 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.TextBox;
 import com.google.gwt.user.client.ui.VerticalPanel;
 import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
-import com.google.gwtjsonrpc.client.RemoteJsonException;
 
 import java.util.HashSet;
 import java.util.List;
@@ -87,6 +86,7 @@
             }
           }
         });
+    savedPanel = BRANCH;
   }
 
   private void display(final List<Branch> listBranches) {
@@ -106,12 +106,13 @@
     super.onInitUI();
 
     addPanel = new FlowPanel();
-    addPanel.setStyleName(Gerrit.RESOURCES.css().addSshKeyPanel());
 
     final Grid addGrid = new Grid(2, 2);
+    addGrid.setStyleName(Gerrit.RESOURCES.css().addBranch());
+    final int texBoxLength = 50;
 
     nameTxtBox = new HintTextBox();
-    nameTxtBox.setVisibleLength(50);
+    nameTxtBox.setVisibleLength(texBoxLength);
     nameTxtBox.setHintText(Util.C.defaultBranchName());
     nameTxtBox.addKeyPressHandler(new KeyPressHandler() {
       @Override
@@ -125,7 +126,7 @@
     addGrid.setWidget(0, 1, nameTxtBox);
 
     irevTxtBox = new HintTextBox();
-    irevTxtBox.setVisibleLength(50);
+    irevTxtBox.setVisibleLength(texBoxLength);
     irevTxtBox.setHintText(Util.C.defaultRevisionSpec());
     irevTxtBox.addKeyPressHandler(new KeyPressHandler() {
       @Override
@@ -164,13 +165,13 @@
   }
 
   private void doAddNewBranch() {
-    String branchName = nameTxtBox.getText();
+    final String branchName = nameTxtBox.getText();
     if ("".equals(branchName)) {
       nameTxtBox.setFocus(true);
       return;
     }
 
-    String rev = irevTxtBox.getText();
+    final String rev = irevTxtBox.getText();
     if ("".equals(rev)) {
       irevTxtBox.setText("HEAD");
       Scheduler.get().scheduleDeferred(new ScheduledCommand() {
@@ -183,41 +184,70 @@
       return;
     }
 
-    if (!branchName.startsWith(Branch.R_REFS)) {
-      branchName = Branch.R_HEADS + branchName;
-    }
-
     addBranch.setEnabled(false);
     Util.PROJECT_SVC.addBranch(getProjectKey(), branchName, rev,
-        new GerritCallback<ListBranchesResult>() {
-          public void onSuccess(final ListBranchesResult result) {
+        new GerritCallback<AddBranchResult>() {
+          public void onSuccess(final AddBranchResult result) {
             addBranch.setEnabled(true);
-            nameTxtBox.setText("");
-            irevTxtBox.setText("");
-            display(result.getBranches());
+            if (!result.hasError()) {
+              nameTxtBox.setText("");
+              irevTxtBox.setText("");
+              display(result.getListBranchesResult().getBranches());
+            } else {
+              final AddBranchResult.Error error = result.getError();
+              final String msg;
+              switch (error.getType()) {
+                case INVALID_NAME:
+                  selectAllAndFocus(nameTxtBox);
+                  msg = Gerrit.M.invalidBranchName(branchName);
+                  break;
+
+                case INVALID_REVISION:
+                  selectAllAndFocus(irevTxtBox);
+                  msg = Gerrit.M.invalidRevision(rev);
+                  break;
+
+                case BRANCH_CREATION_NOT_ALLOWED_UNDER_REFNAME_PREFIX:
+                  selectAllAndFocus(nameTxtBox);
+                  msg =
+                      Gerrit.M.branchCreationNotAllowedUnderRefnamePrefix(error
+                          .getRefname());
+                  break;
+
+                case BRANCH_ALREADY_EXISTS:
+                  selectAllAndFocus(nameTxtBox);
+                  msg = Gerrit.M.branchAlreadyExists(error.getRefname());
+                  break;
+
+                case BRANCH_CREATION_CONFLICT:
+                  selectAllAndFocus(nameTxtBox);
+                  msg =
+                      Gerrit.M.branchCreationConflict(branchName,
+                          error.getRefname());
+                  break;
+
+                default:
+                  msg =
+                      Gerrit.M.branchCreationFailed(branchName,
+                          error.toString());
+              }
+              new ErrorDialog(msg).center();
+            }
           }
 
           @Override
           public void onFailure(final Throwable caught) {
-            if (caught instanceof InvalidNameException
-                || caught instanceof RemoteJsonException
-                && caught.getMessage().equals(InvalidNameException.MESSAGE)) {
-              nameTxtBox.selectAll();
-              nameTxtBox.setFocus(true);
-
-            } else if (caught instanceof InvalidRevisionException
-                || caught instanceof RemoteJsonException
-                && caught.getMessage().equals(InvalidRevisionException.MESSAGE)) {
-              irevTxtBox.selectAll();
-              irevTxtBox.setFocus(true);
-            }
-
             addBranch.setEnabled(true);
             super.onFailure(caught);
           }
         });
   }
 
+  private static void selectAllAndFocus(final TextBox textBox) {
+    textBox.selectAll();
+    textBox.setFocus(true);
+  }
+
   private class BranchesTable extends FancyFlexTable<Branch> {
     boolean canDelete;
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectDashboardsScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectDashboardsScreen.java
new file mode 100644
index 0000000..f779861
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/admin/ProjectDashboardsScreen.java
@@ -0,0 +1,62 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.admin;
+
+import com.google.gerrit.client.dashboards.DashboardList;
+import com.google.gerrit.client.dashboards.DashboardsTable;
+import com.google.gerrit.client.rpc.ScreenLoadCallback;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwt.user.client.ui.FlowPanel;
+
+public class ProjectDashboardsScreen extends ProjectScreen {
+  private DashboardsTable dashes;
+  Project.NameKey project;
+
+  public ProjectDashboardsScreen(final Project.NameKey project) {
+    super(project);
+    this.project = project;
+  }
+
+  @Override
+  protected void onLoad() {
+    super.onLoad();
+    DashboardList.all(getProjectKey(),
+        new ScreenLoadCallback<JsArray<DashboardList>>(this) {
+      @Override
+      protected void preDisplay(JsArray<DashboardList> result) {
+        dashes.display(result);
+      }
+    });
+    savedPanel = DASHBOARDS;
+  }
+
+  @Override
+  protected void onInitUI() {
+    super.onInitUI();
+    dashes = new DashboardsTable(project);
+    FlowPanel fp = new FlowPanel();
+    fp.add(dashes);
+    add(fp);
+    dashes.setSavePointerId("dashboards/project/" + getProjectKey().get());
+    display();
+  }
+
+  @Override
+  public void registerKeys() {
+    super.registerKeys();
+    dashes.setRegisterKeys(true);
+  }
+}
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 944a169..40921309 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
@@ -15,36 +15,43 @@
 package com.google.gerrit.client.admin;
 
 import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.download.DownloadPanel;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
 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.client.AccountGeneralPreferences.DownloadCommand;
+import com.google.gerrit.reviewdb.client.InheritedBoolean;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.Project.InheritableBoolean;
 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;
 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;
 import com.google.gwt.user.client.ui.ListBox;
-import com.google.gwt.user.client.ui.Panel;
 import com.google.gwt.user.client.ui.VerticalPanel;
+import com.google.gwt.user.client.ui.Widget;
 import com.google.gwtexpui.globalkey.client.NpTextArea;
 
 public class ProjectInfoScreen extends ProjectScreen {
+  private String projectName;
   private Project project;
 
-  private Panel projectOptionsPanel;
-  private CheckBox requireChangeID;
+  private LabeledWidgetsGrid grid;
+
+  // Section: Project Options
+  private ListBox requireChangeID;
   private ListBox submitType;
   private ListBox state;
-  private CheckBox useContentMerge;
+  private ListBox contentMerge;
 
-  private Panel agreementsPanel;
-  private CheckBox useContributorAgreements;
-  private CheckBox useSignedOffBy;
+  // Section: Contributor Agreements
+  private ListBox contributorAgreements;
+  private ListBox signedOffBy;
 
   private NpTextArea descTxt;
   private Button saveProject;
@@ -53,6 +60,7 @@
 
   public ProjectInfoScreen(final Project.NameKey toShow) {
     super(toShow);
+    projectName = toShow.get();
   }
 
   @Override
@@ -67,9 +75,13 @@
       }
     });
 
+    add(new ProjectDownloadPanel(projectName, true));
+
     initDescription();
+    grid = new LabeledWidgetsGrid();
     initProjectOptions();
     initAgreements();
+    add(grid);
     add(saveProject);
   }
 
@@ -89,6 +101,7 @@
             display(result);
           }
         });
+    savedPanel = INFO;
   }
 
   private void enableForm(final boolean canModifyAgreements,
@@ -96,10 +109,10 @@
       final boolean canModifyState) {
     submitType.setEnabled(canModifyMergeType);
     state.setEnabled(canModifyState);
-    useContentMerge.setEnabled(canModifyMergeType);
+    contentMerge.setEnabled(canModifyMergeType);
     descTxt.setEnabled(canModifyDescription);
-    useContributorAgreements.setEnabled(canModifyAgreements);
-    useSignedOffBy.setEnabled(canModifyAgreements);
+    contributorAgreements.setEnabled(canModifyAgreements);
+    signedOffBy.setEnabled(canModifyAgreements);
     requireChangeID.setEnabled(canModifyMergeType);
   }
 
@@ -118,8 +131,7 @@
   }
 
   private void initProjectOptions() {
-    projectOptionsPanel = new VerticalPanel();
-    projectOptionsPanel.add(new SmallHeading(Util.C.headingProjectOptions()));
+    grid.addHeader(new SmallHeading(Util.C.headingProjectOptions()));
 
     submitType = new ListBox();
     for (final Project.SubmitType type : Project.SubmitType.values()) {
@@ -132,29 +144,34 @@
       }
     });
     saveEnabler.listenTo(submitType);
-    projectOptionsPanel.add(submitType);
+    grid.add(Util.C.headingProjectSubmitType(), 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);
+    grid.add(Util.C.headingProjectState(), state);
 
-    useContentMerge = new CheckBox(Util.C.useContentMerge(), true);
-    saveEnabler.listenTo(useContentMerge);
-    projectOptionsPanel.add(useContentMerge);
+    contentMerge = newInheritedBooleanBox();
+    saveEnabler.listenTo(contentMerge);
+    grid.add(Util.C.useContentMerge(), contentMerge);
 
-    requireChangeID = new CheckBox(Util.C.requireChangeID(), true);
+    requireChangeID = newInheritedBooleanBox();
     saveEnabler.listenTo(requireChangeID);
-    projectOptionsPanel.add(requireChangeID);
+    grid.addHtml(Util.C.requireChangeID(), requireChangeID);
+  }
 
-    add(projectOptionsPanel);
+  private static ListBox newInheritedBooleanBox() {
+    ListBox box = new ListBox();
+    for (InheritableBoolean b : InheritableBoolean.values()) {
+      box.addItem(b.name(), b.name());
+    }
+    return box;
   }
 
   /**
-   * Enables the {@link #useContentMerge} checkbox if the selected submit type
+   * Enables the {@link #contentMerge} checkbox if the selected submit type
    * allows the usage of content merge.
    * If the submit type (currently only 'Fast Forward Only') does not allow
    * content merge the useContentMerge checkbox gets disabled.
@@ -162,26 +179,27 @@
   private void setEnabledForUseContentMerge() {
     if (SubmitType.FAST_FORWARD_ONLY.equals(Project.SubmitType
         .valueOf(submitType.getValue(submitType.getSelectedIndex())))) {
-      useContentMerge.setEnabled(false);
-      useContentMerge.setValue(false);
+      contentMerge.setEnabled(false);
+      final InheritedBoolean inheritedBoolean = new InheritedBoolean();
+      inheritedBoolean.setValue(InheritableBoolean.FALSE);
+      setBool(contentMerge, inheritedBoolean);
     } else {
-      useContentMerge.setEnabled(submitType.isEnabled());
+      contentMerge.setEnabled(submitType.isEnabled());
     }
   }
 
   private void initAgreements() {
-    agreementsPanel = new VerticalPanel();
-    agreementsPanel.add(new SmallHeading(Util.C.headingAgreements()));
+    grid.addHeader(new SmallHeading(Util.C.headingAgreements()));
 
-    useContributorAgreements = new CheckBox(Util.C.useContributorAgreements());
-    saveEnabler.listenTo(useContributorAgreements);
-    agreementsPanel.add(useContributorAgreements);
+    contributorAgreements = newInheritedBooleanBox();
+    if (Gerrit.getConfig().isUseContributorAgreements()) {
+      saveEnabler.listenTo(contributorAgreements);
+      grid.add(Util.C.useContributorAgreements(), contributorAgreements);
+    }
 
-    useSignedOffBy = new CheckBox(Util.C.useSignedOffBy(), true);
-    saveEnabler.listenTo(useSignedOffBy);
-    agreementsPanel.add(useSignedOffBy);
-
-    add(agreementsPanel);
+    signedOffBy = newInheritedBooleanBox();
+    saveEnabler.listenTo(signedOffBy);
+    grid.addHtml(Util.C.useSignedOffBy(), signedOffBy);
   }
 
   private void setSubmitType(final Project.SubmitType newSubmitType) {
@@ -209,21 +227,54 @@
     }
   }
 
+  private void setBool(ListBox box, InheritedBoolean inheritedBoolean) {
+    int inheritedIndex = -1;
+    for (int i = 0; i < box.getItemCount(); i++) {
+      if (box.getValue(i).startsWith(InheritableBoolean.INHERIT.name())) {
+        inheritedIndex = i;
+      }
+      if (box.getValue(i).startsWith(inheritedBoolean.value.name())) {
+        box.setSelectedIndex(i);
+      }
+    }
+    if (inheritedIndex >= 0) {
+      if (project.getParent(Gerrit.getConfig().getWildProject()) == null) {
+        if (box.getSelectedIndex() == inheritedIndex) {
+          for (int i = 0; i < box.getItemCount(); i++) {
+            if (box.getValue(i).equals(InheritableBoolean.FALSE.name())) {
+              box.setSelectedIndex(i);
+              break;
+            }
+          }
+        }
+        box.removeItem(inheritedIndex);
+      } else {
+        box.setItemText(inheritedIndex, InheritableBoolean.INHERIT.name() + " ("
+            + inheritedBoolean.inheritedValue + ")");
+      }
+    }
+  }
+
+  private static InheritableBoolean getBool(ListBox box) {
+    int i = box.getSelectedIndex();
+    if (i >= 0) {
+      final String selectedValue = box.getValue(i);
+      if (selectedValue.startsWith(InheritableBoolean.INHERIT.name())) {
+        return InheritableBoolean.INHERIT;
+      }
+      return InheritableBoolean.valueOf(selectedValue);
+    }
+    return InheritableBoolean.INHERIT;
+  }
+
   void display(final ProjectDetail result) {
     project = result.project;
 
-    final boolean isall =
-        Gerrit.getConfig().getWildProject().equals(project.getNameKey());
-    projectOptionsPanel.setVisible(!isall);
-    agreementsPanel.setVisible(!isall);
-    useContributorAgreements.setVisible(Gerrit.getConfig()
-        .isUseContributorAgreements());
-
     descTxt.setText(project.getDescription());
-    useContributorAgreements.setValue(project.isUseContributorAgreements());
-    useSignedOffBy.setValue(project.isUseSignedOffBy());
-    useContentMerge.setValue(project.isUseContentMerge());
-    requireChangeID.setValue(project.isRequireChangeID());
+    setBool(contributorAgreements, result.useContributorAgreements);
+    setBool(signedOffBy, result.useSignedOffBy);
+    setBool(contentMerge, result.useContentMerge);
+    setBool(requireChangeID, result.requireChangeID);
     setSubmitType(project.getSubmitType());
     setState(project.getState());
 
@@ -232,10 +283,10 @@
 
   private void doSave() {
     project.setDescription(descTxt.getText().trim());
-    project.setUseContributorAgreements(useContributorAgreements.getValue());
-    project.setUseSignedOffBy(useSignedOffBy.getValue());
-    project.setUseContentMerge(useContentMerge.getValue());
-    project.setRequireChangeID(requireChangeID.getValue());
+    project.setUseContributorAgreements(getBool(contributorAgreements));
+    project.setUseSignedOffBy(getBool(signedOffBy));
+    project.setUseContentMerge(getBool(contentMerge));
+    project.setRequireChangeID(getBool(requireChangeID));
     if (submitType.getSelectedIndex() >= 0) {
       project.setSubmitType(Project.SubmitType.valueOf(submitType
           .getValue(submitType.getSelectedIndex())));
@@ -256,4 +307,58 @@
           }
         });
   }
+
+  public class ProjectDownloadPanel extends DownloadPanel {
+    public ProjectDownloadPanel(String project, boolean isAllowsAnonymous) {
+      super(project, null, isAllowsAnonymous);
+    }
+
+    @Override
+    public void populateDownloadCommandLinks() {
+      if (!urls.isEmpty()) {
+        if (allowedCommands.contains(DownloadCommand.CHECKOUT)
+            || allowedCommands.contains(DownloadCommand.DEFAULT_DOWNLOADS)) {
+          commands.add(cmdLinkfactory.new CloneCommandLink());
+        }
+      }
+    }
+  }
+
+  private class LabeledWidgetsGrid extends FlexTable {
+    private String labelSuffix;
+
+    public LabeledWidgetsGrid() {
+      super();
+      labelSuffix = ":";
+    }
+
+    private void addHeader(Widget widget) {
+      int row = getRowCount();
+      insertRow(row);
+      setWidget(row, 0, widget);
+      getCellFormatter().getElement(row, 0).setAttribute("colSpan", "2");
+    }
+
+    private void add(String label, boolean labelIsHtml, Widget widget) {
+      int row = getRowCount();
+      insertRow(row);
+      if (label != null) {
+        if (labelIsHtml) {
+          setHTML(row, 0, label + labelSuffix);
+        } else {
+          setText(row, 0, label + labelSuffix);
+        }
+      }
+      setWidget(row, 1, widget);
+    }
+
+    public void add(String label, Widget widget) {
+      add(label, false, widget);
+    }
+
+    public void addHtml(String label, Widget widget) {
+      add(label, true, widget);
+    }
+
+  }
 }
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 9125622..ee58420 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
@@ -14,38 +14,82 @@
 
 package com.google.gerrit.client.admin;
 
+import static com.google.gerrit.common.PageLinks.ADMIN_PROJECTS;
+
 import com.google.gerrit.client.Dispatcher;
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.GitwebLink;
 import com.google.gerrit.client.projects.ProjectInfo;
 import com.google.gerrit.client.projects.ProjectMap;
-import com.google.gerrit.client.rpc.ScreenLoadCallback;
-import com.google.gerrit.client.ui.Hyperlink;
+import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.ui.FilteredUserInterface;
+import com.google.gerrit.client.ui.HighlightingInlineHyperlink;
+import com.google.gerrit.client.ui.IgnoreOutdatedFilterResultsCallbackWrapper;
+import com.google.gerrit.client.ui.ProjectSearchLink;
 import com.google.gerrit.client.ui.ProjectsTable;
 import com.google.gerrit.client.ui.Screen;
 import com.google.gerrit.common.PageLinks;
+import com.google.gwt.event.dom.client.KeyUpEvent;
+import com.google.gwt.event.dom.client.KeyUpHandler;
+import com.google.gwt.http.client.URL;
 import com.google.gwt.user.client.History;
 import com.google.gwt.user.client.ui.Anchor;
+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.gwtexpui.globalkey.client.NpTextBox;
 
-public class ProjectListScreen extends Screen {
+public class ProjectListScreen extends Screen implements FilteredUserInterface {
   private ProjectsTable projects;
+  private NpTextBox filterTxt;
+  private String subname;
+
+  public ProjectListScreen() {
+  }
+
+  public ProjectListScreen(String params) {
+    for (String kvPair : params.split("[,;&]")) {
+      String[] kv = kvPair.split("=", 2);
+      if (kv.length != 2 || kv[0].isEmpty()) {
+        continue;
+      }
+
+      if ("filter".equals(kv[0])) {
+        subname = URL.decodeQueryString(kv[1]);
+      }
+    }
+  }
 
   @Override
   protected void onLoad() {
     super.onLoad();
-    ProjectMap.all(new ScreenLoadCallback<ProjectMap>(this) {
-      @Override
-      protected void preDisplay(final ProjectMap result) {
-        projects.display(result);
-        projects.finishDisplay();
-      }
-    });
+    display();
+    refresh();
+  }
+
+  private void refresh() {
+    setToken(subname == null || "".equals(subname) ? ADMIN_PROJECTS
+        : ADMIN_PROJECTS + "?filter=" + URL.encodeQueryString(subname));
+    ProjectMap.match(subname,
+        new IgnoreOutdatedFilterResultsCallbackWrapper<ProjectMap>(this,
+            new GerritCallback<ProjectMap>() {
+              @Override
+              public void onSuccess(ProjectMap result) {
+                projects.display(result);
+              }
+            }));
+  }
+
+  @Override
+  public String getCurrentFilter() {
+    return subname;
   }
 
   @Override
   protected void onInitUI() {
     super.onInitUI();
     setPageTitle(Util.C.projectListTitle());
+    initPageHeader();
 
     projects = new ProjectsTable() {
       @Override
@@ -64,7 +108,7 @@
       }
 
       private String link(final ProjectInfo item) {
-        return Dispatcher.toProjectAdmin(item.name_key(), ProjectScreen.INFO);
+        return Dispatcher.toProject(item.name_key());
       }
 
       @Override
@@ -78,7 +122,10 @@
 
       @Override
       protected void populate(final int row, final ProjectInfo k) {
-        table.setWidget(row, 1, new Hyperlink(k.name(), link(k)));
+        FlowPanel fp = new FlowPanel();
+        fp.add(new ProjectSearchLink(k.name_key()));
+        fp.add(new HighlightingInlineHyperlink(k.name(), link(k), subname));
+        table.setWidget(row, 1, fp);
         table.setText(row, 2, k.description());
         GitwebLink l = Gerrit.getGitwebLink();
         if (l != null) {
@@ -94,6 +141,34 @@
     add(projects);
   }
 
+  private void initPageHeader() {
+    final HorizontalPanel hp = new HorizontalPanel();
+    hp.setStyleName(Gerrit.RESOURCES.css().projectFilterPanel());
+    final Label filterLabel = new Label(Util.C.projectFilter());
+    filterLabel.setStyleName(Gerrit.RESOURCES.css().projectFilterLabel());
+    hp.add(filterLabel);
+    filterTxt = new NpTextBox();
+    filterTxt.setValue(subname);
+    filterTxt.addKeyUpHandler(new KeyUpHandler() {
+      @Override
+      public void onKeyUp(KeyUpEvent event) {
+        subname = filterTxt.getValue();
+        refresh();
+      }
+    });
+    hp.add(filterTxt);
+    add(hp);
+  }
+
+  @Override
+  public void onShowView() {
+    super.onShowView();
+    if (subname != null) {
+      filterTxt.setCursorPos(subname.length());
+    }
+    filterTxt.setFocus(true);
+  }
+
   @Override
   public void registerKeys() {
     super.registerKeys();
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 dd5b070..fdf3ab8 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
@@ -14,27 +14,33 @@
 
 package com.google.gerrit.client.admin;
 
-import static com.google.gerrit.client.Dispatcher.toProjectAdmin;
-
-import com.google.gerrit.client.ui.MenuScreen;
+import com.google.gerrit.client.ui.Screen;
 import com.google.gerrit.reviewdb.client.Project;
 
-public abstract class ProjectScreen extends MenuScreen {
+public abstract class ProjectScreen extends Screen {
   public static final String INFO = "info";
   public static final String BRANCH = "branches";
   public static final String ACCESS = "access";
+  public static final String DASHBOARDS = "dashboards";
+
+  protected static String savedPanel;
+  protected static Project.NameKey savedKey;
+
+  public static String getSavedPanel() {
+    return savedPanel;
+  }
+
+  public static Project.NameKey getSavedKey() {
+    return savedKey;
+  }
 
   private final Project.NameKey name;
 
   public ProjectScreen(final Project.NameKey toShow) {
     name = toShow;
-
-    link(Util.C.projectAdminTabGeneral(), toProjectAdmin(name, INFO));
-    link(Util.C.projectAdminTabBranches(), toProjectAdmin(name, BRANCH));
-    link(Util.C.projectAdminTabAccess(), toProjectAdmin(name, ACCESS));
   }
 
-  protected Project.NameKey getProjectKey() {
+  public Project.NameKey getProjectKey() {
     return name;
   }
 
@@ -43,4 +49,10 @@
     super.onInitUI();
     setPageTitle(Util.M.project(name.get()));
   }
+
+  @Override
+  protected void onLoad() {
+    super.onLoad();
+    savedKey = name;
+  }
 }
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 aa5b7b6..1696c43 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
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.client.admin;
 
-import com.google.gerrit.common.data.GroupAdminService;
 import com.google.gerrit.common.data.ProjectAdminService;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gwt.core.client.GWT;
@@ -23,13 +22,9 @@
 public class Util {
   public static final AdminConstants C = GWT.create(AdminConstants.class);
   public static final AdminMessages M = GWT.create(AdminMessages.class);
-  public static final GroupAdminService GROUP_SVC;
   public static final ProjectAdminService PROJECT_SVC;
 
   static {
-    GROUP_SVC = GWT.create(GroupAdminService.class);
-    JsonUtil.bind(GROUP_SVC, "rpc/GroupAdminService");
-
     PROJECT_SVC = GWT.create(ProjectAdminService.class);
     JsonUtil.bind(PROJECT_SVC, "rpc/ProjectAdminService");
 
@@ -45,6 +40,8 @@
         return C.projectSubmitType_FAST_FORWARD_ONLY();
       case MERGE_IF_NECESSARY:
         return C.projectSubmitType_MERGE_IF_NECESSARY();
+      case REBASE_IF_NECESSARY:
+        return C.projectSubmitType_REBASE_IF_NECESSARY();
       case MERGE_ALWAYS:
         return C.projectSubmitType_MERGE_ALWAYS();
       case CHERRY_PICK:
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdConstants.java
index a9b92c2..5d756a1 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdConstants.java
@@ -17,18 +17,6 @@
 import com.google.gwt.i18n.client.Constants;
 
 public interface OpenIdConstants extends Constants {
-  String buttonSignIn();
-  String buttonRegister();
-  String buttonLinkId();
-
-  String rememberMe();
-
-  String notAllowed();
-  String noProvider();
-  String error();
-
   String nameGoogle();
   String nameYahoo();
-
-  String whatIsOpenIDHtml();
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdConstants.properties
index af9a63d..1a8acc1 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdConstants.properties
@@ -1,20 +1,2 @@
-buttonSignIn = Sign In
-buttonRegister = Register
-buttonLinkId = Link Identity
-
-rememberMe = Remember Me
-
-notAllowed = Provider is not allowed.
-noProvider = Provider is not supported, or was incorrectly entered.
-error = Unable to connect with OpenID provider.
-
 nameGoogle = Google Account
 nameYahoo = Yahoo! ID
-
-whatIsOpenIDHtml = \
-  <h2 class="smallHeading" style="margin-top: 25px;">What is OpenID?</h2>\
-  <p>OpenID provides secure single-sign-on, without \
-  revealing your passwords to this website.</p>\
-  <p>There are many OpenID providers available.  You may already \
-  be member of one!</p>\
-  <p><a href="http://openid.net/get/" target="_blank">Get OpenID</a></p>\
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdCss.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdCss.java
deleted file mode 100644
index 5f40cbb..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdCss.java
+++ /dev/null
@@ -1,26 +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.auth.openid;
-
-import com.google.gwt.resources.client.CssResource;
-
-interface OpenIdCss extends CssResource {
-  String loginForm();
-  String logo();
-  String loginLine();
-  String identifier();
-  String directLink();
-  String error();
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdMessages.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdMessages.java
deleted file mode 100644
index 1f4fc5f..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdMessages.java
+++ /dev/null
@@ -1,27 +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.auth.openid;
-
-import com.google.gwt.i18n.client.Messages;
-
-public interface OpenIdMessages extends Messages {
-  String signInAt(String hostname);
-  String registerAt(String hostname);
-  String linkAt(String hostname);
-
-  String signInWith(String who);
-  String registerWith(String who);
-  String linkWith(String who);
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdMessages.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdMessages.properties
deleted file mode 100644
index ea866cc..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdMessages.properties
+++ /dev/null
@@ -1,7 +0,0 @@
-signInAt = Sign In to Gerrit Code Review at {0}
-registerAt = Register with Gerrit Code Review at {0}
-linkAt = Link Another Identity to Gerrit Code Review at {0}
-
-signInWith = Sign in with a {0}
-registerWith = Register with a {0}
-linkWith = Link a {0}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdResources.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdResources.java
deleted file mode 100644
index 992ed0b..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdResources.java
+++ /dev/null
@@ -1,39 +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.auth.openid;
-
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.resources.client.ClientBundle;
-import com.google.gwt.resources.client.DataResource;
-import com.google.gwt.resources.client.ImageResource;
-
-interface OpenIdResources extends ClientBundle {
-  static final OpenIdResources I = GWT.create(OpenIdResources.class);
-
-  @Source("openid.css")
-  OpenIdCss css();
-
-  @Source("identifierBackground.gif")
-  DataResource identifierBackground();
-
-  @Source("openidLogo.png")
-  ImageResource openidLogo();
-
-  @Source("iconGoogle.gif")
-  ImageResource iconGoogle();
-
-  @Source("iconYahoo.gif")
-  ImageResource iconYahoo();
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdSignInDialog.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdSignInDialog.java
deleted file mode 100644
index cd2c493..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdSignInDialog.java
+++ /dev/null
@@ -1,388 +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.auth.openid;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.SignInDialog;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.ui.SmallHeading;
-import com.google.gerrit.common.auth.SignInMode;
-import com.google.gerrit.common.auth.openid.DiscoveryResult;
-import com.google.gerrit.common.auth.openid.OpenIdProviderPattern;
-import com.google.gerrit.common.auth.openid.OpenIdUrls;
-import com.google.gwt.core.client.Scheduler;
-import com.google.gwt.core.client.Scheduler.ScheduledCommand;
-import com.google.gwt.dom.client.FormElement;
-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.resources.client.ImageResource;
-import com.google.gwt.user.client.Cookies;
-import com.google.gwt.user.client.DOM;
-import com.google.gwt.user.client.Window;
-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.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.FormSubmitCompleteEvent;
-import com.google.gwt.user.client.ui.HTML;
-import com.google.gwt.user.client.ui.Hidden;
-import com.google.gwt.user.client.ui.Image;
-import com.google.gwt.user.client.ui.InlineLabel;
-import com.google.gwtexpui.globalkey.client.NpTextBox;
-
-import java.util.Map;
-
-public class OpenIdSignInDialog extends SignInDialog implements
-    FormPanel.SubmitHandler {
-  static {
-    OpenIdResources.I.css().ensureInjected();
-  }
-
-  private final FlowPanel panelWidget;
-  private final FormPanel form;
-  private final FlowPanel formBody;
-  private final FormPanel redirectForm;
-  private final FlowPanel redirectBody;
-
-  private FlowPanel errorLine;
-  private InlineLabel errorMsg;
-
-  private Button login;
-  private NpTextBox providerId;
-  private CheckBox rememberId;
-  private boolean discovering;
-
-  public OpenIdSignInDialog(final SignInMode requestedMode, final String token,
-      final String initialErrorMsg) {
-    super(requestedMode, token);
-
-    formBody = new FlowPanel();
-    formBody.setStyleName(OpenIdResources.I.css().loginForm());
-
-    form = new FormPanel();
-    form.setMethod(FormPanel.METHOD_GET);
-    form.addSubmitHandler(this);
-    form.add(formBody);
-
-    redirectBody = new FlowPanel();
-    redirectBody.setVisible(false);
-    redirectForm = new FormPanel();
-    redirectForm.add(redirectBody);
-
-    panelWidget = new FlowPanel();
-    panelWidget.add(form);
-    panelWidget.add(redirectForm);
-    add(panelWidget);
-
-    createHeaderLogo();
-    createHeaderText();
-    createErrorBox();
-    createIdentBox();
-
-    link(OpenIdUrls.URL_GOOGLE, OpenIdUtil.C.nameGoogle(), OpenIdResources.I
-        .iconGoogle());
-    link(OpenIdUrls.URL_YAHOO, OpenIdUtil.C.nameYahoo(), OpenIdResources.I
-        .iconYahoo());
-
-    if (initialErrorMsg != null) {
-      showError(initialErrorMsg);
-    }
-    formBody.add(new HTML(OpenIdUtil.C.whatIsOpenIDHtml()));
-  }
-
-  @Override
-  public void show() {
-    super.show();
-    providerId.selectAll();
-    Scheduler.get().scheduleDeferred(new ScheduledCommand() {
-      @Override
-      public void execute() {
-        providerId.setFocus(true);
-      }
-    });
-  }
-
-  private void createHeaderLogo() {
-    final FlowPanel headerLogo = new FlowPanel();
-    headerLogo.setStyleName(OpenIdResources.I.css().logo());
-    headerLogo.add(new Image(OpenIdResources.I.openidLogo()));
-    formBody.add(headerLogo);
-  }
-
-  private void createHeaderText() {
-    final FlowPanel headerText = new FlowPanel();
-    final String me = Window.Location.getHostName();
-    final SmallHeading headerLabel = new SmallHeading();
-    switch (mode) {
-      case LINK_IDENTIY:
-        headerLabel.setText(OpenIdUtil.M.linkAt(me));
-        break;
-      case REGISTER:
-        headerLabel.setText(OpenIdUtil.M.registerAt(me));
-        break;
-      case SIGN_IN:
-      default:
-        headerLabel.setText(OpenIdUtil.M.signInAt(me));
-        break;
-    }
-    headerText.add(headerLabel);
-    formBody.add(headerText);
-  }
-
-  private void createErrorBox() {
-    errorLine = new FlowPanel();
-    DOM.setStyleAttribute(errorLine.getElement(), "visibility", "hidden");
-    errorLine.setStyleName(OpenIdResources.I.css().error());
-
-    errorMsg = new InlineLabel();
-    errorLine.add(errorMsg);
-    formBody.add(errorLine);
-  }
-
-  private void showError(final String msgText) {
-    errorMsg.setText(msgText);
-    DOM.setStyleAttribute(errorLine.getElement(), "visibility", "");
-  }
-
-  private void hideError() {
-    DOM.setStyleAttribute(errorLine.getElement(), "visibility", "hidden");
-  }
-
-  private void createIdentBox() {
-    boolean remember = mode == SignInMode.SIGN_IN || mode == SignInMode.REGISTER;
-
-    final FlowPanel group = new FlowPanel();
-    group.setStyleName(OpenIdResources.I.css().loginLine());
-
-    final FlowPanel line1 = new FlowPanel();
-    group.add(line1);
-
-    providerId = new NpTextBox();
-    providerId.setVisibleLength(60);
-    providerId.setStyleName(OpenIdResources.I.css().identifier());
-    providerId.setTabIndex(0);
-    providerId.addKeyPressHandler(new KeyPressHandler() {
-      @Override
-      public void onKeyPress(final KeyPressEvent event) {
-        if (event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER) {
-          event.preventDefault();
-          form.submit();
-        }
-      }
-    });
-    line1.add(providerId);
-
-    login = new Button();
-    switch (mode) {
-      case LINK_IDENTIY:
-        login.setText(OpenIdUtil.C.buttonLinkId());
-        break;
-      case REGISTER:
-        login.setText(OpenIdUtil.C.buttonRegister());
-        break;
-      case SIGN_IN:
-      default:
-        login.setText(OpenIdUtil.C.buttonSignIn());
-        break;
-    }
-    login.addClickHandler(new ClickHandler() {
-      @Override
-      public void onClick(final ClickEvent event) {
-        form.submit();
-      }
-    });
-    login.setTabIndex(remember ? 2 : 1);
-    line1.add(login);
-
-    Button close = new Button(Gerrit.C.signInDialogClose());
-    close.addClickHandler(new ClickHandler() {
-      @Override
-      public void onClick(ClickEvent event) {
-        hide();
-      }
-    });
-    close.setTabIndex(remember ? 3 : 2);
-    line1.add(close);
-
-    if (remember) {
-      rememberId = new CheckBox(OpenIdUtil.C.rememberMe());
-      rememberId.setTabIndex(1);
-      group.add(rememberId);
-
-      String last = Cookies.getCookie(OpenIdUrls.LASTID_COOKIE);
-      if (last != null && !"".equals(last)) {
-        if (last.startsWith("\"") && last.endsWith("\"")) {
-          // Dequote the value. We shouldn't have to do this, but
-          // something is causing some Google Account tokens to get
-          // wrapped up in double quotes when obtained from the cookie.
-          //
-          last = last.substring(1, last.length() - 2);
-        }
-        providerId.setText(last);
-        rememberId.setValue(true);
-      }
-    }
-
-    formBody.add(group);
-  }
-
-  private void link(final String identUrl, final String who,
-      final ImageResource icon) {
-    if (!isAllowedProvider(identUrl)) {
-      return;
-    }
-
-    final ClickHandler i = new ClickHandler() {
-      @Override
-      public void onClick(final ClickEvent event) {
-        event.preventDefault();
-        if (!discovering) {
-          providerId.setText(identUrl);
-          form.submit();
-        }
-      }
-    };
-
-    final FlowPanel line = new FlowPanel();
-    line.addStyleName(OpenIdResources.I.css().directLink());
-
-    final Image img = new Image(icon);
-    img.addClickHandler(i);
-    line.add(img);
-
-    final Anchor text = new Anchor();
-    switch (mode) {
-      case LINK_IDENTIY:
-        text.setText(OpenIdUtil.M.linkWith(who));
-        break;
-      case REGISTER:
-        text.setText(OpenIdUtil.M.registerWith(who));
-        break;
-      case SIGN_IN:
-      default:
-        text.setText(OpenIdUtil.M.signInWith(who));
-        break;
-    }
-    text.setHref(identUrl);
-    text.addClickHandler(i);
-    line.add(text);
-
-    formBody.add(line);
-  }
-
-  private static boolean isAllowedProvider(final String identUrl) {
-    for (OpenIdProviderPattern p : Gerrit.getConfig().getAllowedOpenIDs()) {
-      if (p.matches(identUrl)) {
-        return true;
-      }
-    }
-    return false;
-  }
-
-  private void enable(final boolean on) {
-    providerId.setEnabled(on);
-    login.setEnabled(on);
-  }
-
-  private void onDiscovery(final DiscoveryResult result) {
-    discovering = false;
-
-    switch (result.status) {
-      case VALID:
-        // The provider won't support operation inside an IFRAME,
-        // so we replace our entire application.
-        //
-        redirectForm.setMethod(FormPanel.METHOD_POST);
-        redirectForm.setAction(result.providerUrl);
-        redirectBody.clear();
-        for (final Map.Entry<String, String> e : result.providerArgs.entrySet()) {
-          redirectBody.add(new Hidden(e.getKey(), e.getValue()));
-        }
-        FormElement.as(redirectForm.getElement()).setTarget("_top");
-        redirectForm.submit();
-        break;
-
-      case NOT_ALLOWED:
-        showError(OpenIdUtil.C.notAllowed());
-        enableRetryDiscovery();
-        break;
-
-      case NO_PROVIDER:
-        showError(OpenIdUtil.C.noProvider());
-        enableRetryDiscovery();
-        break;
-
-      case ERROR:
-      default:
-        showError(OpenIdUtil.C.error());
-        enableRetryDiscovery();
-        break;
-    }
-  }
-
-  private void enableRetryDiscovery() {
-    enable(true);
-    providerId.selectAll();
-    providerId.setFocus(true);
-  }
-
-  @Override
-  public void onSubmit(final SubmitEvent event) {
-    event.cancel();
-
-    String openidIdentifier = providerId.getText();
-    if (openidIdentifier == null || openidIdentifier.equals("")) {
-      enable(true);
-      return;
-    }
-
-    if (!openidIdentifier.startsWith("http://")
-        && !openidIdentifier.startsWith("https://")) {
-      openidIdentifier = "http://" + openidIdentifier;
-    }
-
-    if (!isAllowedProvider(openidIdentifier)) {
-      showError(OpenIdUtil.C.notAllowed());
-      enableRetryDiscovery();
-      return;
-    }
-
-    discovering = true;
-    enable(false);
-    hideError();
-
-    final boolean remember = rememberId != null && rememberId.getValue();
-    OpenIdUtil.SVC.discover(openidIdentifier, mode, remember, token,
-        new GerritCallback<DiscoveryResult>() {
-          public void onSuccess(final DiscoveryResult result) {
-            onDiscovery(result);
-          }
-
-          @Override
-          public void onFailure(final Throwable caught) {
-            super.onFailure(caught);
-            enableRetryDiscovery();
-          }
-        });
-  }
-
-  public void onSubmitComplete(final FormSubmitCompleteEvent event) {
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdSsoPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdSsoPanel.java
deleted file mode 100644
index 85dd794..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdSsoPanel.java
+++ /dev/null
@@ -1,68 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.auth.openid;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.common.auth.SignInMode;
-import com.google.gerrit.common.auth.openid.DiscoveryResult;
-import com.google.gwt.dom.client.FormElement;
-import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.FormPanel;
-import com.google.gwt.user.client.ui.Hidden;
-
-import java.util.Map;
-
-public class OpenIdSsoPanel extends FlowPanel {
-  private final FormPanel redirectForm;
-  private final FlowPanel redirectBody;
-  private final String ssoUrl;
-
-  public OpenIdSsoPanel() {
-    super();
-    redirectBody = new FlowPanel();
-    redirectBody.setVisible(false);
-    redirectForm = new FormPanel();
-    redirectForm.add(redirectBody);
-
-    add(redirectForm);
-
-    ssoUrl = Gerrit.getConfig().getOpenIdSsoUrl();
-  }
-
-  public void authenticate(SignInMode requestedMode, final String token) {
-    OpenIdUtil.SVC.discover(ssoUrl, requestedMode, /* remember */ false, token,
-        new GerritCallback<DiscoveryResult>() {
-          public void onSuccess(final DiscoveryResult result) {
-            onDiscovery(result);
-          }
-        });
-  }
-
-  private void onDiscovery(final DiscoveryResult result) {
-    switch (result.status) {
-      case VALID:
-        redirectForm.setMethod(FormPanel.METHOD_POST);
-        redirectForm.setAction(result.providerUrl);
-        redirectBody.clear();
-        for (final Map.Entry<String, String> e : result.providerArgs.entrySet()) {
-          redirectBody.add(new Hidden(e.getKey(), e.getValue()));
-        }
-        FormElement.as(redirectForm.getElement()).setTarget("_top");
-        redirectForm.submit();
-        break;
-    }
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdUtil.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdUtil.java
index 15aeb34..d5b7684 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdUtil.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/OpenIdUtil.java
@@ -14,19 +14,12 @@
 
 package com.google.gerrit.client.auth.openid;
 
-import com.google.gerrit.common.auth.openid.OpenIdService;
 import com.google.gwt.core.client.GWT;
-import com.google.gwtjsonrpc.client.JsonUtil;
 
 public class OpenIdUtil {
   public static final OpenIdConstants C;
-  public static final OpenIdMessages M;
-  public static final OpenIdService SVC;
 
   static {
     C = GWT.create(OpenIdConstants.class);
-    M = GWT.create(OpenIdMessages.class);
-    SVC = GWT.create(OpenIdService.class);
-    JsonUtil.bind(SVC, "rpc/OpenIdService");
   }
 }
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
deleted file mode 100644
index 748fd20..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/iconGoogle.gif
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/iconYahoo.gif b/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/iconYahoo.gif
deleted file mode 100644
index 0820455..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/iconYahoo.gif
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/identifierBackground.gif b/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/identifierBackground.gif
deleted file mode 100644
index cde836c..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/identifierBackground.gif
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/openid.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/openid.css
deleted file mode 100644
index 1e475ee..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/openid.css
+++ /dev/null
@@ -1,69 +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.
- */
-
-@external .gwt-Button;
-
-@url identifierBackground identifierBackground;
-
-.loginForm {
-  margin-left: 10px;
-  margin-right: 10px;
-}
-
-.logo {
-  text-align: right;
-}
-
-.loginLine {
-  margin-bottom: 10px;
-}
-.loginLine div {
-  white-space: nowrap;
-}
-.loginLine .gwt-Button {
-  margin-left: 2px;
-}
-
-.identifier {
-  background: #ffffff identifierBackground no-repeat scroll 5px 50%;
-  padding-left: 25px;
-  border: 1px solid #999999;
-}
-
-.directLink {
-  vertical-align: middle;
-  margin-right: 5px;
-  color: blue;
-  cursor: pointer;
-}
-.directLink:hover {
-  text-decoration: underline;
-}
-.directLink img {
-  margin-right: 3px;
-  border: 0 none;
-}
-
-.error {
-  padding-top: 5px;
-  padding-bottom: 5px;
-}
-.error span {
-  padding-top: 4px;
-  padding-bottom: 4px;
-  padding-left: 10px;
-  padding-right: 10px;
-  background: #fff1a8;
-}
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
deleted file mode 100644
index 9f8fa73..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/openid/openidLogo.png
+++ /dev/null
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/userpass/UserPassConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/userpass/UserPassConstants.java
deleted file mode 100644
index 1af6369..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/userpass/UserPassConstants.java
+++ /dev/null
@@ -1,26 +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.auth.userpass;
-
-import com.google.gwt.i18n.client.Constants;
-
-public interface UserPassConstants extends Constants {
-  String buttonSignIn();
-  String username();
-  String password();
-  String invalidLogin();
-  String usernameRequired();
-  String passwordRequired();
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/userpass/UserPassConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/userpass/UserPassConstants.properties
deleted file mode 100644
index 91204f7b..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/userpass/UserPassConstants.properties
+++ /dev/null
@@ -1,6 +0,0 @@
-buttonSignIn = Sign In
-username = Username
-password = Password
-invalidLogin = Incorrect username or password.
-usernameRequired = Please enter a username.
-passwordRequired = Please enter a password.
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/userpass/UserPassCss.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/userpass/UserPassCss.java
deleted file mode 100644
index 6d97cca1..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/userpass/UserPassCss.java
+++ /dev/null
@@ -1,22 +0,0 @@
-// Copyright (C) 2009 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.auth.userpass;
-
-import com.google.gwt.resources.client.CssResource;
-
-public interface UserPassCss extends CssResource {
-  String loginForm();
-  String error();
-}
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
deleted file mode 100644
index ccdd1ec..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/userpass/UserPassMessages.java
+++ /dev/null
@@ -1,23 +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.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
deleted file mode 100644
index b1eab35..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/userpass/UserPassMessages.properties
+++ /dev/null
@@ -1,2 +0,0 @@
-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/UserPassResources.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/userpass/UserPassResources.java
deleted file mode 100644
index b9474cd..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/userpass/UserPassResources.java
+++ /dev/null
@@ -1,25 +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.auth.userpass;
-
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.resources.client.ClientBundle;
-
-interface UserPassResources extends ClientBundle {
-  static final UserPassResources I = GWT.create(UserPassResources.class);
-
-  @Source("userpass.css")
-  UserPassCss css();
-}
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
deleted file mode 100644
index 3463640..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/userpass/UserPassSignInDialog.java
+++ /dev/null
@@ -1,240 +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.auth.userpass;
-
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.SignInDialog;
-import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gerrit.client.ui.SmallHeading;
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.common.auth.SignInMode;
-import com.google.gerrit.common.auth.userpass.LoginResult;
-import com.google.gwt.core.client.Scheduler;
-import com.google.gwt.core.client.Scheduler.ScheduledCommand;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.dom.client.KeyCodes;
-import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.event.dom.client.KeyPressHandler;
-import com.google.gwt.user.client.DOM;
-import com.google.gwt.user.client.Window.Location;
-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.InlineLabel;
-import com.google.gwt.user.client.ui.PasswordTextBox;
-import com.google.gwt.user.client.ui.TextBox;
-import com.google.gwtexpui.globalkey.client.GlobalKey;
-import com.google.gwtexpui.globalkey.client.NpTextBox;
-
-public class UserPassSignInDialog extends SignInDialog {
-  static {
-    UserPassResources.I.css().ensureInjected();
-  }
-
-  private final FlowPanel formBody;
-
-  private FlowPanel errorLine;
-  private InlineLabel errorMsg;
-
-  private Button login;
-  private Button close;
-  private TextBox username;
-  private TextBox password;
-
-  public UserPassSignInDialog(final String token, final String initialErrorMsg) {
-    super(SignInMode.SIGN_IN, token);
-    setAutoHideEnabled(false);
-
-    formBody = new FlowPanel();
-    formBody.setStyleName(UserPassResources.I.css().loginForm());
-    add(formBody);
-
-    createHeaderText();
-    createErrorBox();
-    createUsernameBox();
-    if (initialErrorMsg != null) {
-      showError(initialErrorMsg);
-    }
-  }
-
-  @Override
-  public void show() {
-    super.show();
-    Scheduler.get().scheduleDeferred(new ScheduledCommand() {
-      @Override
-      public void execute() {
-        username.setFocus(true);
-      }
-    });
-  }
-
-  private void createHeaderText() {
-    final FlowPanel headerText = new FlowPanel();
-    final SmallHeading headerLabel = new SmallHeading();
-    headerLabel.setText(Util.M.signInAt(Location.getHostName()));
-    headerText.add(headerLabel);
-    formBody.add(headerText);
-  }
-
-  private void createErrorBox() {
-    errorLine = new FlowPanel();
-    DOM.setStyleAttribute(errorLine.getElement(), "visibility", "hidden");
-    errorLine.setStyleName(UserPassResources.I.css().error());
-
-    errorMsg = new InlineLabel();
-    errorLine.add(errorMsg);
-    formBody.add(errorLine);
-  }
-
-  private void showError(final String msgText) {
-    errorMsg.setText(msgText);
-    DOM.setStyleAttribute(errorLine.getElement(), "visibility", "");
-  }
-
-  private void hideError() {
-    DOM.setStyleAttribute(errorLine.getElement(), "visibility", "hidden");
-  }
-
-  private void createUsernameBox() {
-    username = new NpTextBox();
-    username.setVisibleLength(25);
-    username.addKeyPressHandler(new KeyPressHandler() {
-      @Override
-      public void onKeyPress(final KeyPressEvent event) {
-        if (event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER) {
-          event.preventDefault();
-          password.selectAll();
-          password.setFocus(true);
-        }
-      }
-    });
-
-    password = new PasswordTextBox();
-    password.setVisibleLength(25);
-    password.addKeyPressHandler(GlobalKey.STOP_PROPAGATION);
-    password.addKeyPressHandler(new KeyPressHandler() {
-      @Override
-      public void onKeyPress(final KeyPressEvent event) {
-        if (event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER) {
-          event.preventDefault();
-          onLogin();
-        }
-      }
-    });
-
-    final FlowPanel buttons = new FlowPanel();
-    buttons.setStyleName(Gerrit.RESOURCES.css().errorDialogButtons());
-
-    login = new Button();
-    login.setText(Util.C.buttonSignIn());
-    login.addClickHandler(new ClickHandler() {
-      @Override
-      public void onClick(final ClickEvent event) {
-        onLogin();
-      }
-    });
-    buttons.add(login);
-
-    close = new Button();
-    DOM.setStyleAttribute(close.getElement(), "marginLeft", "45px");
-    close.setText(Gerrit.C.signInDialogClose());
-    close.addClickHandler(new ClickHandler() {
-      @Override
-      public void onClick(ClickEvent event) {
-        hide();
-      }
-    });
-    buttons.add(close);
-
-    final Grid formGrid = new Grid(3, 2);
-    formGrid.setText(0, 0, Util.C.username());
-    formGrid.setText(1, 0, Util.C.password());
-    formGrid.setWidget(0, 1, username);
-    formGrid.setWidget(1, 1, password);
-    formGrid.setWidget(2, 1, buttons);
-    formBody.add(formGrid);
-
-    username.setTabIndex(1);
-    password.setTabIndex(2);
-    login.setTabIndex(3);
-    close.setTabIndex(4);
-  }
-
-  private void enable(final boolean on) {
-    username.setEnabled(on);
-    password.setEnabled(on);
-    login.setEnabled(on);
-  }
-
-  private void onLogin() {
-    hideError();
-
-    final String user = username.getText();
-    if (user == null || user.equals("")) {
-      showError(Util.C.usernameRequired());
-      username.setFocus(true);
-      return;
-    }
-
-    final String pass = password.getText();
-    if (pass == null || pass.equals("")) {
-      showError(Util.C.passwordRequired());
-      password.setFocus(true);
-      return;
-    }
-
-    enable(false);
-    Util.SVC.authenticate(user, pass, new GerritCallback<LoginResult>() {
-      public void onSuccess(final LoginResult result) {
-        if (result.success) {
-          String to = token;
-          if (!to.startsWith("/")) {
-            to = "/" + to;
-          }
-          if (result.isNew && !token.startsWith(PageLinks.REGISTER + "/")) {
-            to = PageLinks.REGISTER + to;
-          }
-          Location.replace(Location.getPath() + "login" + to);
-        } else {
-          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() {
-            @Override
-            public void execute() {
-              password.setFocus(true);
-            }
-          });
-        }
-      }
-
-      @Override
-      public void onFailure(final Throwable caught) {
-        super.onFailure(caught);
-        enable(true);
-      }
-    });
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/userpass/Util.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/userpass/Util.java
deleted file mode 100644
index 7cfce04..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/userpass/Util.java
+++ /dev/null
@@ -1,30 +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.auth.userpass;
-
-import com.google.gerrit.common.auth.userpass.UserPassAuthService;
-import com.google.gwt.core.client.GWT;
-import com.google.gwtjsonrpc.client.JsonUtil;
-
-public class Util {
-  public static final UserPassConstants C = GWT.create(UserPassConstants.class);
-  public static final UserPassMessages M = GWT.create(UserPassMessages.class);
-  public static final UserPassAuthService SVC;
-
-  static {
-    SVC = GWT.create(UserPassAuthService.class);
-    JsonUtil.bind(SVC, "rpc/UserPassAuthService");
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/userpass/userpass.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/userpass/userpass.css
deleted file mode 100644
index b4a32c9..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/auth/userpass/userpass.css
+++ /dev/null
@@ -1,32 +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.
- */
-
-.loginForm {
-  margin-left: 10px;
-  margin-right: 10px;
-}
-
-.error {
-  padding-top: 5px;
-  padding-bottom: 5px;
-}
-
-.error span {
-  padding-top: 4px;
-  padding-bottom: 4px;
-  padding-left: 10px;
-  padding-right: 10px;
-  background: #fff1a8;
-}
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 05dd5d9..2580542 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
@@ -16,10 +16,11 @@
 
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.NotFoundScreen;
-import com.google.gerrit.client.rpc.NativeList;
+import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
 import com.google.gerrit.client.ui.Screen;
 import com.google.gerrit.reviewdb.client.Account;
+import com.google.gwt.core.client.JsArray;
 
 import java.util.Collections;
 import java.util.Comparator;
@@ -64,15 +65,15 @@
     super.onLoad();
     String who = mine ? "self" : ownerId.toString();
     ChangeList.query(
-        new ScreenLoadCallback<NativeList<ChangeList>>(this) {
+        new ScreenLoadCallback<JsArray<ChangeList>>(this) {
           @Override
-          protected void preDisplay(NativeList<ChangeList> result) {
+          protected void preDisplay(JsArray<ChangeList> result) {
             display(result);
           }
         },
         "is:open owner:" + who,
         "is:open reviewer:" + who + " -owner:" + who,
-        "is:closed owner:" + who + " -age:1w limit:10");
+        "is:closed owner:" + who + " -age:4w limit:10");
   }
 
   @Override
@@ -81,7 +82,7 @@
     table.setRegisterKeys(true);
   }
 
-  private void display(NativeList<ChangeList> result) {
+  private void display(JsArray<ChangeList> result) {
     if (!mine && !hasChanges(result)) {
       // When no results are returned and the data is not for the
       // current user, the target user is presumed to not exist.
@@ -112,7 +113,7 @@
       }
     }
 
-    Collections.sort(out.asList(), outComparator());
+    Collections.sort(Natives.asList(out), outComparator());
 
     table.updateColumnsForLabels(out, in, done);
     outgoing.display(out);
@@ -132,9 +133,9 @@
     };
   }
 
-  private boolean hasChanges(NativeList<ChangeList> result) {
-    for (ChangeList list : result.asList()) {
-      if (!list.isEmpty()) {
+  private boolean hasChanges(JsArray<ChangeList> result) {
+    for (ChangeList list : Natives.asList(result)) {
+      if (list.length() != 0) {
         return true;
       }
     }
@@ -142,7 +143,7 @@
   }
 
   private static String guessName(ChangeList list) {
-    for (ChangeInfo change : list.asList()) {
+    for (ChangeInfo change : Natives.asList(list)) {
       if (change.owner() != null && change.owner().name() != null) {
         return change.owner().name();
       }
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 73036ff..deb867c 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,27 +14,28 @@
 
 package com.google.gerrit.client.changes;
 
+import static com.google.gerrit.common.data.LabelValue.formatValue;
+
 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;
-import com.google.gerrit.client.patches.PatchUtil;
+import com.google.gerrit.client.account.AccountInfo;
+import com.google.gerrit.client.changes.ChangeInfo.ApprovalInfo;
+import com.google.gerrit.client.changes.ChangeInfo.LabelInfo;
 import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.NativeMap;
+import com.google.gerrit.client.rpc.NativeString;
+import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.client.ui.AccountLink;
 import com.google.gerrit.client.ui.AddMemberBox;
 import com.google.gerrit.client.ui.ReviewerSuggestOracle;
-import com.google.gerrit.common.data.AccountInfoCache;
 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.PatchSetPublishDetail;
-import com.google.gerrit.common.data.ReviewerResult;
 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.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArray;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.user.client.DOM;
@@ -50,25 +51,26 @@
 import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
 
 import java.util.ArrayList;
-import java.util.Collections;
-import java.util.Comparator;
+import java.util.Collection;
 import java.util.HashMap;
 import java.util.HashSet;
+import java.util.LinkedHashMap;
 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 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();
+  private ChangeInfo lastChange;
+  private Map<Integer, Integer> rows;
 
   public ApprovalTable() {
-    types = Gerrit.getConfig().getApprovalTypes();
+    rows = new HashMap<Integer, Integer>();
     table = new Grid(1, 3);
     table.addStyleName(Gerrit.RESOURCES.css().infoTable());
 
@@ -103,7 +105,7 @@
     setStyleName(Gerrit.RESOURCES.css().approvalTable());
   }
 
-  private void displayHeader(List<String> labels) {
+  private void displayHeader(Collection<String> labels) {
     table.resizeColumns(2 + labels.size());
 
     final CellFormatter fmt = table.getCellFormatter();
@@ -125,264 +127,218 @@
     fmt.addStyleName(0, col - 1, Gerrit.RESOURCES.css().rightmost());
   }
 
-  public void setAccountInfoCache(final AccountInfoCache aic) {
-    assert aic != null;
-    accountCache = aic;
-  }
+  void display(ChangeInfo change) {
+    lastChange = change;
+    reviewerSuggestOracle.setChange(change.legacy_id());
+    Map<Integer, ApprovalDetail> byUser =
+        new LinkedHashMap<Integer, ApprovalDetail>();
+    Map<Integer, AccountInfo> accounts =
+        new LinkedHashMap<Integer, AccountInfo>();
+    List<String> missingLabels = initLabels(change, accounts, byUser);
 
-  private AccountLink link(final Account.Id id) {
-    return AccountLink.link(accountCache, id);
-  }
-
-  void display(PatchSetPublishDetail detail) {
-    doDisplay(detail.getChange(), detail.getApprovals(),
-        detail.getSubmitRecords());
-  }
-
-  void display(ChangeDetail detail) {
-    doDisplay(detail.getChange(), detail.getApprovals(),
-        detail.getSubmitRecords());
-  }
-
-  private void doDisplay(Change change, List<ApprovalDetail> approvals,
-      List<SubmitRecord> submitRecords) {
-    changeId = change.getId();
-    reviewerSuggestOracle.setChange(changeId);
-
-    List<String> columns = new ArrayList<String>();
-
-    final Element missingList = missing.getElement();
-    while (DOM.getChildCount(missingList) > 0) {
-      DOM.removeChild(missingList, DOM.getChild(missingList, 0));
-    }
-    missing.setVisible(false);
-
-    if (submitRecords != null) {
-      HashSet<String> reportedMissing = new HashSet<String>();
-
-      HashMap<Account.Id, ApprovalDetail> byUser =
-          new HashMap<Account.Id, ApprovalDetail>();
-      for (ApprovalDetail ad : approvals) {
-        byUser.put(ad.getAccount(), ad);
-      }
-
-      for (SubmitRecord rec : submitRecords) {
-        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 MAY:
-              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 : approvals) {
-        for (PatchSetApproval psa : ad.getPatchSetApprovals()) {
-          ApprovalType legacyType = types.byId(psa.getCategoryId());
-          if (legacyType == null) {
-            continue;
-          }
-          String labelName = legacyType.getCategory().getLabelName();
-          if (psa.getValue() != 0 ) {
-            if (psa.getValue() == legacyType.getMax().getValue()) {
-              ad.approved(labelName);
-            } else if (psa.getValue() == legacyType.getMin().getValue()) {
-              ad.rejected(labelName);
-            }
-          }
-          if (!columns.contains(labelName)) {
-            columns.add(labelName);
-          }
-        }
-        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;
-          }
-        });
-      }
+    removeAllChildren(missing.getElement());
+    for (String label : missingLabels) {
+      addMissingLabel(Util.M.needApproval(label));
     }
 
-    if (approvals.isEmpty()) {
+    if (byUser.isEmpty()) {
       table.setVisible(false);
     } else {
-      displayHeader(columns);
-      table.resizeRows(1 + approvals.size());
-      for (int i = 0; i < approvals.size(); i++) {
-        displayRow(i + 1, approvals.get(i), change, columns);
+      displayHeader(change.labels());
+      table.resizeRows(1 + byUser.size());
+      int i = 1;
+      for (ApprovalDetail ad : ApprovalDetail.sort(
+          byUser.values(), change.owner()._account_id())) {
+        displayRow(i++, ad, change, accounts.get(ad.getAccount().get()));
       }
       table.setVisible(true);
     }
 
-    addReviewer.setVisible(Gerrit.isSignedIn());
-
     if (Gerrit.getConfig().testChangeMerge()
-        && !change.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);
+        && change.status() != Change.Status.MERGED
+        && !change.mergeable()) {
+      addMissingLabel(Util.C.messageNeedsRebaseOrHasDependency());
     }
+    missing.setVisible(DOM.getChildCount(missing.getElement()) > 0);
+    addReviewer.setVisible(Gerrit.isSignedIn());
+  }
+
+  private void removeAllChildren(Element el) {
+    for (int i = DOM.getChildCount(el) - 1; i >= 0; i--) {
+      DOM.removeChild(el, DOM.getChild(el, i));
+    }
+  }
+
+  private void addMissingLabel(String text) {
+    Element li = DOM.createElement("li");
+    li.setClassName(Gerrit.RESOURCES.css().missingApproval());
+    DOM.setInnerText(li, text);
+    DOM.appendChild(missing.getElement(), li);
+  }
+
+  private Set<Integer> removableReviewers(ChangeInfo change) {
+    Set<Integer> result =
+        new HashSet<Integer>(change.removable_reviewers().length());
+    for (int i = 0; i < change.removable_reviewers().length(); i++) {
+      result.add(change.removable_reviewers().get(i)._account_id());
+    }
+    return result;
+  }
+
+  private List<String> initLabels(ChangeInfo change,
+      Map<Integer, AccountInfo> accounts,
+      Map<Integer, ApprovalDetail> byUser) {
+    Set<Integer> removableReviewers = removableReviewers(change);
+    List<String> missing = new ArrayList<String>();
+    for (String name : change.labels()) {
+      LabelInfo label = change.label(name);
+
+      String min = null;
+      String max = null;
+      for (String v : label.values()) {
+        if (min == null) {
+          min = v;
+        }
+        if (v.startsWith("+")) {
+          max = v;
+        }
+      }
+
+      if (label.status() == SubmitRecord.Label.Status.NEED) {
+        missing.add(name);
+      }
+
+      if (label.all() != null) {
+        for (ApprovalInfo ai : Natives.asList(label.all())) {
+          if (!accounts.containsKey(ai._account_id())) {
+            accounts.put(ai._account_id(), ai);
+          }
+          int id = ai._account_id();
+          ApprovalDetail ad = byUser.get(id);
+          if (ad == null) {
+            ad = new ApprovalDetail(new Account.Id(id));
+            ad.setCanRemove(removableReviewers.contains(id));
+            byUser.put(id, ad);
+          }
+          if (ai.has_value()) {
+            ad.votable(name);
+            ad.value(name, ai.value());
+            String fv = formatValue(ai.value());
+            if (fv.equals(max)) {
+              ad.approved(name);
+            } else if (ai.value() < 0 && fv.equals(min)) {
+              ad.rejected(name);
+            }
+          }
+        }
+      }
+    }
+    return missing;
   }
 
   private void doAddReviewer() {
-    final String reviewer = addMemberBox.getText();
-    if (reviewer.length() == 0) {
-      return;
+    String reviewer = addMemberBox.getText();
+    if (!reviewer.isEmpty()) {
+      addMemberBox.setEnabled(false);
+      addReviewer(reviewer, false);
     }
-
-    addMemberBox.setEnabled(false);
-    final List<String> reviewers = new ArrayList<String>();
-    reviewers.add(reviewer);
-
-    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) {
+  private static class PostInput extends JavaScriptObject {
+    static PostInput create(String reviewer, boolean confirmed) {
+      PostInput input = createObject().cast();
+      input.init(reviewer, confirmed);
+      return input;
+    }
+
+    private native void init(String reviewer, boolean confirmed) /*-{
+      this.reviewer = reviewer;
+      if (confirmed) {
+        this.confirmed = true;
+      }
+    }-*/;
+
+    protected PostInput() {
+    }
+  }
+
+  private static class ReviewerInfo extends AccountInfo {
+    final Set<String> approvals() {
+      return Natives.keys(_approvals());
+    }
+    final native String approval(String l) /*-{ return this.approvals[l]; }-*/;
+    private final native NativeMap<NativeString> _approvals() /*-{ return this.approvals; }-*/;
+
+    protected ReviewerInfo() {
+    }
+  }
+
+  private static class PostResult extends JavaScriptObject {
+    final native JsArray<ReviewerInfo> reviewers() /*-{ return this.reviewers; }-*/;
+    final native boolean confirm() /*-{ return this.confirm || false; }-*/;
+    final native String error() /*-{ return this.error; }-*/;
+
+    protected PostResult() {
+    }
+  }
+
+  private void addReviewer(final String reviewer, boolean confirmed) {
+    ChangeApi.reviewers(lastChange.legacy_id().get()).post(
+        PostInput.create(reviewer, confirmed),
+        new GerritCallback<PostResult>() {
+          public void onSuccess(PostResult 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 REVIEWER_NOT_FOUND:
-                    r.append(Util.M.reviewerNotFound(e.getName()));
-                    break;
-
-                  case ACCOUNT_INACTIVE:
-                    r.append(Util.M.accountInactive(e.getName()));
-                    break;
-
-                  case CHANGE_NOT_VISIBLE:
-                    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(" - ");
-                    r.append(e.getType());
-                    r.br();
-                    break;
-                }
-              }
-              new ErrorDialog(r).center();
+            if (result.error() == null) {
+              reload();
+            } else if (result.confirm()) {
+              askForConfirmation(result.error());
+            } else {
+              new ErrorDialog(new SafeHtmlBuilder().append(result.error()));
             }
           }
 
-          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);
-                      }
-                    });
+          private void askForConfirmation(String text) {
+            String title = Util.C
+                .approvalTableAddManyReviewersConfirmationDialogTitle();
+            ConfirmationDialog confirmationDialog = new ConfirmationDialog(
+                title, new SafeHtmlBuilder().append(text),
+                new ConfirmationCallback() {
+                  @Override
+                  public void onOk() {
+                    addReviewer(reviewer, true);
+                  }
+                });
             confirmationDialog.center();
           }
 
           @Override
           public void onFailure(final Throwable caught) {
             addMemberBox.setEnabled(true);
-            super.onFailure(caught);
+            if (isNoSuchEntity(caught)) {
+              new ErrorDialog(Util.M.reviewerNotFound(reviewer)).center();
+            } else {
+              super.onFailure(caught);
+            }
           }
         });
   }
 
-  private void displayRow(final int row, final ApprovalDetail ad,
-      final Change change, List<String> columns) {
+  private void displayRow(int row, final ApprovalDetail ad, ChangeInfo change,
+      AccountInfo account) {
     final CellFormatter fmt = table.getCellFormatter();
     int col = 0;
 
-    table.setWidget(row, col++, link(ad.getAccount()));
+    table.setWidget(row, col++, new AccountLink(account));
+    rows.put(account._account_id(), row);
 
     if (ad.canRemove()) {
       final PushButton remove = new PushButton( //
           new Image(Util.R.removeReviewerNormal()), //
           new Image(Util.R.removeReviewerPressed()));
-      remove.setTitle(Util.M.removeReviewer( //
-          FormatUtil.name(accountCache.get(ad.getAccount()))));
+      remove.setTitle(Util.M.removeReviewer(account.name()));
       remove.setStyleName(Gerrit.RESOURCES.css().removeReviewer());
+      remove.addStyleName(Gerrit.RESOURCES.css().link());
       remove.addClickHandler(new ClickHandler() {
         @Override
         public void onClick(ClickEvent event) {
@@ -395,8 +351,12 @@
     }
     fmt.setStyleName(row, col++, Gerrit.RESOURCES.css().removeReviewerCell());
 
-    for (String labelName : columns) {
+    for (String labelName : change.labels()) {
       fmt.setStyleName(row, col, Gerrit.RESOURCES.css().approvalscore());
+      if (!ad.canVote(labelName)) {
+        fmt.addStyleName(row, col, Gerrit.RESOURCES.css().notVotable());
+        fmt.getElement(row, col).setTitle(Gerrit.C.userCannotVoteToolTip());
+      }
 
       if (ad.isRejected(labelName)) {
         table.setWidget(row, col, new Image(Gerrit.RESOURCES.redNot()));
@@ -405,22 +365,14 @@
         table.setWidget(row, col, new Image(Gerrit.RESOURCES.greenCheck()));
 
       } else {
-        ApprovalType legacyType = types.byLabel(labelName);
-        if (legacyType == null) {
+        int v = ad.getValue(labelName);
+        if (v == 0) {
           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) {
+        String vstr = String.valueOf(ad.getValue(labelName));
+        if (v > 0) {
           vstr = "+" + vstr;
           fmt.addStyleName(row, col, Gerrit.RESOURCES.css().posscore());
         } else {
@@ -435,29 +387,23 @@
     fmt.addStyleName(row, col - 1, Gerrit.RESOURCES.css().rightmost());
   }
 
-  private void doRemove(final ApprovalDetail ad, final PushButton remove) {
-    remove.setEnabled(false);
-    PatchUtil.DETAIL_SVC.removeReviewer(changeId, ad.getAccount(),
-        new GerritCallback<ReviewerResult>() {
+  private void reload() {
+    ChangeApi.detail(lastChange.legacy_id().get(),
+        new GerritCallback<ChangeInfo>() {
           @Override
-          public void onSuccess(ReviewerResult result) {
-            if (result.getErrors().isEmpty()) {
-              final ChangeDetail r = result.getChange();
-              display(r);
-            } else {
-              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();
-            }
+          public void onSuccess(ChangeInfo result) {
+            display(result);
+          }
+        });
+  }
+
+  private void doRemove(ApprovalDetail ad, final PushButton remove) {
+    remove.setEnabled(false);
+    ChangeApi.reviewer(lastChange.legacy_id().get(), ad.getAccount().get())
+      .delete(new GerritCallback<JavaScriptObject>() {
+          @Override
+          public void onSuccess(JavaScriptObject result) {
+            reload();
           }
 
           @Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java
new file mode 100644
index 0000000..abd94c9
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeApi.java
@@ -0,0 +1,130 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.NativeString;
+import com.google.gerrit.client.rpc.RestApi;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+
+/**
+ * A collection of static methods which work on the Gerrit REST API for specific
+ * changes.
+ */
+public class ChangeApi {
+  /** Abandon the change, ending its review. */
+  public static void abandon(int id, String msg, AsyncCallback<ChangeInfo> cb) {
+    Input input = Input.create();
+    input.message(emptyToNull(msg));
+    call(id, "abandon").post(input, cb);
+  }
+
+  /** Restore a previously abandoned change to be open again. */
+  public static void restore(int id, String msg, AsyncCallback<ChangeInfo> cb) {
+    Input input = Input.create();
+    input.message(emptyToNull(msg));
+    call(id, "restore").post(input, cb);
+  }
+
+  /** Create a new change that reverts the delta caused by this change. */
+  public static void revert(int id, String msg, AsyncCallback<ChangeInfo> cb) {
+    Input input = Input.create();
+    input.message(emptyToNull(msg));
+    call(id, "revert").post(input, cb);
+  }
+
+  /** Update the topic of a change. */
+  public static void topic(int id, String topic, String msg, AsyncCallback<String> cb) {
+    RestApi call = call(id, "topic");
+    topic = emptyToNull(topic);
+    msg = emptyToNull(msg);
+    if (topic != null || msg != null) {
+      Input input = Input.create();
+      input.topic(topic);
+      input.message(msg);
+      call.put(input, NativeString.unwrap(cb));
+    } else {
+      call.delete(NativeString.unwrap(cb));
+    }
+  }
+
+  public static void detail(int id, AsyncCallback<ChangeInfo> cb) {
+    call(id, "detail").get(cb);
+  }
+
+  public static RestApi revision(PatchSet.Id id) {
+    return change(id.getParentKey().get()).view("revisions").id(id.get());
+  }
+
+  public static RestApi reviewers(int id) {
+    return change(id).view("reviewers");
+  }
+
+  public static RestApi reviewer(int id, int reviewer) {
+    return change(id).view("reviewers").id(reviewer);
+  }
+
+  public static RestApi reviewer(int id, String reviewer) {
+    return change(id).view("reviewers").id(reviewer);
+  }
+
+  /** Submit a specific revision of a change. */
+  public static void submit(int id, String commit, AsyncCallback<SubmitInfo> cb) {
+    SubmitInput in = SubmitInput.create();
+    in.wait_for_merge(true);
+    call(id, commit, "submit").post(in, cb);
+  }
+
+  private static class Input extends JavaScriptObject {
+    final native void topic(String t) /*-{ if(t)this.topic=t; }-*/;
+    final native void message(String m) /*-{ if(m)this.message=m; }-*/;
+
+    static Input create() {
+      return (Input) createObject();
+    }
+
+    protected Input() {
+    }
+  }
+
+  private static class SubmitInput extends JavaScriptObject {
+    final native void wait_for_merge(boolean b) /*-{ this.wait_for_merge=b; }-*/;
+
+    static SubmitInput create() {
+      return (SubmitInput) createObject();
+    }
+
+    protected SubmitInput() {
+    }
+  }
+
+  private static RestApi call(int id, String action) {
+    return change(id).view(action);
+  }
+
+  private static RestApi call(int id, String commit, String action) {
+    return change(id).view("revisions").id(commit).view(action);
+  }
+
+  private static RestApi change(int id) {
+    // TODO Switch to triplet project~branch~id format in URI.
+    return new RestApi("/changes/").id(String.valueOf(id));
+  }
+
+  public static String emptyToNull(String str) {
+    return str == null || str.isEmpty() ? null : str;
+  }
+}
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 d42992f..500f06e 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
@@ -36,7 +36,6 @@
   String allAbandonedChanges();
   String allMergedChanges();
 
-  String changeTableColumnID();
   String changeTableColumnSubject();
   String changeTableColumnOwner();
   String changeTableColumnReviewers();
@@ -45,9 +44,7 @@
   String changeTableColumnLastUpdate();
   String changeTableNone();
 
-  String changeTablePrev();
-  String changeTableNext();
-  String changeTableOpen();
+  String changeItemHelp();
   String changeTableStar();
   String changeTablePagePrev();
   String changeTablePageNext();
@@ -66,6 +63,7 @@
   String patchTableDownloadPreImage();
   String patchTableDownloadPostImage();
   String commitMessage();
+  String fileCommentHeader();
 
   String patchTablePrev();
   String patchTableNext();
@@ -80,6 +78,7 @@
   String changeScreenDependsOn();
   String changeScreenNeededBy();
   String changeScreenComments();
+  String changeScreenAddComment();
 
   String approvalTableReviewer();
   String approvalTableAddReviewer();
@@ -92,14 +91,24 @@
   String changeInfoBlockProject();
   String changeInfoBlockBranch();
   String changeInfoBlockTopic();
+  String changeInfoBlockTopicAlterTopicToolTip();
   String changeInfoBlockUploaded();
   String changeInfoBlockUpdated();
   String changeInfoBlockStatus();
+  String changeInfoBlockSubmitType();
   String changePermalink();
   String changeInfoBlockCanMerge();
   String changeInfoBlockCanMergeYes();
   String changeInfoBlockCanMergeNo();
 
+  String buttonAlterTopic();
+  String buttonAlterTopicBegin();
+  String buttonAlterTopicSend();
+  String buttonAlterTopicCancel();
+  String headingAlterTopicMessage();
+  String alterTopicTitle();
+  String alterTopicLabel();
+
   String includedInTableBranch();
   String includedInTableTag();
 
@@ -122,11 +131,15 @@
   String headingRevertMessage();
   String revertChangeTitle();
 
+  String headingEditCommitMessage();
+  String editCommitMessageToolTip();
+  String titleEditCommitMessage();
+
   String buttonAbandonChangeBegin();
   String buttonAbandonChangeSend();
   String headingAbandonMessage();
   String abandonChangeTitle();
-  String oldVersionHistory();
+  String referenceVersion();
   String baseDiffItem();
   String autoMerge();
 
@@ -156,6 +169,6 @@
   String submitFailed();
   String buttonClose();
 
-  String buttonDiffAllSideBySide();
-  String buttonDiffAllUnified();
+  String diffAllSideBySide();
+  String diffAllUnified();
 }
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 8ceb74c..06bd64d 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
@@ -16,7 +16,6 @@
 allAbandonedChanges = All abandoned changes
 allMergedChanges = All merged changes
 
-changeTableColumnID = ID
 changeTableColumnSubject = Subject
 changeTableColumnOwner = Owner
 changeTableColumnReviewers = Reviewers
@@ -25,9 +24,7 @@
 changeTableColumnLastUpdate = Updated
 changeTableNone = (None)
 
-changeTablePrev = Previous change
-changeTableNext = Next change
-changeTableOpen = Open change
+changeItemHelp = change
 changeTableStar = Star (or unstar) change
 changeTablePagePrev = Previous page of changes
 changeTablePageNext = Next page of changes
@@ -46,6 +43,7 @@
 patchTableDownloadPreImage = old
 patchTableDownloadPostImage = new
 commitMessage = Commit Message
+fileCommentHeader = File Comment:
 
 patchTablePrev = Previous file
 patchTableNext = Next file
@@ -57,6 +55,7 @@
 changeScreenDependsOn = Depends On
 changeScreenNeededBy = Needed By
 changeScreenComments = Comments
+changeScreenAddComment = Add Comment
 
 approvalTableReviewer = Reviewer
 approvalTableAddReviewer = Add Reviewer
@@ -69,14 +68,24 @@
 changeInfoBlockProject = Project
 changeInfoBlockBranch = Branch
 changeInfoBlockTopic = Topic
+changeInfoBlockTopicAlterTopicToolTip = Edit Topic
 changeInfoBlockUploaded = Uploaded
 changeInfoBlockUpdated = Updated
 changeInfoBlockStatus = Status
+changeInfoBlockSubmitType = Submit Type
 changePermalink = Permalink
 changeInfoBlockCanMerge = Can Merge
 changeInfoBlockCanMergeYes = Yes
 changeInfoBlockCanMergeNo = No
 
+buttonAlterTopic = Edit Topic
+buttonAlterTopicBegin = Edit Topic
+buttonAlterTopicSend = Update Topic
+buttonAlterTopicCancel = Cancel
+headingAlterTopicMessage = Edit Topic Message:
+alterTopicTitle = Code Review - Edit Topic
+alterTopicLabel = New Topic Name:
+
 includedInTableBranch = Branch Name
 includedInTableTag = Tag Name
 
@@ -96,7 +105,7 @@
 buttonAbandonChangeSend = Abandon Change
 headingAbandonMessage = Abandon Message:
 abandonChangeTitle = Code Review - Abandon Change
-oldVersionHistory = Old Version History:
+referenceVersion = Reference Version:
 baseDiffItem = Base
 autoMerge = Auto Merge
 
@@ -107,6 +116,10 @@
 headingRevertMessage = Revert Commit Message:
 revertChangeTitle = Code Review - Revert Merged Change
 
+headingEditCommitMessage = Commit Message
+editCommitMessageToolTip = Edit Commit Message
+titleEditCommitMessage = Create New Patch Set
+
 buttonRestoreChangeBegin = Restore Change
 restoreChangeTitle = Code Review - Restore Change
 headingRestoreMessage = Restore Message:
@@ -137,5 +150,5 @@
 submitFailed = Submit Failed
 buttonClose = Close
 
-buttonDiffAllSideBySide = Diff All Side-by-Side
-buttonDiffAllUnified = Diff All Unified
+diffAllSideBySide = All Side-by-Side
+diffAllUnified = All Unified
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 c8b2a66..5cd6cdb 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,6 +15,7 @@
 package com.google.gerrit.client.changes;
 
 import com.google.gerrit.common.data.AccountInfoCache;
+import com.google.gerrit.common.data.SubmitTypeRecord;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSetInfo;
 import com.google.gwt.user.client.ui.Composite;
@@ -35,9 +36,11 @@
     initWidget(hp);
   }
 
-  public void display(Change chg, Boolean starred, PatchSetInfo info,
-      final AccountInfoCache acc) {
-    infoBlock.display(chg, acc);
-    messageBlock.display(chg.getId(), starred, info.getMessage());
+  public void display(Change chg, Boolean starred, Boolean canEditCommitMessage,
+      PatchSetInfo info,
+      final AccountInfoCache acc, SubmitTypeRecord submitTypeRecord) {
+    infoBlock.display(chg, acc, submitTypeRecord);
+    messageBlock.display(chg.currentPatchSetId(), starred,
+      canEditCommitMessage,  info.getMessage());
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfo.java
index 0c8e03e..00693d9 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfo.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfo.java
@@ -14,17 +14,28 @@
 
 package com.google.gerrit.client.changes;
 
+import com.google.gerrit.client.account.AccountInfo;
+import com.google.gerrit.client.rpc.NativeMap;
+import com.google.gerrit.client.rpc.NativeString;
 import com.google.gerrit.client.rpc.Natives;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwt.core.client.JsArrayString;
 import com.google.gwtjsonrpc.client.impl.ser.JavaSqlTimestamp_JsonSerializer;
 
 import java.sql.Timestamp;
 import java.util.Set;
 
 public class ChangeInfo extends JavaScriptObject {
+  public final void init() {
+    if (labels0() != null) {
+      labels0().copyKeysIntoChildren("_name");
+    }
+  }
+
   public final Project.NameKey project_name_key() {
     return new Project.NameKey(project());
   }
@@ -50,7 +61,7 @@
   }
 
   public final String id_abbreviated() {
-    return new Change.Key(id()).abbreviate();
+    return new Change.Key(change_id()).abbreviate();
   }
 
   public final Change.Status status() {
@@ -58,13 +69,15 @@
   }
 
   public final Set<String> labels() {
-    return Natives.keys(labels0());
+    return labels0().keySet();
   }
 
+  public final native String id() /*-{ return this.id; }-*/;
   public final native String project() /*-{ return this.project; }-*/;
   public final native String branch() /*-{ return this.branch; }-*/;
   public final native String topic() /*-{ return this.topic; }-*/;
-  public final native String id() /*-{ return this.id; }-*/;
+  public final native String change_id() /*-{ return this.change_id; }-*/;
+  public final native boolean mergeable() /*-{ return this.mergeable; }-*/;
   private final native String statusRaw() /*-{ return this.status; }-*/;
   public final native String subject() /*-{ return this.subject; }-*/;
   public final native AccountInfo owner() /*-{ return this.owner; }-*/;
@@ -73,8 +86,22 @@
   public final native boolean starred() /*-{ return this.starred ? true : false; }-*/;
   public final native boolean reviewed() /*-{ return this.reviewed ? true : false; }-*/;
   public final native String _sortkey() /*-{ return this._sortkey; }-*/;
-  private final native JavaScriptObject labels0() /*-{ return this.labels; }-*/;
+  private final native NativeMap<LabelInfo> labels0() /*-{ return this.labels; }-*/;
   public final native LabelInfo label(String n) /*-{ return this.labels[n]; }-*/;
+
+  public final native boolean has_permitted_labels()
+  /*-{ return this.hasOwnProperty('permitted_labels') }-*/;
+  private final native NativeMap<JavaScriptObject> _permitted_labels()
+  /*-{ return this.permitted_labels; }-*/;
+  public final Set<String> permitted_labels() {
+    return Natives.keys(_permitted_labels());
+  }
+  public final native JsArrayString permitted_values(String n)
+  /*-{ return this.permitted_labels[n]; }-*/;
+
+  public final native JsArray<AccountInfo> removable_reviewers()
+  /*-{ return this.removable_reviewers; }-*/;
+
   final native int _number() /*-{ return this._number; }-*/;
   final native boolean _more_changes()
   /*-{ return this._more_changes ? true : false; }-*/;
@@ -82,13 +109,6 @@
   protected ChangeInfo() {
   }
 
-  public static class AccountInfo extends JavaScriptObject {
-    public final native String name() /*-{ return this.name; }-*/;
-
-    protected AccountInfo() {
-    }
-  }
-
   public static class LabelInfo extends JavaScriptObject {
     public final SubmitRecord.Label.Status status() {
       if (approved() != null) {
@@ -108,6 +128,16 @@
 
     public final native AccountInfo recommended() /*-{ return this.recommended; }-*/;
     public final native AccountInfo disliked() /*-{ return this.disliked; }-*/;
+
+    public final native JsArray<ApprovalInfo> all() /*-{ return this.all; }-*/;
+
+    private final native NativeMap<NativeString> _values() /*-{ return this.values; }-*/;
+
+    public final Set<String> values() {
+      return Natives.keys(_values());
+    }
+    public final native String value_text(String n) /*-{ return this.values[n]; }-*/;
+
     public final native boolean optional() /*-{ return this.optional ? true : false; }-*/;
     final native short _value()
     /*-{
@@ -120,4 +150,12 @@
     protected LabelInfo() {
     }
   }
+
+  public static class ApprovalInfo extends AccountInfo {
+    public final native boolean has_value() /*-{ return this.hasOwnProperty('value'); }-*/;
+    public final native short value() /*-{ return this.value; }-*/;
+
+    protected ApprovalInfo() {
+    }
+  }
 }
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 3ffacc3..e4dbc32 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfoBlock.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeInfoBlock.java
@@ -17,15 +17,31 @@
 import static com.google.gerrit.client.FormatUtil.mediumFormat;
 
 import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.ui.AccountLink;
 import com.google.gerrit.client.ui.BranchLink;
-import com.google.gerrit.client.ui.ProjectLink;
+import com.google.gerrit.client.ui.CommentedActionDialog;
+import com.google.gerrit.client.ui.InlineHyperlink;
+import com.google.gerrit.client.ui.ProjectSearchLink;
+import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.data.AccountInfoCache;
+import com.google.gerrit.common.data.ChangeDetail;
+import com.google.gerrit.common.data.SubmitTypeRecord;
 import com.google.gerrit.reviewdb.client.Branch;
 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;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.dom.client.KeyPressHandler;
 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.InlineLabel;
+import com.google.gwt.user.client.ui.TextBox;
+import com.google.gwt.user.client.ui.Widget;
 import com.google.gwtexpui.clippy.client.CopyableLabel;
 
 public class ChangeInfoBlock extends Composite {
@@ -36,9 +52,10 @@
   private static final int R_TOPIC = 4;
   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_MERGE_TEST = 8;
-  private static final int R_CNT = 9;
+  private static final int R_SUBMIT_TYPE = 7;
+  private static final int R_STATUS = 8;
+  private static final int R_MERGE_TEST = 9;
+  private static final int R_CNT = 10;
 
   private final Grid table;
 
@@ -59,6 +76,7 @@
     initRow(R_UPLOADED, Util.C.changeInfoBlockUploaded());
     initRow(R_UPDATED, Util.C.changeInfoBlockUpdated());
     initRow(R_STATUS, Util.C.changeInfoBlockStatus());
+    initRow(R_SUBMIT_TYPE, Util.C.changeInfoBlockSubmitType());
     if (Gerrit.getConfig().testChangeMerge()) {
       initRow(R_MERGE_TEST, Util.C.changeInfoBlockCanMerge());
     }
@@ -77,7 +95,8 @@
     table.getCellFormatter().addStyleName(row, 0, Gerrit.RESOURCES.css().header());
   }
 
-  public void display(final Change chg, final AccountInfoCache acc) {
+  public void display(final Change chg, final AccountInfoCache acc,
+      SubmitTypeRecord submitTypeRecord) {
     final Branch.NameKey dst = chg.getDest();
 
     CopyableLabel changeIdLabel =
@@ -86,14 +105,27 @@
     table.setWidget(R_CHANGE_ID, 1, changeIdLabel);
 
     table.setWidget(R_OWNER, 1, AccountLink.link(acc, chg.getOwner()));
-    table.setWidget(R_PROJECT, 1, new ProjectLink(chg.getProject(), chg.getStatus()));
+
+    final FlowPanel p = new FlowPanel();
+    p.add(new ProjectSearchLink(chg.getProject()));
+    p.add(new InlineHyperlink(chg.getProject().get(),
+        PageLinks.toProject(chg.getProject())));
+    table.setWidget(R_PROJECT, 1, p);
+
     table.setWidget(R_BRANCH, 1, new BranchLink(dst.getShortName(), chg
         .getProject(), chg.getStatus(), dst.get(), null));
-    table.setWidget(R_TOPIC, 1, new BranchLink(chg.getTopic(),
-        chg.getProject(), chg.getStatus(), dst.get(), chg.getTopic()));
+    table.setWidget(R_TOPIC, 1, topic(chg));
     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()));
+    String submitType;
+    if (submitTypeRecord.status == SubmitTypeRecord.Status.OK) {
+      submitType = com.google.gerrit.client.admin.Util
+              .toLongString(submitTypeRecord.type);
+    } else {
+      submitType = submitTypeRecord.status.name();
+    }
+    table.setText(R_SUBMIT_TYPE, 1, submitType);
     final Change.Status status = chg.getStatus();
     if (Gerrit.getConfig().testChangeMerge()) {
       if (status.equals(Change.Status.NEW) || status.equals(Change.Status.DRAFT)) {
@@ -107,8 +139,92 @@
 
     if (status.isClosed()) {
       table.getCellFormatter().addStyleName(R_STATUS, 1, Gerrit.RESOURCES.css().closedstate());
+      table.getRowFormatter().setVisible(R_SUBMIT_TYPE, false);
     } else {
       table.getCellFormatter().removeStyleName(R_STATUS, 1, Gerrit.RESOURCES.css().closedstate());
+      table.getRowFormatter().setVisible(R_SUBMIT_TYPE, true);
+    }
+  }
+
+  public Widget topic(final Change chg) {
+    final Branch.NameKey dst = chg.getDest();
+
+    FlowPanel fp = new FlowPanel();
+    fp.addStyleName(Gerrit.RESOURCES.css().changeInfoTopicPanel());
+    fp.add(new BranchLink(chg.getTopic(), chg.getProject(), chg.getStatus(),
+           dst.get(), chg.getTopic()));
+
+    ChangeDetailCache detailCache = ChangeCache.get(chg.getId()).getChangeDetailCache();
+    ChangeDetail changeDetail = detailCache.get();
+
+    if (changeDetail.canEditTopicName()) {
+      final Image edit = new Image(Gerrit.RESOURCES.edit());
+      edit.addStyleName(Gerrit.RESOURCES.css().link());
+      edit.setTitle(Util.C.changeInfoBlockTopicAlterTopicToolTip());
+      edit.addClickHandler(new  ClickHandler() {
+        @Override
+        public void onClick(final ClickEvent event) {
+          new AlterTopicDialog(chg).center();
+        }
+      });
+      fp.add(edit);
+    }
+
+    return fp;
+  }
+
+  private class AlterTopicDialog extends CommentedActionDialog<ChangeDetail>
+      implements KeyPressHandler {
+    TextBox newTopic;
+    Change change;
+
+    AlterTopicDialog(Change chg) {
+      super(Util.C.alterTopicTitle(), Util.C.headingAlterTopicMessage(),
+          new ChangeDetailCache.IgnoreErrorCallback());
+      change = chg;
+
+      newTopic = new TextBox();
+      newTopic.addKeyPressHandler(this);
+      setFocusOn(newTopic);
+      panel.insert(newTopic, 0);
+      panel.insert(new InlineLabel(Util.C.alterTopicLabel()), 0);
+    }
+
+    @Override
+    protected void onLoad() {
+      super.onLoad();
+      newTopic.setText(change.getTopic());
+    }
+
+    private void doTopicEdit() {
+      String topic = newTopic.getText();
+      ChangeApi.topic(change.getId().get(), topic, getMessageText(),
+        new GerritCallback<String>() {
+        @Override
+        public void onSuccess(String result) {
+          sent = true;
+          Gerrit.display(PageLinks.toChange(change.getId()));
+          hide();
+        }
+
+        @Override
+        public void onFailure(final Throwable caught) {
+          enableButtons(true);
+          super.onFailure(caught);
+        }});
+    }
+
+    @Override
+    public void onSend() {
+      doTopicEdit();
+    }
+
+    @Override
+    public void onKeyPress(KeyPressEvent event) {
+      if (event.getSource() == newTopic
+          && event.getNativeEvent().getKeyCode() == KeyCodes.KEY_ENTER) {
+        doTopicEdit();
+      }
     }
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeList.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeList.java
index 560e6b3..7c41ea1 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeList.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeList.java
@@ -14,28 +14,28 @@
 
 package com.google.gerrit.client.changes;
 
-import com.google.gerrit.client.rpc.NativeList;
 import com.google.gerrit.client.rpc.RestApi;
 import com.google.gerrit.common.changes.ListChangesOption;
-import com.google.gwtjsonrpc.common.AsyncCallback;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwt.user.client.rpc.AsyncCallback;
 import com.google.gwtorm.client.KeyUtil;
 
 import java.util.EnumSet;
 
 /** List of changes available from {@code /changes/}. */
-public class ChangeList extends NativeList<ChangeInfo> {
+public class ChangeList extends JsArray<ChangeInfo> {
   private static final String URI = "/changes/";
 
   /** Run 2 or more queries in a single remote invocation. */
   public static void query(
-      AsyncCallback<NativeList<ChangeList>> callback, String... queries) {
+      AsyncCallback<JsArray<ChangeList>> callback, String... queries) {
     assert queries.length >= 2; // At least 2 is required for correct result.
     RestApi call = new RestApi(URI);
     for (String q : queries) {
       call.addParameterRaw("q", KeyUtil.encode(q));
     }
     addOptions(call, ListChangesOption.LABELS);
-    call.send(callback);
+    call.get(callback);
   }
 
   public static void prev(String query,
@@ -49,7 +49,7 @@
     if (!PagedSingleListScreen.MIN_SORTKEY.equals(sortkey)) {
       call.addParameter("P", sortkey);
     }
-    call.send(callback);
+    call.get(callback);
   }
 
   public static void next(String query,
@@ -63,7 +63,7 @@
     if (!PagedSingleListScreen.MAX_SORTKEY.equals(sortkey)) {
       call.addParameter("N", sortkey);
     }
-    call.send(callback);
+    call.get(callback);
   }
 
   private static void addOptions(
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 4bd0828..ba46702 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
@@ -56,6 +56,4 @@
   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 2449613..ffa749d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
@@ -1,3 +1,5 @@
+# Changes to this file should also be made in
+# gerrit-server/src/main/resources/com/google/gerrit/server/change/ChangeMessages.properties
 accountDashboardTitle = Code Review Dashboard for {0}
 changesOpenInProject = Open Changes In {0}
 changesMergedInProject = Merged Changes In {0}
@@ -37,5 +39,3 @@
 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 4dd6b03..bee8ac4 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
@@ -15,29 +15,33 @@
 package com.google.gerrit.client.changes;
 
 import com.google.gerrit.client.Dispatcher;
+import com.google.gerrit.client.FormatUtil;
 import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.account.AccountInfo;
+import com.google.gerrit.client.rpc.GerritCallback;
 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;
 import com.google.gerrit.client.ui.Screen;
-import com.google.gerrit.common.data.AccountInfo;
 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.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.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.Button;
 import com.google.gwt.user.client.ui.DisclosurePanel;
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.Grid;
@@ -59,6 +63,7 @@
   private final Change.Id changeId;
   private final PatchSet.Id openPatchSetId;
   private ChangeDetailCache detailCache;
+  private com.google.gerrit.client.changes.ChangeInfo changeInfo;
 
   private ChangeDescriptionBlock descriptionBlock;
   private ApprovalTable approvals;
@@ -95,14 +100,6 @@
   public ChangeScreen(final Change.Id toShow) {
     changeId = toShow;
     openPatchSetId = null;
-
-    // If we have any diff stored, make sure they are applicable to the
-    // current change, discard them otherwise.
-    //
-    if (currentChangeId != null && !currentChangeId.equals(toShow)) {
-      diffBaseId = null;
-    }
-    currentChangeId = toShow;
   }
 
   public ChangeScreen(final PatchSet.Id toShow) {
@@ -228,7 +225,7 @@
 
     patchesGrid = new Grid(1, 2);
     patchesGrid.setStyleName(Gerrit.RESOURCES.css().selectPatchSetOldVersion());
-    patchesGrid.setText(0, 0, Util.C.oldVersionHistory());
+    patchesGrid.setText(0, 0, Util.C.referenceVersion());
     patchesGrid.setWidget(0, 1, patchesList);
     add(patchesGrid);
 
@@ -260,14 +257,26 @@
   }
 
   @Override
-  public void onValueChange(ValueChangeEvent<ChangeDetail> event) {
+  public void onValueChange(final ValueChangeEvent<ChangeDetail> event) {
     if (isAttached()) {
-      display(event.getValue());
+      // Until this screen is fully migrated to the new API, this call must be
+      // sequential, because we can't start an async get at the source of every
+      // call that might trigger a value change.
+      ChangeApi.detail(event.getValue().getChange().getId().get(),
+          new GerritCallback<com.google.gerrit.client.changes.ChangeInfo>() {
+            @Override
+            public void onSuccess(
+                com.google.gerrit.client.changes.ChangeInfo result) {
+              changeInfo = result;
+              display(event.getValue());
+            }
+          });
     }
   }
 
   private void display(final ChangeDetail detail) {
     displayTitle(detail.getChange().getKey(), detail.getChange().getSubject());
+    discardDiffBaseIfNotApplicable(detail.getChange().getId());
 
     if (Status.MERGED == detail.getChange().getStatus()) {
       includedInPanel.setVisible(true);
@@ -277,16 +286,17 @@
     }
 
     dependencies.setAccountInfoCache(detail.getAccounts());
-    approvals.setAccountInfoCache(detail.getAccounts());
 
     descriptionBlock.display(detail.getChange(),
         detail.isStarred(),
+        detail.canEditCommitMessage(),
         detail.getCurrentPatchSetDetail().getInfo(),
-        detail.getAccounts());
+        detail.getAccounts(), detail.getSubmitTypeRecord());
     dependsOn.display(detail.getDependsOn());
     neededBy.display(detail.getNeededBy());
-    approvals.display(detail);
+    approvals.display(changeInfo);
 
+    patchesList.clear();
     if (detail.getCurrentPatchSetDetail().getInfo().getParents().size() > 1) {
       patchesList.addItem(Util.C.autoMerge());
     } else {
@@ -313,8 +323,9 @@
     boolean depsOpen = false;
     int outdated = 0;
     if (!detail.getChange().getStatus().isClosed()) {
-      if (detail.getDependsOn() != null) {
-        for (final ChangeInfo ci : detail.getDependsOn()) {
+      final List<ChangeInfo> dependsOn = detail.getDependsOn();
+      if (dependsOn != null) {
+        for (final ChangeInfo ci : dependsOn) {
           if (!ci.isLatest()) {
             depsOpen = true;
             outdated++;
@@ -324,8 +335,9 @@
         }
       }
     }
-    if (detail.getNeededBy() != null) {
-      for (final ChangeInfo ci : detail.getNeededBy()) {
+    final List<ChangeInfo> neededBy = detail.getNeededBy();
+    if (neededBy != null) {
+      for (final ChangeInfo ci : neededBy) {
         if ((ci.getStatus() == Change.Status.NEW) ||
             (ci.getStatus() == Change.Status.SUBMITTED) ||
             (ci.getStatus() == Change.Status.DRAFT)) {
@@ -348,6 +360,13 @@
     patchSetsBlock.setRegisterKeys(true);
   }
 
+  private static void discardDiffBaseIfNotApplicable(final Change.Id toShow) {
+    if (currentChangeId != null && !currentChangeId.equals(toShow)) {
+      diffBaseId = null;
+    }
+    currentChangeId = toShow;
+  }
+
   private void addComments(final ChangeDetail detail) {
     comments.clear();
 
@@ -369,13 +388,11 @@
     for (int i = 0; i < msgList.size(); i++) {
       final ChangeMessage msg = msgList.get(i);
 
-      final AccountInfo author;
+      AccountInfo author;
       if (msg.getAuthor() != null) {
-        author = accts.get(msg.getAuthor());
+        author = FormatUtil.asInfo(accts.get(msg.getAuthor()));
       } else {
-        final Account gerrit = new Account(null);
-        gerrit.setFullName(Util.C.messageNoAuthor());
-        author = new AccountInfo(gerrit);
+        author = AccountInfo.create(0, Util.C.messageNoAuthor(), null);
       }
 
       boolean isRecent;
@@ -397,6 +414,15 @@
       comments.add(cp);
     }
 
+    final Button b = new Button(Util.C.changeScreenAddComment());
+    b.addClickHandler(new ClickHandler() {
+        @Override
+        public void onClick(final ClickEvent event) {
+            PatchSet.Id currentPatchSetId = patchSetsBlock.getCurrentPatchSet().getId();
+            Gerrit.display(Dispatcher.toPublish(currentPatchSetId));
+        }
+    });
+    comments.add(b);
     comments.setVisible(msgList.size() > 0);
   }
 
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 ef4ef52..97a5a09 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
@@ -16,10 +16,7 @@
 
 import static com.google.gerrit.client.FormatUtil.shortFormat;
 
-import com.google.gerrit.client.FormatUtil;
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.patches.PatchUtil;
-import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.ui.AccountLink;
 import com.google.gerrit.client.ui.BranchLink;
 import com.google.gerrit.client.ui.ChangeLink;
@@ -27,70 +24,36 @@
 import com.google.gerrit.client.ui.NeedsSignInKeyCommand;
 import com.google.gerrit.client.ui.ProjectLink;
 import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.common.data.AccountInfo;
 import com.google.gerrit.common.data.AccountInfoCache;
-import com.google.gerrit.common.data.ApprovalSummary;
-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.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.user.client.DOM;
 import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
-import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.HTMLTable.Cell;
 import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
-import com.google.gwt.user.client.ui.Image;
-import com.google.gwt.user.client.ui.InlineLabel;
-import com.google.gwt.user.client.ui.UIObject;
 
 import java.util.ArrayList;
 import java.util.HashSet;
 import java.util.List;
-import java.util.Map;
 import java.util.Set;
 
 public class ChangeTable extends NavigationTable<ChangeInfo> {
   private static final int C_STAR = 1;
-  private static final int C_ID = 2;
-  private static final int C_SUBJECT = 3;
-  private static final int C_OWNER = 4;
-  private static final int C_PROJECT = 5;
-  private static final int C_BRANCH = 6;
-  private static final int C_LAST_UPDATE = 7;
-  private static final int BASE_COLUMNS = 8;
+  private static final int C_SUBJECT = 2;
+  private static final int C_OWNER = 3;
+  private static final int C_PROJECT = 4;
+  private static final int C_BRANCH = 5;
+  private static final int C_LAST_UPDATE = 6;
+  private static final int COLUMNS = 7;
 
   private final List<Section> sections;
   private AccountInfoCache accountCache = AccountInfoCache.empty();
-  private final List<ApprovalType> approvalTypes;
-  private final int columns;
 
   public ChangeTable() {
-    this(false);
-  }
-
-  public ChangeTable(boolean showApprovals) {
-    approvalTypes = Gerrit.getConfig().getApprovalTypes().getApprovalTypes();
-    if (showApprovals) {
-      columns = BASE_COLUMNS + approvalTypes.size();
-    } else {
-      columns = BASE_COLUMNS;
-    }
-
-    keysNavigation.add(new PrevKeyCommand(0, 'k', Util.C.changeTablePrev()));
-    keysNavigation.add(new NextKeyCommand(0, 'j', Util.C.changeTableNext()));
-    keysNavigation.add(new OpenKeyCommand(0, 'o', Util.C.changeTableOpen()));
-    keysNavigation.add(new OpenKeyCommand(0, KeyCodes.KEY_ENTER, Util.C
-        .changeTableOpen()));
+    super(Util.C.changeItemHelp());
 
     if (Gerrit.isSignedIn()) {
       keysAction.add(new StarKeyCommand(0, 's', Util.C.changeTableStar()));
@@ -98,29 +61,15 @@
 
     sections = new ArrayList<Section>();
     table.setText(0, C_STAR, "");
-    table.setText(0, C_ID, Util.C.changeTableColumnID());
     table.setText(0, C_SUBJECT, Util.C.changeTableColumnSubject());
     table.setText(0, C_OWNER, Util.C.changeTableColumnOwner());
     table.setText(0, C_PROJECT, Util.C.changeTableColumnProject());
     table.setText(0, C_BRANCH, Util.C.changeTableColumnBranch());
     table.setText(0, C_LAST_UPDATE, Util.C.changeTableColumnLastUpdate());
-    for (int i = BASE_COLUMNS; i < columns; i++) {
-      final ApprovalType type = approvalTypes.get(i - BASE_COLUMNS);
-      final ApprovalCategory cat = type.getCategory();
-      String text = cat.getAbbreviatedName();
-      if (text == null) {
-        text = cat.getName();
-      }
-      table.setText(0, i, text);
-      if (text != null) {
-        table.getCellFormatter().getElement(0, i).setTitle(cat.getName());
-      }
-    }
 
     final FlexCellFormatter fmt = table.getFlexCellFormatter();
     fmt.addStyleName(0, C_STAR, Gerrit.RESOURCES.css().iconHeader());
-    fmt.addStyleName(0, C_ID, Gerrit.RESOURCES.css().cID());
-    for (int i = C_ID; i < columns; i++) {
+    for (int i = C_SUBJECT; i < COLUMNS; i++) {
       fmt.addStyleName(0, i, Gerrit.RESOURCES.css().dataHeader());
     }
 
@@ -164,7 +113,7 @@
     insertRow(row);
     table.setText(row, 0, Util.C.changeTableNone());
     final FlexCellFormatter fmt = table.getFlexCellFormatter();
-    fmt.setColSpan(row, 0, columns);
+    fmt.setColSpan(row, 0, COLUMNS);
     fmt.setStyleName(row, 0, Gerrit.RESOURCES.css().emptySection());
   }
 
@@ -178,17 +127,12 @@
     super.applyDataRowStyle(row);
     final CellFormatter fmt = table.getCellFormatter();
     fmt.addStyleName(row, C_STAR, Gerrit.RESOURCES.css().iconCell());
-    for (int i = C_ID; i < columns; i++) {
+    for (int i = C_SUBJECT; i < COLUMNS; i++) {
       fmt.addStyleName(row, i, Gerrit.RESOURCES.css().dataCell());
     }
-    fmt.addStyleName(row, C_ID, Gerrit.RESOURCES.css().cID());
     fmt.addStyleName(row, C_SUBJECT, Gerrit.RESOURCES.css().cSUBJECT());
-    fmt.addStyleName(row, C_PROJECT, Gerrit.RESOURCES.css().cPROJECT());
-    fmt.addStyleName(row, C_BRANCH, Gerrit.RESOURCES.css().cPROJECT());
+    fmt.addStyleName(row, C_OWNER, Gerrit.RESOURCES.css().cOWNER());
     fmt.addStyleName(row, C_LAST_UPDATE, Gerrit.RESOURCES.css().cLastUpdate());
-    for (int i = BASE_COLUMNS; i < columns; i++) {
-      fmt.addStyleName(row, i, Gerrit.RESOURCES.css().cAPPROVAL());
-    }
   }
 
   private void populateChangeRow(final int row, final ChangeInfo c,
@@ -196,12 +140,10 @@
     ChangeCache cache = ChangeCache.get(c.getId());
     cache.getChangeInfoCache().set(c);
 
-    final String idstr = c.getKey().abbreviate();
     table.setWidget(row, C_ARROW, null);
     if (Gerrit.isSignedIn()) {
       table.setWidget(row, C_STAR, StarredChanges.createIcon(c.getId(), c.isStarred()));
     }
-    table.setWidget(row, C_ID, new TableChangeLink(idstr, c));
 
     String s = Util.cropSubject(c.getSubject());
     if (c.getStatus() != null && c.getStatus() != Change.Status.NEW) {
@@ -251,7 +193,7 @@
       s.titleRow = table.getRowCount();
       table.setText(s.titleRow, 0, s.titleText);
       final FlexCellFormatter fmt = table.getFlexCellFormatter();
-      fmt.setColSpan(s.titleRow, 0, columns);
+      fmt.setColSpan(s.titleRow, 0, COLUMNS);
       fmt.addStyleName(s.titleRow, 0, Gerrit.RESOURCES.css().sectionHeader());
     } else {
       s.titleRow = -1;
@@ -292,110 +234,6 @@
     table.removeRow(row);
   }
 
-  private void displayApprovals(final int row, final ApprovalSummary summary,
-      final AccountInfoCache aic, final boolean highlightUnreviewed) {
-    final CellFormatter fmt = table.getCellFormatter();
-    final Map<ApprovalCategory.Id, PatchSetApproval> approvals =
-        summary.getApprovalMap();
-    int col = BASE_COLUMNS;
-    boolean haveReview = false;
-
-    boolean showUsernameInReviewCategory = false;
-
-    if (Gerrit.isSignedIn()) {
-      AccountGeneralPreferences prefs = Gerrit.getUserAccount().getGeneralPreferences();
-
-      if (prefs.isShowUsernameInReviewCategory()) {
-        showUsernameInReviewCategory = true;
-      }
-    }
-
-    for (final ApprovalType type : approvalTypes) {
-      final PatchSetApproval ca = approvals.get(type.getCategory().getId());
-
-      fmt.removeStyleName(row, col, Gerrit.RESOURCES.css().negscore());
-      fmt.removeStyleName(row, col, Gerrit.RESOURCES.css().posscore());
-      fmt.addStyleName(row, col, Gerrit.RESOURCES.css().singleLine());
-
-      if (ca == null || ca.getValue() == 0) {
-        table.clearCell(row, col);
-
-      } else {
-        haveReview = true;
-
-        final ApprovalCategoryValue acv = type.getValue(ca);
-        final AccountInfo ai = aic.get(ca.getAccountId());
-
-        if (type.isMaxNegative(ca)) {
-
-          if (showUsernameInReviewCategory) {
-            FlowPanel fp = new FlowPanel();
-            fp.add(new Image(Gerrit.RESOURCES.redNot()));
-            fp.add(new InlineLabel(FormatUtil.name(ai)));
-            table.setWidget(row, col, fp);
-          } else {
-            table.setWidget(row, col, new Image(Gerrit.RESOURCES.redNot()));
-          }
-
-        } else if (type.isMaxPositive(ca)) {
-
-          if (showUsernameInReviewCategory) {
-            FlowPanel fp = new FlowPanel();
-            fp.add(new Image(Gerrit.RESOURCES.greenCheck()));
-            fp.add(new InlineLabel(FormatUtil.name(ai)));
-            table.setWidget(row, col, fp);
-          } else {
-            table.setWidget(row, col, new Image(Gerrit.RESOURCES.greenCheck()));
-          }
-
-        } else {
-          String vstr = String.valueOf(ca.getValue());
-
-          if (showUsernameInReviewCategory) {
-            vstr = vstr + " " + FormatUtil.name(ai);
-          }
-
-          if (ca.getValue() > 0) {
-            vstr = "+" + vstr;
-            fmt.addStyleName(row, col, Gerrit.RESOURCES.css().posscore());
-          } else {
-            fmt.addStyleName(row, col, Gerrit.RESOURCES.css().negscore());
-          }
-          table.setText(row, col, vstr);
-        }
-
-        // Some web browsers ignore the embedded newline; some like it;
-        // so we include a space before the newline to accommodate both.
-        //
-        fmt.getElement(row, col).setTitle(
-            acv.getName() + " \nby " + FormatUtil.nameEmail(ai));
-      }
-
-      col++;
-    }
-
-    final Element tr = DOM.getParent(fmt.getElement(row, 0));
-    UIObject.setStyleName(tr, Gerrit.RESOURCES.css().needsReview(), !haveReview
-        && highlightUnreviewed);
-  }
-
-  GerritCallback<ApprovalSummarySet> approvalFormatter(final int dataBegin,
-      final int rows, final boolean highlightUnreviewed) {
-    return new GerritCallback<ApprovalSummarySet>() {
-      @Override
-      public void onSuccess(final ApprovalSummarySet as) {
-        Map<Change.Id, ApprovalSummary> ids = as.getSummaryMap();
-        AccountInfoCache aic = as.getAccountInfoCache();
-        for (int row = dataBegin; row < dataBegin + rows; row++) {
-          final ChangeInfo c = getRowItem(row);
-          if (ids.containsKey(c.getId())) {
-            displayApprovals(row, ids.get(c.getId()), aic, highlightUnreviewed);
-          }
-        }
-      }
-    };
-  }
-
   public class StarKeyCommand extends NeedsSignInKeyCommand {
     public StarKeyCommand(int mask, char key, String help) {
       super(mask, key, help);
@@ -419,15 +257,10 @@
     }
   }
 
-  public enum ApprovalViewType {
-    NONE, USER, STRONGEST
-  }
-
   public static class Section {
     String titleText;
 
     ChangeTable parent;
-    final ApprovalViewType viewType;
     final Account.Id ownerId;
     int titleRow = -1;
     int dataBegin;
@@ -436,17 +269,15 @@
     private ChangeRowFormatter changeRowFormatter;
 
     public Section() {
-      this(null, ApprovalViewType.NONE, null);
+      this(null, null);
     }
 
     public Section(final String titleText) {
-      this(titleText, ApprovalViewType.NONE, null);
+      this(titleText, null);
     }
 
-    public Section(final String titleText, final ApprovalViewType view,
-        final Account.Id owner) {
+    public Section(final String titleText, final Account.Id owner) {
       setTitleText(titleText);
-      viewType = view;
       ownerId = owner;
     }
 
@@ -493,19 +324,6 @@
           parent.populateChangeRow(dataBegin + i, c, changeRowFormatter);
           cids.add(c.getId());
         }
-
-        switch (viewType) {
-          case NONE:
-            break;
-          case USER:
-            PatchUtil.DETAIL_SVC.userApprovals(cids, ownerId, parent
-                .approvalFormatter(dataBegin, rows, true));
-            break;
-          case STRONGEST:
-            PatchUtil.DETAIL_SVC.strongestApprovals(cids, parent
-                .approvalFormatter(dataBegin, rows, false));
-            break;
-        }
       }
     }
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable2.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable2.java
index 20dd80f..03cc11d 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable2.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeTable2.java
@@ -18,9 +18,9 @@
 
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.changes.ChangeInfo.LabelInfo;
+import com.google.gerrit.client.ui.AccountLink;
 import com.google.gerrit.client.ui.BranchLink;
 import com.google.gerrit.client.ui.ChangeLink;
-import com.google.gerrit.client.ui.InlineHyperlink;
 import com.google.gerrit.client.ui.NavigationTable;
 import com.google.gerrit.client.ui.NeedsSignInKeyCommand;
 import com.google.gerrit.client.ui.ProjectLink;
@@ -29,7 +29,6 @@
 import com.google.gwt.dom.client.Element;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
-import com.google.gwt.event.dom.client.KeyCodes;
 import com.google.gwt.event.dom.client.KeyPressEvent;
 import com.google.gwt.user.client.DOM;
 import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
@@ -39,6 +38,7 @@
 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 java.util.ArrayList;
 import java.util.Collections;
@@ -46,35 +46,28 @@
 
 public class ChangeTable2 extends NavigationTable<ChangeInfo> {
   private static final int C_STAR = 1;
-  private static final int C_ID = 2;
-  private static final int C_SUBJECT = 3;
-  private static final int C_OWNER = 4;
-  private static final int C_PROJECT = 5;
-  private static final int C_BRANCH = 6;
-  private static final int C_LAST_UPDATE = 7;
-  private static final int BASE_COLUMNS = 8;
+  private static final int C_SUBJECT = 2;
+  private static final int C_OWNER = 3;
+  private static final int C_PROJECT = 4;
+  private static final int C_BRANCH = 5;
+  private static final int C_LAST_UPDATE = 6;
+  private static final int BASE_COLUMNS = 7;
 
   private final List<Section> sections;
   private int columns;
   private List<String> labelNames;
 
   public ChangeTable2() {
+    super(Util.C.changeItemHelp());
     columns = BASE_COLUMNS;
     labelNames = Collections.emptyList();
 
-    keysNavigation.add(new PrevKeyCommand(0, 'k', Util.C.changeTablePrev()));
-    keysNavigation.add(new NextKeyCommand(0, 'j', Util.C.changeTableNext()));
-    keysNavigation.add(new OpenKeyCommand(0, 'o', Util.C.changeTableOpen()));
-    keysNavigation.add(
-        new OpenKeyCommand(0, KeyCodes.KEY_ENTER, Util.C.changeTableOpen()));
-
     if (Gerrit.isSignedIn()) {
       keysAction.add(new StarKeyCommand(0, 's', Util.C.changeTableStar()));
     }
 
     sections = new ArrayList<Section>();
     table.setText(0, C_STAR, "");
-    table.setText(0, C_ID, Util.C.changeTableColumnID());
     table.setText(0, C_SUBJECT, Util.C.changeTableColumnSubject());
     table.setText(0, C_OWNER, Util.C.changeTableColumnOwner());
     table.setText(0, C_PROJECT, Util.C.changeTableColumnProject());
@@ -83,8 +76,7 @@
 
     final FlexCellFormatter fmt = table.getFlexCellFormatter();
     fmt.addStyleName(0, C_STAR, Gerrit.RESOURCES.css().iconHeader());
-    fmt.addStyleName(0, C_ID, Gerrit.RESOURCES.css().cID());
-    for (int i = C_ID; i < columns; i++) {
+    for (int i = C_SUBJECT; i < columns; i++) {
       fmt.addStyleName(0, i, Gerrit.RESOURCES.css().dataHeader());
     }
 
@@ -136,13 +128,11 @@
     super.applyDataRowStyle(row);
     final CellFormatter fmt = table.getCellFormatter();
     fmt.addStyleName(row, C_STAR, Gerrit.RESOURCES.css().iconCell());
-    for (int i = C_ID; i < columns; i++) {
+    for (int i = C_SUBJECT; i < columns; i++) {
       fmt.addStyleName(row, i, Gerrit.RESOURCES.css().dataCell());
     }
-    fmt.addStyleName(row, C_ID, Gerrit.RESOURCES.css().cID());
     fmt.addStyleName(row, C_SUBJECT, Gerrit.RESOURCES.css().cSUBJECT());
-    fmt.addStyleName(row, C_PROJECT, Gerrit.RESOURCES.css().cPROJECT());
-    fmt.addStyleName(row, C_BRANCH, Gerrit.RESOURCES.css().cPROJECT());
+    fmt.addStyleName(row, C_OWNER, Gerrit.RESOURCES.css().cOWNER());
     fmt.addStyleName(row, C_LAST_UPDATE, Gerrit.RESOURCES.css().cLastUpdate());
     for (int i = BASE_COLUMNS; i < columns; i++) {
       fmt.addStyleName(row, i, Gerrit.RESOURCES.css().cAPPROVAL());
@@ -152,7 +142,7 @@
   public void updateColumnsForLabels(ChangeList... lists) {
     labelNames = new ArrayList<String>();
     for (ChangeList list : lists) {
-      for (int i = 0; i < list.size(); i++) {
+      for (int i = 0; i < list.length(); i++) {
         for (String name : list.get(i).labels()) {
           if (!labelNames.contains(name)) {
             labelNames.add(name);
@@ -198,7 +188,6 @@
           c.legacy_id(),
           c.starred()));
     }
-    table.setWidget(row, C_ID, new TableChangeLink(c.id_abbreviated(), c));
 
     String subject = Util.cropSubject(c.subject());
     Change.Status status = c.status();
@@ -207,14 +196,12 @@
     }
     table.setWidget(row, C_SUBJECT, new TableChangeLink(subject, c));
 
-    String owner = "";
-    if (c.owner() != null && c.owner().name() != null) {
-      owner = c.owner().name();
+    if (c.owner() != null) {
+      table.setWidget(row, C_OWNER, new AccountLink(c.owner(), status));
+    } else {
+      table.setText(row, C_OWNER, "");
     }
 
-    table.setWidget(row, C_OWNER, new InlineHyperlink(owner,
-        PageLinks.toAccountQuery(owner, c.status())));
-
     table.setWidget(
         row, C_PROJECT, new ProjectLink(c.project_name_key(), c.status()));
     table.setWidget(row, C_BRANCH, new BranchLink(c.project_name_key(), c
@@ -299,9 +286,9 @@
   public void addSection(final Section s) {
     assert s.parent == null;
 
-    if (s.titleText != null) {
-      s.titleRow = table.getRowCount();
-      table.setText(s.titleRow, 0, s.titleText);
+    s.parent = this;
+    s.titleRow = table.getRowCount();
+    if (s.displayTitle()) {
       final FlexCellFormatter fmt = table.getFlexCellFormatter();
       fmt.setColSpan(s.titleRow, 0, columns);
       fmt.addStyleName(s.titleRow, 0, Gerrit.RESOURCES.css().sectionHeader());
@@ -309,7 +296,6 @@
       s.titleRow = -1;
     }
 
-    s.parent = this;
     s.dataBegin = table.getRowCount();
     insertNoneRow(s.dataBegin);
     sections.add(s);
@@ -369,6 +355,7 @@
   public static class Section {
     ChangeTable2 parent;
     String titleText;
+    Widget titleWidget;
     int titleRow = -1;
     int dataBegin;
     int rows;
@@ -380,13 +367,33 @@
 
     public void setTitleText(final String text) {
       titleText = text;
+      titleWidget = null;
       if (titleRow >= 0) {
         parent.table.setText(titleRow, 0, titleText);
       }
     }
 
+    public void setTitleWidget(final Widget title) {
+      titleWidget = title;
+      titleText = null;
+      if (titleRow >= 0) {
+        parent.table.setWidget(titleRow, 0, title);
+      }
+    }
+
+    public boolean displayTitle() {
+      if (titleText != null) {
+        setTitleText(titleText);
+        return true;
+      } else if(titleWidget != null) {
+        setTitleWidget(titleWidget);
+        return true;
+      }
+      return false;
+    }
+
     public void display(ChangeList changeList) {
-      final int sz = changeList != null ? changeList.size() : 0;
+      final int sz = changeList != null ? changeList.length() : 0;
       final boolean hadData = rows > 0;
 
       if (hadData) {
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 eff3cd5..ea184df 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
@@ -14,24 +14,33 @@
 
 package com.google.gerrit.client.changes;
 
+import com.google.gerrit.client.ErrorDialog;
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.ui.ChangeLink;
 import com.google.gerrit.client.ui.CommentLinkProcessor;
+import com.google.gerrit.client.ui.CommentedActionDialog;
+import com.google.gerrit.client.ui.TextBoxChangeListener;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.common.data.ChangeDetail;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gwt.user.client.ui.Composite;
-
+import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.dom.client.PreElement;
 import com.google.gwt.dom.client.Style.Display;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.uibinder.client.UiBinder;
 import com.google.gwt.uibinder.client.UiField;
+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.Image;
 import com.google.gwt.user.client.ui.SimplePanel;
 import com.google.gwtexpui.clippy.client.CopyableLabel;
 import com.google.gwtexpui.globalkey.client.KeyCommandSet;
 import com.google.gwtexpui.safehtml.client.SafeHtml;
 import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
+import com.google.gwtjsonrpc.common.AsyncCallback;
 
 public class CommitMessageBlock extends Composite {
   interface Binder extends UiBinder<HTMLPanel, CommitMessageBlock> {
@@ -60,13 +69,44 @@
   }
 
   public void display(final String commitMessage) {
-    display(null, null, commitMessage);
+    display(null, null, false, commitMessage);
   }
 
-  public void display(Change.Id changeId, Boolean starred, String commitMessage) {
-    starPanel.clear();
+  private abstract class CommitMessageEditDialog extends CommentedActionDialog<ChangeDetail> {
+    private final String originalMessage;
+    public CommitMessageEditDialog(final String title, final String heading,
+        final String commitMessage, AsyncCallback<ChangeDetail> callback) {
+      super(title, heading, callback);
+      originalMessage = commitMessage.trim();
+      message.setCharacterWidth(72);
+      message.setVisibleLines(20);
+      message.setText(originalMessage);
+      message.addStyleName(Gerrit.RESOURCES.css().changeScreenDescription());
+      sendButton.setEnabled(false);
 
-    if (changeId != null && starred != null && Gerrit.isSignedIn()) {
+      new TextBoxChangeListener(message) {
+        public void onTextChanged(String newText) {
+          // Trim the new text so we don't consider trailing
+          // newlines as changes
+          sendButton.setEnabled(!newText.trim().equals(originalMessage));
+        }
+      };
+    }
+
+    public String getMessageText() {
+      // As we rely on commit message lines ending in LF, we convert CRLF to
+      // LF. Additionally, the commit message should be trimmed to remove any
+      // excess newlines at the end, but we need to make sure it still has at
+      // least one trailing newline.
+      return message.getText().replaceAll("\r\n", "\n").trim() + '\n';
+    }
+  }
+
+  public void display(final PatchSet.Id patchSetId,
+      Boolean starred, Boolean canEditCommitMessage, final String commitMessage) {
+    starPanel.clear();
+    if (patchSetId != null && starred != null && Gerrit.isSignedIn()) {
+      Change.Id changeId = patchSetId.getParentKey();
       StarredChanges.Icon star = StarredChanges.createIcon(changeId, starred);
       star.setStyleName(Gerrit.RESOURCES.css().changeScreenStarIcon());
       starPanel.add(star);
@@ -77,9 +117,46 @@
     }
 
     permalinkPanel.clear();
-    if (changeId != null) {
+    if (patchSetId != null) {
+      final Change.Id changeId = patchSetId.getParentKey();
       permalinkPanel.add(new ChangeLink(Util.C.changePermalink(), changeId));
-      permalinkPanel.add(new CopyableLabel(ChangeLink.permalink(changeId), false));
+      permalinkPanel.add(new CopyableLabel(ChangeLink.permalink(changeId),
+          false));
+      if (canEditCommitMessage) {
+        final Image edit = new Image(Gerrit.RESOURCES.edit());
+        edit.setTitle(Util.C.editCommitMessageToolTip());
+        edit.addStyleName(Gerrit.RESOURCES.css().link());
+        edit.addClickHandler(new ClickHandler() {
+          @Override
+          public void onClick(final ClickEvent event) {
+            new CommitMessageEditDialog(Util.C.titleEditCommitMessage(),
+                Util.C.headingEditCommitMessage(),
+                commitMessage,
+                new ChangeDetailCache.IgnoreErrorCallback() {}) {
+
+              @Override
+              public void onSend() {
+                Util.MANAGE_SVC.createNewPatchSet(patchSetId, getMessageText(),
+                    new AsyncCallback<ChangeDetail>() {
+                    @Override
+                    public void onSuccess(ChangeDetail result) {
+                      Gerrit.display(PageLinks.toChange(changeId));
+                      hide();
+                    }
+
+                    @Override
+                    public void onFailure(Throwable caught) {
+                      enableButtons(true);
+                      new ErrorDialog(caught.getMessage()).center();
+                    }
+                });
+              }
+            }.center();
+          }
+        });
+
+        permalinkPanel.add(edit);
+      }
     }
 
     String[] splitCommitMessage = commitMessage.split("\n", 2);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CustomDashboardScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CustomDashboardScreen.java
index c9f1dc6..840ebaa 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CustomDashboardScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/CustomDashboardScreen.java
@@ -14,97 +14,43 @@
 
 package com.google.gerrit.client.changes;
 
-import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.client.rpc.NativeList;
-import com.google.gerrit.client.rpc.ScreenLoadCallback;
 import com.google.gerrit.client.ui.Screen;
-import com.google.gwt.http.client.URL;
-
-import java.util.ArrayList;
-import java.util.List;
 
 public class CustomDashboardScreen extends Screen implements ChangeListScreen {
-  private String title;
-  private List<String> titles;
-  private List<String> queries;
-  private ChangeTable2 table;
-  private List<ChangeTable2.Section> sections;
+  private DashboardTable table;
+  private String params;
 
   public CustomDashboardScreen(String params) {
-    titles = new ArrayList<String>();
-    queries = new ArrayList<String>();
-    for (String kvPair : params.split("[,;&]")) {
-      String[] kv = kvPair.split("=", 2);
-      if (kv.length != 2 || kv[0].isEmpty()) {
-        continue;
-      }
-
-      if ("title".equals(kv[0])) {
-        title = URL.decodeQueryString(kv[1]);
-      } else {
-        titles.add(URL.decodeQueryString(kv[0]));
-        queries.add(URL.decodeQueryString(kv[1]));
-      }
-    }
+    this.params = params;
   }
 
   @Override
   protected void onInitUI() {
+    table = new DashboardTable(params) {
+      @Override
+      protected void onLoad() {
+        super.onLoad();
+      }
+
+      @Override
+      public void finishDisplay() {
+        super.finishDisplay();
+        display();
+      }
+    };
+
     super.onInitUI();
 
+    String title = table.getTitle();
     if (title != null) {
       setWindowTitle(title);
       setPageTitle(title);
     }
 
-    table = new ChangeTable2();
-    table.addStyleName(Gerrit.RESOURCES.css().accountDashboard());
-
-    sections = new ArrayList<ChangeTable2.Section>();
-    for (String title : titles) {
-      ChangeTable2.Section s = new ChangeTable2.Section();
-      s.setTitleText(title);
-      table.addSection(s);
-      sections.add(s);
-    }
     add(table);
   }
 
   @Override
-  protected void onLoad() {
-    super.onLoad();
-
-    if (queries.isEmpty()) {
-      display();
-    } else if (queries.size() == 1) {
-      ChangeList.next(queries.get(0),
-          0, PagedSingleListScreen.MAX_SORTKEY,
-          new ScreenLoadCallback<ChangeList>(this) {
-            @Override
-            protected void preDisplay(ChangeList result) {
-              table.updateColumnsForLabels(result);
-              sections.get(0).display(result);
-              table.finishDisplay();
-            }
-        });
-    } else {
-      ChangeList.query(
-          new ScreenLoadCallback<NativeList<ChangeList>>(this) {
-            @Override
-            protected void preDisplay(NativeList<ChangeList> result) {
-              table.updateColumnsForLabels(
-                  result.asList().toArray(new ChangeList[result.size()]));
-              for (int i = 0; i < result.size(); i++) {
-                sections.get(i).display(result.get(i));
-              }
-              table.finishDisplay();
-            }
-          },
-          queries.toArray(new String[queries.size()]));
-    }
-  }
-
-  @Override
   public void registerKeys() {
     super.registerKeys();
     table.setRegisterKeys(true);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DashboardTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DashboardTable.java
new file mode 100644
index 0000000..7b387ab
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DashboardTable.java
@@ -0,0 +1,109 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.rpc.Natives;
+import com.google.gerrit.client.ui.InlineHyperlink;
+import com.google.gerrit.common.PageLinks;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwt.http.client.URL;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.ListIterator;
+
+public class DashboardTable extends ChangeTable2 {
+  private List<Section> sections;
+  private String title;
+  private List<String> titles;
+  private List<String> queries;
+
+  public DashboardTable(String params) {
+    titles = new ArrayList<String>();
+    queries = new ArrayList<String>();
+    String foreach = null;
+    for (String kvPair : params.split("[,;&]")) {
+      String[] kv = kvPair.split("=", 2);
+      if (kv.length != 2 || kv[0].isEmpty()) {
+        continue;
+      }
+
+      if ("title".equals(kv[0])) {
+        title = URL.decodeQueryString(kv[1]);
+      } else if ("foreach".equals(kv[0])) {
+        foreach = URL.decodeQueryString(kv[1]);
+      } else {
+        titles.add(URL.decodeQueryString(kv[0]));
+        queries.add(URL.decodeQueryString(kv[1]));
+      }
+    }
+
+    if (foreach != null) {
+      ListIterator<String> it = queries.listIterator();
+      while (it.hasNext()) {
+        it.set(it.next() + " " + foreach);
+      }
+    }
+
+    addStyleName(Gerrit.RESOURCES.css().accountDashboard());
+
+    sections = new ArrayList<ChangeTable2.Section>();
+    int i = 0;
+    for (String title : titles) {
+      Section s = new Section();
+      s.setTitleWidget(new InlineHyperlink(title, PageLinks.toChangeQuery(queries.get(i++))));
+      addSection(s);
+      sections.add(s);
+    }
+  }
+
+  public String getTitle() {
+    return title;
+  }
+
+  @Override
+  protected void onLoad() {
+    super.onLoad();
+
+    if (queries.size() == 1) {
+      ChangeList.next(queries.get(0),
+          0, PagedSingleListScreen.MAX_SORTKEY,
+          new GerritCallback<ChangeList>() {
+            @Override
+            public void onSuccess(ChangeList result) {
+              updateColumnsForLabels(result);
+              sections.get(0).display(result);
+              finishDisplay();
+            }
+        });
+    } else if (! queries.isEmpty()) {
+      ChangeList.query(
+          new GerritCallback<JsArray<ChangeList>>() {
+            @Override
+            public void onSuccess(JsArray<ChangeList> result) {
+              List<ChangeList> cls = Natives.asList(result);
+              updateColumnsForLabels(cls.toArray(new ChangeList[cls.size()]));
+              for (int i = 0; i < cls.size(); i++) {
+                sections.get(i).display(cls.get(i));
+              }
+              finishDisplay();
+            }
+          },
+          queries.toArray(new String[queries.size()]));
+    }
+  }
+}
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
deleted file mode 100644
index c095d4d..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DownloadCommandLink.java
+++ /dev/null
@@ -1,81 +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.changes;
-
-import com.google.gerrit.client.Gerrit;
-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.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.common.VoidResult;
-
-abstract class DownloadCommandLink extends Anchor implements ClickHandler {
-  final AccountGeneralPreferences.DownloadCommand cmdType;
-
-  DownloadCommandLink(AccountGeneralPreferences.DownloadCommand cmdType,
-      String text) {
-    super(text);
-    this.cmdType = cmdType;
-    setStyleName(Gerrit.RESOURCES.css().downloadLink());
-    Accessibility.setRole(getElement(), Accessibility.ROLE_TAB);
-    addClickHandler(this);
-  }
-
-  @Override
-  public void onClick(ClickEvent event) {
-    event.preventDefault();
-    event.stopPropagation();
-
-    select();
-
-    if (Gerrit.isSignedIn()) {
-      // If the user is signed-in, remember this choice for future panels.
-      //
-      AccountGeneralPreferences pref =
-          Gerrit.getUserAccount().getGeneralPreferences();
-      pref.setDownloadCommand(cmdType);
-      com.google.gerrit.client.account.Util.ACCOUNT_SVC.changePreferences(pref,
-          new AsyncCallback<VoidResult>() {
-            @Override
-            public void onFailure(Throwable caught) {
-            }
-
-            @Override
-            public void onSuccess(VoidResult result) {
-            }
-          });
-    }
-  }
-
-  public AccountGeneralPreferences.DownloadCommand getCmdType() {
-    return cmdType;
-  }
-
-  void select() {
-    DownloadCommandPanel parent = (DownloadCommandPanel) getParent();
-    for (Widget w : parent) {
-      if (w != this && w instanceof DownloadCommandLink) {
-        w.removeStyleName(Gerrit.RESOURCES.css().downloadLink_Active());
-      }
-    }
-    parent.setCurrentCommand(this);
-    addStyleName(Gerrit.RESOURCES.css().downloadLink_Active());
-  }
-
-  abstract void setCurrentUrl(DownloadUrlLink link);
-}
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
deleted file mode 100644
index 1516da1..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DownloadCommandPanel.java
+++ /dev/null
@@ -1,78 +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.changes;
-
-import com.google.gerrit.client.Gerrit;
-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;
-
-class DownloadCommandPanel extends FlowPanel {
-  private DownloadCommandLink currentCommand;
-  private DownloadUrlLink currentUrl;
-
-  DownloadCommandPanel() {
-    setStyleName(Gerrit.RESOURCES.css().downloadLinkList());
-    Accessibility.setRole(getElement(), Accessibility.ROLE_TABLIST);
-  }
-
-  boolean isEmpty() {
-    return getWidgetCount() == 0;
-  }
-
-  void select(AccountGeneralPreferences.DownloadCommand cmdType) {
-    DownloadCommandLink first = null;
-
-    for (Widget w : this) {
-      if (w instanceof DownloadCommandLink) {
-        final DownloadCommandLink d = (DownloadCommandLink) w;
-        if (first == null) {
-          first = d;
-        }
-        if (d.cmdType == cmdType) {
-          d.select();
-          return;
-        }
-      }
-    }
-
-    // If none matched the requested type, select the first in the
-    // group as that will at least give us an initial baseline.
-    if (first != null) {
-      first.select();
-    }
-  }
-
-  void setCurrentUrl(DownloadUrlLink link) {
-    currentUrl = link;
-    update();
-  }
-
-  void setCurrentCommand(DownloadCommandLink cmd) {
-    currentCommand = cmd;
-    update();
-  }
-
-  private void update() {
-    if (currentCommand != null && currentUrl != null) {
-      currentCommand.setCurrentUrl(currentUrl);
-    } else if (currentCommand != null &&
-        currentCommand.getCmdType().equals(DownloadCommand.REPO_DOWNLOAD)) {
-      currentCommand.setCurrentUrl(null);
-    }
-  }
-}
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
deleted file mode 100644
index 2afafaa..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DownloadUrlLink.java
+++ /dev/null
@@ -1,77 +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.changes;
-
-import com.google.gerrit.client.Gerrit;
-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.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.common.VoidResult;
-
-class DownloadUrlLink extends Anchor implements ClickHandler {
-  final AccountGeneralPreferences.DownloadScheme urlType;
-  final String urlData;
-
-  DownloadUrlLink(AccountGeneralPreferences.DownloadScheme urlType, String text,
-      String urlData) {
-    super(text);
-    this.urlType = urlType;
-    this.urlData = urlData;
-    setStyleName(Gerrit.RESOURCES.css().downloadLink());
-    Accessibility.setRole(getElement(), Accessibility.ROLE_TAB);
-    addClickHandler(this);
-  }
-
-  @Override
-  public void onClick(ClickEvent event) {
-    event.preventDefault();
-    event.stopPropagation();
-
-    select();
-
-    if (Gerrit.isSignedIn()) {
-      // If the user is signed-in, remember this choice for future panels.
-      //
-      AccountGeneralPreferences pref =
-          Gerrit.getUserAccount().getGeneralPreferences();
-      pref.setDownloadUrl(urlType);
-      com.google.gerrit.client.account.Util.ACCOUNT_SVC.changePreferences(pref,
-          new AsyncCallback<VoidResult>() {
-            @Override
-            public void onFailure(Throwable caught) {
-            }
-
-            @Override
-            public void onSuccess(VoidResult result) {
-            }
-          });
-    }
-  }
-
-  void select() {
-    DownloadUrlPanel parent = (DownloadUrlPanel) getParent();
-    for (Widget w : parent) {
-      if (w != this && w instanceof DownloadUrlLink) {
-        w.removeStyleName(Gerrit.RESOURCES.css().downloadLink_Active());
-      }
-    }
-    parent.setCurrentUrl(this);
-    addStyleName(Gerrit.RESOURCES.css().downloadLink_Active());
-  }
-}
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
deleted file mode 100644
index b3c9ebc..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/DownloadUrlPanel.java
+++ /dev/null
@@ -1,62 +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.changes;
-
-import com.google.gerrit.client.Gerrit;
-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;
-
-class DownloadUrlPanel extends FlowPanel {
-  private final DownloadCommandPanel commandPanel;
-
-  DownloadUrlPanel(final DownloadCommandPanel commandPanel) {
-    this.commandPanel = commandPanel;
-    setStyleName(Gerrit.RESOURCES.css().downloadLinkList());
-    Accessibility.setRole(getElement(), Accessibility.ROLE_TABLIST);
-  }
-
-  boolean isEmpty() {
-    return getWidgetCount() == 0;
-  }
-
-  void select(AccountGeneralPreferences.DownloadScheme urlType) {
-    DownloadUrlLink first = null;
-
-    for (Widget w : this) {
-      if (w instanceof DownloadUrlLink) {
-        final DownloadUrlLink d = (DownloadUrlLink) w;
-        if (first == null) {
-          first = d;
-        }
-        if (d.urlType == urlType) {
-          d.select();
-          return;
-        }
-      }
-    }
-
-    // If none matched the requested type, select the first in the
-    // group as that will at least give us an initial baseline.
-    if (first != null) {
-      first.select();
-    }
-  }
-
-  void setCurrentUrl(DownloadUrlLink link) {
-    commandPanel.setCurrentUrl(link);
-  }
-}
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 23ce178..c21c68d 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,7 +21,7 @@
 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.gwtjsonrpc.common.AsyncCallback;
+import com.google.gwt.user.client.rpc.AsyncCallback;
 import com.google.gwt.user.client.ui.HorizontalPanel;
 import com.google.gwtexpui.globalkey.client.KeyCommand;
 
@@ -116,9 +116,9 @@
 
   protected void display(final ChangeList result) {
     changes = result;
-    if (!changes.isEmpty()) {
+    if (changes.length() != 0) {
       final ChangeInfo f = changes.get(0);
-      final ChangeInfo l = changes.get(changes.size() - 1);
+      final ChangeInfo l = changes.get(changes.length() - 1);
 
       prev.setTargetHistoryToken(anchorPrefix + ",p," + f._sortkey());
       next.setTargetHistoryToken(anchorPrefix + ",n," + l._sortkey());
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 7e314cc..76f77a2 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
@@ -18,33 +18,27 @@
 import com.google.gerrit.client.FormatUtil;
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.GitwebLink;
+import com.google.gerrit.client.download.DownloadPanel;
 import com.google.gerrit.client.patches.PatchUtil;
 import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.ui.AccountLink;
 import com.google.gerrit.client.ui.CommentedActionDialog;
 import com.google.gerrit.client.ui.ComplexDisclosurePanel;
-import com.google.gerrit.client.ui.InlineHyperlink;
 import com.google.gerrit.client.ui.ListenableAccountDiffPreference;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.data.ChangeDetail;
 import com.google.gerrit.common.data.PatchSetDetail;
 import com.google.gerrit.reviewdb.client.AccountDiffPreference;
-import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
-import com.google.gerrit.reviewdb.client.AuthType;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.ChangeMessage;
-import com.google.gerrit.reviewdb.client.Patch;
-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.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetInfo;
+import com.google.gerrit.reviewdb.client.UserIdentity;
 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.ui.Anchor;
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.DisclosurePanel;
@@ -54,7 +48,6 @@
 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.gwtexpui.clippy.client.CopyableLabel;
 import com.google.gwtjsonrpc.common.VoidResult;
 
 import java.util.HashSet;
@@ -183,7 +176,6 @@
           }
         }
       }
-      populateDiffAllActions(detail);
       body.add(actionsPanel);
     }
   }
@@ -202,212 +194,50 @@
     }
   }
 
-  private void displayDownload() {
-    final Project.NameKey projectKey = changeDetail.getChange().getProject();
-    final String projectName = projectKey.get();
-    final CopyableLabel copyLabel = new CopyableLabel("");
-    final DownloadCommandPanel commands = new DownloadCommandPanel();
-    final DownloadUrlPanel urls = new DownloadUrlPanel(commands);
-    final Set<DownloadScheme> allowedSchemes = Gerrit.getConfig().getDownloadSchemes();
-    final Set<DownloadCommand> allowedCommands = Gerrit.getConfig().getDownloadCommands();
-
-    copyLabel.setStyleName(Gerrit.RESOURCES.css().downloadLinkCopyLabel());
-
-    if (changeDetail.isAllowsAnonymous()
-        && Gerrit.getConfig().getGitDaemonUrl() != null
-        && (allowedSchemes.contains(DownloadScheme.ANON_GIT) ||
-            allowedSchemes.contains(DownloadScheme.DEFAULT_DOWNLOADS))) {
-      StringBuilder r = new StringBuilder();
-      r.append(Gerrit.getConfig().getGitDaemonUrl());
-      r.append(projectName);
-      r.append(" ");
-      r.append(patchSet.getRefName());
-      urls.add(new DownloadUrlLink(DownloadScheme.ANON_GIT, Util.M
-          .anonymousDownload("Git"), r.toString()));
+  public class ChangeDownloadPanel extends DownloadPanel {
+    public ChangeDownloadPanel(String project, String ref, boolean allowAnonymous) {
+      super(project, ref, allowAnonymous);
     }
 
-    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();
-      if (Gerrit.getConfig().getGitHttpUrl() != null) {
-        r.append(Gerrit.getConfig().getGitHttpUrl());
-      } else {
-        r.append(hostPageUrl);
-      }
-      r.append(projectName);
-      r.append(" ");
-      r.append(patchSet.getRefName());
-      urls.add(new DownloadUrlLink(DownloadScheme.ANON_HTTP, Util.M
-          .anonymousDownload("HTTP"), r.toString()));
-    }
-
-    if (Gerrit.getConfig().getSshdAddress() != null
-        && hasUserName()
-        && (allowedSchemes.contains(DownloadScheme.SSH) ||
-            allowedSchemes.contains(DownloadScheme.DEFAULT_DOWNLOADS))) {
-      String sshAddr = Gerrit.getConfig().getSshdAddress();
-      final StringBuilder r = new StringBuilder();
-      r.append("ssh://");
-      r.append(Gerrit.getUserAccount().getUserName());
-      r.append("@");
-      if (sshAddr.startsWith("*:") || "".equals(sshAddr)) {
-        r.append(Window.Location.getHostName());
-      }
-      if (sshAddr.startsWith("*")) {
-        sshAddr = sshAddr.substring(1);
-      }
-      r.append(sshAddr);
-      r.append("/");
-      r.append(projectName);
-      r.append(" ");
-      r.append(patchSet.getRefName());
-      urls.add(new DownloadUrlLink(DownloadScheme.SSH, "SSH", r.toString()));
-    }
-
-    if ((hasUserName() || siteReliesOnHttp())
-        && (allowedSchemes.contains(DownloadScheme.HTTP)
-            || allowedSchemes.contains(DownloadScheme.DEFAULT_DOWNLOADS))) {
-      final StringBuilder r = new StringBuilder();
-      if (Gerrit.getConfig().getGitHttpUrl() != null
-          && (changeDetail.isAllowsAnonymous() || siteReliesOnHttp())) {
-        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());
-      urls.add(new DownloadUrlLink(DownloadScheme.HTTP, "HTTP", r.toString()));
-    }
-
-    if (allowedSchemes.contains(DownloadScheme.REPO_DOWNLOAD)) {
+    @Override
+    public void populateDownloadCommandLinks() {
       // This site prefers usage of the 'repo' tool, so suggest
       // that for easy fetch.
       //
-      final StringBuilder r = new StringBuilder();
-      r.append("repo download ");
-      r.append(projectName);
-      r.append(" ");
-      r.append(changeDetail.getChange().getChangeId());
-      r.append("/");
-      r.append(patchSet.getPatchSetId());
-      final String cmd = r.toString();
-      commands.add(new DownloadCommandLink(DownloadCommand.REPO_DOWNLOAD,
-          "repo download") {
-        @Override
-        void setCurrentUrl(DownloadUrlLink link) {
-          urls.setVisible(false);
-          copyLabel.setText(cmd);
+      if (allowedSchemes.contains(DownloadScheme.REPO_DOWNLOAD)) {
+        commands.add(cmdLinkfactory.new RepoCommandLink(projectName,
+            changeDetail.getChange().getChangeId() + "/"
+            + patchSet.getPatchSetId()));
+      }
+
+      if (!urls.isEmpty()) {
+        if (allowedCommands.contains(DownloadCommand.CHECKOUT)
+            || allowedCommands.contains(DownloadCommand.DEFAULT_DOWNLOADS)) {
+          commands.add(cmdLinkfactory.new CheckoutCommandLink());
         }
-      });
-    }
-
-    if (!urls.isEmpty()) {
-      if (allowedCommands.contains(DownloadCommand.CHECKOUT)
-          || allowedCommands.contains(DownloadCommand.DEFAULT_DOWNLOADS)) {
-        commands.add(new DownloadCommandLink(DownloadCommand.CHECKOUT,
-            "checkout") {
-          @Override
-          void setCurrentUrl(DownloadUrlLink link) {
-            urls.setVisible(true);
-            copyLabel.setText("git fetch " + link.urlData
-                + " && git checkout FETCH_HEAD");
-          }
-        });
-      }
-      if (allowedCommands.contains(DownloadCommand.PULL)
-          || allowedCommands.contains(DownloadCommand.DEFAULT_DOWNLOADS)) {
-        commands.add(new DownloadCommandLink(DownloadCommand.PULL, "pull") {
-          @Override
-          void setCurrentUrl(DownloadUrlLink link) {
-            urls.setVisible(true);
-            copyLabel.setText("git pull " + link.urlData);
-          }
-        });
-      }
-      if (allowedCommands.contains(DownloadCommand.CHERRY_PICK)
-          || allowedCommands.contains(DownloadCommand.DEFAULT_DOWNLOADS)) {
-        commands.add(new DownloadCommandLink(DownloadCommand.CHERRY_PICK,
-            "cherry-pick") {
-          @Override
-          void setCurrentUrl(DownloadUrlLink link) {
-            urls.setVisible(true);
-            copyLabel.setText("git fetch " + link.urlData
-                + " && git cherry-pick FETCH_HEAD");
-          }
-        });
-      }
-      if (allowedCommands.contains(DownloadCommand.FORMAT_PATCH)
-          || allowedCommands.contains(DownloadCommand.DEFAULT_DOWNLOADS)) {
-        commands.add(new DownloadCommandLink(DownloadCommand.FORMAT_PATCH,
-            "patch") {
-          @Override
-          void setCurrentUrl(DownloadUrlLink link) {
-            urls.setVisible(true);
-            copyLabel.setText("git fetch " + link.urlData
-                + " && git format-patch -1 --stdout FETCH_HEAD");
-          }
-        });
+        if (allowedCommands.contains(DownloadCommand.PULL)
+            || allowedCommands.contains(DownloadCommand.DEFAULT_DOWNLOADS)) {
+          commands.add(cmdLinkfactory.new PullCommandLink());
+        }
+        if (allowedCommands.contains(DownloadCommand.CHERRY_PICK)
+            || allowedCommands.contains(DownloadCommand.DEFAULT_DOWNLOADS)) {
+          commands.add(cmdLinkfactory.new CherryPickCommandLink());
+        }
+        if (allowedCommands.contains(DownloadCommand.FORMAT_PATCH)
+            || allowedCommands.contains(DownloadCommand.DEFAULT_DOWNLOADS)) {
+          commands.add(cmdLinkfactory.new FormatPatchCommandLink());
+        }
       }
     }
-
-    final FlowPanel fp = new FlowPanel();
-    if (!commands.isEmpty()) {
-      final AccountGeneralPreferences pref;
-      if (Gerrit.isSignedIn()) {
-        pref = Gerrit.getUserAccount().getGeneralPreferences();
-      } else {
-        pref = new AccountGeneralPreferences();
-        pref.resetToDefaults();
-      }
-      commands.select(pref.getDownloadCommand());
-      urls.select(pref.getDownloadUrl());
-
-      FlowPanel p = new FlowPanel();
-      p.setStyleName(Gerrit.RESOURCES.css().downloadLinkHeader());
-      p.add(commands);
-      final InlineLabel glue = new InlineLabel();
-      glue.setStyleName(Gerrit.RESOURCES.css().downloadLinkHeaderGap());
-      p.add(glue);
-      p.add(urls);
-
-      fp.add(p);
-      fp.add(copyLabel);
-    }
-    infoTable.setWidget(R_DOWNLOAD, 1, fp);
   }
 
-  private static boolean siteReliesOnHttp() {
-    return Gerrit.getConfig().getGitHttpUrl() != null
-        && Gerrit.getConfig().getAuthType() == AuthType.CUSTOM_EXTENSION
-        && !Gerrit.getConfig().siteHasUsernames();
-  }
+  private void displayDownload() {
+    ChangeDownloadPanel dp = new ChangeDownloadPanel(
+      changeDetail.getChange().getProject().get(),
+      patchSet.getRefName(),
+      changeDetail.isAllowsAnonymous());
 
-  private static boolean hasUserName() {
-    return Gerrit.isSignedIn()
-        && Gerrit.getUserAccount().getUserName() != null
-        && Gerrit.getUserAccount().getUserName().length() > 0;
+    infoTable.setWidget(R_DOWNLOAD, 1, dp);
   }
 
   private void displayUserIdentity(final int row, final UserIdentity who) {
@@ -420,8 +250,7 @@
     fp.setStyleName(Gerrit.RESOURCES.css().patchSetUserIdentity());
     if (who.getName() != null) {
       if (who.getAccount() != null) {
-        fp.add(new InlineHyperlink(who.getName(),
-            PageLinks.toAccountQuery(who.getName())));
+        fp.add(new AccountLink(who));
       } else {
         final InlineLabel lbl = new InlineLabel(who.getName());
         lbl.setStyleName(Gerrit.RESOURCES.css().accountName());
@@ -475,11 +304,29 @@
         @Override
         public void onClick(final ClickEvent event) {
           b.setEnabled(false);
-          Util.MANAGE_SVC.submit(patchSet.getId(),
-              new ChangeDetailCache.GerritWidgetCallback(b) {
-                public void onSuccess(ChangeDetail result) {
-                  onSubmitResult(result);
-                }
+          ChangeApi.submit(
+              patchSet.getId().getParentKey().get(),
+              patchSet.getRevision().get(),
+              new GerritCallback<SubmitInfo>() {
+                  public void onSuccess(SubmitInfo result) {
+                    redisplay();
+                  }
+
+                  public void onFailure(Throwable err) {
+                    if (SubmitFailureDialog.isConflict(err)) {
+                      new SubmitFailureDialog(err.getMessage()).center();
+                      redisplay();
+                    } else {
+                      b.setEnabled(true);
+                      super.onFailure(err);
+                    }
+                  }
+
+                  private void redisplay() {
+                    Gerrit.display(
+                        PageLinks.toChange(patchSet.getId().getParentKey()),
+                        new ChangeScreen(patchSet.getId().getParentKey()));
+                  }
               });
         }
       });
@@ -504,8 +351,22 @@
 
             @Override
             public void onSend() {
-              Util.MANAGE_SVC.revertChange(patchSet.getId(), getMessageText(),
-                 createCallback());
+              ChangeApi.revert(changeDetail.getChange().getChangeId(),
+                  getMessageText(), new GerritCallback<ChangeInfo>() {
+                    @Override
+                    public void onSuccess(ChangeInfo result) {
+                      sent = true;
+                      Gerrit.display(PageLinks.toChange(new Change.Id(result
+                          ._number())));
+                      hide();
+                    }
+
+                    @Override
+                    public void onFailure(Throwable caught) {
+                      enableButtons(true);
+                      super.onFailure(caught);
+                    }
+                  });
             }
           }.center();
         }
@@ -527,8 +388,25 @@
 
             @Override
             public void onSend() {
-              Util.MANAGE_SVC.abandonChange(patchSet.getId(), getMessageText(),
-                  createCallback());
+              // TODO: once the other users of ActionDialog have converted to
+              // REST APIs, we can use createCallback() rather than providing
+              // them directly.
+              ChangeApi.abandon(changeDetail.getChange().getChangeId(),
+                  getMessageText(), new GerritCallback<ChangeInfo>() {
+                    @Override
+                    public void onSuccess(ChangeInfo result) {
+                      sent = true;
+                      Gerrit.display(PageLinks.toChange(new Change.Id(result
+                          ._number())));
+                      hide();
+                    }
+
+                    @Override
+                    public void onFailure(Throwable caught) {
+                      enableButtons(true);
+                      super.onFailure(caught);
+                    }
+                  });
             }
           }.center();
         }
@@ -574,8 +452,22 @@
 
             @Override
             public void onSend() {
-              Util.MANAGE_SVC.restoreChange(patchSet.getId(), getMessageText(),
-                  createCallback());
+              ChangeApi.restore(changeDetail.getChange().getChangeId(),
+                  getMessageText(), new GerritCallback<ChangeInfo>() {
+                    @Override
+                    public void onSuccess(ChangeInfo result) {
+                      sent = true;
+                      Gerrit.display(PageLinks.toChange(new Change.Id(result
+                          ._number())));
+                      hide();
+                    }
+
+                    @Override
+                    public void onFailure(Throwable caught) {
+                      enableButtons(true);
+                      super.onFailure(caught);
+                    }
+                  });
             }
           }.center();
         }
@@ -597,35 +489,6 @@
     }
   }
 
-  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()) {
-          openWindow(Dispatcher.toPatchSideBySide(diffBaseId, p.getKey()));
-        }
-      }
-    });
-    actionsPanel.add(diffAllSideBySide);
-
-    final Button diffAllUnified = new Button(Util.C.buttonDiffAllUnified());
-    diffAllUnified.addClickHandler(new ClickHandler() {
-      @Override
-      public void onClick(ClickEvent event) {
-        for (Patch p : detail.getPatches()) {
-          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() {
@@ -739,28 +602,6 @@
         Gerrit.RESOURCES.css().header());
   }
 
-  private void onSubmitResult(final ChangeDetail result) {
-    if (result.getChange().getStatus() == Change.Status.NEW) {
-      // The submit failed. Try to locate the message and display
-      // it to the user, it should be the last one created by Gerrit.
-      //
-      ChangeMessage msg = null;
-      if (result.getMessages() != null && result.getMessages().size() > 0) {
-        for (int i = result.getMessages().size() - 1; i >= 0; i--) {
-          if (result.getMessages().get(i).getAuthor() == null) {
-            msg = result.getMessages().get(i);
-            break;
-          }
-        }
-      }
-
-      if (msg != null) {
-        new SubmitFailureDialog(result, msg).center();
-      }
-    }
-    detailCache.set(result);
-  }
-
   public PatchSet getPatchSet() {
     return patchSet;
   }
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 b9ed4e7..5a6e427 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
@@ -176,6 +176,7 @@
         PatchSetComplexDisclosurePanel patchSetPanel =
             patchSetPanels.get(patchSetId);
         patchSetPanel.setActive(true);
+        patchSetPanel.setOpen(true);
         activePatchSetId = patchSetId;
       }
     } else {
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 b8acd56..d00ef89 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
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.client.changes;
 
+import com.google.gerrit.client.Dispatcher;
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.patches.PatchScreen;
 import com.google.gerrit.client.ui.InlineHyperlink;
@@ -22,11 +23,10 @@
 import com.google.gerrit.client.ui.PatchLink;
 import com.google.gerrit.common.data.PatchSetDetail;
 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.gerrit.reviewdb.client.PatchSet;
 import com.google.gwt.core.client.Scheduler;
 import com.google.gwt.core.client.Scheduler.RepeatingCommand;
 import com.google.gwt.event.dom.client.ClickEvent;
@@ -34,6 +34,8 @@
 import com.google.gwt.event.dom.client.KeyCodes;
 import com.google.gwt.event.dom.client.KeyPressEvent;
 import com.google.gwt.user.client.Command;
+import com.google.gwt.user.client.Window;
+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.HTMLTable.Cell;
@@ -44,7 +46,6 @@
 import com.google.gwtexpui.progress.client.ProgressBar;
 import com.google.gwtexpui.safehtml.client.SafeHtml;
 import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
-import com.google.gwtorm.client.KeyUtil;
 
 import java.util.ArrayList;
 import java.util.List;
@@ -243,8 +244,7 @@
 
     Key thisKey = patch.getKey();
     PatchLink link;
-    if (patchType == PatchScreen.Type.SIDE_BY_SIDE
-        && patch.getPatchType() == Patch.PatchType.UNIFIED) {
+    if (patchType == PatchScreen.Type.SIDE_BY_SIDE) {
       link = new PatchLink.SideBySide("", base, thisKey, index, detail, this);
     } else {
       link = new PatchLink.Unified("", base, thisKey, index, detail, this);
@@ -293,10 +293,6 @@
     return listenablePrefs;
   }
 
-  public void setPreferences(ListenableAccountDiffPreference prefs) {
-    listenablePrefs = prefs;
-  }
-
   private class MyTable extends NavigationTable<Patch> {
     private static final int C_PATH = 2;
     private static final int C_DRAFT = 3;
@@ -394,13 +390,9 @@
       setRowItem(row, patch);
 
       Widget nameCol;
-      if (patch.getPatchType() == Patch.PatchType.UNIFIED) {
-        nameCol = new PatchLink.SideBySide(getDisplayFileName(patch), base,
-            patch.getKey(), row - 1, detail, PatchTable.this);
-      } else {
-        nameCol = new PatchLink.Unified(getDisplayFileName(patch), base,
-            patch.getKey(), row - 1, detail, PatchTable.this);
-      }
+      nameCol = new PatchLink.SideBySide(getDisplayFileName(patch), base,
+          patch.getKey(), row - 1, detail, PatchTable.this);
+
       if (patch.getSourceFileName() != null) {
         final String text;
         if (patch.getChangeType() == Patch.ChangeType.RENAMED) {
@@ -420,18 +412,43 @@
       table.setWidget(row, C_PATH, nameCol);
 
       int C_UNIFIED = C_SIDEBYSIDE + 1;
-      if (patch.getPatchType() == Patch.PatchType.UNIFIED) {
-        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_SIDEBYSIDE, new PatchLink.SideBySide(
+          Util.C.patchTableDiffSideBySide(), base, 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 initializeLastRow(int row) {
+      Anchor sideBySide = new Anchor(Util.C.diffAllSideBySide());
+      sideBySide.addClickHandler(new ClickHandler() {
+        @Override
+        public void onClick(ClickEvent event) {
+          for (Patch p : detail.getPatches()) {
+            openWindow(Dispatcher.toPatchSideBySide(base, p.getKey()));
+          }
+        }
+      });
+      table.setWidget(row, C_SIDEBYSIDE - 2, sideBySide);
+
+      int C_UNIFIED = C_SIDEBYSIDE - 2 + 1;
+      Anchor unified = new Anchor(Util.C.diffAllUnified());
+      unified.addClickHandler(new ClickHandler() {
+        public void onClick(ClickEvent event) {
+          for (Patch p : detail.getPatches()) {
+            openWindow(Dispatcher.toPatchUnified(base, p.getKey()));
+          }
+        };
+      });
+      table.setWidget(row, C_UNIFIED, unified);
+    }
+
+    private void openWindow(String token) {
+      String url = Window.Location.getPath() + "#" + token;
+      Window.open(url, "_blank", null);
+    }
+
     void appendHeader(final SafeHtmlBuilder m) {
       m.openTr();
 
@@ -530,49 +547,9 @@
       appendSize(m, p);
       m.closeTd();
 
-      switch (p.getPatchType()) {
-        case UNIFIED:
-          openlink(m, 2);
-          m.closeTd();
-          break;
-
-        case BINARY: {
-          String base = GWT.getHostPageBaseURL();
-          base += "cat/" + KeyUtil.encode(p.getKey().toString());
-          switch (p.getChangeType()) {
-            case DELETED:
-            case MODIFIED:
-              openlink(m, 1);
-              m.openAnchor();
-              m.setAttribute("href", base + "^1");
-              m.append(Util.C.patchTableDownloadPreImage());
-              closelink(m);
-              break;
-            default:
-              emptycell(m, 1);
-              break;
-          }
-          switch (p.getChangeType()) {
-            case MODIFIED:
-            case ADDED:
-              openlink(m, 1);
-              m.openAnchor();
-              m.setAttribute("href", base + "^0");
-              m.append(Util.C.patchTableDownloadPostImage());
-              closelink(m);
-              break;
-            default:
-              emptycell(m, 1);
-              break;
-          }
-          break;
-        }
-
-        default:
-          emptycell(m, 2);
-          break;
-      }
-
+      // Diff
+      openlink(m, 2);
+      m.closeTd();
       openlink(m, 1);
       m.closeTd();
 
@@ -591,7 +568,7 @@
       m.closeTr();
     }
 
-    void appendTotals(final SafeHtmlBuilder m, int ins, int dels,
+    void appendLastRow(final SafeHtmlBuilder m, int ins, int dels,
         final boolean isReverseDiff) {
       m.openTr();
 
@@ -617,6 +594,12 @@
       m.append(Util.M.patchTableSize_Modify(ins, dels));
       m.closeTd();
 
+      openlink(m, 2);
+      m.closeTd();
+
+      openlink(m, 1);
+      m.closeTd();
+
       m.closeTr();
     }
 
@@ -649,14 +632,19 @@
           case ADDED:
             m.append(Util.M.patchTableSize_Lines(ins));
             break;
+
           case DELETED:
             m.nbsp();
             break;
+
           case MODIFIED:
           case COPIED:
           case RENAMED:
             m.append(Util.M.patchTableSize_Modify(ins, dels));
             break;
+
+          case REWRITE:
+            break;
         }
       } else {
         m.nbsp();
@@ -670,20 +658,6 @@
       m.setAttribute("colspan", colspan);
     }
 
-    private void closelink(final SafeHtmlBuilder m) {
-      m.closeAnchor();
-      m.closeTd();
-    }
-
-    private void emptycell(final SafeHtmlBuilder m, final int colspan) {
-      m.openTd();
-      m.addStyleName(Gerrit.RESOURCES.css().dataCell());
-      m.addStyleName(Gerrit.RESOURCES.css().diffLinkCell());
-      m.setAttribute("colspan", colspan);
-      m.nbsp();
-      m.closeTd();
-    }
-
     @Override
     protected Object getRowItemKey(final Patch item) {
       return item.getKey();
@@ -787,8 +761,9 @@
               return true;
             }
           }
-          table.appendTotals(nc, insertions, deletions, isReverseDiff);
+          table.appendLastRow(nc, insertions, deletions, isReverseDiff);
           table.resetHtml(nc);
+          table.initializeLastRow(row + 1);
           nc = null;
           stage = 1;
           row = 0;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ProjectDashboardScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ProjectDashboardScreen.java
new file mode 100644
index 0000000..899e86b
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ProjectDashboardScreen.java
@@ -0,0 +1,66 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.changes;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.admin.ProjectScreen;
+import com.google.gerrit.client.ui.InlineHyperlink;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gwt.user.client.ui.FlowPanel;
+
+public class ProjectDashboardScreen extends ProjectScreen implements ChangeListScreen {
+  private DashboardTable table;
+  private String params;
+
+  public ProjectDashboardScreen(final Project.NameKey toShow, String params) {
+    super(toShow);
+    this.params = params;
+  }
+
+  @Override
+  protected void onInitUI() {
+    table = new DashboardTable(params) {
+      @Override
+      protected void onLoad() {
+        super.onLoad();
+      }
+
+      @Override
+      public void finishDisplay() {
+        super.finishDisplay();
+        display();
+      }
+    };
+
+    super.onInitUI();
+
+    String title = table.getTitle();
+    if (title != null) {
+      FlowPanel fp = new FlowPanel();
+      fp.setStyleName(Gerrit.RESOURCES.css().screenHeader());
+      fp.add(new InlineHyperlink(title, PageLinks.toCustomDashboard(params)));
+      add(fp);
+    }
+
+    add(table);
+  }
+
+  @Override
+  public void registerKeys() {
+    super.registerKeys();
+    table.setRegisterKeys(true);
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PublishCommentScreen.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/PublishCommentScreen.java
index 4705aad..97949e3 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
@@ -15,29 +15,31 @@
 package com.google.gerrit.client.changes;
 
 import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.changes.ChangeInfo.ApprovalInfo;
+import com.google.gerrit.client.changes.ChangeInfo.LabelInfo;
+import com.google.gerrit.client.patches.AbstractPatchContentTable;
 import com.google.gerrit.client.patches.CommentEditorContainer;
 import com.google.gerrit.client.patches.CommentEditorPanel;
-import com.google.gerrit.client.patches.PatchUtil;
+import com.google.gerrit.client.rpc.CallbackGroup;
 import com.google.gerrit.client.rpc.GerritCallback;
+import com.google.gerrit.client.rpc.Natives;
+import com.google.gerrit.client.rpc.RestApi;
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
 import com.google.gerrit.client.ui.AccountScreen;
+import com.google.gerrit.client.ui.CommentLinkProcessor;
 import com.google.gerrit.client.ui.PatchLink;
 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.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.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArrayString;
 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.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.FormPanel;
@@ -47,13 +49,14 @@
 import com.google.gwt.user.client.ui.VerticalPanel;
 import com.google.gwt.user.client.ui.Widget;
 import com.google.gwtexpui.globalkey.client.NpTextArea;
+import com.google.gwtexpui.safehtml.client.SafeHtml;
+import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
 import com.google.gwtjsonrpc.common.VoidResult;
 
 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;
 
@@ -62,6 +65,7 @@
   private static SavedState lastState;
 
   private final PatchSet.Id patchSetId;
+  private String revision;
   private Collection<ValueRadioButton> approvalButtons;
   private ChangeDescriptionBlock descBlock;
   private ApprovalTable approvals;
@@ -73,6 +77,7 @@
   private Button cancel;
   private boolean saveStateOnUnload = true;
   private List<CommentEditorPanel> commentEditors;
+  private ChangeInfo change;
 
   public PublishCommentScreen(final PatchSet.Id psi) {
     patchSetId = psi;
@@ -141,7 +146,22 @@
   @Override
   protected void onLoad() {
     super.onLoad();
-    Util.DETAIL_SVC.patchSetPublishDetail(patchSetId,
+
+    CallbackGroup cbs = new CallbackGroup();
+    ChangeApi.revision(patchSetId).view("review").get(cbs.add(
+        new AsyncCallback<ChangeInfo>() {
+          @Override
+          public void onSuccess(ChangeInfo result) {
+            result.init();
+            change = result;
+          }
+
+          @Override
+          public void onFailure(Throwable caught) {
+            // Handled by ScreenLoadCallback.onFailure().
+          }
+        }));
+    Util.DETAIL_SVC.patchSetPublishDetail(patchSetId, cbs.addGwtjsonrpc(
         new ScreenLoadCallback<PatchSetPublishDetail>(this) {
           @Override
           protected void preDisplay(final PatchSetPublishDetail result) {
@@ -153,7 +173,7 @@
           protected void postDisplay() {
             message.setFocus(true);
           }
-        });
+        }));
   }
 
   @Override
@@ -222,47 +242,53 @@
     mwrap.add(message);
   }
 
-  private void initApprovals(final PatchSetPublishDetail r, final Panel body) {
-    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 initApprovals(Panel body) {
+    for (String labelName : change.labels()) {
+      initLabel(labelName, body);
     }
   }
 
-  private void initApprovalType(final PatchSetPublishDetail r,
-      final Panel body, final ApprovalType ct, final PermissionRange range) {
-    body.add(new SmallHeading(ct.getCategory().getName() + ":"));
+  private void initLabel(String labelName, Panel body) {
+    if (!change.has_permitted_labels()) {
+      return;
+    }
+    JsArrayString nativeValues = change.permitted_values(labelName);
+    if (nativeValues == null || nativeValues.length() == 0) {
+      return;
+    }
+    List<String> values = new ArrayList<String>(nativeValues.length());
+    for (int i = 0; i < nativeValues.length(); i++) {
+      values.add(nativeValues.get(i));
+    }
+    Collections.reverse(values);
+    LabelInfo label = change.label(labelName);
 
-    final VerticalPanel vp = new VerticalPanel();
-    vp.setStyleName(Gerrit.RESOURCES.css().approvalCategoryList());
-    final List<ApprovalCategoryValue> lst =
-        new ArrayList<ApprovalCategoryValue>(ct.getValues());
-    Collections.reverse(lst);
-    final ApprovalCategory.Id catId = ct.getCategory().getId();
-    final PatchSetApproval prior = r.getChangeApproval(catId);
+    body.add(new SmallHeading(label.name() + ":"));
 
-    for (final ApprovalCategoryValue buttonValue : lst) {
-      if (!range.contains(buttonValue.getValue())) {
-        continue;
+    VerticalPanel vp = new VerticalPanel();
+    vp.setStyleName(Gerrit.RESOURCES.css().labelList());
+
+    Short prior = null;
+    if (label.all() != null) {
+      for (ApprovalInfo app : Natives.asList(label.all())) {
+        if (app._account_id() == Gerrit.getUserAccount().getId().get()) {
+          prior = app.value();
+          break;
+        }
       }
+    }
 
-      final ValueRadioButton b =
-          new ValueRadioButton(buttonValue, ct.getCategory().getName());
-      b.setText(buttonValue.format());
+    for (String value : values) {
+      ValueRadioButton b = new ValueRadioButton(label, value);
+      SafeHtml buf = new SafeHtmlBuilder().append(b.format());
+      buf = CommentLinkProcessor.apply(buf);
+      SafeHtml.set(b, buf);
 
       if (lastState != null && patchSetId.equals(lastState.patchSetId)
-          && lastState.approvals.containsKey(buttonValue.getCategoryId())) {
-        b.setValue(lastState.approvals.get(buttonValue.getCategoryId()).equals(
-            buttonValue));
+          && lastState.approvals.containsKey(label.name())) {
+        b.setValue(lastState.approvals.get(label.name()) == value);
       } else {
-        b.setValue(prior != null ? buttonValue.getValue() == prior.getValue()
-            : buttonValue.getValue() == 0);
+        b.setValue(b.parseValue() == (prior != null ? prior : 0));
       }
 
       approvalButtons.add(b);
@@ -274,13 +300,12 @@
   private void display(final PatchSetPublishDetail r) {
     setPageTitle(Util.M.publishComments(r.getChange().getKey().abbreviate(),
         patchSetId.get()));
-    descBlock.display(r.getChange(), null, r.getPatchSetInfo(), r.getAccounts());
+    descBlock.display(r.getChange(), null, false, r.getPatchSetInfo(), r.getAccounts(),
+        r.getSubmitTypeRecord());
 
     if (r.getChange().getStatus().isOpen()) {
-      initApprovals(r, approvalPanel);
-
-      approvals.setAccountInfoCache(r.getAccounts());
-      approvals.display(r);
+      initApprovals(approvalPanel);
+      approvals.display(change);
     } else {
       approvals.setVisible(false);
     }
@@ -291,6 +316,7 @@
 
     draftsPanel.clear();
     commentEditors = new ArrayList<CommentEditorPanel>();
+    revision = r.getPatchSetInfo().getRevId();
 
     if (!r.getDrafts().isEmpty()) {
       draftsPanel.add(new SmallHeading(Util.C.headingPatchComments()));
@@ -312,7 +338,11 @@
         }
 
         final CommentEditorPanel editor = new CommentEditorPanel(c);
-        editor.setAuthorNameText(Util.M.lineHeader(c.getLine()));
+        if (c.getLine() == AbstractPatchContentTable.R_HEAD) {
+          editor.setAuthorNameText(Util.C.fileCommentHeader());
+        } else {
+          editor.setAuthorNameText(Util.M.lineHeader(c.getLine()));
+        }
         editor.setOpen(true);
         commentEditors.add(editor);
         panel.add(editor);
@@ -347,20 +377,23 @@
   }
 
   private void onSend2(final boolean submit) {
-    final Map<ApprovalCategory.Id, ApprovalCategoryValue.Id> values =
-        new HashMap<ApprovalCategory.Id, ApprovalCategoryValue.Id>();
+    ReviewInput data = ReviewInput.create();
+    data.message(ChangeApi.emptyToNull(message.getText().trim()));
+    data.init();
     for (final ValueRadioButton b : approvalButtons) {
       if (b.getValue()) {
-        values.put(b.value.getCategoryId(), b.value.getId());
+        data.label(b.label.name(), b.parseValue());
       }
     }
 
     enableForm(false);
-    PatchUtil.DETAIL_SVC.publishComments(patchSetId, message.getText().trim(),
-        new HashSet<ApprovalCategoryValue.Id>(values.values()),
-        new GerritCallback<VoidResult>() {
-          public void onSuccess(final VoidResult result) {
-            if(submit) {
+    new RestApi("/changes/")
+      .id(String.valueOf(patchSetId.getParentKey().get()))
+      .view("revisions").id(revision).view("review")
+      .post(data, new GerritCallback<ReviewInput>() {
+          @Override
+          public void onSuccess(ReviewInput result) {
+            if (submit) {
               submit();
             } else {
               saveStateOnUnload = false;
@@ -376,18 +409,39 @@
         });
   }
 
+  private static class ReviewInput extends JavaScriptObject {
+    static ReviewInput create() {
+      return (ReviewInput) createObject();
+    }
+
+    final native void message(String m) /*-{ if(m)this.message=m; }-*/;
+    final native void label(String n, short v) /*-{ this.labels[n]=v; }-*/;
+    final native void init() /*-{
+      this.labels = {};
+      this.strict_labels = true;
+      this.drafts = 'PUBLISH';
+    }-*/;
+
+    protected ReviewInput() {
+    }
+  }
+
   private void submit() {
-    Util.MANAGE_SVC.submit(patchSetId,
-        new GerritCallback<ChangeDetail>() {
-          public void onSuccess(ChangeDetail result) {
+    ChangeApi.submit(patchSetId.getParentKey().get(), revision,
+      new GerritCallback<SubmitInfo>() {
+          public void onSuccess(SubmitInfo result) {
             saveStateOnUnload = false;
             goChange();
           }
 
           @Override
-          public void onFailure(Throwable caught) {
+          public void onFailure(Throwable err) {
+            if (SubmitFailureDialog.isConflict(err)) {
+              new SubmitFailureDialog(err.getMessage()).center();
+            } else {
+              super.onFailure(err);
+            }
             goChange();
-            super.onFailure(caught);
           }
         });
   }
@@ -398,26 +452,41 @@
   }
 
   private static class ValueRadioButton extends RadioButton {
-    final ApprovalCategoryValue value;
+    final LabelInfo label;
+    final String value;
 
-    ValueRadioButton(final ApprovalCategoryValue v, final String label) {
-      super(label);
-      value = v;
+    ValueRadioButton(LabelInfo label, String value) {
+      super(label.name());
+      this.label = label;
+      this.value = value;
+    }
+
+    String format() {
+      return new StringBuilder().append(value).append(' ')
+          .append(label.value_text(value)).toString();
+    }
+
+    short parseValue() {
+      String value = this.value;
+      if (value.startsWith(" ") || value.startsWith("+")) {
+        value = value.substring(1);
+      }
+      return Short.parseShort(value);
     }
   }
 
   private static class SavedState {
     final PatchSet.Id patchSetId;
     final String message;
-    final Map<ApprovalCategory.Id, ApprovalCategoryValue> approvals;
+    final Map<String, String> approvals;
 
     SavedState(final PublishCommentScreen p) {
       patchSetId = p.patchSetId;
       message = p.message.getText();
-      approvals = new HashMap<ApprovalCategory.Id, ApprovalCategoryValue>();
+      approvals = new HashMap<String, String>();
       for (final ValueRadioButton b : p.approvalButtons) {
         if (b.getValue()) {
-          approvals.put(b.value.getCategoryId(), b.value);
+          approvals.put(b.label.name(), b.value);
         }
       }
     }
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 b94fcae..01e294f 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,7 +19,7 @@
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.RevId;
-import com.google.gwtjsonrpc.common.AsyncCallback;
+import com.google.gwt.user.client.rpc.AsyncCallback;
 import com.google.gwtorm.client.KeyUtil;
 
 public class QueryScreen extends PagedSingleListScreen implements
@@ -52,12 +52,11 @@
       @Override
       public final void onSuccess(ChangeList result) {
         if (isAttached()) {
-          if (result.size() == 1 && isSingleQuery(query)) {
+          if (result.length() == 1 && isSingleQuery(query)) {
             ChangeInfo c = result.get(0);
             Change.Id id = c.legacy_id();
             Gerrit.display(PageLinks.toChange(id), new ChangeScreen(id));
           } else {
-            Gerrit.setQueryString(query);
             display(result);
             QueryScreen.this.display();
           }
@@ -67,6 +66,12 @@
   }
 
   @Override
+  public void onShowView() {
+    super.onShowView();
+    Gerrit.setQueryString(query);
+  }
+
+  @Override
   protected void loadPrev() {
     ChangeList.prev(query, pageSize, pos, loadCallback());
   }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarredChanges.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarredChanges.java
index 8b5aa1c..7d2084a 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarredChanges.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/StarredChanges.java
@@ -21,8 +21,6 @@
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.event.dom.client.KeyPressEvent;
-import com.google.gwt.event.shared.EventBus;
-import com.google.gwt.event.shared.SimpleEventBus;
 import com.google.gwt.resources.client.ImageResource;
 import com.google.gwt.user.client.ui.Image;
 import com.google.gwtexpui.globalkey.client.KeyCommand;
@@ -32,7 +30,6 @@
 
 /** Supports the star icon displayed on changes and tracking the status. */
 public class StarredChanges {
-  private static final EventBus eventBus = new SimpleEventBus();
   private static final Event.Type<ChangeStarHandler> TYPE =
       new Event.Type<ChangeStarHandler>();
 
@@ -87,7 +84,7 @@
   public static HandlerRegistration addHandler(
       Change.Id source,
       ChangeStarHandler handler) {
-    return eventBus.addHandlerToSource(TYPE, source, handler);
+    return Gerrit.EVENT_BUS.addHandlerToSource(TYPE, source, handler);
   }
 
   /**
@@ -95,7 +92,7 @@
    * not RPC to the server and does not alter the starred status of a change.
    */
   public static void fireChangeStarEvent(Change.Id id, boolean starred) {
-    eventBus.fireEventFromSource(
+    Gerrit.EVENT_BUS.fireEventFromSource(
         new ChangeStarEvent(id, starred),
         id);
   }
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 bedfb74..70bf4b6 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
@@ -15,13 +15,17 @@
 package com.google.gerrit.client.changes;
 
 import com.google.gerrit.client.ErrorDialog;
-import com.google.gerrit.common.data.ChangeDetail;
-import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
+import com.google.gwtjsonrpc.client.RemoteJsonException;
 
 class SubmitFailureDialog extends ErrorDialog {
-  SubmitFailureDialog(final ChangeDetail result, final ChangeMessage msg) {
-    super(new SafeHtmlBuilder().append(msg.getMessage().trim()).wikify());
+  static boolean isConflict(Throwable err) {
+    return err instanceof RemoteJsonException
+        && 409 == ((RemoteJsonException) err).getCode();
+  }
+
+  SubmitFailureDialog(String msg) {
+    super(new SafeHtmlBuilder().append(msg.trim()).wikify());
     setText(Util.C.submitFailed());
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/SubmitInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/SubmitInfo.java
new file mode 100644
index 0000000..a9206b7
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/SubmitInfo.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.changes;
+
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gwt.core.client.JavaScriptObject;
+
+class SubmitInfo extends JavaScriptObject {
+  final Change.Status status() {
+    return Change.Status.valueOf(statusRaw());
+  }
+
+  private final native String statusRaw() /*-{ return this.status; }-*/;
+
+  protected SubmitInfo() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/DashboardConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/DashboardConstants.java
new file mode 100644
index 0000000..b02f0474
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/DashboardConstants.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.dashboards;
+
+import com.google.gwt.i18n.client.Constants;
+
+public interface DashboardConstants extends Constants {
+  String dashboardName();
+  String dashboardTitle();
+  String dashboardDescription();
+  String dashboardInherited();
+  String dashboardItem();
+  String dashboardDefaultToolTip();
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/DashboardConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/DashboardConstants.properties
new file mode 100644
index 0000000..ac4de7c
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/DashboardConstants.properties
@@ -0,0 +1,6 @@
+dashboardName = Dashboard Name
+dashboardTitle = Dashboard Title
+dashboardDescription = Dashboard Description
+dashboardInherited = Inherited From
+dashboardItem = dashboard
+dashboardDefaultToolTip = Project Default Dashboard
\ No newline at end of file
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/DashboardInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/DashboardInfo.java
new file mode 100644
index 0000000..44d74ab
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/DashboardInfo.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.client.dashboards;
+
+import com.google.gwt.core.client.JavaScriptObject;
+
+public class DashboardInfo extends JavaScriptObject {
+  public final native String id() /*-{ return this.id; }-*/;
+  public final native String title() /*-{ return this.title; }-*/;
+  public final native String project() /*-{ return this.project; }-*/;
+  public final native String definingProject() /*-{ return this.defining_project; }-*/;
+  public final native String ref() /*-{ return this.ref; }-*/;
+  public final native String path() /*-{ return this.path; }-*/;
+  public final native String description() /*-{ return this.description; }-*/;
+  public final native String foreach() /*-{ return this.foreach; }-*/;
+  public final native String url() /*-{ return this.url; }-*/;
+  public final native boolean isDefault() /*-{ return this['default'] ? true : false; }-*/;
+
+  protected DashboardInfo() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/DashboardList.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/DashboardList.java
new file mode 100644
index 0000000..1190f9c
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/DashboardList.java
@@ -0,0 +1,56 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.dashboards;
+
+import com.google.gerrit.client.rpc.RestApi;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwt.http.client.URL;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+
+/** Project dashboards from {@code /projects/<name>/dashboards/}. */
+public class DashboardList extends JsArray<DashboardInfo> {
+  public static void all(Project.NameKey project,
+      AsyncCallback<JsArray<DashboardList>> callback) {
+    base(project).addParameterTrue("inherited").get(callback);
+  }
+
+  public static void getDefault(Project.NameKey project,
+      AsyncCallback<DashboardInfo> callback) {
+    base(project).view("default").addParameterTrue("inherited").get(callback);
+  }
+
+  public static void get(Project.NameKey project, String id,
+      AsyncCallback<DashboardInfo> callback) {
+    base(project).idRaw(encodeDashboardId(id)).get(callback);
+  }
+
+  private static RestApi base(Project.NameKey project) {
+    return new RestApi("/projects/").id(project.get()).view("dashboards");
+  }
+
+  private static String encodeDashboardId(String id) {
+    int c = id.indexOf(':');
+    if (0 <= c) {
+      String ref = URL.encodeQueryString(id.substring(0, c));
+      String path = URL.encodeQueryString(id.substring(c + 1));
+      return ref + ':' + path;
+    }
+    return URL.encodeQueryString(id);
+  }
+
+  protected DashboardList() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/DashboardsTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/DashboardsTable.java
new file mode 100644
index 0000000..00721b7
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/DashboardsTable.java
@@ -0,0 +1,152 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.dashboards;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.rpc.Natives;
+import com.google.gerrit.client.ui.NavigationTable;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwt.user.client.History;
+import com.google.gwt.user.client.ui.Anchor;
+import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
+import com.google.gwt.user.client.ui.Image;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class DashboardsTable extends NavigationTable<DashboardInfo> {
+  Project.NameKey project;
+
+  public DashboardsTable(final Project.NameKey project) {
+    super(Util.C.dashboardItem());
+    this.project = project;
+    initColumnHeaders();
+  }
+
+  protected void initColumnHeaders() {
+    final FlexCellFormatter fmt = table.getFlexCellFormatter();
+    fmt.setColSpan(0, 0, 2);
+    fmt.addStyleName(0, 1, Gerrit.RESOURCES.css().dataHeader());
+    fmt.addStyleName(0, 2, Gerrit.RESOURCES.css().dataHeader());
+    fmt.addStyleName(0, 3, Gerrit.RESOURCES.css().dataHeader());
+    fmt.addStyleName(0, 4, Gerrit.RESOURCES.css().dataHeader());
+
+    table.setText(0, 1, Util.C.dashboardName());
+    table.setText(0, 2, Util.C.dashboardTitle());
+    table.setText(0, 3, Util.C.dashboardDescription());
+    table.setText(0, 4, Util.C.dashboardInherited());
+  }
+
+  public void display(DashboardList dashes) {
+    display(Natives.asList(dashes));
+  }
+
+  public void display(JsArray<DashboardList> in) {
+    Map<String, DashboardInfo> map = new HashMap<String, DashboardInfo>();
+    for (DashboardList list : Natives.asList(in)) {
+      for (DashboardInfo d : Natives.asList(list)) {
+        if (!map.containsKey(d.id())) {
+          map.put(d.id(), d);
+        }
+      }
+    }
+    display(new ArrayList<DashboardInfo>(map.values()));
+  }
+
+  public void display(List<DashboardInfo> list) {
+    while (1 < table.getRowCount()) {
+      table.removeRow(table.getRowCount() - 1);
+    }
+
+    Collections.sort(list, new Comparator<DashboardInfo>() {
+      @Override
+      public int compare(DashboardInfo a, DashboardInfo b) {
+        return a.id().compareTo(b.id());
+      }
+    });
+
+    String ref = null;
+    for(DashboardInfo d : list) {
+      if (!d.ref().equals(ref)) {
+        ref = d.ref();
+        insertTitleRow(table.getRowCount(), ref);
+      }
+      insert(table.getRowCount(), d);
+    }
+
+    finishDisplay();
+  }
+
+  protected void insertTitleRow(final int row, String section) {
+    table.insertRow(row);
+
+    table.setText(row, 0, section);
+
+    final FlexCellFormatter fmt = table.getFlexCellFormatter();
+    fmt.setColSpan(row, 0, 6);
+    fmt.addStyleName(row, 0, Gerrit.RESOURCES.css().sectionHeader());
+  }
+
+  protected void insert(final int row, final DashboardInfo k) {
+    table.insertRow(row);
+
+    applyDataRowStyle(row);
+
+    final FlexCellFormatter fmt = table.getFlexCellFormatter();
+    fmt.addStyleName(row, 1, Gerrit.RESOURCES.css().dataCell());
+    fmt.addStyleName(row, 2, Gerrit.RESOURCES.css().dataCell());
+    fmt.addStyleName(row, 3, Gerrit.RESOURCES.css().dataCell());
+    fmt.addStyleName(row, 4, Gerrit.RESOURCES.css().dataCell());
+    fmt.addStyleName(row, 5, Gerrit.RESOURCES.css().dataCell());
+
+    populate(row, k);
+  }
+
+  protected void populate(final int row, final DashboardInfo k) {
+    if (k.isDefault()) {
+      table.setWidget(row, 1, new Image(Gerrit.RESOURCES.greenCheck()));
+      final FlexCellFormatter fmt = table.getFlexCellFormatter();
+      fmt.getElement(row, 1).setTitle(Util.C.dashboardDefaultToolTip());
+    }
+    table.setWidget(row, 2, new Anchor(k.path(), "#"
+            + PageLinks.toProjectDashboard(new Project.NameKey(k.project()), k.id())));
+    table.setText(row, 3, k.title() != null ? k.title() : k.path());
+    table.setText(row, 4, k.description());
+    if (k.definingProject() != null && !k.definingProject().equals(k.project())) {
+      table.setWidget(row, 5, new Anchor(k.definingProject(), "#"
+          + PageLinks.toProjectDashboards(new Project.NameKey(k.definingProject()))));
+    }
+    setRowItem(row, k);
+  }
+
+  @Override
+  protected Object getRowItemKey(final DashboardInfo item) {
+    return item.id();
+  }
+
+  @Override
+  protected void onOpenRow(final int row) {
+    if (row > 0) {
+      movePointerTo(row);
+    }
+    History.newItem(getRowItem(row).url());
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/Util.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/Util.java
new file mode 100644
index 0000000..b15bf73
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/dashboards/Util.java
@@ -0,0 +1,21 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.dashboards;
+
+import com.google.gwt.core.client.GWT;
+
+public class Util {
+  public static final DashboardConstants C = GWT.create(DashboardConstants.class);
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadCommandLink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadCommandLink.java
new file mode 100644
index 0000000..c5c68db
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadCommandLink.java
@@ -0,0 +1,177 @@
+// 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.download;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadCommand;
+import com.google.gwt.aria.client.Roles;
+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.Widget;
+import com.google.gwtexpui.clippy.client.CopyableLabel;
+import com.google.gwtjsonrpc.common.AsyncCallback;
+import com.google.gwtjsonrpc.common.VoidResult;
+
+public abstract class DownloadCommandLink extends Anchor implements ClickHandler {
+  public static class CopyableCommandLinkFactory {
+    protected CopyableLabel copyLabel = null;
+    protected Widget widget;
+
+    public class CheckoutCommandLink extends DownloadCommandLink {
+      public CheckoutCommandLink () {
+        super(DownloadCommand.CHECKOUT, "checkout");
+      }
+
+      @Override
+      protected void setCurrentUrl(DownloadUrlLink link) {
+        widget.setVisible(true);
+        copyLabel.setText("git fetch " + link.getUrlData()
+            + " && git checkout FETCH_HEAD");
+      }
+    }
+
+    public class PullCommandLink extends DownloadCommandLink {
+      public PullCommandLink() {
+        super(DownloadCommand.PULL, "pull");
+      }
+
+      @Override
+      protected void setCurrentUrl(DownloadUrlLink link) {
+        widget.setVisible(true);
+        copyLabel.setText("git pull " + link.getUrlData());
+      }
+    }
+
+    public class CherryPickCommandLink extends DownloadCommandLink {
+      public CherryPickCommandLink() {
+        super(DownloadCommand.CHERRY_PICK, "cherry-pick");
+      }
+
+      @Override
+      protected void setCurrentUrl(DownloadUrlLink link) {
+        widget.setVisible(true);
+        copyLabel.setText("git fetch " + link.getUrlData()
+            + " && git cherry-pick FETCH_HEAD");
+      }
+    }
+
+    public class FormatPatchCommandLink extends DownloadCommandLink {
+      public FormatPatchCommandLink() {
+        super(DownloadCommand.FORMAT_PATCH, "patch");
+      }
+
+      @Override
+      protected void setCurrentUrl(DownloadUrlLink link) {
+        widget.setVisible(true);
+        copyLabel.setText("git fetch " + link.getUrlData()
+            + " && git format-patch -1 --stdout FETCH_HEAD");
+      }
+    }
+
+    public class RepoCommandLink extends DownloadCommandLink {
+      String projectName;
+      String ref;
+      public RepoCommandLink(String project, String ref) {
+        super(DownloadCommand.REPO_DOWNLOAD, "checkout");
+        this.projectName = project;
+        this.ref = ref;
+      }
+
+      @Override
+      protected void setCurrentUrl(DownloadUrlLink link) {
+        widget.setVisible(false);
+        final StringBuilder r = new StringBuilder();
+        r.append("repo download ");
+        r.append(projectName);
+        r.append(" ");
+        r.append(ref);
+        copyLabel.setText(r.toString());
+      }
+    }
+
+    public class CloneCommandLink extends DownloadCommandLink {
+      public CloneCommandLink() {
+        super(DownloadCommand.CHECKOUT, "clone");
+      }
+
+      @Override
+      protected void setCurrentUrl(DownloadUrlLink link) {
+        widget.setVisible(true);
+        copyLabel.setText("git clone " + link.getUrlData());
+      }
+    }
+
+    public CopyableCommandLinkFactory(CopyableLabel label, Widget widget) {
+      copyLabel = label;
+      this.widget = widget;
+    }
+  }
+
+  final DownloadCommand cmdType;
+
+  public DownloadCommandLink(DownloadCommand cmdType,
+      String text) {
+    super(text);
+    this.cmdType = cmdType;
+    setStyleName(Gerrit.RESOURCES.css().downloadLink());
+    Roles.getTabRole().set(getElement());
+    addClickHandler(this);
+  }
+
+  @Override
+  public void onClick(ClickEvent event) {
+    event.preventDefault();
+    event.stopPropagation();
+
+    select();
+
+    if (Gerrit.isSignedIn()) {
+      // If the user is signed-in, remember this choice for future panels.
+      //
+      AccountGeneralPreferences pref =
+          Gerrit.getUserAccount().getGeneralPreferences();
+      pref.setDownloadCommand(cmdType);
+      com.google.gerrit.client.account.Util.ACCOUNT_SVC.changePreferences(pref,
+          new AsyncCallback<VoidResult>() {
+            @Override
+            public void onFailure(Throwable caught) {
+            }
+
+            @Override
+            public void onSuccess(VoidResult result) {
+            }
+          });
+    }
+  }
+
+  public DownloadCommand getCmdType() {
+    return cmdType;
+  }
+
+  void select() {
+    DownloadCommandPanel parent = (DownloadCommandPanel) getParent();
+    for (Widget w : parent) {
+      if (w != this && w instanceof DownloadCommandLink) {
+        w.removeStyleName(Gerrit.RESOURCES.css().downloadLink_Active());
+      }
+    }
+    parent.setCurrentCommand(this);
+    addStyleName(Gerrit.RESOURCES.css().downloadLink_Active());
+  }
+
+  protected abstract void setCurrentUrl(DownloadUrlLink link);
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadCommandPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadCommandPanel.java
new file mode 100644
index 0000000..d17d6c2
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadCommandPanel.java
@@ -0,0 +1,78 @@
+// 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.download;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadCommand;
+import com.google.gwt.aria.client.Roles;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.Widget;
+
+public class DownloadCommandPanel extends FlowPanel {
+  private DownloadCommandLink currentCommand;
+  private DownloadUrlLink currentUrl;
+
+  public DownloadCommandPanel() {
+    setStyleName(Gerrit.RESOURCES.css().downloadLinkList());
+    Roles.getTablistRole().set(getElement());
+  }
+
+  public boolean isEmpty() {
+    return getWidgetCount() == 0;
+  }
+
+  public void select(AccountGeneralPreferences.DownloadCommand cmdType) {
+    DownloadCommandLink first = null;
+
+    for (Widget w : this) {
+      if (w instanceof DownloadCommandLink) {
+        final DownloadCommandLink d = (DownloadCommandLink) w;
+        if (first == null) {
+          first = d;
+        }
+        if (d.cmdType == cmdType) {
+          d.select();
+          return;
+        }
+      }
+    }
+
+    // If none matched the requested type, select the first in the
+    // group as that will at least give us an initial baseline.
+    if (first != null) {
+      first.select();
+    }
+  }
+
+  void setCurrentUrl(DownloadUrlLink link) {
+    currentUrl = link;
+    update();
+  }
+
+  void setCurrentCommand(DownloadCommandLink cmd) {
+    currentCommand = cmd;
+    update();
+  }
+
+  private void update() {
+    if (currentCommand != null && currentUrl != null) {
+      currentCommand.setCurrentUrl(currentUrl);
+    } else if (currentCommand != null &&
+        currentCommand.getCmdType().equals(DownloadCommand.REPO_DOWNLOAD)) {
+      currentCommand.setCurrentUrl(null);
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadMessages.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadMessages.java
new file mode 100644
index 0000000..a0313ad
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadMessages.java
@@ -0,0 +1,21 @@
+// 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.download;
+
+import com.google.gwt.i18n.client.Messages;
+
+public interface DownloadMessages extends Messages {
+  String anonymousDownload(String protocol);
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadMessages.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadMessages.properties
new file mode 100644
index 0000000..3e34da2
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadMessages.properties
@@ -0,0 +1 @@
+anonymousDownload = Anonymous {0}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadPanel.java
new file mode 100644
index 0000000..350dbed
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadPanel.java
@@ -0,0 +1,78 @@
+// 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.download;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadCommand;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.InlineLabel;
+import com.google.gwtexpui.clippy.client.CopyableLabel;
+
+import java.util.Set;
+
+public abstract class DownloadPanel extends FlowPanel {
+  protected String projectName;
+
+  protected Set<DownloadScheme> allowedSchemes =
+      Gerrit.getConfig().getDownloadSchemes();
+  protected Set<DownloadCommand> allowedCommands =
+      Gerrit.getConfig().getDownloadCommands();
+  protected DownloadCommandLink.CopyableCommandLinkFactory cmdLinkfactory;
+
+  protected DownloadCommandPanel commands = new DownloadCommandPanel();
+  protected DownloadUrlPanel urls = new DownloadUrlPanel(commands);
+  protected CopyableLabel copyLabel = new CopyableLabel("");
+
+  public DownloadPanel(String project, String ref, boolean allowAnonymous) {
+    this.projectName = project;
+
+    copyLabel.setStyleName(Gerrit.RESOURCES.css().downloadLinkCopyLabel());
+    urls.add(DownloadUrlLink.createDownloadUrlLinks(project, ref, allowAnonymous));
+    cmdLinkfactory = new DownloadCommandLink.CopyableCommandLinkFactory(
+        copyLabel, urls);
+
+    populateDownloadCommandLinks();
+    setupWidgets();
+  }
+
+  protected void setupWidgets() {
+    if (!commands.isEmpty()) {
+      final AccountGeneralPreferences pref;
+      if (Gerrit.isSignedIn()) {
+        pref = Gerrit.getUserAccount().getGeneralPreferences();
+      } else {
+        pref = new AccountGeneralPreferences();
+        pref.resetToDefaults();
+      }
+      commands.select(pref.getDownloadCommand());
+      urls.select(pref.getDownloadUrl());
+
+      FlowPanel p = new FlowPanel();
+      p.setStyleName(Gerrit.RESOURCES.css().downloadLinkHeader());
+      p.add(commands);
+      final InlineLabel glue = new InlineLabel();
+      glue.setStyleName(Gerrit.RESOURCES.css().downloadLinkHeaderGap());
+      p.add(glue);
+      p.add(urls);
+
+      add(p);
+      add(copyLabel);
+    }
+  }
+
+  protected abstract void populateDownloadCommandLinks();
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadUrlLink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadUrlLink.java
new file mode 100644
index 0000000..8bc897f
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadUrlLink.java
@@ -0,0 +1,264 @@
+// 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.download;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences.DownloadScheme;
+import com.google.gerrit.reviewdb.client.AuthType;
+import com.google.gwt.aria.client.Roles;
+import com.google.gwt.event.dom.client.ClickEvent;
+import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.user.client.Window;
+import com.google.gwt.user.client.ui.Anchor;
+import com.google.gwt.user.client.ui.Widget;
+import com.google.gwtjsonrpc.common.AsyncCallback;
+import com.google.gwtjsonrpc.common.VoidResult;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Set;
+
+public class DownloadUrlLink extends Anchor implements ClickHandler {
+  public static class DownloadRefUrlLink extends DownloadUrlLink {
+    protected String projectName;
+    protected String ref;
+
+    protected DownloadRefUrlLink(DownloadScheme urlType,
+        String text, String project, String ref) {
+      super(urlType, text);
+      this.projectName = project;
+      this.ref = ref;
+    }
+
+    protected void appendRef(StringBuilder r) {
+      if (ref != null) {
+        r.append(" ");
+        r.append(ref);
+      }
+    }
+  }
+
+  public static class AnonGitLink extends DownloadRefUrlLink {
+    public AnonGitLink(String project, String ref) {
+      super(DownloadScheme.ANON_GIT, Util.M.anonymousDownload("Git"), project, ref);
+    }
+
+    @Override
+    public String getUrlData() {
+      StringBuilder r = new StringBuilder();
+      r.append(Gerrit.getConfig().getGitDaemonUrl());
+      r.append(projectName);
+      appendRef(r);
+      return r.toString();
+    }
+  }
+
+  public static class AnonHttpLink extends DownloadRefUrlLink {
+    public AnonHttpLink(String project, String ref) {
+      super(DownloadScheme.ANON_HTTP, Util.M.anonymousDownload("HTTP"), project, ref);
+    }
+
+    @Override
+    public String getUrlData() {
+      StringBuilder r = new StringBuilder();
+      if (Gerrit.getConfig().getGitHttpUrl() != null) {
+        r.append(Gerrit.getConfig().getGitHttpUrl());
+      } else {
+        r.append(hostPageUrl);
+      }
+      r.append(projectName);
+      appendRef(r);
+      return r.toString();
+    }
+  }
+
+  public static class SshLink extends DownloadRefUrlLink {
+    public SshLink(String project, String ref) {
+      super(DownloadScheme.SSH, "SSH", project, ref);
+    }
+
+    @Override
+    public String getUrlData() {
+      String sshAddr = Gerrit.getConfig().getSshdAddress();
+      final StringBuilder r = new StringBuilder();
+      r.append("ssh://");
+      r.append(Gerrit.getUserAccount().getUserName());
+      r.append("@");
+      if (sshAddr.startsWith("*:") || "".equals(sshAddr)) {
+        r.append(Window.Location.getHostName());
+      }
+      if (sshAddr.startsWith("*")) {
+        sshAddr = sshAddr.substring(1);
+      }
+      r.append(sshAddr);
+      r.append("/");
+      r.append(projectName);
+      appendRef(r);
+      return r.toString();
+    }
+  }
+
+  public static class HttpLink extends DownloadRefUrlLink {
+    protected boolean anonymous;
+
+    public HttpLink(String project, String ref, boolean anonymous) {
+      super(DownloadScheme.HTTP, "HTTP", project, ref);
+      this.anonymous = anonymous;
+    }
+
+    @Override
+    public String getUrlData() {
+      final StringBuilder r = new StringBuilder();
+      if (Gerrit.getConfig().getGitHttpUrl() != null
+          && (anonymous || siteReliesOnHttp())) {
+        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);
+      appendRef(r);
+      return r.toString();
+    }
+  }
+
+  public static boolean siteReliesOnHttp() {
+    return Gerrit.getConfig().getGitHttpUrl() != null
+        && Gerrit.getConfig().getAuthType() == AuthType.CUSTOM_EXTENSION
+        && !Gerrit.getConfig().siteHasUsernames();
+  }
+
+  public static List<DownloadUrlLink> createDownloadUrlLinks(String project,
+      String ref, boolean allowAnonymous) {
+    List<DownloadUrlLink> urls = new ArrayList<DownloadUrlLink>();
+    Set<DownloadScheme> allowedSchemes = Gerrit.getConfig().getDownloadSchemes();
+
+    if (allowAnonymous
+        && Gerrit.getConfig().getGitDaemonUrl() != null
+        && (allowedSchemes.contains(DownloadScheme.ANON_GIT) ||
+            allowedSchemes.contains(DownloadScheme.DEFAULT_DOWNLOADS))) {
+      urls.add(new DownloadUrlLink.AnonGitLink(project, ref));
+    }
+
+    if (allowAnonymous
+        && (allowedSchemes.contains(DownloadScheme.ANON_HTTP) ||
+            allowedSchemes.contains(DownloadScheme.DEFAULT_DOWNLOADS))) {
+      urls.add(new DownloadUrlLink.AnonHttpLink(project, ref));
+    }
+
+    if (Gerrit.getConfig().getSshdAddress() != null
+        && hasUserName()
+        && (allowedSchemes.contains(DownloadScheme.SSH) ||
+            allowedSchemes.contains(DownloadScheme.DEFAULT_DOWNLOADS))) {
+      urls.add(new DownloadUrlLink.SshLink(project, ref));
+    }
+
+    if ((hasUserName() || siteReliesOnHttp())
+        && (allowedSchemes.contains(DownloadScheme.HTTP)
+            || allowedSchemes.contains(DownloadScheme.DEFAULT_DOWNLOADS))) {
+      urls.add(new DownloadUrlLink.HttpLink(project, ref, allowAnonymous));
+    }
+    return urls;
+  }
+
+  private static boolean hasUserName() {
+    return Gerrit.isSignedIn()
+        && Gerrit.getUserAccount().getUserName() != null
+        && Gerrit.getUserAccount().getUserName().length() > 0;
+  }
+
+  protected DownloadScheme urlType;
+  protected String urlData;
+  protected String hostPageUrl = GWT.getHostPageBaseURL();
+
+  public DownloadUrlLink(DownloadScheme urlType, String text, String urlData) {
+    this(text);
+    this.urlType = urlType;
+    this.urlData = urlData;
+  }
+
+  public DownloadUrlLink(DownloadScheme urlType, String text) {
+    this(text);
+    this.urlType = urlType;
+  }
+
+  public DownloadUrlLink(String text) {
+    super(text);
+    setStyleName(Gerrit.RESOURCES.css().downloadLink());
+    Roles.getTabRole().set(getElement());
+    addClickHandler(this);
+
+    if (!hostPageUrl.endsWith("/")) {
+      hostPageUrl += "/";
+    }
+  }
+
+  public String getUrlData() {
+    return urlData;
+  }
+
+  @Override
+  public void onClick(ClickEvent event) {
+    event.preventDefault();
+    event.stopPropagation();
+
+    select();
+
+    if (Gerrit.isSignedIn()) {
+      // If the user is signed-in, remember this choice for future panels.
+      //
+      AccountGeneralPreferences pref =
+          Gerrit.getUserAccount().getGeneralPreferences();
+      pref.setDownloadUrl(urlType);
+      com.google.gerrit.client.account.Util.ACCOUNT_SVC.changePreferences(pref,
+          new AsyncCallback<VoidResult>() {
+            @Override
+            public void onFailure(Throwable caught) {
+            }
+
+            @Override
+            public void onSuccess(VoidResult result) {
+            }
+          });
+    }
+  }
+
+  void select() {
+    DownloadUrlPanel parent = (DownloadUrlPanel) getParent();
+    for (Widget w : parent) {
+      if (w != this && w instanceof DownloadUrlLink) {
+        w.removeStyleName(Gerrit.RESOURCES.css().downloadLink_Active());
+      }
+    }
+    parent.setCurrentUrl(this);
+    addStyleName(Gerrit.RESOURCES.css().downloadLink_Active());
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadUrlPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadUrlPanel.java
new file mode 100644
index 0000000..8d07498
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/DownloadUrlPanel.java
@@ -0,0 +1,70 @@
+// 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.download;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.reviewdb.client.AccountGeneralPreferences;
+import com.google.gwt.aria.client.Roles;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.Widget;
+
+import java.util.Collection;
+
+public class DownloadUrlPanel extends FlowPanel {
+  private final DownloadCommandPanel commandPanel;
+
+  public DownloadUrlPanel(final DownloadCommandPanel commandPanel) {
+    this.commandPanel = commandPanel;
+    setStyleName(Gerrit.RESOURCES.css().downloadLinkList());
+    Roles.getTablistRole().set(getElement());
+  }
+
+  public boolean isEmpty() {
+    return getWidgetCount() == 0;
+  }
+
+  public void select(AccountGeneralPreferences.DownloadScheme urlType) {
+    DownloadUrlLink first = null;
+
+    for (Widget w : this) {
+      if (w instanceof DownloadUrlLink) {
+        final DownloadUrlLink d = (DownloadUrlLink) w;
+        if (first == null) {
+          first = d;
+        }
+        if (d.urlType == urlType) {
+          d.select();
+          return;
+        }
+      }
+    }
+
+    // If none matched the requested type, select the first in the
+    // group as that will at least give us an initial baseline.
+    if (first != null) {
+      first.select();
+    }
+  }
+
+  void setCurrentUrl(DownloadUrlLink link) {
+    commandPanel.setCurrentUrl(link);
+  }
+
+  public void add(Collection<DownloadUrlLink> links) {
+    for (Widget link: links) {
+      add(link);
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/Util.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/Util.java
new file mode 100644
index 0000000..510361b
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/download/Util.java
@@ -0,0 +1,21 @@
+// 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.download;
+
+import com.google.gwt.core.client.GWT;
+
+public class Util {
+  public static final DownloadMessages M = GWT.create(DownloadMessages.class);
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/editText.png b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editText.png
new file mode 100644
index 0000000..188e1c1
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/editText.png
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/gerrit.css
index 7512d8c..9425443 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
@@ -13,31 +13,15 @@
  * limitations under the License.
  */
 
-@external .gwt-Button;
-@external .gwt-TabBar;
-@external .gwt-TabBarFirst;
-@external .gwt-TabBarItem;
-@external .gwt-TabBarItem-selected;
-@external .gwt-TabBarRest;
-@external .gwt-TabPanelBottom;
-@external .gwt-TextBox;
-@external .gwt-Hyperlink;
-@external .gwt-CheckBox;
-@external .gwt-DisclosurePanel, .header, .content;
-@external .gwt-InlineLabel;
-@external .gwt-InlineHyperlink;
-@external .gwt-RadioButton;
-
-@external .searchPanel;
-@external .smallHeading;
-@external .wdi;
-@external .wdd;
-@external .wdc;
+/**
+ * Make every single class external so users can rely on their names
+ */
+@external .*;
 
 @def black #000000;
 @def white #ffffff;
 @def norm-font  Arial Unicode MS, Arial, sans-serif;
-@def mono-font 'Lucida Console', 'Lucida Sans Typewriter', Monaco, monospace;
+@def mono-font  monospace;
 
 @eval backgroundColor com.google.gerrit.client.Gerrit.getTheme().backgroundColor;
 @eval topMenuColor com.google.gerrit.client.Gerrit.getTheme().topMenuColor;
@@ -52,28 +36,32 @@
   gwt-image: "greenCheck";
 }
 
+/** Override various GWT defaults */
 .gerritTopMenu {
   font-size: 9pt;
   padding-top: 5px;
   padding-left: 5px;
   padding-right: 5px;
-  background: topMenuColor;
-}
-.gerritTopMenu .gwt-TabBar .gwt-TabBarItem,
-.gerritTopMenu .gwt-TabBar .gwt-TabBarRest,
-.gerritTopMenu .gwt-TabBar .gwt-TabPanelBottom {
-  background: topMenuColor;
-}
-.gerritTopMenu .gwt-TabBar .gwt-TabBarItem-selected {
-  background: selectionColor;
+  background: transparent;
 }
 
 .gerritBody {
-  font-size: 11pt;
+  font-size: small;
   padding-left: 5px;
   padding-right: 5px;
 }
 
+a,
+a:visited {
+  color: #0654ac;
+  text-decoration: none;
+}
+
+a:hover {
+  color: #0654ac;
+  text-decoration: underline;
+}
+
 .version,
 .keyhelp {
   color: #a0adcc;
@@ -118,6 +106,9 @@
   padding: 0.2em 0.2em 0.2em 0.5em;
 }
 
+.link {
+  cursor: pointer;
+}
 
 /** MenuScreen **/
 .menuScreenMenuBar {
@@ -141,6 +132,9 @@
   background: selectionColor;
 }
 
+.menuItem.activeRow {
+  background: selectionColor;
+}
 
 /** CommentPanel **/
 .commentPanelBorder {
@@ -235,6 +229,7 @@
   font-size: 9pt;
   display: inline;
   white-space: nowrap;
+  padding-left: 6px;
 }
 .menuItem {
   padding-left: 5px;
@@ -253,33 +248,46 @@
 .topmenuTDglue {
   width: 100%;
 }
+
 .topmenuMenuLeft {
   width: 300px;
-  border-left: 1px solid topMenuColor;
-  border-right: 1px solid topMenuColor;
-  border-bottom: 1px solid topMenuColor;
+  font-size: 9pt;
+  padding-top: 5px;
+  padding-left: 5px;
+  padding-right: 5px;
+  background: none;
+  position: relative;
+  top: 0;
+}
+.topmenuMenuLeft tbody tr td table {
+  border: 0;
+}
+.topmenuMenuLeft tbody tr td table.gwt-TabBar {
+  border-bottom: 1px solid #DDD;
+}
+.topmenuMenuLeft .gwt-TextBox {
+  width: 250px;
+}
+.topmenuMenuLeft .gwt-Button {
+  padding: 3px 6px;
 }
 .topmenuMenuLeft .gwt-TabBarFirst {
   display: none;
 }
 .topmenuMenuLeft .gwt-TabBarItem {
   margin: 0px;
-  background: topMenuColor;
+  background: transparent;
   padding-top: 0px;
   padding-bottom: 1px;
   padding-left: 1em;
   padding-right: 1em;
-  border-right: 1px solid black;
-}
-.topmenuMenuLeft .gwt-TabBarItem-selected {
-  background: selectionColor;
 }
 .topmenuMenuLeft .gwt-TabBarRest {
-  background: topMenuColor;
+  background: transparent;
   padding-top: 0px;
 }
 .topmenuMenuLeft .gwt-TabPanelBottom {
-  background: topMenuColor;
+  background: transparent;
   border-top: none;
   border-left: none;
   border-right: none;
@@ -297,21 +305,37 @@
   text-align: right;
 }
 .menuBarUserName {
-  font-weight: bold;
   padding-left: 5px;
   padding-right: 5px;
   white-space: nowrap;
 }
+.menuBarUserNameAvatar {
+  vertical-align: middle;
+}
+.menuBarUserNameFocusPanel {
+  display: inline;
+}
+.menuBarUserNamePanel {
+  display: inline;
+  cursor: pointer;
+  font-weight: bold;
+}
+.userInfoPopup {
+  border: 1px solid black;
+  background: white;
+  box-shadow: 3px 3px 5px #888;
+}
 .searchPanel {
   white-space: nowrap;
+  display: inline;
 }
 .searchPanel .gwt-TextBox {
-  font-size: 7pt;
+  font-size: 9pt;
 }
 .searchPanel .gwt-Button {
-  font-size: 7pt;
+  font-size: 9pt;
   margin-left: 2px;
-  padding: 1px;
+  padding: 3px 6px;
 }
 
 /** RPC Status **/
@@ -323,6 +347,7 @@
 }
 
 .rpcStatus {
+  position: fixed;
   padding-top: 4px;
   padding-bottom: 4px;
   padding-left: 10px;
@@ -489,6 +514,12 @@
   padding-right: 5px;
   border-right: 1px solid trimColor;
   border-bottom: 1px solid trimColor;
+  vertical-align: middle;
+  height: 20px;
+}
+
+.changeTable a.gwt-InlineHyperlink {
+  color: #222 !important;
 }
 
 .accountDashboard.changeTable tr {
@@ -510,20 +541,13 @@
   background: selectionColor !important;
 }
 
-.changeTable .cID {
-  width: 3.5em;
-}
-.changeTable .dataCell.cID {
-  font-family: mono-font;
-}
-
 .changeTable .cSUBJECT div {
   text-overflow: ellipsis;
   overflow: hidden;
   white-space: nowrap;
 }
 
-.changeTable .cPROJECT {
+.changeTable .cOWNER {
   white-space: nowrap;
 }
 
@@ -601,6 +625,11 @@
   margin-right: 1em;
 }
 
+.reviewedPanelBottom {
+  float: right;
+  font-size: small;
+}
+
 
 /** PatchContentTable **/
 .patchContentTable {
@@ -626,6 +655,11 @@
 .patchContentTable .diffText {
   white-space: pre;
   padding-left: 0.2em;
+  border-left: thin solid #b0bdcc;
+}
+
+.patchContentTable .diffTextForBinaryInSideBySide {
+ width: 50%;
 }
 
 .patchContentTable .diffTextFileHeader {
@@ -655,13 +689,18 @@
 .patchContentTable tr.commentHolder .iconCell {
   background: white;
 }
+.patchContentTable tr.commentHolder .iconCellOfFileCommentRow {
+  background: trimColor;
+}
 .patchContentTable td.commentHolder {
   padding-left: 0;
   padding-right: 0;
   border-top: 1px solid black;
-  border-left: 1px solid black;
   border-right: 1px solid black;
 }
+.patchContentTable td.commentHolderLeftmost {
+  border-left: 1px solid black;
+}
 .patchContentTable td.commentHolder.commentPanelLast {
   border-bottom: 1px solid black;
 }
@@ -684,8 +723,8 @@
 .lineNumber {
   padding-left: 0.2em;
   white-space: pre;
-  width: 3.5em;
-  text-align: right;
+  width: 1.5em;
+  text-align: center;
   padding-right: 0.2em;
   background: white;
   border-bottom: 1px solid white;
@@ -693,6 +732,9 @@
 .lineNumber.rightmost {
   border-left: thin solid #b0bdcc;
 }
+.lineNumber.rightBorder {
+  border-right: thin solid #b0bdcc;
+}
 .lineNumber a {
   color: #888;
   text-decoration: none;
@@ -703,6 +745,9 @@
   font-weight: bold;
   text-align: center;
 }
+.patchContentTable td.fileColumnHeader.unifiedTableHeader {
+  text-align: left;
+}
 .lineNumber.fileColumnHeader {
   border-bottom: 1px solid trimColor;
 }
@@ -770,6 +815,11 @@
   text-decoration: underline;
 }
 
+.patchContentTable td.cellsNextToFileComment {
+  background: trimColor;
+  border-top: trimColor;
+  border-bottom: trimColor;
+}
 .patchContentTable .activeRow .iconCell,
 .patchContentTable .activeRow .lineNumber {
   background: selectionColor;
@@ -778,13 +828,22 @@
 .patchContentTable .activeRow .lineNumber,
 .patchContentTable .activeRow .fileLine,
 .patchContentTable .activeRow .diffText,
+.patchContentTable .activeRow td.commentHolder,
 .patchContentTable .activeRow .wdc,
 .patchContentTable .activeRow .wdd,
 .patchContentTable .activeRow .wdi,
+.patchContentTable .activeRow .iconCellOfFileCommentRow,
 .patchContentTable .activeRow td.commentHolder.commentPanelLast  {
   border-bottom: 1px solid blue;
 }
 
+.patchContentTable .fileCommentBorder .iconCellOfFileCommentRow,
+.patchContentTable .fileCommentBorder .lineNumber,
+.patchContentTable .fileCommentBorder .diffText {
+  height: 20px;
+  background: trimColor;
+  border-bottom: 1px solid trimColor;
+}
 
 /** Change **/
 .changeScreenStarIcon {
@@ -821,10 +880,11 @@
   margin-left: 10px;
 }
 
-.changeScreenDescription {
+.changeScreenDescription,
+.changeScreenDescription textarea {
   white-space: pre;
   font-family: mono-font;
-  font-size: 8pt;
+  font-size: 9pt;
 }
 .changeScreenDescription p {
   margin-top: 0px;
@@ -861,6 +921,14 @@
   border-right: 1px solid trimColor;
 }
 
+.sideBySideTableBinaryHeader {
+  border-right: thin solid #b0bdcc;
+  border-left:  thin solid #b0bdcc;
+  width: 100%;
+  color: grey;
+  font-weight: bold;
+}
+
 .infoTable td.approvalrole {
   width: 5em;
   border-left: none;
@@ -871,6 +939,9 @@
 .infoTable td.approvalscore {
   text-align: center;
 }
+.infoTable td.notVotable {
+  background: #F5F5F5;
+}
 .infoTable td.negscore {
   color: red;
 }
@@ -888,6 +959,14 @@
   margin-right: 15px;
 }
 
+.changeInfoTopicPanel img {
+  float: right;
+}
+
+.changeInfoTopicPanel a {
+  float: left;
+}
+
 .infoBlock {
   border-collapse: collapse;
   border-spacing: 0;
@@ -901,6 +980,11 @@
   white-space: nowrap;
 }
 
+.infoBlock td td {
+  padding-left: 0px;
+  border-right: 0px;
+}
+
 .infoBlock td.topmost {
   border-top: 1px solid trimColor;
 }
@@ -1044,6 +1128,13 @@
   padding-left: 0px;
 }
 
+/** UnifiedScreen **/
+.unifiedTable {
+  width: 100%;
+  border: 1px solid #B0BDCC;
+  display: table;
+}
+
 /** SideBySideScreen **/
 .sideBySideScreenSideBySideTable {
   width: 100%;
@@ -1068,6 +1159,8 @@
   margin-left: 1em;
   margin-right: 5em;
   font-weight: bold;
+  font-size: medium;
+  font-family: Arial Unicode;
 }
 
 /** Patch History Table **/
@@ -1143,12 +1236,27 @@
   color: grey;
 }
 
+.addBranch {
+  margin-top: 10px;
+  background-color: trimColor;
+  padding: 5px 5px 5px 5px;
+}
+
 .addSshKeyPanel {
   margin-top: 10px;
   background-color: trimColor;
   padding: 5px 5px 5px 5px;
 }
 
+.addSshKeyPanel ol {
+  margin-top: 0px;
+  margin-bottom: 5px;
+}
+
+.addSshKeyPanel td {
+  width: 100%;
+}
+
 .createGroupLink {
   margin-bottom: 10px;
 }
@@ -1238,10 +1346,10 @@
   width: 45em;
 }
 
-.projectAdminApprovalCategoryRangeLine {
+.projectAdminLabelRangeLine {
   white-space: nowrap;
 }
-.projectAdminApprovalCategoryValue {
+.projectAdminLabelValue {
   font-family: mono-font;
   font-size: small;
 }
@@ -1253,7 +1361,7 @@
   font-weight: bold;
   white-space: nowrap;
 }
-.publishCommentsScreen .approvalCategoryList {
+.publishCommentsScreen .labelList {
   margin-bottom: 10px;
   margin-left: 10px;
   background: trimColor;
@@ -1269,7 +1377,7 @@
 .publishCommentsScreen .coverMessage textarea {
   font-size: small;
 }
-.publishCommentsScreen .approvalCategoryList .gwt-RadioButton {
+.publishCommentsScreen .labelList .gwt-RadioButton {
   font-size: smaller;
 }
 .publishCommentsScreen .patchComments {
@@ -1357,9 +1465,6 @@
 .groupOwnerTextBox {
   margin-bottom: 2px;
 }
-.groupTypePanel {
-  margin-bottom: 3px;
-}
 .groupTypeSelectListBox {
   margin-bottom: 2px;
 }
@@ -1397,3 +1502,14 @@
 /** PluginListScreen **/
 .pluginsTable {
 }
+
+/** ProjectListScreen **/
+.projectFilterPanel {
+  margin-bottom: 10px;
+}
+.projectFilterPanel input {
+  width: 200px;
+}
+.projectFilterLabel {
+  margin-right: 5px;
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupApi.java
new file mode 100644
index 0000000..0b178f2
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupApi.java
@@ -0,0 +1,263 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.groups;
+
+import com.google.gerrit.client.VoidResult;
+import com.google.gerrit.client.account.AccountInfo;
+import com.google.gerrit.client.rpc.NativeString;
+import com.google.gerrit.client.rpc.Natives;
+import com.google.gerrit.client.rpc.RestApi;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+
+import java.util.Set;
+
+/**
+ * A collection of static methods which work on the Gerrit REST API for specific
+ * groups.
+ */
+public class GroupApi {
+  /** Create a new group */
+  public static void createGroup(String groupName, AsyncCallback<GroupInfo> cb) {
+    JavaScriptObject in = JavaScriptObject.createObject();
+    new RestApi("/groups/").id(groupName).ifNoneMatch().put(in, cb);
+  }
+
+  public static void getGroupDetail(String group, AsyncCallback<GroupInfo> cb) {
+    group(group).view("detail").get(cb);
+  }
+
+  /** Get the name of a group */
+  public static void getGroupName(AccountGroup.UUID group,
+      AsyncCallback<NativeString> cb) {
+    group(group).view("name").get(cb);
+  }
+
+  /** Check if the current user is owner of a group */
+  public static void isGroupOwner(String groupName, final AsyncCallback<Boolean> cb) {
+    GroupMap.myOwned(groupName, new AsyncCallback<GroupMap>() {
+      @Override
+      public void onSuccess(GroupMap result) {
+        cb.onSuccess(!result.isEmpty());
+      }
+      @Override
+      public void onFailure(Throwable caught) {
+        cb.onFailure(caught);
+      }
+    });
+  }
+
+  /** Rename a group */
+  public static void renameGroup(AccountGroup.UUID group,
+      String newName, AsyncCallback<VoidResult> cb) {
+    GroupInput in = GroupInput.create();
+    in.name(newName);
+    group(group).view("name").put(in, cb);
+  }
+
+  /** Set description for a group */
+  public static void setGroupDescription(AccountGroup.UUID group,
+      String description, AsyncCallback<VoidResult> cb) {
+    RestApi call = group(group).view("description");
+    if (description != null && !description.isEmpty()) {
+      GroupInput in = GroupInput.create();
+      in.description(description);
+      call.put(in, cb);
+    } else {
+      call.delete(cb);
+    }
+  }
+
+  /** Set owner for a group */
+  public static void setGroupOwner(AccountGroup.UUID group,
+      String owner, AsyncCallback<GroupInfo> cb) {
+    GroupInput in = GroupInput.create();
+    in.owner(owner);
+    group(group).view("owner").put(in, cb);
+  }
+
+  /** Set the options for a group */
+  public static void setGroupOptions(AccountGroup.UUID group,
+      boolean isVisibleToAll, AsyncCallback<VoidResult> cb) {
+    GroupOptionsInput in = GroupOptionsInput.create();
+    in.visibleToAll(isVisibleToAll);
+    group(group).view("options").put(in, cb);
+  }
+
+  /** Add member to a group. */
+  public static void addMember(AccountGroup.UUID group, String member,
+      AsyncCallback<AccountInfo> cb) {
+    members(group).id(member).put(cb);
+  }
+
+  /** Add members to a group. */
+  public static void addMembers(AccountGroup.UUID group,
+      Set<String> members,
+      final AsyncCallback<JsArray<AccountInfo>> cb) {
+    if (members.size() == 1) {
+      addMember(group,
+          members.iterator().next(),
+          new AsyncCallback<AccountInfo>() {
+            @Override
+            public void onSuccess(AccountInfo result) {
+              cb.onSuccess(Natives.arrayOf(result));
+            }
+
+            @Override
+            public void onFailure(Throwable caught) {
+              cb.onFailure(caught);
+            }
+          });
+    } else {
+      MemberInput input = MemberInput.create();
+      for (String member : members) {
+        input.add_member(member);
+      }
+      members(group).post(input, cb);
+    }
+  }
+
+  /** Remove members from a group. */
+  public static void removeMembers(AccountGroup.UUID group,
+      Set<Integer> ids, final AsyncCallback<VoidResult> cb) {
+    if (ids.size() == 1) {
+      members(group).id(ids.iterator().next().toString()).delete(cb);
+    } else {
+      MemberInput in = MemberInput.create();
+      for (Integer id : ids) {
+        in.add_member(id.toString());
+      }
+      group(group).view("members.delete").post(in, cb);
+    }
+  }
+
+  /** Include a group into a group. */
+  public static void addIncludedGroup(AccountGroup.UUID group, String include,
+      AsyncCallback<GroupInfo> cb) {
+    groups(group).id(include).put(cb);
+  }
+
+  /** Include groups into a group. */
+  public static void addIncludedGroups(AccountGroup.UUID group,
+      Set<String> includedGroups,
+      final AsyncCallback<JsArray<GroupInfo>> cb) {
+    if (includedGroups.size() == 1) {
+      addIncludedGroup(group,
+          includedGroups.iterator().next(),
+          new AsyncCallback<GroupInfo>() {
+            @Override
+            public void onSuccess(GroupInfo result) {
+              cb.onSuccess(Natives.arrayOf(result));
+            }
+
+            @Override
+            public void onFailure(Throwable caught) {
+              cb.onFailure(caught);
+            }
+          });
+    } else {
+      IncludedGroupInput input = IncludedGroupInput.create();
+      for (String includedGroup : includedGroups) {
+        input.add_group(includedGroup);
+      }
+      groups(group).post(input, cb);
+    }
+  }
+
+  /** Remove included groups from a group. */
+  public static void removeIncludedGroups(AccountGroup.UUID group,
+      Set<AccountGroup.UUID> ids, final AsyncCallback<VoidResult> cb) {
+    if (ids.size() == 1) {
+      AccountGroup.UUID g = ids.iterator().next();
+      groups(group).id(g.get()).delete(cb);
+    } else {
+      IncludedGroupInput in = IncludedGroupInput.create();
+      for (AccountGroup.UUID g : ids) {
+        in.add_group(g.get());
+      }
+      group(group).view("groups.delete").post(in, cb);
+    }
+  }
+
+  private static RestApi members(AccountGroup.UUID group) {
+    return group(group).view("members");
+  }
+
+  private static RestApi groups(AccountGroup.UUID group) {
+    return group(group).view("groups");
+  }
+
+  private static RestApi group(AccountGroup.UUID group) {
+    return group(group.get());
+  }
+
+  private static RestApi group(String group) {
+    return new RestApi("/groups/").id(group);
+  }
+
+  private static class GroupInput extends JavaScriptObject {
+    final native void description(String d) /*-{ if(d)this.description=d; }-*/;
+    final native void name(String n) /*-{ if(n)this.name=n; }-*/;
+    final native void owner(String o) /*-{ if(o)this.owner=o; }-*/;
+
+    static GroupInput create() {
+      return (GroupInput) createObject();
+    }
+
+    protected GroupInput() {
+    }
+  }
+
+  private static class GroupOptionsInput extends JavaScriptObject {
+    final native void visibleToAll(boolean v) /*-{ if(v)this.visible_to_all=v; }-*/;
+
+    static GroupOptionsInput create() {
+      return (GroupOptionsInput) createObject();
+    }
+
+    protected GroupOptionsInput() {
+    }
+  }
+
+  private static class MemberInput extends JavaScriptObject {
+    final native void init() /*-{ this.members = []; }-*/;
+    final native void add_member(String n) /*-{ this.members.push(n); }-*/;
+
+    static MemberInput create() {
+      MemberInput m = (MemberInput) createObject();
+      m.init();
+      return m;
+    }
+
+    protected MemberInput() {
+    }
+  }
+
+  private static class IncludedGroupInput extends JavaScriptObject {
+    final native void init() /*-{ this.groups = []; }-*/;
+    final native void add_group(String n) /*-{ this.groups.push(n); }-*/;
+
+    static IncludedGroupInput create() {
+      IncludedGroupInput g = (IncludedGroupInput) createObject();
+      g.init();
+      return g;
+    }
+
+    protected IncludedGroupInput() {
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupInfo.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupInfo.java
new file mode 100644
index 0000000..3b4abe5
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupInfo.java
@@ -0,0 +1,67 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.groups;
+
+import com.google.gerrit.client.account.AccountInfo;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwt.http.client.URL;
+
+public class GroupInfo extends JavaScriptObject {
+  public final AccountGroup.Id getGroupId() {
+    return new AccountGroup.Id(group_id());
+  }
+
+  public final AccountGroup.UUID getGroupUUID() {
+    return new AccountGroup.UUID(URL.decodePathSegment(id()));
+  }
+
+  public final native String id() /*-{ return this.id; }-*/;
+  public final native String name() /*-{ return this.name; }-*/;
+  public final native GroupOptionsInfo options() /*-{ return this.options; }-*/;
+  public final native String description() /*-{ return this.description; }-*/;
+  public final native String url() /*-{ return this.url; }-*/;
+  public final native String owner() /*-{ return this.owner; }-*/;
+  public final native void owner(String o) /*-{ if(o)this.owner=o; }-*/;
+  public final native JsArray<AccountInfo> members() /*-{ return this.members; }-*/;
+  public final native JsArray<GroupInfo> includes() /*-{ return this.includes; }-*/;
+
+  private final native int group_id() /*-{ return this.group_id; }-*/;
+  private final native String owner_id() /*-{ return this.owner_id; }-*/;
+  private final native void owner_id(String o) /*-{ if(o)this.owner_id=o; }-*/;
+
+  public final AccountGroup.UUID getOwnerUUID() {
+    String owner = owner_id();
+    if (owner != null) {
+        return new AccountGroup.UUID(URL.decodePathSegment(owner));
+    }
+    return null;
+  }
+
+  public final void setOwnerUUID(AccountGroup.UUID uuid) {
+    owner_id(URL.encodePathSegment(uuid.get()));
+  }
+
+  protected GroupInfo() {
+  }
+
+  public static class GroupOptionsInfo extends JavaScriptObject {
+    public final native boolean isVisibleToAll() /*-{ return this['visible_to_all'] ? true : false; }-*/;
+
+    protected GroupOptionsInfo() {
+    }
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupList.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupList.java
new file mode 100644
index 0000000..94e5e58
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupList.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.groups;
+
+import com.google.gerrit.client.rpc.RestApi;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+
+/** Groups available from {@code /groups/} or {@code /accounts/{id}/groups}. */
+public class GroupList extends JsArray<GroupInfo> {
+  public static void my(AsyncCallback<GroupList> callback) {
+    new RestApi("/accounts/self/groups").get(callback);
+  }
+
+  public static void included(AccountGroup.UUID group,
+      AsyncCallback<GroupList> callback) {
+    new RestApi("/groups/").id(group.get()).view("groups").get(callback);
+  }
+
+  protected GroupList() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupMap.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupMap.java
new file mode 100644
index 0000000..6ba00ae
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/GroupMap.java
@@ -0,0 +1,54 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.groups;
+
+import com.google.gerrit.client.rpc.NativeMap;
+import com.google.gerrit.client.rpc.RestApi;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+
+/** Groups available from {@code /groups/}. */
+public class GroupMap extends NativeMap<GroupInfo> {
+  public static void all(AsyncCallback<GroupMap> callback) {
+    groups().get(NativeMap.copyKeysIntoChildren(callback));
+  }
+
+  public static void match(String match, AsyncCallback<GroupMap> cb) {
+    if (match == null || "".equals(match)) {
+      all(cb);
+    } else {
+      groups().addParameter("m", match).get(NativeMap.copyKeysIntoChildren(cb));
+    }
+  }
+
+  public static void myOwned(AsyncCallback<GroupMap> cb) {
+    myOwnedGroups().get(NativeMap.copyKeysIntoChildren(cb));
+  }
+
+  public static void myOwned(String groupName, AsyncCallback<GroupMap> cb) {
+    myOwnedGroups().addParameter("q", groupName).get(
+        NativeMap.copyKeysIntoChildren(cb));
+  }
+
+  private static RestApi myOwnedGroups() {
+    return groups().addParameterTrue("owned");
+  }
+
+  private static RestApi groups() {
+    return new RestApi("/groups/");
+  }
+
+  protected GroupMap() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/MemberList.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/MemberList.java
new file mode 100644
index 0000000..d788a1d
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/groups/MemberList.java
@@ -0,0 +1,31 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.groups;
+
+import com.google.gerrit.client.account.AccountInfo;
+import com.google.gerrit.client.rpc.RestApi;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+
+public class MemberList extends JsArray<AccountInfo> {
+  public static void all(AccountGroup.UUID group,
+      AsyncCallback<MemberList> callback) {
+    new RestApi("/groups/").id(group.get()).view("members").get(callback);
+  }
+
+  protected MemberList() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/gwt_override.css b/gerrit-gwtui/src/main/java/com/google/gerrit/client/gwt_override.css
index ed0b32c..f9a8cc0 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/gwt_override.css
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/gwt_override.css
@@ -42,6 +42,12 @@
   white-space: nowrap;
 }
 
+.gwt-TabBar .gwt-TabBarItem,
+.gwt-TabBar .gwt-TabBarRest,
+.gwt-TabPanelBottom {
+  background: transparent;
+}
+
 .gwt-TabBar {
   border-bottom: 1px solid black;
 }
@@ -49,16 +55,20 @@
   display: none;
 }
 .gwt-TabBar .gwt-TabBarItem {
-  margin: 0px;
+  color: #353535;
+  margin: 0;
   background: trimColor;
   padding-top: 0.5em;
   padding-bottom: 1px;
   padding-left: 1em;
   padding-right: 1em;
-  border-right: 1px solid black;
+  border-bottom: 3px solid transparent;
+  border-right: 0;
 }
 .gwt-TabBar .gwt-TabBarItem-selected {
+  color: #990000;
   background: selectionColor;
+  border-bottom-color: #990000;
 }
 .gwt-TabBar .gwt-TabBarRest {
   background: trimColor;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/AbstractPatchContentTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/AbstractPatchContentTable.java
index 24a2ae5..8de3251 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
@@ -1,4 +1,4 @@
-// Copyright (C) 2008 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.
@@ -15,17 +15,19 @@
 package com.google.gerrit.client.patches;
 
 import com.google.gerrit.client.Dispatcher;
+import com.google.gerrit.client.FormatUtil;
 import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.account.AccountInfo;
 import com.google.gerrit.client.changes.PatchTable;
 import com.google.gerrit.client.changes.Util;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.ui.CommentPanel;
 import com.google.gerrit.client.ui.NavigationTable;
 import com.google.gerrit.client.ui.NeedsSignInKeyCommand;
-import com.google.gerrit.common.data.AccountInfo;
 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.common.data.PatchSetDetail;
 import com.google.gerrit.prettify.client.ClientSideFormatter;
 import com.google.gerrit.prettify.common.PrettyFormatter;
 import com.google.gerrit.prettify.common.SparseFileContent;
@@ -38,6 +40,8 @@
 import com.google.gwt.event.dom.client.BlurHandler;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.DoubleClickEvent;
+import com.google.gwt.event.dom.client.DoubleClickHandler;
 import com.google.gwt.event.dom.client.FocusEvent;
 import com.google.gwt.event.dom.client.FocusHandler;
 import com.google.gwt.event.dom.client.KeyCodes;
@@ -45,33 +49,44 @@
 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.Event;
 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.Image;
 import com.google.gwt.user.client.ui.UIObject;
 import com.google.gwt.user.client.ui.Widget;
 import com.google.gwtexpui.globalkey.client.GlobalKey;
 import com.google.gwtexpui.globalkey.client.KeyCommand;
 import com.google.gwtexpui.globalkey.client.KeyCommandSet;
+import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
+
+import org.eclipse.jgit.diff.Edit;
 
 import java.util.ArrayList;
 import java.util.List;
 
 public abstract class AbstractPatchContentTable extends NavigationTable<Object>
     implements CommentEditorContainer, FocusHandler, BlurHandler {
+  public static final int R_HEAD = 0;
+  static final short FILE_SIDE_A = (short) 0;
+  static final short FILE_SIDE_B = (short) 1;
   protected PatchTable fileList;
   protected AccountInfoCache accountCache = AccountInfoCache.empty();
   protected Patch.Key patchKey;
   protected PatchSet.Id idSideA;
   protected PatchSet.Id idSideB;
   protected boolean onlyOneHunk;
+  protected PatchSetSelectBox headerSideA;
+  protected PatchSetSelectBox headerSideB;
+  protected Image iconA;
+  protected Image iconB;
 
   private final KeyCommandSet keysComment;
   private HandlerRegistration regComment;
   private final KeyCommandSet keysOpenByEnter;
   private HandlerRegistration regOpenByEnter;
+  boolean isDisplayBinary;
 
   protected AbstractPatchContentTable() {
     keysNavigation.add(new PrevKeyCommand(0, 'k', PatchUtil.C.linePrev()));
@@ -105,6 +120,57 @@
     table.setStyleName(Gerrit.RESOURCES.css().patchContentTable());
   }
 
+  abstract void createFileCommentEditorOnSideA();
+
+  abstract void createFileCommentEditorOnSideB();
+
+  abstract PatchScreen.Type getPatchScreenType();
+
+  protected void initHeaders(PatchScript script, PatchSetDetail detail) {
+    PatchScreen.Type type = getPatchScreenType();
+    headerSideA = new PatchSetSelectBox(PatchSetSelectBox.Side.A, type);
+    headerSideA.display(detail, script, patchKey, idSideA, idSideB);
+    headerSideA.addDoubleClickHandler(new DoubleClickHandler() {
+      @Override
+      public void onDoubleClick(DoubleClickEvent event) {
+        if (headerSideA.isFileOrCommitMessage()) {
+          createFileCommentEditorOnSideA();
+        }
+      }
+    });
+    headerSideB = new PatchSetSelectBox(PatchSetSelectBox.Side.B, type);
+    headerSideB.display(detail, script, patchKey, idSideA, idSideB);
+    headerSideB.addDoubleClickHandler(new DoubleClickHandler() {
+      @Override
+      public void onDoubleClick(DoubleClickEvent event) {
+        if (headerSideB.isFileOrCommitMessage()) {
+          createFileCommentEditorOnSideB();
+        }
+      }
+    });
+
+    // Prepare icons.
+    iconA = new Image(Gerrit.RESOURCES.addFileComment());
+    iconA.setTitle(PatchUtil.C.addFileCommentToolTip());
+    iconA.addStyleName(Gerrit.RESOURCES.css().link());
+    iconA.addClickHandler(new ClickHandler() {
+      @Override
+      public void onClick(ClickEvent event) {
+        createFileCommentEditorOnSideA();
+      }
+    });
+    iconB = new Image(Gerrit.RESOURCES.addFileComment());
+    iconB.setTitle(PatchUtil.C.addFileCommentToolTip());
+    iconB.addStyleName(Gerrit.RESOURCES.css().link());
+    iconB.addClickHandler(new ClickHandler() {
+      @Override
+      public void onClick(ClickEvent event) {
+        createFileCommentEditorOnSideB();
+      }
+    });
+  }
+
+  @Override
   public void notifyDraftDelta(final int delta) {
     if (fileList != null) {
       fileList.notifyDraftDelta(patchKey, delta);
@@ -137,8 +203,8 @@
             p = p.getParent();
           }
 
-          if (Gerrit.RESOURCES.css().commentHolder().equals(
-              table.getCellFormatter().getStyleName(row - 1, cell))) {
+          if (table.getCellFormatter().getStyleName(row - 1, cell)
+              .contains(Gerrit.RESOURCES.css().commentHolder())) {
             table.getCellFormatter().addStyleName(row - 1, cell,
                 Gerrit.RESOURCES.css().commentPanelLast());
           }
@@ -167,12 +233,47 @@
   }
 
   public void display(final Patch.Key k, final PatchSet.Id a,
-      final PatchSet.Id b, final PatchScript s) {
+      final PatchSet.Id b, final PatchScript s, final PatchSetDetail d) {
     patchKey = k;
     idSideA = a;
     idSideB = b;
 
-    render(s);
+    render(s, d);
+  }
+
+  protected boolean hasDifferences(PatchScript script) {
+    return hasEdits(script) || hasMeta(script);
+  }
+
+  public boolean isPureMetaChange(PatchScript script) {
+    return !hasEdits(script) && hasMeta(script);
+  }
+
+  // True if there are differences between the two patch sets
+  private boolean hasEdits(PatchScript script) {
+    for (Edit e : script.getEdits()) {
+      if (e.getType() != Edit.Type.EMPTY) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  // True if this change is a mode change or a pure rename/copy
+  private boolean hasMeta(PatchScript script) {
+    return !script.getPatchHeader().isEmpty();
+  }
+
+  protected void appendNoDifferences(SafeHtmlBuilder m) {
+    m.openTr();
+    m.openTd();
+    m.setAttribute("colspan", 5);
+    m.openDiv();
+    m.addStyleName(Gerrit.RESOURCES.css().patchNoDifference());
+    m.append(PatchUtil.C.noDifference());
+    m.closeDiv();
+    m.closeTd();
+    m.closeTr();
   }
 
   protected SparseHtmlFile getSparseHtmlFileA(PatchScript s) {
@@ -191,32 +292,27 @@
   protected SparseHtmlFile getSparseHtmlFileB(PatchScript s) {
     AccountDiffPreference dp = new AccountDiffPreference(s.getDiffPrefs());
 
+    SparseFileContent b = s.getB();
     PrettyFormatter f = ClientSideFormatter.FACTORY.get();
     f.setDiffPrefs(dp);
-    f.setFileName(s.getB().getPath());
+    f.setFileName(b.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());
+    if (s.getA().isWholeFile() && !b.isWholeFile()) {
+      b = b.apply(s.getA(), s.getEdits());
     }
+    f.format(b);
     return f;
   }
 
-  protected abstract void render(PatchScript script);
+  protected abstract void render(PatchScript script, final PatchSetDetail detail);
 
   protected abstract void onInsertComment(PatchLine pl);
 
   public abstract void display(CommentDetail comments, boolean expandComments);
 
   @Override
-  protected MyFlexTable createFlexTable() {
-    return new DoubleClickFlexTable();
-  }
-
-  @Override
   protected Object getRowItemKey(final Object item) {
     return null;
   }
@@ -240,6 +336,8 @@
         case INSERT:
         case REPLACE:
           return true;
+        case CONTEXT:
+          break;
       }
     } else if (o instanceof CommentList) {
       return true;
@@ -365,12 +463,6 @@
     return getRowItem(row) instanceof CommentList;
   }
 
-  /** 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
@@ -379,7 +471,7 @@
   protected void createCommentEditor(final int suggestRow, final int column,
       final int line, final short file) {
     if (Gerrit.isSignedIn()) {
-      if (1 <= line) {
+      if (R_HEAD <= line) {
         final Patch.Key parentKey;
         final short side;
         switch (file) {
@@ -406,15 +498,21 @@
         newComment.setSide(side);
         newComment.setMessage("");
 
-        createCommentEditor(suggestRow, column, newComment).setFocus(true);
+        findOrCreateCommentEditor(suggestRow, column, newComment, true)
+            .setFocus(true);
       }
     } else {
       Gerrit.doSignIn(History.getToken());
     }
   }
 
-  private CommentEditorPanel createCommentEditor(final int suggestRow,
-      final int column, final PatchLineComment newComment) {
+  protected void updateCursor(final PatchLineComment newComment) {
+  }
+
+  abstract void insertFileCommentRow(final int row);
+
+  private CommentEditorPanel findOrCreateCommentEditor(final int suggestRow,
+      final int column, final PatchLineComment newComment, final boolean create) {
     int row = suggestRow;
     int spans[] = new int[column + 1];
     FIND_ROW: while (row < table.getRowCount()) {
@@ -427,7 +525,9 @@
         spans[col] = table.getFlexCellFormatter().getRowSpan(row, cell);
         if (col == column) {
           final Widget w = table.getWidget(row, cell);
-          if (w instanceof CommentEditorPanel) {
+          if (w instanceof CommentEditorPanel
+              && ((CommentEditorPanel) w).getComment().getKey().getParentKey()
+                  .equals(newComment.getKey().getParentKey())) {
             // Don't insert two editors on the same position, it doesn't make
             // any sense to the user.
             //
@@ -441,6 +541,7 @@
               break FIND_ROW;
             }
             row++;
+            cell--;
           } else {
             break FIND_ROW;
           }
@@ -448,7 +549,7 @@
       }
     }
 
-    if (newComment == null) {
+    if (newComment == null || !create) {
       return null;
     }
 
@@ -469,7 +570,11 @@
       }
     }
     if (needInsert || !isCommentRow) {
-      insertRow(row);
+      if (newComment.getLine() == R_HEAD) {
+        insertFileCommentRow(row);
+      } else {
+        insertRow(row);
+      }
       styleCommentRow(row);
     }
     table.setWidget(row, column, ed);
@@ -507,6 +612,7 @@
       }
     }
 
+    updateCursor(newComment);
     return ed;
   }
 
@@ -543,24 +649,40 @@
       }
     }
     if (removeRow) {
-      for (int r = row - 1; 0 <= r; r--) {
-        boolean data = false;
-        for (int c = 0; c < table.getCellCount(r); c++) {
-          data |= table.getWidget(r, c) != null;
-          final int s = table.getFlexCellFormatter().getRowSpan(r, c) - 1;
-          if (r + s == row) {
-            table.getFlexCellFormatter().setRowSpan(r, c, s);
-          }
-        }
-        if (!data) {
-          break;
+      destroyCommentRow(row);
+    } else {
+      destroyComment(row, col, span);
+    }
+  }
+
+  protected void destroyCommentRow(int row) {
+    for (int r = row - 1; 0 <= r; r--) {
+      boolean data = false;
+      for (int c = 0; c < table.getCellCount(r); c++) {
+        data |= table.getWidget(r, c) != null;
+        final int s = table.getFlexCellFormatter().getRowSpan(r, c) - 1;
+        if (r + s == row) {
+          table.getFlexCellFormatter().setRowSpan(r, c, s);
         }
       }
-      table.removeRow(row);
-    } else if (span != 1) {
+      if (!data) {
+        break;
+      }
+    }
+    table.removeRow(row);
+  }
+
+  private void destroyComment(int row, int col, int span) {
+    table.getFlexCellFormatter().setStyleName(//
+        row, col, Gerrit.RESOURCES.css().diffText());
+
+    if (span != 1) {
       table.getFlexCellFormatter().setRowSpan(row, col, 1);
       for (int r = row + 1; r < row + span; r++) {
-        table.insertCell(r, col + 1);
+        table.insertCell(r, col);
+
+        table.getFlexCellFormatter().setStyleName(//
+            r, col, Gerrit.RESOURCES.css().diffText());
       }
     }
   }
@@ -575,7 +697,7 @@
       styleLastCommentCell(row, col);
 
     } else {
-      final AccountInfo author = accountCache.get(line.getAuthor());
+      final AccountInfo author = FormatUtil.asInfo(accountCache.get(line.getAuthor()));
       final PublishedCommentPanel panel =
           new PublishedCommentPanel(author, line);
       panel.setOpen(expandComment);
@@ -632,6 +754,9 @@
         Gerrit.RESOURCES.css().commentPanelLast());
     fmt.setStyleName(row, col, Gerrit.RESOURCES.css().commentHolder());
     fmt.addStyleName(row, col, Gerrit.RESOURCES.css().commentPanelLast());
+    if (!fmt.getStyleName(row, col - 1).contains(Gerrit.RESOURCES.css().commentHolder())) {
+      fmt.addStyleName(row, col, Gerrit.RESOURCES.css().commentHolderLeftmost());
+    }
   }
 
   protected static class CommentList {
@@ -640,42 +765,6 @@
         new ArrayList<PublishedCommentPanel>();
   }
 
-  protected class DoubleClickFlexTable extends MyFlexTable {
-    public DoubleClickFlexTable() {
-      sinkEvents(Event.ONDBLCLICK | Event.ONCLICK);
-    }
-
-    @Override
-    public void onBrowserEvent(final Event event) {
-      switch (DOM.eventGetType(event)) {
-        case Event.ONCLICK: {
-          // Find out which cell was actually clicked.
-          final Element td = getEventTargetCell(event);
-          if (td == null) {
-            break;
-          }
-          final int row = rowOf(td);
-          if (getRowItem(row) != null) {
-            movePointerTo(row);
-            onCellSingleClick(rowOf(td), columnOf(td));
-            return;
-          }
-          break;
-        }
-        case Event.ONDBLCLICK: {
-          // Find out which cell was actually clicked.
-          Element td = getEventTargetCell(event);
-          if (td == null) {
-            return;
-          }
-          onCellDoubleClick(rowOf(td), columnOf(td));
-          return;
-        }
-      }
-      super.onBrowserEvent(event);
-    }
-  }
-
   public static class NoOpKeyCommand extends NeedsSignInKeyCommand {
     public NoOpKeyCommand(int mask, int key, String help) {
       super(mask, key, help);
@@ -804,22 +893,22 @@
     private void createReplyEditor() {
       final PatchLineComment newComment = newComment();
       newComment.setMessage("");
-      createEditor(newComment).setFocus(true);
+      findOrCreateEditor(newComment, true).setFocus(true);
     }
 
     private void cannedReply(String message) {
-      CommentEditorPanel p = createEditor(null);
+      final PatchLineComment newComment = newComment();
+      newComment.setMessage(message);
+      CommentEditorPanel p = findOrCreateEditor(newComment, false);
       if (p == null) {
-        final PatchLineComment newComment = newComment();
-        newComment.setMessage(message);
-
         enableButtons(false);
         PatchUtil.DETAIL_SVC.saveDraft(newComment,
             new GerritCallback<PatchLineComment>() {
+              @Override
               public void onSuccess(final PatchLineComment result) {
                 enableButtons(true);
                 notifyDraftDelta(1);
-                createEditor(result).setOpen(false);
+                findOrCreateEditor(result, true).setOpen(false);
               }
 
               @Override
@@ -836,10 +925,11 @@
       }
     }
 
-    private CommentEditorPanel createEditor(final PatchLineComment newComment) {
+    private CommentEditorPanel findOrCreateEditor(
+        PatchLineComment newComment, boolean create) {
       int row = rowOf(getElement());
       int column = columnOf(getElement());
-      return createCommentEditor(row + 1, column, newComment);
+      return findOrCreateCommentEditor(row + 1, column, newComment, create);
     }
 
     private PatchLineComment newComment() {
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 b0d825b..b609f15 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
@@ -198,6 +198,10 @@
     return comment.getKey().get() == null;
   }
 
+  public PatchLineComment getComment() {
+    return comment;
+  }
+
   @Override
   public void onDoubleClick(final DoubleClickEvent event) {
     edit();
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 4801e65..8c9c56b 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
@@ -36,6 +36,7 @@
   String patchHistoryTitle();
   String disabledOnLargeFiles();
   String intralineFailure();
+  String intralineTimeout();
   String illegalNumberOfColumns();
 
   String upToChange();
@@ -67,6 +68,8 @@
   String reviewedAnd();
   String next();
   String download();
+  String addFileCommentToolTip();
+  String addFileCommentByDoubleClick();
 
   String buttonReplyDone();
   String cannedReplyDone();
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 11823ac..5acdb5f 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
@@ -18,6 +18,7 @@
 patchSet = Patch Set
 disabledOnLargeFiles = Disabled on very large source files.
 intralineFailure = Intraline difference not available due to server error.
+intralineTimeout = Intraline difference not available due to timeout.
 illegalNumberOfColumns = The number of columns cannot be zero or negative
 
 upToChange = Up to change
@@ -49,6 +50,8 @@
 reviewedAnd = Reviewed &
 next = next
 download = Download
+addFileCommentToolTip = Click to add file comment
+addFileCommentByDoubleClick = Double click to add file comment
 
 fileTypeSymlink = Type: Symbolic Link
 fileTypeGitlink = Type: Git Commit in Subproject
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 4f8731c..676cea6 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScreen.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScreen.java
@@ -20,12 +20,9 @@
 import com.google.gerrit.client.RpcStatus;
 import com.google.gerrit.client.changes.CommitMessageBlock;
 import com.google.gerrit.client.changes.PatchTable;
-import com.google.gerrit.client.changes.PatchTable.PatchValidator;
 import com.google.gerrit.client.changes.Util;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.client.rpc.ScreenLoadCallback;
-import com.google.gerrit.client.ui.ChangeLink;
-import com.google.gerrit.client.ui.InlineHyperlink;
 import com.google.gerrit.client.ui.ListenableAccountDiffPreference;
 import com.google.gerrit.client.ui.Screen;
 import com.google.gerrit.common.data.PatchScript;
@@ -37,23 +34,14 @@
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gwt.core.client.Scheduler;
 import com.google.gwt.core.client.Scheduler.ScheduledCommand;
-import com.google.gwt.event.dom.client.ClickEvent;
-import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.event.dom.client.KeyPressEvent;
 import com.google.gwt.event.logical.shared.ValueChangeEvent;
 import com.google.gwt.event.logical.shared.ValueChangeHandler;
 import com.google.gwt.event.shared.HandlerRegistration;
-import com.google.gwtjsonrpc.common.AsyncCallback;
-import com.google.gwt.user.client.ui.Anchor;
-import com.google.gwt.user.client.ui.CheckBox;
 import com.google.gwt.user.client.ui.FlowPanel;
-import com.google.gwt.user.client.ui.Label;
 import com.google.gwtexpui.globalkey.client.GlobalKey;
 import com.google.gwtexpui.globalkey.client.KeyCommand;
 import com.google.gwtexpui.globalkey.client.KeyCommandSet;
-import com.google.gwtexpui.safehtml.client.SafeHtml;
-import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
-import com.google.gwtjsonrpc.common.VoidResult;
 
 public abstract class PatchScreen extends Screen implements
     CommentEditorContainer {
@@ -110,14 +98,10 @@
   protected PatchScriptSettingsPanel settingsPanel;
   protected TopView topView;
 
-  private CheckBox reviewedCheckBox;
-  private FlowPanel reviewedPanel;
-  private InlineHyperlink reviewedLink;
+  private ReviewedPanels reviewedPanels;
   private HistoryTable historyTable;
   private FlowPanel topPanel;
   private FlowPanel contentPanel;
-  private PatchTableHeader header;
-  private Label noDifference;
   private AbstractPatchContentTable contentTable;
   private CommitMessageBlock commitMessageBlock;
   private NavLinks topNav;
@@ -129,6 +113,7 @@
   /** The index of the file we are currently looking at among the fileList */
   private int patchIndex;
   private ListenableAccountDiffPreference prefs;
+  private HandlerRegistration prefsHandler;
 
   /** Keys that cause an action on this screen */
   private KeyCommandSet keysNavigation;
@@ -136,6 +121,7 @@
   private HandlerRegistration regNavigation;
   private HandlerRegistration regAction;
   private boolean intralineFailure;
+  private boolean intralineTimeout;
 
   /**
    * How this patch should be displayed in the patch screen.
@@ -156,76 +142,16 @@
     idSideB = id.getParentKey();
     this.patchIndex = patchIndex;
 
-    prefs = fileList != null ? fileList.getPreferences() :
-                               new ListenableAccountDiffPreference();
+    prefs = fileList != null
+        ? fileList.getPreferences()
+        : new ListenableAccountDiffPreference();
     if (Gerrit.isSignedIn()) {
       prefs.reset();
     }
-    prefs.addValueChangeHandler(
-        new ValueChangeHandler<AccountDiffPreference>() {
-          @Override
-          public void onValueChange(ValueChangeEvent<AccountDiffPreference> event) {
-            update(event.getValue());
-          }
-        });
-
-    reviewedPanel = new FlowPanel();
+    reviewedPanels = new ReviewedPanels();
     settingsPanel = new PatchScriptSettingsPanel(prefs);
   }
 
-  private void populateReviewedPanel(){
-    reviewedPanel.clear();
-
-    reviewedCheckBox = new CheckBox(PatchUtil.C.reviewedAnd() + " ");
-    reviewedCheckBox.addValueChangeHandler(new ValueChangeHandler<Boolean>() {
-      @Override
-      public void onValueChange(ValueChangeEvent<Boolean> event) {
-        setReviewedByCurrentUser(event.getValue());
-      }
-    });
-
-    reviewedPanel.add(reviewedCheckBox);
-    reviewedPanel.add(getReviewedAnchor());
-  }
-
-  private Anchor getReviewedAnchor() {
-    SafeHtmlBuilder text = new SafeHtmlBuilder();
-    text.append(PatchUtil.C.next());
-    text.append(SafeHtml.asis(Util.C.nextPatchLinkIcon()));
-
-    Anchor reviewedAnchor = new Anchor("");
-    SafeHtml.set(reviewedAnchor, text);
-
-    final PatchValidator unreviewedValidator = new PatchValidator() {
-      public boolean isValid(Patch patch) {
-        return !patch.isReviewedByCurrentUser();
-      }
-    };
-
-    int nextUnreviewedPatchIndex =
-        fileList.getNextPatch(patchIndex, true, unreviewedValidator,
-            fileList.PREFERENCE_VALIDATOR);
-
-    if (nextUnreviewedPatchIndex > -1) {
-      // Create invisible patch link to change page
-      reviewedLink =
-          fileList.createLink(nextUnreviewedPatchIndex, getPatchScreenType(),
-              null, null);
-      reviewedLink.setText("");
-    } else {
-      reviewedLink = new ChangeLink("", patchKey.getParentKey());
-    }
-    reviewedAnchor.addClickHandler(new ClickHandler() {
-      @Override
-      public void onClick(ClickEvent event) {
-        setReviewedByCurrentUser(true);
-        reviewedLink.go();
-      }
-    });
-
-    return reviewedAnchor;
-  }
-
   @Override
   public void notifyDraftDelta(int delta) {
     lastScript = null;
@@ -238,10 +164,10 @@
 
   private void update(AccountDiffPreference dp) {
     // Did the user just turn on auto-review?
-    if (!reviewedCheckBox.getValue() && prefs.getOld().isManualReview()
+    if (!reviewedPanels.getValue() && prefs.getOld().isManualReview()
         && !dp.isManualReview()) {
-      reviewedCheckBox.setValue(true);
-      setReviewedByCurrentUser(true);
+      reviewedPanels.setValue(true);
+      reviewedPanels.setReviewedByCurrentUser(true);
     }
 
     if (lastScript != null && canReuse(dp, lastScript)) {
@@ -294,7 +220,7 @@
     super.onInitUI();
 
     if (Gerrit.isSignedIn()) {
-      setTitleFarEast(reviewedPanel);
+      setTitleFarEast(reviewedPanels.top);
     }
 
     keysNavigation = new KeyCommandSet(Gerrit.C.sectionNavigation());
@@ -316,12 +242,6 @@
     topPanel = new FlowPanel();
     add(topPanel);
 
-    header = new PatchTableHeader(getPatchScreenType());
-
-    noDifference = new Label(PatchUtil.C.noDifference());
-    noDifference.setStyleName(Gerrit.RESOURCES.css().patchNoDifference());
-    noDifference.setVisible(false);
-
     contentTable = createContentTable();
     contentTable.fileList = fileList;
 
@@ -330,13 +250,19 @@
 
     add(topNav);
     contentPanel = new FlowPanel();
-    contentPanel.setStyleName(Gerrit.RESOURCES.css()
-        .sideBySideScreenSideBySideTable());
-    contentPanel.add(header);
-    contentPanel.add(noDifference);
+    if (getPatchScreenType() == PatchScreen.Type.SIDE_BY_SIDE) {
+      contentPanel.setStyleName(//
+          Gerrit.RESOURCES.css().sideBySideScreenSideBySideTable());
+    } else {
+      contentPanel.setStyleName(Gerrit.RESOURCES.css().unifiedTable());
+    }
+
     contentPanel.add(contentTable);
     add(contentPanel);
     add(bottomNav);
+    if (Gerrit.isSignedIn()) {
+      add(reviewedPanels.bottom);
+    }
 
     if (fileList != null) {
       topNav.display(patchIndex, getPatchScreenType(), fileList);
@@ -344,25 +270,6 @@
     }
   }
 
-  void setReviewedByCurrentUser(boolean reviewed) {
-    if (fileList != null) {
-      fileList.updateReviewedStatus(patchKey, reviewed);
-    }
-
-    PatchUtil.DETAIL_SVC.setReviewedByCurrentUser(patchKey, reviewed,
-        new AsyncCallback<VoidResult>() {
-          @Override
-          public void onFailure(Throwable arg0) {
-            // nop
-          }
-
-          @Override
-          public void onSuccess(VoidResult result) {
-            // nop
-          }
-        });
-  }
-
   @Override
   protected void onLoad() {
     super.onLoad();
@@ -388,6 +295,10 @@
 
   @Override
   protected void onUnload() {
+    if (prefsHandler != null) {
+      prefsHandler.removeHandler();
+      prefsHandler = null;
+    }
     if (regNavigation != null) {
       regNavigation.removeHandler();
       regNavigation = null;
@@ -449,7 +360,7 @@
     final int rpcseq = ++rpcSequence;
     lastScript = null;
     settingsPanel.setEnabled(false);
-    populateReviewedPanel();
+    reviewedPanels.populate(patchKey, fileList, patchIndex, getPatchScreenType());
     if (isFirst && fileList != null) {
       fileList.movePointerTo(patchKey);
     }
@@ -500,18 +411,22 @@
 
     historyTable.display(script.getHistory());
 
-    // True if there are differences between the two patch sets
-    boolean hasEdits = !script.getEdits().isEmpty();
-    // True if this change is a mode change or a pure rename/copy
-    boolean hasMeta = !script.getPatchHeader().isEmpty();
+    for (Patch p : patchSetDetail.getPatches()) {
+      if (p.getKey().equals(patchKey)) {
+        if (p.getPatchType().equals(Patch.PatchType.BINARY)) {
+          contentTable.isDisplayBinary = true;
+        }
+        break;
+      }
+    }
 
-    boolean hasDifferences = hasEdits || hasMeta;
-    boolean pureMetaChange = !hasEdits && hasMeta;
-
-    if (contentTable instanceof SideBySideTable && pureMetaChange) {
+    if (contentTable instanceof SideBySideTable
+        && contentTable.isPureMetaChange(script)
+        && !contentTable.isDisplayBinary) {
       // User asked for SideBySide (or a link guessed, wrong) and we can't
-      // show a binary or pure-rename change there accurately. Switch to
-      // the unified view instead.
+      // show a pure-rename change there accurately. Switch to
+      // the unified view instead. User can set file comments on binary file
+      // in SideBySide view.
       //
       contentTable.removeFromParent();
       contentTable = new UnifiedDiffTable();
@@ -520,14 +435,11 @@
       setToken(Dispatcher.toPatchUnified(idSideA, patchKey));
     }
 
-    header.display(patchSetDetail, script, patchKey, idSideA, idSideB);
+    contentTable.display(patchKey, idSideA, idSideB, script, patchSetDetail);
+    contentTable.display(script.getCommentDetail(), script.isExpandAllComments());
+    contentTable.finishDisplay();
+    contentTable.setRegisterKeys(isCurrentView());
 
-    if (hasDifferences) {
-      contentTable.display(patchKey, idSideA, idSideB, script);
-      contentTable.display(script.getCommentDetail(), script.isExpandAllComments());
-      contentTable.finishDisplay();
-    }
-    showPatch(hasDifferences);
     settingsPanel.setEnableSmallFileFeatures(!script.isHugeFile());
     settingsPanel.setEnableIntralineDifference(script.hasIntralineDifference());
     settingsPanel.setEnabled(true);
@@ -542,7 +454,7 @@
       boolean isReviewed = false;
       if (isFirst && !prefs.get().isManualReview()) {
         isReviewed = true;
-        setReviewedByCurrentUser(isReviewed);
+        reviewedPanels.setReviewedByCurrentUser(isReviewed);
       } else {
         for (Patch p : patchSetDetail.getPatches()) {
           if (p.getKey().equals(patchKey)) {
@@ -551,30 +463,37 @@
           }
         }
       }
-      reviewedCheckBox.setValue(isReviewed);
+      reviewedPanels.setValue(isReviewed);
     }
 
     intralineFailure = isFirst && script.hasIntralineFailure();
+    intralineTimeout = isFirst && script.hasIntralineTimeout();
   }
 
   @Override
   public void onShowView() {
     super.onShowView();
+    if (prefsHandler == null) {
+      prefsHandler = prefs.addValueChangeHandler(
+          new ValueChangeHandler<AccountDiffPreference>() {
+            @Override
+            public void onValueChange(ValueChangeEvent<AccountDiffPreference> event) {
+              update(event.getValue());
+            }
+          });
+    }
     if (intralineFailure) {
       intralineFailure = false;
       new ErrorDialog(PatchUtil.C.intralineFailure()).show();
+    } else if (intralineTimeout) {
+      intralineTimeout = false;
+      new ErrorDialog(PatchUtil.C.intralineTimeout()).show();
     }
     if (topView != null && prefs.get().isRetainHeader()) {
       setTopView(topView);
     }
   }
 
-  private void showPatch(final boolean showPatch) {
-    noDifference.setVisible(!showPatch);
-    contentTable.setVisible(showPatch);
-    contentTable.setRegisterKeys(isCurrentView() && showPatch);
-  }
-
   public void setTopView(TopView tv) {
     topView = tv;
     topPanel.clear();
@@ -587,6 +506,8 @@
         break;
       case FILES:       topPanel.add(fileList);
         break;
+      case MAIN:
+        break;
     }
   }
 
@@ -621,9 +542,9 @@
 
     @Override
     public void onKeyPress(final KeyPressEvent event) {
-      final boolean isReviewed = !reviewedCheckBox.getValue();
-      reviewedCheckBox.setValue(isReviewed);
-      setReviewedByCurrentUser(isReviewed);
+      final boolean isReviewed = !reviewedPanels.getValue();
+      reviewedPanels.setValue(isReviewed);
+      reviewedPanels.setReviewedByCurrentUser(isReviewed);
     }
   }
 
@@ -634,10 +555,7 @@
 
     @Override
     public void onKeyPress(final KeyPressEvent event) {
-      if (reviewedLink != null) {
-        setReviewedByCurrentUser(true);
-        reviewedLink.go();
-      }
+      reviewedPanels.go();
     }
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchScriptSettingsPanel.java
index a689259..8e76e47 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
@@ -27,10 +27,6 @@
 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.uibinder.client.UiBinder;
 import com.google.gwt.uibinder.client.UiField;
 import com.google.gwt.uibinder.client.UiHandler;
@@ -43,14 +39,13 @@
 import com.google.gwt.user.client.ui.Widget;
 import com.google.gwtjsonrpc.common.VoidResult;
 
-public class PatchScriptSettingsPanel extends Composite implements
-    HasValueChangeHandlers<AccountDiffPreference> {
+public class PatchScriptSettingsPanel extends Composite {
   private static MyUiBinder uiBinder = GWT.create(MyUiBinder.class);
 
   interface MyUiBinder extends UiBinder<Widget, PatchScriptSettingsPanel> {
   }
 
-  private ListenableAccountDiffPreference listenablePrefs;
+  private final ListenableAccountDiffPreference listenablePrefs;
   private boolean enableIntralineDifference = true;
   private boolean enableSmallFileFeatures = true;
 
@@ -139,12 +134,6 @@
     display();
   }
 
-  @Override
-  public HandlerRegistration addValueChangeHandler(
-      ValueChangeHandler<AccountDiffPreference> handler) {
-    return super.addHandler(handler, ValueChangeEvent.getType());
-  }
-
   public void setEnabled(final boolean on) {
     if (on) {
       setEnabledCounter++;
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchSetSelectBox.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchSetSelectBox.java
index afaf7fd..df12b70 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchSetSelectBox.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchSetSelectBox.java
@@ -21,18 +21,20 @@
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gwt.core.client.GWT;
-import com.google.gwt.dom.client.DivElement;
-import com.google.gwt.dom.client.Style.Display;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
+import com.google.gwt.event.dom.client.DoubleClickEvent;
+import com.google.gwt.event.dom.client.DoubleClickHandler;
 import com.google.gwt.resources.client.CssResource;
 import com.google.gwt.uibinder.client.UiBinder;
 import com.google.gwt.uibinder.client.UiField;
 import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Event;
 import com.google.gwt.user.client.ui.Anchor;
 import com.google.gwt.user.client.ui.Composite;
 import com.google.gwt.user.client.ui.HTMLPanel;
 import com.google.gwt.user.client.ui.Image;
+import com.google.gwt.user.client.ui.Label;
 import com.google.gwtorm.client.KeyUtil;
 
 import java.util.HashMap;
@@ -49,7 +51,9 @@
 
     String hidden();
 
-    String downloadLink();
+    String sideMarker();
+
+    String patchSetLabel();
   }
 
   public enum Side {
@@ -64,6 +68,7 @@
   Side side;
   PatchScreen.Type screenType;
   Map<Integer, Anchor> links;
+  private Label patchSet;
 
   @UiField
   HTMLPanel linkPanel;
@@ -71,9 +76,6 @@
   @UiField
   BoxStyle style;
 
-  @UiField
-  DivElement sideMarker;
-
   public PatchSetSelectBox(Side side, final PatchScreen.Type type) {
     this.side = side;
     this.screenType = type;
@@ -81,8 +83,8 @@
     initWidget(uiBinder.createAndBindUi(this));
   }
 
-  public void display(final PatchSetDetail detail, final PatchScript script, Patch.Key key,
-      PatchSet.Id idSideA, PatchSet.Id idSideB) {
+  public void display(final PatchSetDetail detail, final PatchScript script,
+      Patch.Key key, PatchSet.Id idSideA, PatchSet.Id idSideB) {
     this.script = script;
     this.patchKey = key;
     this.idSideA = idSideA;
@@ -92,10 +94,18 @@
 
     linkPanel.clear();
 
+    if (isFileOrCommitMessage()) {
+      linkPanel.setTitle(PatchUtil.C.addFileCommentByDoubleClick());
+    }
+
+    patchSet = new Label(PatchUtil.C.patchSet());
+    patchSet.addStyleName(style.patchSetLabel());
+    linkPanel.add(patchSet);
+
     if (screenType == PatchScreen.Type.UNIFIED) {
-      sideMarker.setInnerText((side == Side.A) ? "(-)" : "(+)");
-    } else {
-      sideMarker.getStyle().setDisplay(Display.NONE);
+      Label sideMarker = new Label((side == Side.A) ? "(-)" : "(+)");
+      sideMarker.addStyleName(style.sideMarker());
+      linkPanel.add(sideMarker);
     }
 
     Anchor baseLink = null;
@@ -133,6 +143,12 @@
     }
   }
 
+  public void addDoubleClickHandler(DoubleClickHandler handler) {
+    linkPanel.sinkEvents(Event.ONDBLCLICK);
+    linkPanel.addHandler(handler, DoubleClickEvent.getType());
+    patchSet.addDoubleClickHandler(handler);
+  }
+
   private Anchor createLink(String label, final PatchSet.Id id) {
     final Anchor anchor = new Anchor(label);
     anchor.addClickHandler(new ClickHandler() {
@@ -161,18 +177,23 @@
     return anchor;
   }
 
+  public boolean isFileOrCommitMessage() {
+    return !((side == Side.A && 0 >= script.getA().size()) || //
+    (side == Side.B && 0 >= script.getB().size()));
+  }
+
   private Anchor createDownloadLink() {
     boolean isCommitMessage = Patch.COMMIT_MSG.equals(script.getNewName());
-
-    if (isCommitMessage || (side == Side.A && 0 >= script.getA().size())
-        || (side == Side.B && 0 >= script.getB().size())) {
+    if (isCommitMessage || //
+        (side == Side.A && 0 >= script.getA().size()) || //
+        (side == Side.B && 0 >= script.getB().size())) {
       return null;
     }
 
-    Patch.Key key =
-        (idSideA == null) ? patchKey : (new Patch.Key(idSideA, patchKey.get()));
+    Patch.Key key = (idActive == null) ? //
+        patchKey : (new Patch.Key(idActive, patchKey.get()));
 
-    String sideURL = (side == Side.A) ? "1" : "0";
+    String sideURL = (idActive == null) ? "1" : "0";
     final String base = GWT.getHostPageBaseURL() + "cat/";
 
     Image image = new Image(Gerrit.RESOURCES.downloadIcon());
@@ -180,7 +201,6 @@
     final Anchor anchor = new Anchor();
     anchor.setHref(base + KeyUtil.encode(key.toString()) + "^" + sideURL);
     anchor.setTitle(PatchUtil.C.download());
-    anchor.setStyleName(style.downloadLink());
     DOM.insertBefore(anchor.getElement(), image.getElement(),
         DOM.getFirstChild(anchor.getElement()));
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchSetSelectBox.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchSetSelectBox.ui.xml
index 2fd183c..338e950 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchSetSelectBox.ui.xml
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchSetSelectBox.ui.xml
@@ -17,60 +17,37 @@
 
 <ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
     xmlns:g='urn:import:com.google.gwt.user.client.ui'>
-
-
   <ui:with field='res' type='com.google.gerrit.client.GerritResources'/>
-  <ui:with field='cons' type='com.google.gerrit.client.patches.PatchConstants'/>
   <ui:style type='com.google.gerrit.client.patches.PatchSetSelectBox.BoxStyle'>
     @eval selectionColor com.google.gerrit.client.Gerrit.getTheme().selectionColor;
-    @eval trimColor com.google.gerrit.client.Gerrit.getTheme().trimColor;
     @eval backgroundColor com.google.gerrit.client.Gerrit.getTheme().backgroundColor;
 
-    .wrapper {
-      width: 100%;
-      text-align: center;
-      font-size: 0; /* inline-block spacing fix */
-    }
-
     .linkPanel {
-      display: inline-block;
+      font-size: 12px;
+      white-space: normal;
     }
 
     .linkPanel > div {
+      padding-left: 3px;
+      padding-right: 3px;
+      vertical-align: middle;
       display: inline-block;
-      float: left;
-    }
-
-    .linkPanel {
-      overflow: hidden; /* div clear fix */
-      font-size: 12px;
-    }
-
-    .linkPanel > a {
-      padding: 3px;
-      display: inline-block;
-      text-decoration: none;
-      float: left;
     }
 
     .patchSetLabel {
       font-weight: bold;
-      float: left;
-      padding: 3px;
     }
 
     .sideMarker {
-      padding: 3px;
+      font-family: monospace;
     }
 
-    .downloadLink {
-      float: left;
-      padding: 1px !important;
-      margin-left: 3px;
-    }
-
-    .downloadLink > a {
-      text-size: 0;
+    .linkPanel > a {
+      padding-left: 3px;
+      padding-right: 3px;
+      text-decoration: none;
+      vertical-align: middle;
+      display: inline-block;
     }
 
     .selected {
@@ -78,21 +55,13 @@
       background-color: selectionColor;
     }
 
-    .sideMarker {
-      font-family: monospace;
-      float: left;
-    }
-
     .hidden {
       visibility: hidden;
     }
   </ui:style>
 
-  <g:HTMLPanel styleName='wrapper'>
-    <g:HTMLPanel styleName='{style.linkPanel}' ui:field='linkPanel'>
-      <div class='{style.patchSetLabel}'><ui:text from="{cons.patchSet}" /></div>
-      <div class='{style.sideMarker}' ui:field='sideMarker'></div>
-    </g:HTMLPanel>
+  <g:HTMLPanel>
+    <g:HTMLPanel styleName='{style.linkPanel}' ui:field='linkPanel'/>
   </g:HTMLPanel>
 </ui:UiBinder>
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchTableHeader.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchTableHeader.java
deleted file mode 100644
index 3dd8908..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchTableHeader.java
+++ /dev/null
@@ -1,71 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.patches;
-
-import com.google.gerrit.common.data.PatchScript;
-import com.google.gerrit.common.data.PatchSetDetail;
-import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gwt.core.client.GWT;
-import com.google.gwt.uibinder.client.UiBinder;
-import com.google.gwt.uibinder.client.UiField;
-import com.google.gwt.uibinder.client.UiTemplate;
-import com.google.gwt.user.client.ui.Composite;
-import com.google.gwt.user.client.ui.HTMLPanel;
-import com.google.gwt.user.client.ui.SimplePanel;
-
-public class PatchTableHeader extends Composite {
-
-  @UiTemplate("PatchTableHeaderSideBySide.ui.xml")
-  interface SideBySideBinder extends UiBinder<HTMLPanel, PatchTableHeader> {
-  }
-
-  @UiTemplate("PatchTableHeaderUnified.ui.xml")
-  interface UnifiedBinder extends UiBinder<HTMLPanel, PatchTableHeader> {
-  }
-
-  private static SideBySideBinder uiBinderS = GWT.create(SideBySideBinder.class);
-  private static UnifiedBinder uiBinderU = GWT.create(UnifiedBinder.class);
-
-  @UiField
-  SimplePanel sideAPanel;
-
-  @UiField
-  SimplePanel sideBPanel;
-
-  PatchSetSelectBox listA;
-  PatchSetSelectBox listB;
-
-  public PatchTableHeader(PatchScreen.Type type) {
-    listA = new PatchSetSelectBox(PatchSetSelectBox.Side.A, type);
-    listB = new PatchSetSelectBox(PatchSetSelectBox.Side.B, type);
-
-    if (type == PatchScreen.Type.SIDE_BY_SIDE) {
-      initWidget(uiBinderS.createAndBindUi(this));
-    } else {
-      initWidget(uiBinderU.createAndBindUi(this));
-    }
-
-    sideAPanel.add(listA);
-    sideBPanel.add(listB);
-  }
-
-
-  public void display(final PatchSetDetail detail, PatchScript script, final Patch.Key patchKey,
-      final PatchSet.Id idSideA, final PatchSet.Id idSideB) {
-    listA.display(detail, script, patchKey, idSideA, idSideB);
-    listB.display(detail, script, patchKey, idSideA, idSideB);
-  }
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchTableHeaderSideBySide.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchTableHeaderSideBySide.ui.xml
deleted file mode 100644
index d6fd717..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchTableHeaderSideBySide.ui.xml
+++ /dev/null
@@ -1,65 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-Copyright (C) 2012 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
-    xmlns:g='urn:import:com.google.gwt.user.client.ui'>
-
-
-  <ui:style>
-    @eval trimColor com.google.gerrit.client.Gerrit.getTheme().trimColor;
-
-    .wrapper {
-      width: 100%;
-      background-color: trimColor;
-      overflow: hidden;
-      font-size: 0; /* inline-block spacing fix */
-    }
-
-    .wrapper .box {
-      width: 100%;
-      text-align: center;
-    }
-
-    .leftWrapper {
-      width: 50%;
-      float: left;
-    }
-
-    .rightWrapper {
-      width: 50%;
-      overflow: hidden;
-    }
-
-    .leftBox {
-      float:left;
-    }
-
-    .rightBox {
-      float: right;
-    }
-  </ui:style>
-
-  <g:HTMLPanel styleName="{style.wrapper}">
-    <div class='{style.leftWrapper}'>
-      <g:SimplePanel addStyleNames='{style.box} {style.leftBox}' ui:field='sideAPanel'/>
-    </div>
-    <div class='{style.rightWrapper}'>
-      <g:SimplePanel addStyleNames='{style.box} {style.rightBox}' ui:field='sideBPanel'/>
-    </div>
-  </g:HTMLPanel>
-</ui:UiBinder>
-
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchTableHeaderUnified.ui.xml b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchTableHeaderUnified.ui.xml
deleted file mode 100644
index 24acfa3..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/PatchTableHeaderUnified.ui.xml
+++ /dev/null
@@ -1,42 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-Copyright (C) 2012 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-
-<ui:UiBinder xmlns:ui='urn:ui:com.google.gwt.uibinder'
-    xmlns:g='urn:import:com.google.gwt.user.client.ui'>
-
-  <ui:style>
-    @eval trimColor com.google.gerrit.client.Gerrit.getTheme().trimColor;
-
-    .wrapper {
-      width: 100%;
-      background-color: trimColor;
-      font-size: 0; /* inline-block spacing fix */
-    }
-
-    .wrapper .box {
-      width: 100%;
-      text-align: left;
-      margin-left: 3px;
-    }
-  </ui:style>
-
-  <g:HTMLPanel styleName="{style.wrapper}">
-    <g:SimplePanel addStyleNames='{style.box}' ui:field='sideAPanel'/>
-    <g:SimplePanel addStyleNames='{style.box}' ui:field='sideBPanel'/>
-  </g:HTMLPanel>
-</ui:UiBinder>
-
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/ReviewedPanels.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/ReviewedPanels.java
new file mode 100644
index 0000000..68df45d
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/patches/ReviewedPanels.java
@@ -0,0 +1,178 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.patches;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.VoidResult;
+import com.google.gerrit.client.changes.PatchTable;
+import com.google.gerrit.client.changes.PatchTable.PatchValidator;
+import com.google.gerrit.client.changes.Util;
+import com.google.gerrit.client.rpc.RestApi;
+import com.google.gerrit.client.ui.ChangeLink;
+import com.google.gerrit.client.ui.InlineHyperlink;
+import com.google.gerrit.reviewdb.client.Patch;
+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.logical.shared.ValueChangeEvent;
+import com.google.gwt.event.logical.shared.ValueChangeHandler;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+import com.google.gwt.user.client.ui.Anchor;
+import com.google.gwt.user.client.ui.CheckBox;
+import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwtexpui.safehtml.client.SafeHtml;
+import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
+
+public class ReviewedPanels {
+
+  public final FlowPanel top;
+  public final FlowPanel bottom;
+
+  private Patch.Key patchKey;
+  private PatchTable fileList;
+  private InlineHyperlink reviewedLink;
+  private CheckBox checkBoxTop;
+  private CheckBox checkBoxBottom;
+
+  public ReviewedPanels() {
+    this.top = new FlowPanel();
+    this.bottom = new FlowPanel();
+    this.bottom.setStyleName(Gerrit.RESOURCES.css().reviewedPanelBottom());
+  }
+
+  public void populate(Patch.Key pk, PatchTable pt, int patchIndex,
+      PatchScreen.Type patchScreenType) {
+    patchKey = pk;
+    fileList = pt;
+    reviewedLink = createReviewedLink(patchIndex, patchScreenType);
+
+    top.clear();
+    checkBoxTop = createReviewedCheckbox();
+    top.add(checkBoxTop);
+    top.add(createReviewedAnchor());
+
+    bottom.clear();
+    checkBoxBottom = createReviewedCheckbox();
+    bottom.add(checkBoxBottom);
+    bottom.add(createReviewedAnchor());
+  }
+
+  private CheckBox createReviewedCheckbox() {
+    final CheckBox checkBox = new CheckBox(PatchUtil.C.reviewedAnd() + " ");
+    checkBox.addValueChangeHandler(new ValueChangeHandler<Boolean>() {
+      @Override
+      public void onValueChange(ValueChangeEvent<Boolean> event) {
+        final boolean value = event.getValue();
+        setReviewedByCurrentUser(value);
+        if (checkBoxTop.getValue() != value) {
+          checkBoxTop.setValue(value);
+        }
+        if (checkBoxBottom.getValue() != value) {
+          checkBoxBottom.setValue(value);
+        }
+      }
+    });
+    return checkBox;
+  }
+
+  public boolean getValue() {
+    return checkBoxTop.getValue();
+  }
+
+  public void setValue(final boolean value) {
+    checkBoxTop.setValue(value);
+    checkBoxBottom.setValue(value);
+  }
+
+  public void setReviewedByCurrentUser(boolean reviewed) {
+    if (fileList != null) {
+      fileList.updateReviewedStatus(patchKey, reviewed);
+    }
+
+    PatchSet.Id ps = patchKey.getParentKey();
+    RestApi api = new RestApi("/changes/").id(ps.getParentKey().get())
+        .view("revisions").id(ps.get())
+        .view("files").id(patchKey.getFileName())
+        .view("reviewed");
+
+    AsyncCallback<VoidResult> cb = new AsyncCallback<VoidResult>() {
+      @Override
+      public void onFailure(Throwable arg0) {
+        // nop
+      }
+
+      @Override
+      public void onSuccess(VoidResult result) {
+        // nop
+      }
+    };
+    if (reviewed) {
+      api.put(cb);
+    } else {
+      api.delete(cb);
+    }
+  }
+
+  public void go() {
+    if (reviewedLink != null) {
+      setReviewedByCurrentUser(true);
+      reviewedLink.go();
+    }
+  }
+
+  private InlineHyperlink createReviewedLink(final int patchIndex,
+      final PatchScreen.Type patchScreenType) {
+    final PatchValidator unreviewedValidator = new PatchValidator() {
+      public boolean isValid(Patch patch) {
+        return !patch.isReviewedByCurrentUser();
+      }
+    };
+
+    InlineHyperlink reviewedLink = new ChangeLink("", patchKey.getParentKey());
+    if (fileList != null) {
+      int nextUnreviewedPatchIndex =
+          fileList.getNextPatch(patchIndex, true, unreviewedValidator,
+              fileList.PREFERENCE_VALIDATOR);
+
+      if (nextUnreviewedPatchIndex > -1) {
+        // Create invisible patch link to change page
+        reviewedLink =
+            fileList.createLink(nextUnreviewedPatchIndex, patchScreenType,
+                null, null);
+        reviewedLink.setText("");
+      }
+    }
+    return reviewedLink;
+  }
+
+  private Anchor createReviewedAnchor() {
+    SafeHtmlBuilder text = new SafeHtmlBuilder();
+    text.append(PatchUtil.C.next());
+    text.append(SafeHtml.asis(Util.C.nextPatchLinkIcon()));
+
+    Anchor reviewedAnchor = new Anchor("");
+    SafeHtml.set(reviewedAnchor, text);
+
+    reviewedAnchor.addClickHandler(new ClickHandler() {
+      @Override
+      public void onClick(ClickEvent event) {
+        setReviewedByCurrentUser(true);
+        reviewedLink.go();
+      }
+    });
+
+    return reviewedAnchor;
+  }
+}
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 ec63a83..15ab951 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
@@ -23,23 +23,26 @@
 import com.google.gerrit.common.data.CommentDetail;
 import com.google.gerrit.common.data.PatchScript;
 import com.google.gerrit.common.data.PatchScript.FileMode;
+import com.google.gerrit.common.data.PatchSetDetail;
 import com.google.gerrit.prettify.common.EditList;
 import com.google.gerrit.prettify.common.SparseHtmlFile;
 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.user.client.DOM;
+import com.google.gwt.user.client.Element;
 import com.google.gwt.user.client.ui.Anchor;
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
 import com.google.gwt.user.client.ui.HasVerticalAlignment;
 import com.google.gwt.user.client.ui.InlineLabel;
+import com.google.gwt.user.client.ui.UIObject;
 import com.google.gwtexpui.safehtml.client.SafeHtml;
 import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
 import org.eclipse.jgit.diff.Edit;
 
 import java.util.ArrayList;
 import java.util.Iterator;
-import java.util.List;
 
 public class SideBySideTable extends AbstractPatchContentTable {
   private static final int A = 2;
@@ -48,10 +51,21 @@
 
   private SparseHtmlFile a;
   private SparseHtmlFile b;
+  protected boolean isFileCommentBorderRowExist;
+
+  protected void createFileCommentEditorOnSideA() {
+    createCommentEditor(R_HEAD + 1, A, R_HEAD, FILE_SIDE_A);
+    return;
+  }
+
+  protected void createFileCommentEditorOnSideB() {
+    createCommentEditor(R_HEAD + 1, B, R_HEAD, FILE_SIDE_B);
+    return;
+  }
 
   @Override
   protected void onCellDoubleClick(final int row, int column) {
-    if (column > 0 && getRowItem(row) instanceof PatchLine) {
+    if (column > C_ARROW && getRowItem(row) instanceof PatchLine) {
       final PatchLine line = (PatchLine) getRowItem(row);
       if (column == 1 || column == A) {
         createCommentEditor(row + 1, A, line.getLineA(), (short) 0);
@@ -63,6 +77,7 @@
 
   @Override
   protected void onCellSingleClick(int row, int column) {
+    super.onCellSingleClick(row, column);
     if (column == 1 || column == 4) {
       onCellDoubleClick(row, column);
     }
@@ -75,106 +90,138 @@
   }
 
   @Override
-  protected void render(final PatchScript script) {
-    a = getSparseHtmlFileA(script);
-    b = getSparseHtmlFileB(script);
+  protected void render(final PatchScript script, final PatchSetDetail detail) {
     final ArrayList<Object> lines = new ArrayList<Object>();
     final SafeHtmlBuilder nc = new SafeHtmlBuilder();
-    final boolean intraline =
-        script.getDiffPrefs().isIntralineDifference()
-            && script.hasIntralineDifference();
-
-    if (script.getFileModeA() != FileMode.FILE
-        || script.getFileModeB() != FileMode.FILE) {
-      openLine(nc);
-      appendModeLine(nc, script.getFileModeA());
-      appendModeLine(nc, script.getFileModeB());
-      closeLine(nc);
-      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(new SkippedLine(lastA, lastB, hunk.getCurB() - lastB));
+    allocateTableHeader(script, nc);
+    lines.add(null);
+    if (!isDisplayBinary) {
+      if (script.getFileModeA() != FileMode.FILE
+          || script.getFileModeB() != FileMode.FILE) {
+        openLine(nc);
+        appendModeLine(nc, script.getFileModeA());
+        appendModeLine(nc, script.getFileModeB());
+        closeLine(nc);
+        lines.add(null);
       }
 
-      while (hunk.next()) {
-        if (hunk.isContextLine()) {
-          openLine(nc);
-          final SafeHtml ctx = a.getSafeHtmlLine(hunk.getCurA());
-          appendLineNumber(nc, hunk.getCurA(), false);
-          appendLineText(nc, CONTEXT, ctx, false, false);
-          if (ignoreWS && b.contains(hunk.getCurB())) {
-            appendLineText(nc, CONTEXT, b, hunk.getCurB(), false);
-          } else {
-            appendLineText(nc, CONTEXT, ctx, false, false);
-          }
-          appendLineNumber(nc, hunk.getCurB(), true);
-          closeLine(nc);
-          hunk.incBoth();
-          lines.add(new PatchLine(CONTEXT, hunk.getCurA(), hunk.getCurB()));
-
-        } else if (hunk.isModifiedLine()) {
-          final boolean del = hunk.isDeletedA();
-          final boolean ins = hunk.isInsertedB();
-          final boolean full =
-              intraline && hunk.getCurEdit().getType() != Edit.Type.REPLACE;
-          openLine(nc);
-
-          if (del) {
-            appendLineNumber(nc, hunk.getCurA(), false);
-            appendLineText(nc, DELETE, a, hunk.getCurA(), full);
-            hunk.incA();
-          } else if (hunk.getCurEdit().getType() == Edit.Type.REPLACE) {
-            appendLineNumber(nc, false);
-            appendLineNone(nc, DELETE);
-          } else {
-            appendLineNumber(nc, false);
-            appendLineNone(nc, CONTEXT);
+      if (hasDifferences(script)) {
+        int lastA = 0;
+        int lastB = 0;
+        final boolean ignoreWS = script.isIgnoreWhitespace();
+        a = getSparseHtmlFileA(script);
+        b = getSparseHtmlFileB(script);
+        final boolean intraline =
+            script.getDiffPrefs().isIntralineDifference()
+                && script.hasIntralineDifference();
+        for (final EditList.Hunk hunk : script.getHunks()) {
+          if (!hunk.isStartOfFile()) {
+            appendSkipLine(nc, hunk.getCurB() - lastB);
+            lines.add(new SkippedLine(lastA, lastB, hunk.getCurB() - lastB));
           }
 
-          if (ins) {
-            appendLineText(nc, INSERT, b, hunk.getCurB(), full);
-            appendLineNumber(nc, hunk.getCurB(), true);
-            hunk.incB();
-          } else if (hunk.getCurEdit().getType() == Edit.Type.REPLACE) {
-            appendLineNone(nc, INSERT);
-            appendLineNumber(nc, true);
-          } else {
-            appendLineNone(nc, CONTEXT);
-            appendLineNumber(nc, true);
+          while (hunk.next()) {
+            if (hunk.isContextLine()) {
+              openLine(nc);
+              final SafeHtml ctx = a.getSafeHtmlLine(hunk.getCurA());
+              appendLineNumber(nc, hunk.getCurA(), false);
+              appendLineText(nc, CONTEXT, ctx, false, false);
+              if (ignoreWS && b.contains(hunk.getCurB())) {
+                appendLineText(nc, CONTEXT, b, hunk.getCurB(), false);
+              } else {
+                appendLineText(nc, CONTEXT, ctx, false, false);
+              }
+              appendLineNumber(nc, hunk.getCurB(), true);
+              closeLine(nc);
+              hunk.incBoth();
+              lines.add(new PatchLine(CONTEXT, hunk.getCurA(), hunk.getCurB()));
+
+            } else if (hunk.isModifiedLine()) {
+              final boolean del = hunk.isDeletedA();
+              final boolean ins = hunk.isInsertedB();
+              final boolean full =
+                  intraline && hunk.getCurEdit().getType() != Edit.Type.REPLACE;
+              openLine(nc);
+
+              if (del) {
+                appendLineNumber(nc, hunk.getCurA(), false);
+                appendLineText(nc, DELETE, a, hunk.getCurA(), full);
+                hunk.incA();
+              } else if (hunk.getCurEdit().getType() == Edit.Type.REPLACE) {
+                appendLineNumber(nc, false);
+                appendLineNone(nc, DELETE);
+              } else {
+                appendLineNumber(nc, false);
+                appendLineNone(nc, CONTEXT);
+              }
+
+              if (ins) {
+                appendLineText(nc, INSERT, b, hunk.getCurB(), full);
+                appendLineNumber(nc, hunk.getCurB(), true);
+                hunk.incB();
+              } else if (hunk.getCurEdit().getType() == Edit.Type.REPLACE) {
+                appendLineNone(nc, INSERT);
+                appendLineNumber(nc, true);
+              } else {
+                appendLineNone(nc, CONTEXT);
+                appendLineNumber(nc, true);
+              }
+
+              closeLine(nc);
+
+              if (del && ins) {
+                lines.add(new PatchLine(REPLACE, hunk.getCurA(), hunk.getCurB()));
+              } else if (del) {
+                lines.add(new PatchLine(DELETE, hunk.getCurA(), -1));
+              } else if (ins) {
+                lines.add(new PatchLine(INSERT, -1, hunk.getCurB()));
+              }
+            }
           }
-
-          closeLine(nc);
-
-          if (del && ins) {
-            lines.add(new PatchLine(REPLACE, hunk.getCurA(), hunk.getCurB()));
-          } else if (del) {
-            lines.add(new PatchLine(DELETE, hunk.getCurA(), 0));
-          } else if (ins) {
-            lines.add(new PatchLine(INSERT, 0, hunk.getCurB()));
+          lastA = hunk.getCurA();
+          lastB = hunk.getCurB();
+        }
+        if (lastB != b.size()) {
+          appendSkipLine(nc, b.size() - lastB);
+          lines.add(new SkippedLine(lastA, lastB, b.size() - lastB));
+        }
+      }
+    }else{
+      // Display the patch header for binary
+      for (final String line : script.getPatchHeader()) {
+        appendFileHeader(nc, line);
+      }
+    }
+    if (!hasDifferences(script)) {
+      appendNoDifferences(nc);
+    }
+    resetHtml(nc);
+    populateTableHeader(script, detail);
+    if (hasDifferences(script)) {
+      initScript(script);
+      if (!isDisplayBinary) {
+        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));
           }
         }
       }
-      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));
-      }
+  private void populateTableHeader(final PatchScript script,
+      final PatchSetDetail detail) {
+    initHeaders(script, detail);
+    table.setWidget(R_HEAD, A, headerSideA);
+    table.setWidget(R_HEAD, B, headerSideB);
+
+    // Populate icons to lineNumber column header.
+    if (headerSideA.isFileOrCommitMessage()) {
+      table.setWidget(R_HEAD, A - 1, iconA);
+    }
+    if (headerSideB.isFileOrCommitMessage()) {
+      table.setWidget(R_HEAD, B + 1, iconB);
     }
   }
 
@@ -202,6 +249,11 @@
   }
 
   @Override
+  protected PatchScreen.Type getPatchScreenType() {
+    return PatchScreen.Type.SIDE_BY_SIDE;
+  }
+
+  @Override
   public void display(final CommentDetail cd, boolean expandComments) {
     if (cd.isEmpty()) {
       return;
@@ -209,51 +261,163 @@
     setAccountInfoCache(cd.getAccounts());
 
     for (int row = 0; row < table.getRowCount();) {
-      if (getRowItem(row) instanceof PatchLine) {
+      final Iterator<PatchLineComment> ai;
+      final Iterator<PatchLineComment> bi;
+
+      if (row == R_HEAD) {
+        ai = cd.getForA(R_HEAD).iterator();
+        bi = cd.getForB(R_HEAD).iterator();
+      } else if (getRowItem(row) instanceof PatchLine) {
         final PatchLine pLine = (PatchLine) getRowItem(row);
-        final List<PatchLineComment> fora = cd.getForA(pLine.getLineA());
-        final List<PatchLineComment> forb = cd.getForB(pLine.getLineB());
-        row++;
-
-        final Iterator<PatchLineComment> ai = fora.iterator();
-        final Iterator<PatchLineComment> bi = forb.iterator();
-        while (ai.hasNext() && bi.hasNext()) {
-          final PatchLineComment ac = ai.next();
-          final PatchLineComment bc = bi.next();
-          insertRow(row);
-          bindComment(row, A, ac, !ai.hasNext(), expandComments);
-          bindComment(row, B, bc, !bi.hasNext(), expandComments);
-          row++;
-        }
-
-        row = finish(ai, row, A, expandComments);
-        row = finish(bi, row, B, expandComments);
+        ai = cd.getForA(pLine.getLineA()).iterator();
+        bi = cd.getForB(pLine.getLineB()).iterator();
       } else {
         row++;
+        continue;
       }
+
+      row++;
+      while (ai.hasNext() && bi.hasNext()) {
+        final PatchLineComment ac = ai.next();
+        final PatchLineComment bc = bi.next();
+        if (ac.getLine() == R_HEAD) {
+          insertFileCommentRow(row);
+        } else {
+          insertRow(row);
+        }
+        bindComment(row, A, ac, !ai.hasNext(), expandComments);
+        bindComment(row, B, bc, !bi.hasNext(), expandComments);
+        row++;
+      }
+
+      row = finish(ai, row, A, expandComments);
+      row = finish(bi, row, B, expandComments);
     }
   }
 
+  private void defaultStyle(final int row, final CellFormatter fmt) {
+    fmt.addStyleName(row, A - 1, Gerrit.RESOURCES.css().lineNumber());
+    fmt.addStyleName(row, A, Gerrit.RESOURCES.css().diffText());
+    if (isDisplayBinary) {
+      fmt.addStyleName(row, A, Gerrit.RESOURCES.css().diffTextForBinaryInSideBySide());
+    }
+    fmt.addStyleName(row, B, Gerrit.RESOURCES.css().diffText());
+    fmt.addStyleName(row, B + 1, Gerrit.RESOURCES.css().lineNumber());
+    fmt.addStyleName(row, B + 1, Gerrit.RESOURCES.css().rightmost());
+  }
+
   @Override
   protected void insertRow(final int row) {
     super.insertRow(row);
     final CellFormatter fmt = table.getCellFormatter();
-    fmt.addStyleName(row, A - 1, Gerrit.RESOURCES.css().lineNumber());
-    fmt.addStyleName(row, A, Gerrit.RESOURCES.css().diffText());
-    fmt.addStyleName(row, B, Gerrit.RESOURCES.css().diffText());
-    fmt.addStyleName(row, B + 1, Gerrit.RESOURCES.css().lineNumber());
+    defaultStyle(row, fmt);
+  }
+
+  @Override
+  protected void insertFileCommentRow(final int row) {
+    table.insertRow(row);
+    final CellFormatter fmt = table.getCellFormatter();
+    fmt.addStyleName(row, C_ARROW, Gerrit.RESOURCES.css().iconCellOfFileCommentRow());
+    defaultStyle(row, fmt);
+
+    fmt.addStyleName(row, C_ARROW, //
+        Gerrit.RESOURCES.css().cellsNextToFileComment());
+    fmt.addStyleName(row, A - 1, //
+        Gerrit.RESOURCES.css().cellsNextToFileComment());
+    fmt.addStyleName(row, B + 1, //
+        Gerrit.RESOURCES.css().cellsNextToFileComment());
+    createFileCommentBorderRow(row);
+  }
+
+  private void createFileCommentBorderRow(final int row) {
+    if (row == 1 && !isFileCommentBorderRowExist) {
+      isFileCommentBorderRowExist = true;
+      table.insertRow(R_HEAD + 2);
+
+      final CellFormatter fmt = table.getCellFormatter();
+
+      fmt.addStyleName(R_HEAD + 2, C_ARROW, //
+          Gerrit.RESOURCES.css().iconCellOfFileCommentRow());
+      defaultStyle(R_HEAD + 2, fmt);
+
+      final Element iconCell = fmt.getElement(R_HEAD + 2, C_ARROW);
+      UIObject.setStyleName(DOM.getParent(iconCell), Gerrit.RESOURCES.css()
+          .fileCommentBorder(), true);
+    }
   }
 
   private int finish(final Iterator<PatchLineComment> i, int row, final int col, boolean expandComment) {
     while (i.hasNext()) {
       final PatchLineComment c = i.next();
-      insertRow(row);
+      if (c.getLine() == R_HEAD) {
+        insertFileCommentRow(row);
+      } else {
+        insertRow(row);
+      }
       bindComment(row, col, c, !i.hasNext(), expandComment);
       row++;
     }
     return row;
   }
 
+  private void allocateTableHeader(PatchScript script, final SafeHtmlBuilder m) {
+    m.openTr();
+
+    m.openTd();
+    m.addStyleName(Gerrit.RESOURCES.css().iconCell());
+    m.addStyleName(Gerrit.RESOURCES.css().fileColumnHeader());
+    m.closeTd();
+
+    m.openTd();
+    m.setStyleName(Gerrit.RESOURCES.css().lineNumber());
+    m.addStyleName(Gerrit.RESOURCES.css().fileColumnHeader());
+    m.nbsp();
+    m.closeTd();
+
+    m.openTd();
+    m.setStyleName(Gerrit.RESOURCES.css().fileColumnHeader());
+    m.addStyleName(Gerrit.RESOURCES.css().fileLine());
+    m.closeTd();
+
+    m.openTd();
+    m.setStyleName(Gerrit.RESOURCES.css().fileColumnHeader());
+    m.addStyleName(Gerrit.RESOURCES.css().fileLine());
+    m.closeTd();
+
+    m.openTd();
+    m.setStyleName(Gerrit.RESOURCES.css().lineNumber());
+    m.addStyleName(Gerrit.RESOURCES.css().fileColumnHeader());
+    m.addStyleName(Gerrit.RESOURCES.css().rightmost());
+    m.closeTd();
+
+    m.closeTr();
+  }
+
+  private void appendFileHeader(final SafeHtmlBuilder m, final String line) {
+    m.openTr();
+
+    m.openTd();
+    m.addStyleName(Gerrit.RESOURCES.css().iconCell());
+    m.closeTd();
+
+    m.openTd();
+    m.setStyleName(Gerrit.RESOURCES.css().lineNumber());
+    m.nbsp();
+    m.closeTd();
+
+    m.openTd();
+    m.setStyleName(Gerrit.RESOURCES.css().sideBySideTableBinaryHeader());
+    m.setAttribute("colspan", 2);
+    m.append(line);
+    m.closeTd();
+
+    m.openTd();
+    m.nbsp();
+    m.closeTd();
+
+    m.closeTr();
+  }
+
   private void appendSkipLine(final SafeHtmlBuilder m, final int skipCnt) {
     m.openTr();
 
@@ -310,9 +474,7 @@
 
     CellFormatter fmt = table.getCellFormatter();
     for (int i = 0 + offset; i < loopTo + offset; i++) {
-      // The overridden version of insertRow adds some css classes we don't
-      // want.
-      super.insertRow(row + i);
+      insertRow(row + i);
       table.getRowFormatter().setVerticalAlign(row + i,
           HasVerticalAlignment.ALIGN_TOP);
       int lineA = line.getStartA() + i;
@@ -438,6 +600,8 @@
           m.addStyleName("wdi");
         }
         break;
+      case REPLACE:
+        break;
     }
     m.append(lineHtml);
     m.closeTd();
@@ -463,4 +627,13 @@
   private void closeLine(final SafeHtmlBuilder m) {
     m.closeTr();
   }
+
+  @Override
+  protected void destroyCommentRow(final int row) {
+    super.destroyCommentRow(row);
+    if (row == R_HEAD + 1) {
+      table.removeRow(row);
+      isFileCommentBorderRowExist = false;
+    }
+  }
 }
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 f0f619e..82df54a 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
@@ -22,12 +22,17 @@
 import com.google.gerrit.common.data.CommentDetail;
 import com.google.gerrit.common.data.PatchScript;
 import com.google.gerrit.common.data.PatchScript.DisplayMethod;
+import com.google.gerrit.common.data.PatchSetDetail;
 import com.google.gerrit.prettify.common.EditList;
 import com.google.gerrit.prettify.common.EditList.Hunk;
 import com.google.gerrit.prettify.common.SparseHtmlFile;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gwt.core.client.GWT;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.Element;
+import com.google.gwt.user.client.ui.UIObject;
 import com.google.gwt.user.client.ui.HTMLTable.CellFormatter;
 import com.google.gwtexpui.safehtml.client.SafeHtml;
 import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
@@ -48,9 +53,14 @@
         }
       };
 
+  protected boolean isFileCommentBorderRowExist;
+  // Cursors.
+  protected int rowOfTableHeaderB;
+  protected int borderRowOfFileComment;
+
   @Override
   protected void onCellDoubleClick(final int row, final int column) {
-    if (getRowItem(row) instanceof PatchLine) {
+    if (column > C_ARROW && getRowItem(row) instanceof PatchLine) {
       final PatchLine pl = (PatchLine) getRowItem(row);
       switch (pl.getType()) {
         case DELETE:
@@ -60,12 +70,42 @@
         case INSERT:
           createCommentEditor(row + 1, PC, pl.getLineB(), (short) 1);
           break;
+        case REPLACE:
+          break;
+      }
+    }
+  }
+
+  @Override
+  protected void updateCursor(final PatchLineComment newComment) {
+    if (newComment.getLine() == R_HEAD) {
+      final PatchSet.Id psId =
+          newComment.getKey().getParentKey().getParentKey();
+      switch (newComment.getSide()) {
+        case FILE_SIDE_A:
+          if (idSideA == null && idSideB.equals(psId)) {
+            rowOfTableHeaderB++;
+            borderRowOfFileComment++;
+            return;
+          }
+          break;
+        case FILE_SIDE_B:
+          if (idSideA != null && idSideA.equals(psId)) {
+            rowOfTableHeaderB++;
+            borderRowOfFileComment++;
+            return;
+          }
+          if (idSideB.equals(psId)) {
+            borderRowOfFileComment++;
+            return;
+          }
       }
     }
   }
 
   @Override
   protected void onCellSingleClick(int row, int column) {
+    super.onCellSingleClick(row, column);
     if (column == 1 || column == 2) {
       if (!"".equals(table.getText(row, column))) {
         onCellDoubleClick(row, column);
@@ -74,6 +114,43 @@
   }
 
   @Override
+  protected void destroyCommentRow(final int row) {
+    super.destroyCommentRow(row);
+    if (this.rowOfTableHeaderB + 1 == row && row + 1 == borderRowOfFileComment) {
+      table.removeRow(row);
+      isFileCommentBorderRowExist = false;
+    }
+  }
+
+  @Override
+  public void remove(CommentEditorPanel panel) {
+    super.remove(panel);
+    if (panel.getComment().getLine() == AbstractPatchContentTable.R_HEAD) {
+      final PatchSet.Id psId =
+          panel.getComment().getKey().getParentKey().getParentKey();
+      switch (panel.getComment().getSide()) {
+        case FILE_SIDE_A:
+          if (idSideA == null && idSideB.equals(psId)) {
+            rowOfTableHeaderB--;
+            borderRowOfFileComment--;
+            return;
+          }
+          break;
+        case FILE_SIDE_B:
+          if (idSideA != null && idSideA.equals(psId)) {
+            rowOfTableHeaderB--;
+            borderRowOfFileComment--;
+            return;
+          }
+          if (idSideB.equals(psId)) {
+            borderRowOfFileComment--;
+            return;
+          }
+      }
+    }
+  }
+
+  @Override
   protected void onInsertComment(final PatchLine pl) {
     final int row = getCurrentRow();
     switch (pl.getType()) {
@@ -84,6 +161,8 @@
       case INSERT:
         createCommentEditor(row + 1, PC, pl.getLineB(), (short) 1);
         break;
+      case REPLACE:
+        break;
     }
   }
 
@@ -93,123 +172,178 @@
     nc.closeElement("img");
   }
 
+  protected void createFileCommentEditorOnSideA() {
+    createCommentEditor(R_HEAD + 1, PC, R_HEAD, FILE_SIDE_A);
+    return;
+  }
+
+  protected void createFileCommentEditorOnSideB() {
+    createCommentEditor(rowOfTableHeaderB + 1, PC, R_HEAD, FILE_SIDE_B);
+    createFileCommentBorderRow();
+  }
+
+  private void populateTableHeader(final PatchScript script,
+      final PatchSetDetail detail) {
+    initHeaders(script, detail);
+    table.setWidget(R_HEAD, PC, headerSideA);
+    table.setWidget(rowOfTableHeaderB, PC, headerSideB);
+    table.getFlexCellFormatter().addStyleName(R_HEAD, PC,
+        Gerrit.RESOURCES.css().unifiedTableHeader());
+    table.getFlexCellFormatter().addStyleName(rowOfTableHeaderB, PC,
+        Gerrit.RESOURCES.css().unifiedTableHeader());
+
+    // Add icons to lineNumber column header
+    if (headerSideA.isFileOrCommitMessage()) {
+      table.setWidget(R_HEAD, 1, iconA);
+    }
+    if (headerSideB.isFileOrCommitMessage()) {
+      table.setWidget(rowOfTableHeaderB, 2, iconB);
+    }
+  }
+
+  private void allocateTableHeader(SafeHtmlBuilder nc) {
+    rowOfTableHeaderB = 1;
+    borderRowOfFileComment = 2;
+    for (int i = R_HEAD; i < borderRowOfFileComment; i++) {
+      openTableHeaderLine(nc);
+      padLineNumberOnTableHeaderForSideA(nc);
+      padLineNumberOnTableHeaderForSideB(nc);
+      nc.openTd();
+      nc.setStyleName(Gerrit.RESOURCES.css().fileLine());
+      nc.addStyleName(Gerrit.RESOURCES.css().fileColumnHeader());
+      nc.closeTd();
+      closeLine(nc);
+    }
+  }
+
   @Override
-  protected void render(final PatchScript script) {
-    final SparseHtmlFile a = getSparseHtmlFileA(script);
-    final SparseHtmlFile b = getSparseHtmlFileB(script);
+  protected void render(final PatchScript script, final PatchSetDetail detail) {
     final SafeHtmlBuilder nc = new SafeHtmlBuilder();
+    allocateTableHeader(nc);
 
     // Display the patch header
     for (final String line : script.getPatchHeader()) {
       appendFileHeader(nc, line);
     }
-
-    if (script.getDisplayMethodA() == DisplayMethod.IMG
-        || script.getDisplayMethodB() == DisplayMethod.IMG) {
-      final String rawBase = GWT.getHostPageBaseURL() + "cat/";
-
-      nc.openTr();
-      nc.setAttribute("valign", "center");
-      nc.setAttribute("align", "center");
-
-      nc.openTd();
-      nc.nbsp();
-      nc.closeTd();
-
-      nc.openTd();
-      nc.nbsp();
-      nc.closeTd();
-
-      nc.openTd();
-      nc.nbsp();
-      nc.closeTd();
-
-      nc.openTd();
-      if (script.getDisplayMethodA() == DisplayMethod.IMG) {
-        if (idSideA == null) {
-          appendImgTag(nc, rawBase + KeyUtil.encode(patchKey.toString()) + "^1");
-        } else {
-          Patch.Key k = new Patch.Key(idSideA, patchKey.get());
-          appendImgTag(nc, rawBase + KeyUtil.encode(k.toString()) + "^0");
-        }
-      }
-      if (script.getDisplayMethodB() == DisplayMethod.IMG) {
-        appendImgTag(nc, rawBase + KeyUtil.encode(patchKey.toString()) + "^0");
-      }
-      nc.closeTd();
-
-      nc.closeTr();
-    }
-
-    final boolean syntaxHighlighting =
-        script.getDiffPrefs().isSyntaxHighlighting();
     final ArrayList<PatchLine> lines = new ArrayList<PatchLine>();
-    for (final EditList.Hunk hunk : script.getHunks()) {
-      appendHunkHeader(nc, hunk);
-      while (hunk.next()) {
-        if (hunk.isContextLine()) {
-          openLine(nc);
-          appendLineNumber(nc, hunk.getCurA());
-          appendLineNumber(nc, hunk.getCurB());
-          appendLineText(nc, false, CONTEXT, a, hunk.getCurA());
-          closeLine(nc);
-          hunk.incBoth();
-          lines.add(new PatchLine(CONTEXT, hunk.getCurA(), hunk.getCurB()));
+    if (!isDisplayBinary) {
+      final SparseHtmlFile a = getSparseHtmlFileA(script);
+      final SparseHtmlFile b = getSparseHtmlFileB(script);
+      if (script.getDisplayMethodA() == DisplayMethod.IMG
+          || script.getDisplayMethodB() == DisplayMethod.IMG) {
+        final String rawBase = GWT.getHostPageBaseURL() + "cat/";
 
-        } else if (hunk.isDeletedA()) {
-          openLine(nc);
-          appendLineNumber(nc, hunk.getCurA());
-          padLineNumber(nc);
-          appendLineText(nc, syntaxHighlighting, DELETE, a, hunk.getCurA());
-          closeLine(nc);
-          hunk.incA();
-          lines.add(new PatchLine(DELETE, hunk.getCurA(), 0));
-          if (a.size() == hunk.getCurA()
-              && script.getA().isMissingNewlineAtEnd()) {
-            appendNoLF(nc);
+        nc.openTr();
+        nc.setAttribute("valign", "center");
+        nc.setAttribute("align", "center");
+
+        nc.openTd();
+        nc.nbsp();
+        nc.closeTd();
+
+        nc.openTd();
+        nc.nbsp();
+        nc.closeTd();
+
+        nc.openTd();
+        nc.nbsp();
+        nc.closeTd();
+
+        nc.openTd();
+        if (script.getDisplayMethodA() == DisplayMethod.IMG) {
+          if (idSideA == null) {
+            appendImgTag(nc, rawBase + KeyUtil.encode(patchKey.toString()) + "^1");
+          } else {
+            Patch.Key k = new Patch.Key(idSideA, patchKey.get());
+            appendImgTag(nc, rawBase + KeyUtil.encode(k.toString()) + "^0");
           }
+        }
+        if (script.getDisplayMethodB() == DisplayMethod.IMG) {
+          appendImgTag(nc, rawBase + KeyUtil.encode(patchKey.toString()) + "^0");
+        }
+        nc.closeTd();
 
-        } else if (hunk.isInsertedB()) {
-          openLine(nc);
-          padLineNumber(nc);
-          appendLineNumber(nc, hunk.getCurB());
-          appendLineText(nc, syntaxHighlighting, INSERT, b, hunk.getCurB());
-          closeLine(nc);
-          hunk.incB();
-          lines.add(new PatchLine(INSERT, 0, hunk.getCurB()));
-          if (b.size() == hunk.getCurB()
-              && script.getB().isMissingNewlineAtEnd()) {
-            appendNoLF(nc);
+        nc.closeTr();
+      }
+
+      if (hasDifferences(script)) {
+        final boolean syntaxHighlighting =
+            script.getDiffPrefs().isSyntaxHighlighting();
+        for (final EditList.Hunk hunk : script.getHunks()) {
+          appendHunkHeader(nc, hunk);
+          while (hunk.next()) {
+            if (hunk.isContextLine()) {
+              openLine(nc);
+              appendLineNumberForSideA(nc, hunk.getCurA());
+              appendLineNumberForSideB(nc, hunk.getCurB());
+              appendLineText(nc, false, CONTEXT, a, hunk.getCurA());
+              closeLine(nc);
+              hunk.incBoth();
+              lines.add(new PatchLine(CONTEXT, hunk.getCurA(), hunk.getCurB()));
+
+            } else if (hunk.isDeletedA()) {
+              openLine(nc);
+              appendLineNumberForSideA(nc, hunk.getCurA());
+              padLineNumberForSideB(nc);
+              appendLineText(nc, syntaxHighlighting, DELETE, a, hunk.getCurA());
+              closeLine(nc);
+              hunk.incA();
+              lines.add(new PatchLine(DELETE, hunk.getCurA(), -1));
+              if (a.size() == hunk.getCurA()
+                  && script.getA().isMissingNewlineAtEnd()) {
+                appendNoLF(nc);
+              }
+
+            } else if (hunk.isInsertedB()) {
+              openLine(nc);
+              padLineNumberForSideA(nc);
+              appendLineNumberForSideB(nc, hunk.getCurB());
+              appendLineText(nc, syntaxHighlighting, INSERT, b, hunk.getCurB());
+              closeLine(nc);
+              hunk.incB();
+              lines.add(new PatchLine(INSERT, -1, hunk.getCurB()));
+              if (b.size() == hunk.getCurB()
+                  && script.getB().isMissingNewlineAtEnd()) {
+                appendNoLF(nc);
+              }
+            }
           }
         }
       }
     }
+    if (!hasDifferences(script)) {
+      appendNoDifferences(nc);
+    }
     resetHtml(nc);
-    initScript(script);
-
-    int row = script.getPatchHeader().size();
-    final CellFormatter fmt = table.getCellFormatter();
-    final Iterator<PatchLine> iLine = lines.iterator();
-    while (iLine.hasNext()) {
-      final PatchLine l = iLine.next();
-      final String n;
-      switch (l.getType()) {
-        case CONTEXT:
-          n = Gerrit.RESOURCES.css().diffTextCONTEXT();
-          break;
-        case DELETE:
-          n = Gerrit.RESOURCES.css().diffTextDELETE();
-          break;
-        case INSERT:
-          n = Gerrit.RESOURCES.css().diffTextINSERT();
-          break;
-        default:
-          continue;
+    populateTableHeader(script, detail);
+    if (hasDifferences(script)) {
+      initScript(script);
+      if (!isDisplayBinary) {
+        int row = script.getPatchHeader().size();
+        final CellFormatter fmt = table.getCellFormatter();
+        final Iterator<PatchLine> iLine = lines.iterator();
+        while (iLine.hasNext()) {
+          final PatchLine l = iLine.next();
+          final String n;
+          switch (l.getType()) {
+            case CONTEXT:
+              n = Gerrit.RESOURCES.css().diffTextCONTEXT();
+              break;
+            case DELETE:
+              n = Gerrit.RESOURCES.css().diffTextDELETE();
+              break;
+            case INSERT:
+              n = Gerrit.RESOURCES.css().diffTextINSERT();
+              break;
+            default:
+              continue;
+          }
+          while (!fmt.getStyleName(row, PC).contains(n)) {
+            row++;
+          }
+          setRowItem(row++, l);
+        }
       }
-      while (!fmt.getStyleName(row, PC).contains(n)) {
-        row++;
-      }
-      setRowItem(row++, l);
     }
   }
 
@@ -222,10 +356,28 @@
 
     final ArrayList<PatchLineComment> all = new ArrayList<PatchLineComment>();
     for (int row = 0; row < table.getRowCount();) {
-      if (getRowItem(row) instanceof PatchLine) {
+      final List<PatchLineComment> fora;
+      final List<PatchLineComment> forb;
+      if (row == R_HEAD) {
+        fora = cd.getForA(R_HEAD);
+        forb = cd.getForB(R_HEAD);
+        row++;
+
+        if (!fora.isEmpty()) {
+          row = insert(fora, row, expandComments);
+        }
+        rowOfTableHeaderB = row;
+        borderRowOfFileComment = row + 1;
+        if (!forb.isEmpty()) {
+          row++;// Skip the Header of sideB.
+          row = insert(forb, row, expandComments);
+          borderRowOfFileComment = row;
+          createFileCommentBorderRow();
+        }
+      } else if (getRowItem(row) instanceof PatchLine) {
         final PatchLine pLine = (PatchLine) getRowItem(row);
-        final List<PatchLineComment> fora = cd.getForA(pLine.getLineA());
-        final List<PatchLineComment> forb = cd.getForB(pLine.getLineB());
+        fora = cd.getForA(pLine.getLineA());
+        forb = cd.getForB(pLine.getLineB());
         row++;
 
         if (!fora.isEmpty() && !forb.isEmpty()) {
@@ -243,36 +395,84 @@
         }
       } else {
         row++;
+        continue;
       }
     }
   }
 
+  private void defaultStyle(final int row, final CellFormatter fmt) {
+    fmt.addStyleName(row, PC - 2, Gerrit.RESOURCES.css().lineNumber());
+    fmt.addStyleName(row, PC - 2, Gerrit.RESOURCES.css().rightBorder());
+    fmt.addStyleName(row, PC - 1, Gerrit.RESOURCES.css().lineNumber());
+    fmt.addStyleName(row, PC, Gerrit.RESOURCES.css().diffText());
+  }
 
   @Override
   protected void insertRow(final int row) {
     super.insertRow(row);
     final CellFormatter fmt = table.getCellFormatter();
-    fmt.addStyleName(row, PC - 2, Gerrit.RESOURCES.css().lineNumber());
-    fmt.addStyleName(row, PC - 1, Gerrit.RESOURCES.css().lineNumber());
-    fmt.addStyleName(row, PC, Gerrit.RESOURCES.css().diffText());
+    defaultStyle(row, fmt);
+  }
+
+  @Override
+  protected PatchScreen.Type getPatchScreenType() {
+    return PatchScreen.Type.UNIFIED;
   }
 
   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);
+      if (c.getLine() == R_HEAD) {
+        insertFileCommentRow(row);
+      } else {
+        insertRow(row);
+      }
       bindComment(row, PC, c, !ci.hasNext(), expandComment);
       row++;
     }
     return row;
   }
 
+  @Override
+  protected void insertFileCommentRow(final int row) {
+    table.insertRow(row);
+    final CellFormatter fmt = table.getCellFormatter();
+
+    fmt.addStyleName(row, C_ARROW, //
+        Gerrit.RESOURCES.css().iconCellOfFileCommentRow());
+    defaultStyle(row, fmt);
+
+    fmt.addStyleName(row, C_ARROW, //
+        Gerrit.RESOURCES.css().cellsNextToFileComment());
+    fmt.addStyleName(row, PC - 2, //
+        Gerrit.RESOURCES.css().cellsNextToFileComment());
+    fmt.addStyleName(row, PC - 1, //
+        Gerrit.RESOURCES.css().cellsNextToFileComment());
+  }
+
+  private void createFileCommentBorderRow() {
+    if (!isFileCommentBorderRowExist) {
+      isFileCommentBorderRowExist = true;
+      table.insertRow(borderRowOfFileComment);
+      final CellFormatter fmt = table.getCellFormatter();
+      fmt.addStyleName(borderRowOfFileComment, C_ARROW, //
+          Gerrit.RESOURCES.css().iconCellOfFileCommentRow());
+      defaultStyle(borderRowOfFileComment, fmt);
+
+      final Element iconCell =
+          fmt.getElement(borderRowOfFileComment, C_ARROW);
+      UIObject.setStyleName(DOM.getParent(iconCell), //
+          Gerrit.RESOURCES.css().fileCommentBorder(), true);
+    }
+  }
+
   private void appendFileHeader(final SafeHtmlBuilder m, final String line) {
     openLine(m);
-    padLineNumber(m);
-    padLineNumber(m);
+    padLineNumberForSideA(m);
+    padLineNumberForSideB(m);
 
     m.openTd();
+    m.setStyleName(Gerrit.RESOURCES.css().fileLine());
     m.addStyleName(Gerrit.RESOURCES.css().diffText());
     m.addStyleName(Gerrit.RESOURCES.css().diffTextFileHeader());
     m.append(line);
@@ -282,10 +482,11 @@
 
   private void appendHunkHeader(final SafeHtmlBuilder m, final Hunk hunk) {
     openLine(m);
-    padLineNumber(m);
-    padLineNumber(m);
+    padLineNumberForSideA(m);
+    padLineNumberForSideB(m);
 
     m.openTd();
+    m.setStyleName(Gerrit.RESOURCES.css().fileLine());
     m.addStyleName(Gerrit.RESOURCES.css().diffText());
     m.addStyleName(Gerrit.RESOURCES.css().diffTextHunkHeader());
     m.append("@@ -");
@@ -323,6 +524,7 @@
       final SparseHtmlFile src, final int i) {
     final SafeHtml text = src.getSafeHtmlLine(i);
     m.openTd();
+    m.setStyleName(Gerrit.RESOURCES.css().fileLine());
     m.addStyleName(Gerrit.RESOURCES.css().diffText());
     switch (type) {
       case CONTEXT:
@@ -346,14 +548,16 @@
         m.append("+");
         m.append(text);
         break;
+      case REPLACE:
+        break;
     }
     m.closeTd();
   }
 
   private void appendNoLF(final SafeHtmlBuilder m) {
     openLine(m);
-    padLineNumber(m);
-    padLineNumber(m);
+    padLineNumberForSideA(m);
+    padLineNumberForSideB(m);
     m.openTd();
     m.addStyleName(Gerrit.RESOURCES.css().diffText());
     m.addStyleName(Gerrit.RESOURCES.css().diffTextNoLF());
@@ -370,20 +574,58 @@
     m.closeTd();
   }
 
+  private void openTableHeaderLine(final SafeHtmlBuilder m) {
+    m.openTr();
+    m.openTd();
+    m.setStyleName(Gerrit.RESOURCES.css().iconCell());
+    m.addStyleName(Gerrit.RESOURCES.css().fileColumnHeader());
+    m.closeTd();
+  }
+
   private void closeLine(final SafeHtmlBuilder m) {
     m.closeTr();
   }
 
-  private void padLineNumber(final SafeHtmlBuilder m) {
+  private void padLineNumberForSideB(final SafeHtmlBuilder m) {
     m.openTd();
     m.setStyleName(Gerrit.RESOURCES.css().lineNumber());
     m.closeTd();
   }
 
-  private void appendLineNumber(final SafeHtmlBuilder m, final int idx) {
+  private void padLineNumberForSideA(final SafeHtmlBuilder m) {
+    m.openTd();
+    m.setStyleName(Gerrit.RESOURCES.css().lineNumber());
+    m.addStyleName(Gerrit.RESOURCES.css().rightBorder());
+    m.closeTd();
+  }
+
+  private void appendLineNumberForSideB(final SafeHtmlBuilder m, final int idx) {
     m.openTd();
     m.setStyleName(Gerrit.RESOURCES.css().lineNumber());
     m.append(SafeHtml.asis("<a href=\"javascript:void(0)\">"+ (idx + 1) + "</a>"));
     m.closeTd();
   }
+
+  private void appendLineNumberForSideA(final SafeHtmlBuilder m, final int idx) {
+    m.openTd();
+    m.setStyleName(Gerrit.RESOURCES.css().lineNumber());
+    m.addStyleName(Gerrit.RESOURCES.css().rightBorder());
+    m.append(SafeHtml.asis("<a href=\"javascript:void(0)\">"+ (idx + 1) + "</a>"));
+    m.closeTd();
+  }
+
+  private void padLineNumberOnTableHeaderForSideB(final SafeHtmlBuilder m) {
+    m.openTd();
+    m.setStyleName(Gerrit.RESOURCES.css().lineNumber());
+    m.addStyleName(Gerrit.RESOURCES.css().fileColumnHeader());
+    m.closeTd();
+  }
+
+  private void padLineNumberOnTableHeaderForSideA(final SafeHtmlBuilder m) {
+    m.openTd();
+    m.setStyleName(Gerrit.RESOURCES.css().lineNumber());
+    m.addStyleName(Gerrit.RESOURCES.css().fileColumnHeader());
+    m.addStyleName(Gerrit.RESOURCES.css().rightBorder());
+    m.closeTd();
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/plugins/PluginMap.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/plugins/PluginMap.java
index 6eca206..e4c5159 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/plugins/PluginMap.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/plugins/PluginMap.java
@@ -16,13 +16,14 @@
 
 import com.google.gerrit.client.rpc.NativeMap;
 import com.google.gerrit.client.rpc.RestApi;
-import com.google.gwtjsonrpc.common.AsyncCallback;
+import com.google.gwt.user.client.rpc.AsyncCallback;
 
 /** Plugins available from {@code /plugins/}. */
 public class PluginMap extends NativeMap<PluginInfo> {
   public static void all(AsyncCallback<PluginMap> callback) {
-    new RestApi("/plugins/").addParameterTrue("all")
-        .send(NativeMap.copyKeysIntoChildren(callback));
+    new RestApi("/plugins/")
+      .addParameterTrue("all")
+      .get(NativeMap.copyKeysIntoChildren(callback));
   }
 
   protected PluginMap() {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
new file mode 100644
index 0000000..a6676dc
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectApi.java
@@ -0,0 +1,51 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.gerrit.client.projects;
+
+import com.google.gerrit.client.VoidResult;
+import com.google.gerrit.client.rpc.RestApi;
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+
+public class ProjectApi {
+  /** Create a new project */
+  public static void createProject(String projectName, String parent,
+      Boolean createEmptyCcommit, Boolean permissionsOnly,
+      AsyncCallback<VoidResult> asyncCallback) {
+    ProjectInput input = ProjectInput.create();
+    input.setName(projectName);
+    input.setParent(parent);
+    input.setPermissionsOnly(permissionsOnly);
+    input.setCreateEmptyCommit(createEmptyCcommit);
+    new RestApi("/projects/").id(projectName).ifNoneMatch()
+        .put(input, asyncCallback);
+  }
+
+  private static class ProjectInput extends JavaScriptObject {
+    static ProjectInput create() {
+      return (ProjectInput) createObject();
+    }
+
+    protected ProjectInput() {
+    }
+
+    final native void setName(String n) /*-{ if(n)this.name=n; }-*/;
+
+    final native void setParent(String p) /*-{ if(p)this.parent=p; }-*/;
+
+    final native void setPermissionsOnly(boolean po) /*-{ if(po)this.permissions_only=po; }-*/;
+
+    final native void setCreateEmptyCommit(boolean cc) /*-{ if(cc)this.create_empty_commit=cc; }-*/;
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectMap.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectMap.java
index 408919e..bbeabd3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectMap.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/projects/ProjectMap.java
@@ -16,8 +16,7 @@
 
 import com.google.gerrit.client.rpc.NativeMap;
 import com.google.gerrit.client.rpc.RestApi;
-import com.google.gwtjsonrpc.common.AsyncCallback;
-import com.google.gwt.http.client.URL;
+import com.google.gwt.user.client.rpc.AsyncCallback;
 
 /** Projects available from {@code /projects/}. */
 public class ProjectMap extends NativeMap<ProjectInfo> {
@@ -26,7 +25,7 @@
         .addParameterRaw("type", "ALL")
         .addParameterTrue("all")
         .addParameterTrue("d") // description
-        .send(NativeMap.copyKeysIntoChildren(callback));
+        .get(NativeMap.copyKeysIntoChildren(callback));
   }
 
   public static void permissions(AsyncCallback<ProjectMap> callback) {
@@ -34,7 +33,7 @@
         .addParameterRaw("type", "PERMISSIONS")
         .addParameterTrue("all")
         .addParameterTrue("d") // description
-        .send(NativeMap.copyKeysIntoChildren(callback));
+        .get(NativeMap.copyKeysIntoChildren(callback));
   }
 
   public static void parentCandidates(AsyncCallback<ProjectMap> callback) {
@@ -42,15 +41,28 @@
         .addParameterRaw("type", "PARENT_CANDIDATES")
         .addParameterTrue("all")
         .addParameterTrue("d") // description
-        .send(NativeMap.copyKeysIntoChildren(callback));
+        .get(NativeMap.copyKeysIntoChildren(callback));
   }
 
   public static void suggest(String prefix, int limit, AsyncCallback<ProjectMap> cb) {
-    new RestApi("/projects/" + URL.encode(prefix).replaceAll("[?]", "%3F"))
-        .addParameterRaw("type", "ALL")
+    new RestApi("/projects/")
+        .addParameter("p", prefix)
         .addParameter("n", limit)
+        .addParameterRaw("type", "ALL")
         .addParameterTrue("d") // description
-        .send(NativeMap.copyKeysIntoChildren(cb));
+        .get(NativeMap.copyKeysIntoChildren(cb));
+  }
+
+  public static void match(String match, AsyncCallback<ProjectMap> cb) {
+    if (match == null || "".equals(match)) {
+      all(cb);
+    } else {
+      new RestApi("/projects/")
+          .addParameter("m", match)
+          .addParameterRaw("type", "ALL")
+          .addParameterTrue("d") // description
+          .get(NativeMap.copyKeysIntoChildren(cb));
+    }
   }
 
   protected ProjectMap() {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/queryIcon.png b/gerrit-gwtui/src/main/java/com/google/gerrit/client/queryIcon.png
new file mode 100644
index 0000000..5aace51
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/queryIcon.png
Binary files differ
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/CallbackGroup.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/CallbackGroup.java
new file mode 100644
index 0000000..0b1f905
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/CallbackGroup.java
@@ -0,0 +1,121 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.rpc;
+
+import com.google.gwt.user.client.rpc.AsyncCallback;
+
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * Class for grouping together callbacks and calling them in order.
+ * <p>
+ * Callbacks are added to the group with {@link #add(AsyncCallback)}, which
+ * returns a wrapped callback suitable for passing to an asynchronous RPC call.
+ * The enclosing group buffers returned results and ensures that
+ * {@code onSuccess} is called exactly once for each callback in the group, in
+ * the same order that callbacks were added. This allows callers to, for
+ * example, use a {@link ScreenLoadCallback} as the last callback in the list
+ * and only display the screen once all callbacks have succeeded.
+ * <p>
+ * In the event of a failure, the <em>first</em> caught exception is sent to
+ * <em>all</em> callbacks' {@code onFailure} methods, in order; subsequent
+ * successes or failures are all ignored. Note that this means
+ * {@code onFailure} may be called with an exception unrelated to the callback
+ * processing it.
+ */
+public class CallbackGroup {
+  private final List<Object> callbacks;
+  private final Map<Object, Object> results;
+  private boolean failed;
+
+  public CallbackGroup() {
+    callbacks = new ArrayList<Object>();
+    results = new HashMap<Object, Object>();
+  }
+
+  public <T> AsyncCallback<T> add(final AsyncCallback<T> cb) {
+    callbacks.add(cb);
+    return new AsyncCallback<T>() {
+      @Override
+      public void onSuccess(T result) {
+        results.put(cb, result);
+        CallbackGroup.this.onSuccess();
+      }
+
+      @Override
+      public void onFailure(Throwable caught) {
+        CallbackGroup.this.onFailure(caught);
+      }
+    };
+  }
+
+  public <T> com.google.gwtjsonrpc.common.AsyncCallback<T> addGwtjsonrpc(
+      final com.google.gwtjsonrpc.common.AsyncCallback<T> cb) {
+    callbacks.add(cb);
+    return new com.google.gwtjsonrpc.common.AsyncCallback<T>() {
+      @Override
+      public void onSuccess(T result) {
+        results.put(cb, result);
+        CallbackGroup.this.onSuccess();
+      }
+
+      @Override
+      public void onFailure(Throwable caught) {
+        CallbackGroup.this.onFailure(caught);
+      }
+    };
+  }
+
+  private void onSuccess() {
+    if (results.size() < callbacks.size()) {
+      return;
+    }
+    for (Object o : callbacks) {
+      Object result = results.get(o);
+      if (o instanceof AsyncCallback) {
+        @SuppressWarnings("unchecked")
+        AsyncCallback<Object> cb = (AsyncCallback<Object>) o;
+        cb.onSuccess(result);
+      } else {
+        @SuppressWarnings("unchecked")
+        com.google.gwtjsonrpc.common.AsyncCallback<Object> cb =
+            (com.google.gwtjsonrpc.common.AsyncCallback<Object>) o;
+        cb.onSuccess(result);
+      }
+    }
+  }
+
+  private void onFailure(Throwable caught) {
+    if (failed) {
+      return;
+    }
+    failed = true;
+    for (Object o : callbacks) {
+      if (o instanceof AsyncCallback) {
+        @SuppressWarnings("unchecked")
+        AsyncCallback<Object> cb = (AsyncCallback<Object>) o;
+        cb.onFailure(caught);
+      } else {
+        @SuppressWarnings("unchecked")
+        com.google.gwtjsonrpc.common.AsyncCallback<Object> cb =
+            (com.google.gwtjsonrpc.common.AsyncCallback<Object>) o;
+        cb.onFailure(caught);
+      }
+    }
+  }
+}
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 dce5bb6..06d1f0b 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,14 +24,15 @@
 import com.google.gerrit.common.errors.NoSuchGroupException;
 import com.google.gerrit.common.errors.NotSignedInException;
 import com.google.gwt.core.client.GWT;
-import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.gwt.user.client.rpc.InvocationException;
 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> {
+public abstract class GerritCallback<T> implements
+    com.google.gwtjsonrpc.common.AsyncCallback<T>,
+    com.google.gwt.user.client.rpc.AsyncCallback<T> {
   public void onFailure(final Throwable caught) {
     if (isNotSignedIn(caught) || isInvalidXSRF(caught)) {
       new NotSignedInDialog().center();
@@ -53,7 +54,9 @@
       d.center();
 
     } else if (isNameAlreadyUsed(caught)) {
-      new ErrorDialog(Gerrit.C.nameAlreadyUsedBody()).center();
+      final String msg = caught.getMessage();
+      final String alreadyUsedName = msg.substring(NameAlreadyUsedException.MESSAGE.length());
+      new ErrorDialog(Gerrit.M.nameAlreadyUsedBody(alreadyUsedName)).center();
 
     } else if (isNoSuchGroup(caught)) {
       final String msg = caught.getMessage();
@@ -76,14 +79,16 @@
         && caught.getMessage().equals(JsonConstants.ERROR_INVALID_XSRF);
   }
 
-  private static boolean isNotSignedIn(final Throwable caught) {
-    return caught instanceof RemoteJsonException
-        && caught.getMessage().equals(NotSignedInException.MESSAGE);
+  private static boolean isNotSignedIn(Throwable caught) {
+    return RestApi.isNotSignedIn(caught)
+        || (caught instanceof RemoteJsonException
+           && caught.getMessage().equals(NotSignedInException.MESSAGE));
   }
 
-  protected static boolean isNoSuchEntity(final Throwable caught) {
-    return caught instanceof RemoteJsonException
-        && caught.getMessage().equals(NoSuchEntityException.MESSAGE);
+  protected static boolean isNoSuchEntity(Throwable caught) {
+    return RestApi.isNotFound(caught)
+        || (caught instanceof RemoteJsonException
+            && caught.getMessage().equals(NoSuchEntityException.MESSAGE));
   }
 
   protected static boolean isInactiveAccount(final Throwable caught) {
@@ -98,7 +103,7 @@
 
   private static boolean isNameAlreadyUsed(final Throwable caught) {
     return caught instanceof RemoteJsonException
-        && caught.getMessage().equals(NameAlreadyUsedException.MESSAGE);
+        && caught.getMessage().startsWith(NameAlreadyUsedException.MESSAGE);
   }
 
   private static boolean isNoSuchGroup(final Throwable caught) {
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/NativeList.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/NativeList.java
deleted file mode 100644
index e820fe0..0000000
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/NativeList.java
+++ /dev/null
@@ -1,55 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.client.rpc;
-
-import com.google.gwt.core.client.JavaScriptObject;
-
-import java.util.AbstractList;
-import java.util.List;
-
-/** A read-only list of native JavaScript objects stored in a JSON array. */
-public class NativeList<T extends JavaScriptObject> extends JavaScriptObject {
-  protected NativeList() {
-  }
-
-  public final List<T> asList() {
-    return new AbstractList<T>() {
-      @Override
-      public T set(int index, T element) {
-        T old = NativeList.this.get(index);
-        NativeList.this.set0(index, element);
-        return old;
-      }
-
-      @Override
-      public T get(int index) {
-        return NativeList.this.get(index);
-      }
-
-      @Override
-      public int size() {
-        return NativeList.this.size();
-      }
-    };
-  }
-
-  public final boolean isEmpty() {
-    return size() == 0;
-  }
-
-  public final native int size() /*-{ return this.length; }-*/;
-  public final native T get(int i) /*-{ return this[i]; }-*/;
-  private final native void set0(int i, T v) /*-{ this[i] = v; }-*/;
-}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/NativeMap.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/NativeMap.java
index cde9041..d56eeaf 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/NativeMap.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/NativeMap.java
@@ -15,7 +15,8 @@
 package com.google.gerrit.client.rpc;
 
 import com.google.gwt.core.client.JavaScriptObject;
-import com.google.gwtjsonrpc.common.AsyncCallback;
+import com.google.gwt.core.client.JsArray;
+import com.google.gwt.user.client.rpc.AsyncCallback;
 
 import java.util.Set;
 
@@ -53,7 +54,7 @@
     return Natives.keys(this);
   }
 
-  public final native NativeList<T> values()
+  public final native JsArray<T> values()
   /*-{
     var s = this;
     var v = [];
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/NativeString.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/NativeString.java
new file mode 100644
index 0000000..573c5e7
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/NativeString.java
@@ -0,0 +1,48 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.rpc;
+
+import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.user.client.rpc.AsyncCallback;
+
+/** Wraps a String that was returned from a JSON API. */
+public final class NativeString extends JavaScriptObject {
+  static NativeString wrap(String value) {
+    NativeString ns = (NativeString) createObject();
+    ns.set(value);
+    return ns;
+  }
+
+  public final native String asString() /*-{ return this.s; }-*/;
+  private final native void set(String v) /*-{ this.s = v; }-*/;
+
+  public static final AsyncCallback<NativeString>
+  unwrap(final AsyncCallback<String> cb) {
+    return new AsyncCallback<NativeString>() {
+      @Override
+      public void onSuccess(NativeString result) {
+        cb.onSuccess(result != null ? result.asString() : null);
+      }
+
+      @Override
+      public void onFailure(Throwable caught) {
+        cb.onFailure(caught);
+      }
+    };
+  }
+
+  protected NativeString() {
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/Natives.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/Natives.java
index a6c609c..41f0f23 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/Natives.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/Natives.java
@@ -15,9 +15,12 @@
 package com.google.gerrit.client.rpc;
 
 import com.google.gwt.core.client.JavaScriptObject;
+import com.google.gwt.core.client.JsArray;
 import com.google.gwt.json.client.JSONObject;
 
+import java.util.AbstractList;
 import java.util.Collections;
+import java.util.List;
 import java.util.Set;
 
 public class Natives {
@@ -32,16 +35,51 @@
     return Collections.emptySet();
   }
 
-  public static <T extends JavaScriptObject> T parseJSON(String json) {
-    if (parser == null) {
-      parser = bestJsonParser();
+  public static <T extends JavaScriptObject> List<T> asList(
+      final JsArray<T> arr) {
+    if (arr == null) {
+      return null;
     }
-    // javac generics bug
-    return Natives.<T>parse0(parser, json);
+    return new AbstractList<T>() {
+      @Override
+      public T set(int index, T element) {
+        T old = arr.get(index);
+        arr.set(index, element);
+        return old;
+      }
+
+      @Override
+      public T get(int index) {
+        return arr.get(index);
+      }
+
+      @Override
+      public int size() {
+        return arr.length();
+      }
+    };
+  }
+
+  public static <T extends JavaScriptObject> JsArray<T> arrayOf(T element) {
+    JsArray<T> arr = JavaScriptObject.createArray().cast();
+    arr.push(element);
+    return arr;
+  }
+
+  @SuppressWarnings("unchecked")
+  public static <T extends JavaScriptObject> T parseJSON(String json) {
+    if (json.startsWith("\"")) {
+      return (T) NativeString.wrap(parseString(parser, json));
+    }
+    return Natives.<T> parseObject(parser, json); // javac generics bug
   }
 
   private static native <T extends JavaScriptObject>
-  T parse0(JavaScriptObject p, String s)
+  T parseObject(JavaScriptObject p, String s)
+  /*-{ return p(s); }-*/;
+
+  private static native
+  String parseString(JavaScriptObject p, String s)
   /*-{ return p(s); }-*/;
 
   private static JavaScriptObject parser;
@@ -52,6 +90,10 @@
     return function(s) { return eval('(' + s + ')'); };
   }-*/;
 
+  static {
+    parser = bestJsonParser();
+  }
+
   private Natives() {
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RestApi.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RestApi.java
index 650cacd..4ee63c6 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RestApi.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/RestApi.java
@@ -14,23 +14,38 @@
 
 package com.google.gerrit.client.rpc;
 
+import static com.google.gwt.http.client.RequestBuilder.DELETE;
+import static com.google.gwt.http.client.RequestBuilder.GET;
+import static com.google.gwt.http.client.RequestBuilder.POST;
+import static com.google.gwt.http.client.RequestBuilder.PUT;
+
+import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.RpcStatus;
 import com.google.gwt.core.client.GWT;
 import com.google.gwt.core.client.JavaScriptObject;
 import com.google.gwt.http.client.Request;
 import com.google.gwt.http.client.RequestBuilder;
+import com.google.gwt.http.client.RequestBuilder.Method;
 import com.google.gwt.http.client.RequestCallback;
 import com.google.gwt.http.client.RequestException;
 import com.google.gwt.http.client.Response;
 import com.google.gwt.http.client.URL;
+import com.google.gwt.json.client.JSONException;
+import com.google.gwt.json.client.JSONObject;
+import com.google.gwt.json.client.JSONParser;
+import com.google.gwt.json.client.JSONValue;
+import com.google.gwt.user.client.rpc.AsyncCallback;
 import com.google.gwt.user.client.rpc.StatusCodeException;
-import com.google.gwtjsonrpc.client.RemoteJsonException;
-import com.google.gwtjsonrpc.client.ServerUnavailableException;
-import com.google.gwtjsonrpc.common.AsyncCallback;
-import com.google.gwtjsonrpc.common.JsonConstants;
 
 /** Makes a REST API call to the server. */
 public class RestApi {
+  private static final int SC_UNAVAILABLE = 2;
+  private static final int SC_BAD_TRANSPORT = 3;
+  private static final int SC_BAD_RESPONSE = 4;
+  private static final String JSON_TYPE = "application/json";
+  private static final String JSON_UTF8 = JSON_TYPE + "; charset=utf-8";
+  private static final String TEXT_TYPE = "text/plain";
+
   /**
    * Expected JSON content body prefix that prevents XSSI.
    * <p>
@@ -42,75 +57,131 @@
    */
   private static final String JSON_MAGIC = ")]}'\n";
 
-  private class MyRequestCallback<T extends JavaScriptObject> implements
-      RequestCallback {
-    private final boolean wasGet;
+  /** True if err is a StatusCodeException reporting Not Found. */
+  public static boolean isNotFound(Throwable err) {
+    return isStatus(err, Response.SC_NOT_FOUND);
+  }
+
+  /** True if err is describing a user that is currently anonymous. */
+  public static boolean isNotSignedIn(Throwable err) {
+    if (err instanceof StatusCodeException) {
+      StatusCodeException sce = (StatusCodeException) err;
+      if (sce.getStatusCode() == Response.SC_UNAUTHORIZED) {
+        return true;
+      }
+      return sce.getStatusCode() == Response.SC_FORBIDDEN
+          && (sce.getEncodedResponse().equals("Authentication required")
+              || sce.getEncodedResponse().startsWith("Must be signed-in"));
+    }
+    return false;
+  }
+
+  /** True if err is a StatusCodeException with a specific HTTP code. */
+  public static boolean isStatus(Throwable err, int status) {
+    return err instanceof StatusCodeException
+        && ((StatusCodeException) err).getStatusCode() == status;
+  }
+
+  /** Is the Gerrit Code Review server likely to return this status? */
+  public static boolean isExpected(int statusCode) {
+    switch (statusCode) {
+      case SC_UNAVAILABLE:
+      case 400: // Bad Request
+      case 401: // Unauthorized
+      case 403: // Forbidden
+      case 404: // Not Found
+      case 405: // Method Not Allowed
+      case 409: // Conflict
+      case 412: // Precondition Failed
+      case 429: // Too Many Requests (RFC 6585)
+        return true;
+
+      default:
+        // Assume any other code is not expected. These may be
+        // local proxy server errors outside of our control.
+        return false;
+    }
+  }
+
+  private static class HttpCallback<T extends JavaScriptObject>
+      implements RequestCallback {
     private final AsyncCallback<T> cb;
 
-    public MyRequestCallback(boolean wasGet, AsyncCallback<T> cb) {
-      this.wasGet = wasGet;
+    HttpCallback(AsyncCallback<T> cb) {
       this.cb = cb;
     }
 
     @Override
     public void onResponseReceived(Request req, Response res) {
       int status = res.getStatusCode();
-      if (status != 200) {
+      if (status == Response.SC_NO_CONTENT) {
+        cb.onSuccess(null);
         RpcStatus.INSTANCE.onRpcComplete();
-        if ((400 <= status && status < 600) && isTextBody(res)) {
-          cb.onFailure(new RemoteJsonException(res.getText(), status, null));
-        } else {
-          cb.onFailure(new StatusCodeException(status, res.getStatusText()));
+
+      } else if (200 <= status && status < 300) {
+        if (!isJsonBody(res)) {
+          RpcStatus.INSTANCE.onRpcComplete();
+          cb.onFailure(new StatusCodeException(SC_BAD_RESPONSE, "Expected "
+              + JSON_TYPE + "; received Content-Type: "
+              + res.getHeader("Content-Type")));
+          return;
         }
-        return;
-      }
 
-      if (!isJsonBody(res)) {
+        T data;
+        try {
+          // javac generics bug
+          data = RestApi.<T>cast(parseJson(res));
+        } catch (JSONException e) {
+          RpcStatus.INSTANCE.onRpcComplete();
+          cb.onFailure(new StatusCodeException(SC_BAD_RESPONSE,
+              "Invalid JSON: " + e.getMessage()));
+          return;
+        }
+
+        cb.onSuccess(data);
         RpcStatus.INSTANCE.onRpcComplete();
-        cb.onFailure(new RemoteJsonException("Invalid JSON"));
-        return;
-      }
 
-      String json = res.getText();
-      if (!json.startsWith(JSON_MAGIC)) {
+      } else {
+        String msg;
+        if (isTextBody(res)) {
+          msg = res.getText().trim();
+        } else if (isJsonBody(res)) {
+          JSONValue v;
+          try {
+            v = parseJson(res);
+          } catch (JSONException e) {
+            v = null;
+          }
+          if (v != null && v.isString() != null) {
+            msg = v.isString().stringValue();
+          } else {
+            msg = trimJsonMagic(res.getText()).trim();
+          }
+        } else {
+          msg = res.getStatusText();
+        }
+
         RpcStatus.INSTANCE.onRpcComplete();
-        cb.onFailure(new RemoteJsonException("Invalid JSON"));
-        return;
+        cb.onFailure(new StatusCodeException(status, msg));
       }
-      json = json.substring(JSON_MAGIC.length());
-
-      if (wasGet && json.startsWith("{\"_authkey\":")) {
-        RestApi.this.resendPost(cb, json);
-        return;
-      }
-
-      T data;
-      try {
-        // javac generics bug
-        data = Natives.<T> parseJSON(json);
-      } catch (RuntimeException e) {
-        RpcStatus.INSTANCE.onRpcComplete();
-        cb.onFailure(new RemoteJsonException("Invalid JSON"));
-        return;
-      }
-
-      cb.onSuccess(data);
-      RpcStatus.INSTANCE.onRpcComplete();
     }
 
     @Override
     public void onError(Request req, Throwable err) {
       RpcStatus.INSTANCE.onRpcComplete();
       if (err.getMessage().contains("XmlHttpRequest.status")) {
-        cb.onFailure(new ServerUnavailableException());
+        cb.onFailure(new StatusCodeException(
+            SC_UNAVAILABLE,
+            RpcConstants.C.errorServerUnavailable()));
       } else {
-        cb.onFailure(err);
+        cb.onFailure(new StatusCodeException(SC_BAD_TRANSPORT, err.getMessage()));
       }
     }
   }
 
   private StringBuilder url;
   private boolean hasQueryParams;
+  private String ifNoneMatch;
 
   /**
    * Initialize a new API call.
@@ -131,10 +202,40 @@
     url.append(name);
   }
 
+  public RestApi view(String name) {
+    return idRaw(name);
+  }
+
+  public RestApi id(String id) {
+    return idRaw(URL.encodeQueryString(id));
+  }
+
+  public RestApi id(int id) {
+    return idRaw(Integer.toString(id));
+  }
+
+  public RestApi idRaw(String name) {
+    if (hasQueryParams) {
+      throw new IllegalStateException();
+    }
+    if (url.charAt(url.length() - 1) != '/') {
+      url.append('/');
+    }
+    url.append(name);
+    return this;
+  }
+
   public RestApi addParameter(String name, String value) {
     return addParameterRaw(name, URL.encodeQueryString(value));
   }
 
+  public RestApi addParameter(String name, String... value) {
+    for (String val : value) {
+      addParameter(name, val);
+    }
+    return this;
+  }
+
   public RestApi addParameterTrue(String name) {
     return addParameterRaw(name, null);
   }
@@ -165,40 +266,87 @@
     return this;
   }
 
-  public <T extends JavaScriptObject> void send(final AsyncCallback<T> cb) {
-    RequestBuilder req = new RequestBuilder(RequestBuilder.GET, url.toString());
-    req.setHeader("Accept", JsonConstants.JSON_TYPE);
-    req.setCallback(new MyRequestCallback<T>(true, cb));
+  public RestApi ifNoneMatch() {
+    return ifNoneMatch("*");
+  }
+
+  public RestApi ifNoneMatch(String etag) {
+    ifNoneMatch = etag;
+    return this;
+  }
+
+  public String url() {
+    return url.toString();
+  }
+
+  public <T extends JavaScriptObject> void get(AsyncCallback<T> cb) {
+    send(GET, cb);
+  }
+
+  public <T extends JavaScriptObject> void delete(AsyncCallback<T> cb) {
+    send(DELETE, cb);
+  }
+
+  private <T extends JavaScriptObject> void send(
+      Method method, AsyncCallback<T> cb) {
+    HttpCallback<T> httpCallback = new HttpCallback<T>(cb);
     try {
       RpcStatus.INSTANCE.onRpcStart();
-      req.send();
+      request(method).sendRequest(null, httpCallback);
     } catch (RequestException e) {
-      RpcStatus.INSTANCE.onRpcComplete();
-      cb.onFailure(e);
+      httpCallback.onError(null, e);
     }
   }
 
-  private <T extends JavaScriptObject> void resendPost(
-      final AsyncCallback<T> cb, String token) {
-    RequestBuilder req = new RequestBuilder(RequestBuilder.POST, url.toString());
-    req.setHeader("Accept", JsonConstants.JSON_TYPE);
-    req.setHeader("Content-Type", JsonConstants.JSON_TYPE);
-    req.setRequestData(token);
-    req.setCallback(new MyRequestCallback<T>(false, cb));
+  public <T extends JavaScriptObject> void post(
+      JavaScriptObject content,
+      AsyncCallback<T> cb) {
+    sendJSON(POST, content, cb);
+  }
+
+  public <T extends JavaScriptObject> void put(AsyncCallback<T> cb) {
+    send(PUT, cb);
+  }
+
+  public <T extends JavaScriptObject> void put(
+      JavaScriptObject content,
+      AsyncCallback<T> cb) {
+    sendJSON(PUT, content, cb);
+  }
+
+  private <T extends JavaScriptObject> void sendJSON(
+      Method method, JavaScriptObject content,
+      AsyncCallback<T> cb) {
+    HttpCallback<T> httpCallback = new HttpCallback<T>(cb);
     try {
-      req.send();
+      RpcStatus.INSTANCE.onRpcStart();
+      String body = new JSONObject(content).toString();
+      RequestBuilder req = request(method);
+      req.setHeader("Content-Type", JSON_UTF8);
+      req.sendRequest(body, httpCallback);
     } catch (RequestException e) {
-      RpcStatus.INSTANCE.onRpcComplete();
-      cb.onFailure(e);
+      httpCallback.onError(null, e);
     }
   }
 
+  private RequestBuilder request(Method method) {
+    RequestBuilder req = new RequestBuilder(method, url());
+    if (ifNoneMatch != null) {
+      req.setHeader("If-None-Match", ifNoneMatch);
+    }
+    req.setHeader("Accept", JSON_TYPE);
+    if (Gerrit.getXGerritAuth() != null) {
+      req.setHeader("X-Gerrit-Auth", Gerrit.getXGerritAuth());
+    }
+    return req;
+  }
+
   private static boolean isJsonBody(Response res) {
-    return isContentType(res, JsonConstants.JSON_TYPE);
+    return isContentType(res, JSON_TYPE);
   }
 
   private static boolean isTextBody(Response res) {
-    return isContentType(res, "text/plain");
+    return isContentType(res, TEXT_TYPE);
   }
 
   private static boolean isContentType(Response res, String want) {
@@ -212,4 +360,35 @@
     }
     return want.equals(type);
   }
+
+  private static JSONValue parseJson(Response res)
+      throws JSONException {
+    String json = trimJsonMagic(res.getText());
+    if (json.isEmpty()) {
+      throw new JSONException("response was empty");
+    }
+    return JSONParser.parseStrict(json);
+  }
+
+  private static String trimJsonMagic(String json) {
+    if (json.startsWith(JSON_MAGIC)) {
+      json = json.substring(JSON_MAGIC.length());
+    }
+    return json;
+  }
+
+  @SuppressWarnings("unchecked")
+  private static <T extends JavaScriptObject> T cast(JSONValue val) {
+    if (val.isObject() != null) {
+      return (T) val.isObject().getJavaScriptObject();
+    } else if (val.isArray() != null) {
+      return (T) val.isArray().getJavaScriptObject();
+    } else if (val.isString() != null) {
+      return (T) NativeString.wrap(val.isString().stringValue());
+    } else if (val.isNull() != null) {
+      return null;
+    } else {
+      throw new JSONException("unsupported JSON type");
+    }
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/ScreenLoadCallback.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/ScreenLoadCallback.java
index eb13834..b8b9209 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/ScreenLoadCallback.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/ScreenLoadCallback.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.NotFoundScreen;
+import com.google.gerrit.client.NotSignedInDialog;
 import com.google.gerrit.client.ui.Screen;
 import com.google.gerrit.common.errors.NoSuchEntityException;
 
@@ -43,7 +44,11 @@
   @Override
   public void onFailure(final Throwable caught) {
     if (isNoSuchEntity(caught)) {
-      Gerrit.display(screen.getToken(), new NotFoundScreen());
+      if (Gerrit.isSignedIn()) {
+        Gerrit.display(screen.getToken(), new NotFoundScreen());
+      } else {
+        new NotSignedInDialog().center();
+      }
     } else {
       super.onFailure(caught);
     }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/TransformCallback.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/TransformCallback.java
index 2cd22cb..93c86a2 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/TransformCallback.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/rpc/TransformCallback.java
@@ -14,7 +14,7 @@
 
 package com.google.gerrit.client.rpc;
 
-import com.google.gwtjsonrpc.common.AsyncCallback;
+import com.google.gwt.user.client.rpc.AsyncCallback;
 
 /** Transforms a value and passes it on to another callback. */
 public abstract class TransformCallback<I, O> implements AsyncCallback<I>{
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 5da00cd..278e159 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
@@ -20,7 +20,6 @@
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gwt.user.client.ui.SuggestOracle;
-import com.google.gwtexpui.safehtml.client.HighlightSuggestOracle;
 
 import java.util.ArrayList;
 import java.util.HashMap;
@@ -28,14 +27,14 @@
 import java.util.Map;
 
 /** Suggestion Oracle for AccountGroup entities. */
-public class AccountGroupSuggestOracle extends HighlightSuggestOracle {
+public class AccountGroupSuggestOracle extends SuggestAfterTypingNCharsOracle {
   private Map<String, AccountGroup.UUID> priorResults =
       new HashMap<String, AccountGroup.UUID>();
 
   private Project.NameKey projectName;
 
   @Override
-  public void onRequestSuggestions(final Request req, final Callback callback) {
+  public void _onRequestSuggestions(final Request req, final Callback callback) {
     RpcStatus.hide(new Runnable() {
       public void run() {
         SuggestUtil.SVC.suggestAccountGroupForProject(
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLink.java
index 790102c..fd52777 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLink.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountLink.java
@@ -16,32 +16,51 @@
 
 import com.google.gerrit.client.FormatUtil;
 import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.account.AccountInfo;
 import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.common.data.AccountInfo;
 import com.google.gerrit.common.data.AccountInfoCache;
 import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.UserIdentity;
 
 /** Link to any user's account dashboard. */
 public class AccountLink extends InlineHyperlink {
   /** Create a link after locating account details from an active cache. */
-  public static AccountLink link(final AccountInfoCache cache,
-      final Account.Id id) {
-    final AccountInfo ai = cache.get(id);
+  public static AccountLink link(AccountInfoCache cache, Account.Id id) {
+    com.google.gerrit.common.data.AccountInfo ai = cache.get(id);
     return ai != null ? new AccountLink(ai) : null;
   }
 
-  public AccountLink(final AccountInfo ai) {
-    super(FormatUtil.name(ai), PageLinks.toAccountQuery(owner(ai)));
-    setTitle(FormatUtil.nameEmail(ai));
+  public AccountLink(com.google.gerrit.common.data.AccountInfo ai) {
+    this(FormatUtil.asInfo(ai));
+  }
+
+  public AccountLink(UserIdentity ident) {
+    this(AccountInfo.create(
+        ident.getAccount().get(),
+        ident.getName(),
+        ident.getEmail()));
+  }
+
+  public AccountLink(AccountInfo info) {
+    this(info, Change.Status.NEW);
+  }
+
+  public AccountLink(AccountInfo info, Change.Status status) {
+    super(FormatUtil.name(info), PageLinks.toAccountQuery(owner(info), status));
+    setTitle(FormatUtil.nameEmail(info));
   }
 
   private static String owner(AccountInfo ai) {
-    if (ai.getPreferredEmail() != null) {
-      return ai.getPreferredEmail();
-    } else if (ai.getFullName() != null) {
-      return ai.getFullName();
+    if (ai.email() != null) {
+      return ai.email();
+    } else if (ai.name() != null) {
+      return ai.name();
+    } else if (ai._account_id() != 0) {
+      return "" + ai._account_id();
+    } else {
+      return "";
     }
-    return "" + ai.getId().get();
   }
 
   @Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountSuggestOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountSuggestOracle.java
index bcf6438..0bf0ea9 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountSuggestOracle.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/AccountSuggestOracle.java
@@ -19,15 +19,14 @@
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gerrit.common.data.AccountInfo;
 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 Account entities. */
-public class AccountSuggestOracle extends HighlightSuggestOracle {
+public class AccountSuggestOracle extends SuggestAfterTypingNCharsOracle {
   @Override
-  public void onRequestSuggestions(final Request req, final Callback callback) {
+  public void _onRequestSuggestions(final Request req, final Callback callback) {
     RpcStatus.hide(new Runnable() {
       public void run() {
         SuggestUtil.SVC.suggestAccount(req.getQuery(), Boolean.TRUE,
@@ -54,11 +53,11 @@
     }
 
     public String getDisplayString() {
-      return FormatUtil.nameEmail(info);
+      return FormatUtil.nameEmail(FormatUtil.asInfo(info));
     }
 
     public String getReplacementString() {
-      return FormatUtil.nameEmail(info);
+      return FormatUtil.nameEmail(FormatUtil.asInfo(info));
     }
   }
 }
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 72a011f..d4aaa4c 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
@@ -72,6 +72,7 @@
     nameTxt.addSelectionHandler(new SelectionHandler<Suggestion>() {
       @Override
       public void onSelection(SelectionEvent<Suggestion> event) {
+        nameTxtBox.setFocus(true);
         if (submitOnSelection) {
           submitOnSelection = false;
           doAdd();
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommandMenuItem.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommandMenuItem.java
index fdb5c56..72bf06c 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommandMenuItem.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/CommandMenuItem.java
@@ -15,10 +15,10 @@
 package com.google.gerrit.client.ui;
 
 import com.google.gerrit.client.Gerrit;
+import com.google.gwt.aria.client.Roles;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.user.client.Command;
-import com.google.gwt.user.client.ui.Accessibility;
 import com.google.gwt.user.client.ui.Anchor;
 
 public class CommandMenuItem extends Anchor implements ClickHandler {
@@ -27,7 +27,7 @@
   public CommandMenuItem(final String text, final Command cmd) {
     super(text);
     setStyleName(Gerrit.RESOURCES.css().menuItem());
-    Accessibility.setRole(getElement(), Accessibility.ROLE_MENUITEM);
+    Roles.getMenuitemRole().set(getElement());
     addClickHandler(this);
     command = cmd;
   }
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 ddd2b27..8f8c7ea 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
@@ -16,7 +16,7 @@
 
 import com.google.gerrit.client.FormatUtil;
 import com.google.gerrit.client.Gerrit;
-import com.google.gerrit.common.data.AccountInfo;
+import com.google.gerrit.client.account.AccountInfo;
 import com.google.gwt.event.dom.client.BlurEvent;
 import com.google.gwt.event.dom.client.BlurHandler;
 import com.google.gwt.event.dom.client.ClickEvent;
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
index 26b61f5..263703e 100644
--- 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
@@ -25,6 +25,7 @@
 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.FocusWidget;
 import com.google.gwt.user.client.ui.PopupPanel;
 import com.google.gwtexpui.globalkey.client.GlobalKey;
 import com.google.gwtexpui.globalkey.client.NpTextArea;
@@ -38,6 +39,7 @@
   protected final Button cancelButton;
   protected final FlowPanel buttonPanel;
   protected AsyncCallback<T> callback;
+  protected FocusWidget focusOn;
 
   protected boolean sent = false;
 
@@ -45,7 +47,6 @@
       AsyncCallback<T> callback) {
     super(/* auto hide */false, /* modal */true);
     this.callback = callback;
-
     setGlassEnabled(true);
     setText(title);
 
@@ -55,7 +56,7 @@
     message.setCharacterWidth(60);
     message.setVisibleLines(10);
     DOM.setElementPropertyBoolean(message.getElement(), "spellcheck", true);
-
+    setFocusOn(message);
     sendButton = new Button(Util.C.commentedActionButtonSend());
     sendButton.addClickHandler(new ClickHandler() {
       @Override
@@ -91,6 +92,10 @@
     addCloseHandler(this);
   }
 
+  public void setFocusOn(FocusWidget focusWidget) {
+    focusOn = focusWidget;
+  }
+
   public void enableButtons(boolean enable) {
     sendButton.setEnabled(enable);
     cancelButton.setEnabled(enable);
@@ -100,7 +105,9 @@
   public void center() {
     super.center();
     GlobalKey.dialog(this);
-    message.setFocus(true);
+    if (focusOn != null) {
+      focusOn.setFocus(true);
+    }
   }
 
   @Override
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FilteredUserInterface.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FilteredUserInterface.java
new file mode 100644
index 0000000..02244d0
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/FilteredUserInterface.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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;
+
+public interface FilteredUserInterface {
+  /**
+   * Return the value by which the user interface is currently filtered.
+   *
+   * @return value by which the user interface is currently filtered,
+   *         <code>null</code> or empty String if currently no filter is applied
+   */
+  public String getCurrentFilter();
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/HighlightingInlineHyperlink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/HighlightingInlineHyperlink.java
new file mode 100644
index 0000000..00825a3
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/HighlightingInlineHyperlink.java
@@ -0,0 +1,38 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.ui;
+
+
+public class HighlightingInlineHyperlink extends InlineHyperlink {
+
+  private String toHighlight;
+
+  public HighlightingInlineHyperlink(final String text, final String token,
+      final String toHighlight) {
+    super(text, token);
+    this.toHighlight = toHighlight;
+    highlight(text, toHighlight);
+  }
+
+  @Override
+  public void setText(String text) {
+    super.setText(text);
+    highlight(text, toHighlight);
+  }
+
+  private void highlight(final String text, final String toHighlight) {
+    setHTML(Util.highlight(text, toHighlight));
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/HighlightingProjectsTable.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/HighlightingProjectsTable.java
new file mode 100644
index 0000000..fc60360
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/HighlightingProjectsTable.java
@@ -0,0 +1,37 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.projects.ProjectInfo;
+import com.google.gerrit.client.projects.ProjectMap;
+import com.google.gwt.user.client.ui.InlineHTML;
+
+public class HighlightingProjectsTable extends ProjectsTable {
+  private String toHighlight;
+
+  public void display(final ProjectMap projects, final String toHighlight) {
+    this.toHighlight = toHighlight;
+    super.display(projects);
+  }
+
+  @Override
+  protected void populate(final int row, final ProjectInfo k) {
+    table.setWidget(row, 1,
+        new InlineHTML(Util.highlight(k.name(), toHighlight)));
+    table.setText(row, 2, k.description());
+
+    setRowItem(row, k);
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/IgnoreOutdatedFilterResultsCallbackWrapper.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/IgnoreOutdatedFilterResultsCallbackWrapper.java
new file mode 100644
index 0000000..c9cadcc
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/IgnoreOutdatedFilterResultsCallbackWrapper.java
@@ -0,0 +1,50 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.rpc.GerritCallback;
+
+/**
+ * GerritCallback to be used on user interfaces that allow filtering to handle
+ * RPC's that request filtering. The user may change the filter quickly so that
+ * a response may be outdated when the client receives it. In this case the
+ * response must be ignored because the responses to RCP's may come out-of-order
+ * and an outdated response would overwrite the correct result which was
+ * received before.
+ */
+public class IgnoreOutdatedFilterResultsCallbackWrapper<T> extends GerritCallback<T> {
+  private final FilteredUserInterface filteredUI;
+  private final String myFilter;
+  private final GerritCallback<T> cb;
+
+  public IgnoreOutdatedFilterResultsCallbackWrapper(
+      final FilteredUserInterface filteredUI, final GerritCallback<T> cb) {
+    this.filteredUI = filteredUI;
+    this.myFilter = filteredUI.getCurrentFilter();
+    this.cb = cb;
+  }
+
+  @Override
+  public void onSuccess(final T result) {
+    if ((myFilter == null && filteredUI.getCurrentFilter() == null)
+        || (myFilter != null && myFilter.equals(filteredUI.getCurrentFilter()))) {
+      cb.onSuccess(result);
+    }
+    // Else ignore the result, the user has already changed the filter
+    // and the result is not relevant anymore. If multiple RPC's are
+    // fired the results may come back out-of-order and a non-relevant
+    // result could overwrite the correct result if not ignored.
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/LinkMenuBar.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/LinkMenuBar.java
index 0353281..fe7ffe9 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/LinkMenuBar.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/LinkMenuBar.java
@@ -15,20 +15,21 @@
 package com.google.gerrit.client.ui;
 
 import com.google.gerrit.client.Gerrit;
+import com.google.gwt.aria.client.Roles;
 import com.google.gwt.user.client.Command;
-import com.google.gwt.user.client.ui.Accessibility;
 import com.google.gwt.user.client.ui.Composite;
 import com.google.gwt.user.client.ui.FlowPanel;
 import com.google.gwt.user.client.ui.Widget;
 
-public class LinkMenuBar extends Composite {
+public class LinkMenuBar extends Composite implements ScreenLoadHandler {
   private final FlowPanel body;
 
   public LinkMenuBar() {
     body = new FlowPanel();
     initWidget(body);
     setStyleName(Gerrit.RESOURCES.css().linkMenuBar());
-    Accessibility.setRole(getElement(), Accessibility.ROLE_MENUBAR);
+    Roles.getMenubarRole().set(getElement());
+    Gerrit.EVENT_BUS.addHandler(ScreenLoadEvent.TYPE, this);
   }
 
   public void addItem(final String text, final Command imp) {
@@ -66,4 +67,7 @@
     }
     body.add(i);
   }
+
+  public void onScreenLoad(ScreenLoadEvent event) {
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/LinkMenuItem.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/LinkMenuItem.java
index f076a2c..20569c3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/LinkMenuItem.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/LinkMenuItem.java
@@ -15,14 +15,15 @@
 package com.google.gerrit.client.ui;
 
 import com.google.gerrit.client.Gerrit;
+import com.google.gwt.aria.client.Roles;
 import com.google.gwt.dom.client.AnchorElement;
-import com.google.gwt.user.client.ui.Accessibility;
 
-public class LinkMenuItem extends InlineHyperlink {
+public class LinkMenuItem extends InlineHyperlink implements ScreenLoadHandler {
   public LinkMenuItem(final String text, final String targetHistoryToken) {
     super(text, targetHistoryToken);
     setStyleName(Gerrit.RESOURCES.css().menuItem());
-    Accessibility.setRole(getElement(), Accessibility.ROLE_MENUITEM);
+    Roles.getMenuitemRole().set(getElement());
+    Gerrit.EVENT_BUS.addHandler(ScreenLoadEvent.TYPE, this);
   }
 
   @Override
@@ -30,4 +31,12 @@
     super.go();
     AnchorElement.as(getElement()).blur();
   }
+
+  public void onScreenLoad(ScreenLoadEvent event) {
+    if (event.getScreen().getToken().equals(getTargetHistoryToken())){
+      addStyleName(Gerrit.RESOURCES.css().activeRow());
+    } else {
+      removeStyleName(Gerrit.RESOURCES.css().activeRow());
+    }
+  }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ListenableOldValue.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ListenableOldValue.java
index 5c3e127..47bca2b 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ListenableOldValue.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ListenableOldValue.java
@@ -23,8 +23,11 @@
   }
 
   public void set(final T value) {
-    oldValue = get();
-    super.set(value);
-    oldValue = null; // allow it to be gced before the next event
+    try {
+      oldValue = get();
+      super.set(value);
+    } finally {
+      oldValue = null; // allow it to be gced before the next event
+    }
   }
 }
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 8d74bc3..63c2636 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
@@ -16,10 +16,12 @@
 
 import com.google.gerrit.client.Gerrit;
 import com.google.gwt.dom.client.Document;
+import com.google.gwt.event.dom.client.KeyCodes;
 import com.google.gwt.event.dom.client.KeyPressEvent;
 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.Event;
 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;
@@ -34,6 +36,41 @@
 import java.util.Map.Entry;
 
 public abstract class NavigationTable<RowItem> extends FancyFlexTable<RowItem> {
+  protected class MyFlexTable extends FancyFlexTable.MyFlexTable {
+    public MyFlexTable() {
+      sinkEvents(Event.ONDBLCLICK | Event.ONCLICK);
+    }
+
+    @Override
+    public void onBrowserEvent(final Event event) {
+      switch (DOM.eventGetType(event)) {
+        case Event.ONCLICK: {
+          // Find out which cell was actually clicked.
+          final Element td = getEventTargetCell(event);
+          if (td == null) {
+            break;
+          }
+          final int row = rowOf(td);
+          if (getRowItem(row) != null) {
+            onCellSingleClick(rowOf(td), columnOf(td));
+            return;
+          }
+          break;
+        }
+        case Event.ONDBLCLICK: {
+          // Find out which cell was actually clicked.
+          Element td = getEventTargetCell(event);
+          if (td == null) {
+            return;
+          }
+          onCellDoubleClick(rowOf(td), columnOf(td));
+          return;
+        }
+      }
+      super.onBrowserEvent(event);
+    }
+  }
+
   @SuppressWarnings("serial")
   private static final LinkedHashMap<String, Object> savedPositions =
       new LinkedHashMap<String, Object>(10, 0.75f, true) {
@@ -54,6 +91,15 @@
   private boolean computedScrollType;
   private ScrollPanel parentScrollPanel;
 
+  protected NavigationTable(String itemHelpName) {
+    this();
+    keysNavigation.add(new PrevKeyCommand(0, 'k', Util.M.helpListPrev(itemHelpName)));
+    keysNavigation.add(new NextKeyCommand(0, 'j', Util.M.helpListNext(itemHelpName)));
+    keysNavigation.add(new OpenKeyCommand(0, 'o', Util.M.helpListOpen(itemHelpName)));
+    keysNavigation.add(new OpenKeyCommand(0, KeyCodes.KEY_ENTER,
+                                                  Util.M.helpListOpen(itemHelpName)));
+  }
+
   protected NavigationTable() {
     pointer = new Image(Gerrit.RESOURCES.arrowRight());
     keysNavigation = new KeyCommandSet(Gerrit.C.sectionNavigation());
@@ -91,6 +137,16 @@
     }
   }
 
+  /** Invoked when the user double clicks on a table cell. */
+  protected void onCellDoubleClick(int row, int column) {
+    onOpenRow(row);
+  }
+
+  /** Invoked when the user clicks on a table cell. */
+  protected void onCellSingleClick(int row, int column) {
+    movePointerTo(row);
+  }
+
   protected int getCurrentRow() {
     return currentRow;
   }
@@ -259,6 +315,11 @@
     super.onUnload();
   }
 
+  @Override
+  protected MyFlexTable createFlexTable() {
+    return new MyFlexTable();
+  }
+
   public class PrevKeyCommand extends KeyCommand {
     public PrevKeyCommand(int mask, char key, String help) {
       super(mask, key, help);
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/OnEditEnabler.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/OnEditEnabler.java
index b574db1..a7d49a4 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/OnEditEnabler.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/OnEditEnabler.java
@@ -33,6 +33,7 @@
 import com.google.gwt.user.client.ui.FocusWidget;
 import com.google.gwt.user.client.ui.ListBox;
 import com.google.gwt.user.client.ui.TextBoxBase;
+import com.google.gwt.user.client.ui.ValueBoxBase;
 
 import java.util.HashMap;
 import java.util.Map;
@@ -139,6 +140,17 @@
     if (widget.isEnabled() ||
         ! (e.getSource() instanceof FocusWidget) ||
         ! ((FocusWidget) e.getSource()).isEnabled() ) {
+      if (e.getSource() instanceof ValueBoxBase) {
+        final TextBoxBase box = ((TextBoxBase) e.getSource());
+        Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+          @Override
+          public void execute() {
+            if (box.getValue().trim().length() == 0) {
+              widget.setEnabled(false);
+            }
+          }
+        });
+      }
       return;
     }
 
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectListPopup.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectListPopup.java
index 217ca5a..cac7667 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectListPopup.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectListPopup.java
@@ -14,36 +14,46 @@
 
 package com.google.gerrit.client.ui;
 
+import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.account.Util;
 import com.google.gerrit.client.projects.ProjectMap;
 import com.google.gerrit.client.rpc.GerritCallback;
 import com.google.gwt.event.dom.client.ClickEvent;
 import com.google.gwt.event.dom.client.ClickHandler;
 import com.google.gwt.event.dom.client.KeyCodes;
+import com.google.gwt.event.dom.client.KeyUpEvent;
+import com.google.gwt.event.dom.client.KeyUpHandler;
 import com.google.gwt.user.client.Window;
 import com.google.gwt.user.client.ui.Button;
 import com.google.gwt.user.client.ui.FlowPanel;
+import com.google.gwt.user.client.ui.HorizontalPanel;
+import com.google.gwt.user.client.ui.Label;
 import com.google.gwt.user.client.ui.PopupPanel;
 import com.google.gwt.user.client.ui.ScrollPanel;
 import com.google.gwtexpui.globalkey.client.GlobalKey;
 import com.google.gwtexpui.globalkey.client.HidePopupPanelCommand;
+import com.google.gwtexpui.globalkey.client.NpTextBox;
 import com.google.gwtexpui.user.client.PluginSafeDialogBox;
 
 /** It creates a popup containing all the projects. */
-public class ProjectListPopup {
-  private ProjectsTable projectsTab;
+public class ProjectListPopup implements FilteredUserInterface {
+  private HighlightingProjectsTable projectsTab;
   private PluginSafeDialogBox popup;
+  private NpTextBox filterTxt;
+  private HorizontalPanel filterPanel;
+  private String subname;
   private Button close;
   private ScrollPanel sp;
   private PopupPanel.PositionCallback popupPosition;
   private int preferredTop;
   private int preferredLeft;
-  private boolean popingUp;
+  private boolean poppingUp;
   private boolean firstPopupLoad = true;
 
   public void initPopup(final String popupText, final String currentPageLink) {
     createWidgets(popupText, currentPageLink);
     final FlowPanel pfp = new FlowPanel();
+    pfp.add(filterPanel);
     sp = new ScrollPanel(projectsTab);
     sp.setSize("100%", "100%");
     pfp.add(sp);
@@ -84,13 +94,30 @@
   protected void openRow(String projectName) {
   }
 
-  public boolean isPopingUp() {
-    return popingUp;
+  public boolean isPoppingUp() {
+    return poppingUp;
   }
 
   private void createWidgets(final String popupText,
       final String currentPageLink) {
-    projectsTab = new ProjectsTable() {
+    filterPanel = new HorizontalPanel();
+    filterPanel.setStyleName(Gerrit.RESOURCES.css().projectFilterPanel());
+    final Label filterLabel =
+        new Label(com.google.gerrit.client.admin.Util.C.projectFilter());
+    filterLabel.setStyleName(Gerrit.RESOURCES.css().projectFilterLabel());
+    filterPanel.add(filterLabel);
+    filterTxt = new NpTextBox();
+    filterTxt.setValue(subname);
+    filterTxt.addKeyUpHandler(new KeyUpHandler() {
+      @Override
+      public void onKeyUp(KeyUpEvent event) {
+        subname = filterTxt.getValue();
+        populateProjects();
+      }
+    });
+    filterPanel.add(filterTxt);
+
+    projectsTab = new HighlightingProjectsTable() {
       @Override
       protected void movePointerTo(final int row, final boolean scroll) {
         super.movePointerTo(row, scroll);
@@ -119,7 +146,7 @@
   }
 
   public void displayPopup() {
-    popingUp = true;
+    poppingUp = true;
     if (firstPopupLoad) { // For sizing/positioning, delay display until loaded
       populateProjects();
     } else {
@@ -132,7 +159,8 @@
       }
       projectsTab.setRegisterKeys(true);
       projectsTab.finishDisplay();
-      popingUp = false;
+      filterTxt.setFocus(true);
+      poppingUp = false;
     }
   }
 
@@ -146,15 +174,22 @@
   }
 
   protected void populateProjects() {
-    ProjectMap.all(new GerritCallback<ProjectMap>() {
-      @Override
-      public void onSuccess(final ProjectMap result) {
-        projectsTab.display(result);
-        if (firstPopupLoad) { // Display was delayed until table was loaded
-          firstPopupLoad = false;
-          displayPopup();
-        }
-      }
-    });
+    ProjectMap.match(subname,
+        new IgnoreOutdatedFilterResultsCallbackWrapper<ProjectMap>(this,
+            new GerritCallback<ProjectMap>() {
+              @Override
+              public void onSuccess(final ProjectMap result) {
+                projectsTab.display(result, subname);
+                if (firstPopupLoad) { // Display was delayed until table was loaded
+                  firstPopupLoad = false;
+                  displayPopup();
+                }
+              }
+            }));
+  }
+
+  @Override
+  public String getCurrentFilter() {
+    return subname;
   }
 }
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 25ed258..80364cf 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
@@ -17,12 +17,12 @@
 import com.google.gerrit.client.RpcStatus;
 import com.google.gerrit.client.projects.ProjectMap;
 import com.google.gerrit.client.rpc.GerritCallback;
-import com.google.gwtexpui.safehtml.client.HighlightSuggestOracle;
+import com.google.gerrit.client.rpc.Natives;
 
 /** Suggestion Oracle for Project.NameKey entities. */
-public class ProjectNameSuggestOracle extends HighlightSuggestOracle {
+public class ProjectNameSuggestOracle extends SuggestAfterTypingNCharsOracle {
   @Override
-  public void onRequestSuggestions(final Request req, final Callback callback) {
+  public void _onRequestSuggestions(final Request req, final Callback callback) {
     RpcStatus.hide(new Runnable() {
       @Override
       public void run() {
@@ -30,7 +30,7 @@
             new GerritCallback<ProjectMap>() {
               @Override
               public void onSuccess(ProjectMap map) {
-                callback.onSuggestionsReady(req, new Response(map.values().asList()));
+                callback.onSuggestionsReady(req, new Response(Natives.asList(map.values())));
               }
             });
       }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectSearchLink.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectSearchLink.java
new file mode 100644
index 0000000..0ae29bf
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ProjectSearchLink.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.client.ui;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gerrit.client.admin.Util;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gwt.user.client.DOM;
+import com.google.gwt.user.client.ui.Image;
+
+public class ProjectSearchLink extends InlineHyperlink {
+
+  public ProjectSearchLink(Project.NameKey projectName) {
+    super(" ", PageLinks.toProjectDashboard(projectName, "default"));
+    setTitle(Util.C.projectListQueryLink());
+    final Image image = new Image(Gerrit.RESOURCES.queryIcon());
+    DOM.insertBefore(getElement(), image.getElement(),
+        DOM.getFirstChild(getElement()));
+  }
+}
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 0cbe194..a3a5052 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
@@ -17,10 +17,7 @@
 import com.google.gerrit.client.Gerrit;
 import com.google.gerrit.client.projects.ProjectInfo;
 import com.google.gerrit.client.projects.ProjectMap;
-import com.google.gwt.event.dom.client.KeyCodes;
-import com.google.gwt.user.client.DOM;
-import com.google.gwt.user.client.Element;
-import com.google.gwt.user.client.Event;
+import com.google.gerrit.client.rpc.Natives;
 import com.google.gwt.user.client.ui.FlexTable.FlexCellFormatter;
 
 import java.util.Collections;
@@ -30,11 +27,7 @@
 public class ProjectsTable extends NavigationTable<ProjectInfo> {
 
   public ProjectsTable() {
-    keysNavigation.add(new PrevKeyCommand(0, 'k', Util.C.projectListPrev()));
-    keysNavigation.add(new NextKeyCommand(0, 'j', Util.C.projectListNext()));
-    keysNavigation.add(new OpenKeyCommand(0, 'o', Util.C.projectListOpen()));
-    keysNavigation.add(new OpenKeyCommand(0, KeyCodes.KEY_ENTER,
-                                                  Util.C.projectListOpen()));
+    super(Util.C.projectItemHelp());
     initColumnHeaders();
   }
 
@@ -48,43 +41,6 @@
   }
 
   @Override
-  protected MyFlexTable createFlexTable() {
-    MyFlexTable table = new MyFlexTable() {
-      @Override
-      public void onBrowserEvent(final Event event) {
-        switch (DOM.eventGetType(event)) {
-          case Event.ONCLICK: {
-            // Find out which cell was actually clicked.
-            final Element td = getEventTargetCell(event);
-            if (td == null) {
-              break;
-            }
-            final int row = rowOf(td);
-            if (getRowItem(row) != null) {
-              ProjectsTable.this.movePointerTo(row);
-              return;
-            }
-            break;
-          }
-          case Event.ONDBLCLICK: {
-            // Find out which cell was actually clicked.
-            Element td = getEventTargetCell(event);
-            if (td == null) {
-              return;
-            }
-            onOpenRow(rowOf(td));
-            return;
-          }
-        }
-        super.onBrowserEvent(event);
-      }
-    };
-
-    table.sinkEvents(Event.ONDBLCLICK | Event.ONCLICK);
-    return table;
-  }
-
-  @Override
   protected Object getRowItemKey(final ProjectInfo item) {
     return item.name();
   }
@@ -100,7 +56,7 @@
     while (1 < table.getRowCount())
       table.removeRow(table.getRowCount() - 1);
 
-    List<ProjectInfo> list = projects.values().asList();
+    List<ProjectInfo> list = Natives.asList(projects.values());
     Collections.sort(list, new Comparator<ProjectInfo>() {
       @Override
       public int compare(ProjectInfo a, ProjectInfo b) {
@@ -120,7 +76,6 @@
 
     final FlexCellFormatter fmt = table.getFlexCellFormatter();
     fmt.addStyleName(row, 1, Gerrit.RESOURCES.css().dataCell());
-    fmt.addStyleName(row, 1, Gerrit.RESOURCES.css().cPROJECT());
     fmt.addStyleName(row, 2, Gerrit.RESOURCES.css().dataCell());
 
     populate(row, k);
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
index 747ef40..3f1de2b 100644
--- 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
@@ -22,18 +22,17 @@
 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 {
+public class ReviewerSuggestOracle extends SuggestAfterTypingNCharsOracle {
 
   private Change.Id changeId;
 
   @Override
-  protected void onRequestSuggestions(final Request req, final Callback callback) {
+  protected void _onRequestSuggestions(final Request req, final Callback callback) {
     RpcStatus.hide(new Runnable() {
       public void run() {
         SuggestUtil.SVC.suggestChangeReviewer(changeId, req.getQuery(),
@@ -66,7 +65,7 @@
     public String getDisplayString() {
       final AccountInfo accountInfo = reviewerInfo.getAccountInfo();
       if (accountInfo != null) {
-        return FormatUtil.nameEmail(accountInfo);
+        return FormatUtil.nameEmail(FormatUtil.asInfo(accountInfo));
       }
       return reviewerInfo.getGroup().getName() + " ("
           + Util.C.suggestedGroupLabel() + ")";
@@ -75,7 +74,7 @@
     public String getReplacementString() {
       final AccountInfo accountInfo = reviewerInfo.getAccountInfo();
       if (accountInfo != null) {
-        return FormatUtil.nameEmail(accountInfo);
+        return FormatUtil.nameEmail(FormatUtil.asInfo(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 845a046..e7c2d84 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
@@ -39,6 +39,7 @@
   private String token;
   private boolean requiresSignIn;
   private String windowTitle;
+  private Widget titleWidget;
 
   protected Screen() {
     initWidget(new FlowPanel());
@@ -65,8 +66,12 @@
     me.add(header = new Grid(1, Cols.values().length));
     me.add(body = new FlowPanel());
 
+    headerText = new InlineLabel();
+    if (titleWidget == null) {
+      titleWidget = headerText;
+    }
     FlowPanel title = new FlowPanel();
-    title.add(headerText = new InlineLabel());
+    title.add(titleWidget);
     title.setStyleName(Gerrit.RESOURCES.css().screenHeader());
     header.setWidget(0, Cols.Title.ordinal(), title);
 
@@ -99,6 +104,10 @@
     header.setVisible(value);
   }
 
+  public void setTitle(final Widget w) {
+    titleWidget = w;
+  }
+
   protected void setTitleEast(final Widget w) {
     header.setWidget(0, Cols.East.ordinal(), w);
   }
@@ -155,6 +164,8 @@
       Gerrit.setWindowTitle(this, windowTitle);
     }
     Gerrit.updateMenus(this);
+    Gerrit.EVENT_BUS.fireEvent(new ScreenLoadEvent(this));
+    Gerrit.setQueryString(null);
     registerKeys();
   }
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ScreenLoadEvent.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ScreenLoadEvent.java
new file mode 100644
index 0000000..562e53a
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ScreenLoadEvent.java
@@ -0,0 +1,42 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.ui;
+
+import com.google.gwt.event.shared.GwtEvent;
+
+public class ScreenLoadEvent extends GwtEvent<ScreenLoadHandler> {
+  private final Screen screen;
+
+  public ScreenLoadEvent(Screen screen) {
+    super();
+    this.screen = screen;
+  }
+
+  public static final Type<ScreenLoadHandler> TYPE = new Type<ScreenLoadHandler>();
+
+  @Override
+  protected void dispatch(ScreenLoadHandler handler) {
+    handler.onScreenLoad(this);
+  }
+
+  @Override
+  public GwtEvent.Type<ScreenLoadHandler> getAssociatedType() {
+    return TYPE;
+  }
+
+  public Screen getScreen(){
+    return screen;
+  }
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ScreenLoadHandler.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ScreenLoadHandler.java
new file mode 100644
index 0000000..a91becf
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/ScreenLoadHandler.java
@@ -0,0 +1,21 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.shared.EventHandler;
+
+public interface ScreenLoadHandler extends EventHandler {
+  public void onScreenLoad(ScreenLoadEvent event);
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/SuggestAfterTypingNCharsOracle.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/SuggestAfterTypingNCharsOracle.java
new file mode 100644
index 0000000..4f54ba5
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/SuggestAfterTypingNCharsOracle.java
@@ -0,0 +1,36 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client.ui;
+
+import com.google.gerrit.client.Gerrit;
+import com.google.gwtexpui.safehtml.client.HighlightSuggestOracle;
+
+/**
+ * Suggest oracle that only provides suggestions if the user has typed at least
+ * as many characters as configured by 'suggest.from'. If 'suggest.from' is set
+ * to 0, suggestions will always be provided.
+ */
+public abstract class SuggestAfterTypingNCharsOracle extends HighlightSuggestOracle {
+
+  @Override
+  protected void onRequestSuggestions(final Request request, final Callback done) {
+    final int suggestFrom = Gerrit.getConfig().getSuggestFrom();
+    if (suggestFrom == 0 || request.getQuery().length() >= suggestFrom) {
+      _onRequestSuggestions(request, done);
+    }
+  }
+
+  protected abstract void _onRequestSuggestions(Request request, Callback done);
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/TextBoxChangeListener.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/TextBoxChangeListener.java
new file mode 100644
index 0000000..ca8ea1d
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/TextBoxChangeListener.java
@@ -0,0 +1,96 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.core.client.Scheduler;
+import com.google.gwt.core.client.Scheduler.ScheduledCommand;
+import com.google.gwt.event.dom.client.FocusEvent;
+import com.google.gwt.event.dom.client.FocusHandler;
+import com.google.gwt.event.dom.client.KeyDownEvent;
+import com.google.gwt.event.dom.client.KeyDownHandler;
+import com.google.gwt.event.dom.client.KeyPressEvent;
+import com.google.gwt.event.dom.client.KeyPressHandler;
+import com.google.gwt.event.dom.client.MouseUpEvent;
+import com.google.gwt.event.dom.client.MouseUpHandler;
+import com.google.gwt.event.shared.GwtEvent;
+import com.google.gwt.user.client.ui.TextBoxBase;
+
+public abstract class TextBoxChangeListener implements KeyPressHandler, KeyDownHandler, MouseUpHandler {
+
+  private String oldText;
+
+  public TextBoxChangeListener(final TextBoxBase tb) {
+    oldText = tb.getText();
+
+    tb.addKeyPressHandler(this);
+
+    // Is there another way to capture middle button X11 pastes in browsers
+    // which do not yet support ONPASTE events (Firefox)?
+    tb.addMouseUpHandler(this);
+
+    // Resetting the "original text" on focus ensures that we are
+    // up to date with non-user updates of the text (calls to
+    // setText()...) and also up to date with user changes which
+    // occurred after enabling "widget".
+    tb.addFocusHandler(new FocusHandler() {
+        @Override
+        public void onFocus(FocusEvent event) {
+          oldText = tb.getText();
+        }
+      });
+
+    // CTRL-V Pastes in Chrome seem only detectable via BrowserEvents or
+    // KeyDownEvents, the latter is better.
+    tb.addKeyDownHandler(this);
+  }
+
+
+  @Override
+  public void onKeyPress(final KeyPressEvent e) {
+    on(e);
+  }
+
+  @Override
+  public void onKeyDown(final KeyDownEvent e) {
+    on(e);
+  }
+
+  @Override
+  public void onMouseUp(final MouseUpEvent e) {
+    on(e);
+  }
+
+  private void on(final GwtEvent<?> e) {
+    final TextBoxBase tb = (TextBoxBase) e.getSource();
+
+    if (!tb.getText().equals(oldText)) {
+      onTextChanged(tb.getText());
+      oldText = tb.getText();
+    } else {
+      // The text appears to not always get updated until the handlers complete.
+      Scheduler.get().scheduleDeferred(new ScheduledCommand() {
+        @Override
+        public void execute() {
+          if (!tb.getText().equals(oldText)) {
+            onTextChanged(tb.getText());
+            oldText = tb.getText();
+          }
+        }
+      });
+    }
+  }
+
+  public abstract void onTextChanged(String newText);
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/UIConstants.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/UIConstants.java
index ebbb049..1919cd3 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/UIConstants.java
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/UIConstants.java
@@ -22,7 +22,5 @@
 
   String projectName();
   String projectDescription();
-  String projectListOpen();
-  String projectListPrev();
-  String projectListNext();
+  String projectItemHelp();
 }
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/UIConstants.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/UIConstants.properties
index aa00cee..8a72355 100644
--- a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/UIConstants.properties
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/UIConstants.properties
@@ -3,6 +3,4 @@
 
 projectName = Project Name
 projectDescription = Project Description
-projectListOpen = Select project
-projectListPrev = Previous project
-projectListNext = Next project
+projectItemHelp = project
\ No newline at end of file
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/UIMessages.java b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/UIMessages.java
new file mode 100644
index 0000000..fcb846f4
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/UIMessages.java
@@ -0,0 +1,23 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.i18n.client.Messages;
+
+public interface UIMessages extends Messages {
+  String helpListOpen(String item);
+  String helpListPrev(String item);
+  String helpListNext(String item);
+}
diff --git a/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/UIMessages.properties b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/UIMessages.properties
new file mode 100644
index 0000000..1439245
--- /dev/null
+++ b/gerrit-gwtui/src/main/java/com/google/gerrit/client/ui/UIMessages.properties
@@ -0,0 +1,3 @@
+helpListOpen = Select {0}
+helpListPrev = Previous {0}
+helpListNext = Next {0}
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 98f13ef..804eee1 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
@@ -15,7 +15,35 @@
 package com.google.gerrit.client.ui;
 
 import com.google.gwt.core.client.GWT;
+import com.google.gwtexpui.safehtml.client.SafeHtmlBuilder;
 
 public class Util {
   public static final UIConstants C = GWT.create(UIConstants.class);
+  public static final UIMessages M = GWT.create(UIMessages.class);
+
+  public static String highlight(final String text, final String toHighlight) {
+    final SafeHtmlBuilder b = new SafeHtmlBuilder();
+    if (toHighlight == null || "".equals(toHighlight)) {
+      b.append(text);
+      return b.toSafeHtml().asString();
+    }
+
+    int pos = 0;
+    int endPos = 0;
+    while ((pos = text.toLowerCase().indexOf(
+        toHighlight.toLowerCase(), pos)) > -1) {
+      if (pos > endPos) {
+        b.append(text.substring(endPos, pos));
+      }
+      endPos = pos + toHighlight.length();
+      b.openElement("b");
+      b.append(text.substring(pos, endPos));
+      b.closeElement("b");
+      pos = endPos;
+    }
+    if (endPos < text.length()) {
+      b.append(text.substring(endPos));
+    }
+    return b.toSafeHtml().asString();
+  }
 }
diff --git a/gerrit-httpd/pom.xml b/gerrit-httpd/pom.xml
index ceacb66..65641aa 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.5-SNAPSHOT</version>
+    <version>2.6-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-httpd</artifactId>
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CacheBasedWebSession.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CacheBasedWebSession.java
index ca3d287..96792f0 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CacheBasedWebSession.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/CacheBasedWebSession.java
@@ -25,7 +25,6 @@
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.AuthMethod;
 import com.google.gerrit.server.account.AuthResult;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.config.AuthConfig;
@@ -36,6 +35,8 @@
 
 import org.eclipse.jgit.http.server.GitSmartHttpTools;
 
+import java.util.EnumSet;
+
 import javax.servlet.http.Cookie;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
@@ -67,12 +68,12 @@
   private final AuthConfig authConfig;
   private final Provider<AnonymousUser> anonymousProvider;
   private final IdentifiedUser.RequestFactory identified;
-  private AccessPath accessPath;
+  private final EnumSet<AccessPath> okPaths = EnumSet.of(AccessPath.UNKNOWN);
   private Cookie outCookie;
-  private AuthMethod authMethod;
 
   private Key key;
   private Val val;
+  private CurrentUser user;
 
   @Inject
   CacheBasedWebSession(final HttpServletRequest request,
@@ -87,31 +88,22 @@
     this.anonymousProvider = anonymousProvider;
     this.identified = identified;
 
-    if (GitSmartHttpTools.isGitClient(request)) {
-      accessPath = AccessPath.GIT;
-    } else {
-      accessPath = AccessPath.WEB_UI;
-    }
+    if (!GitSmartHttpTools.isGitClient(request)) {
+      String cookie = readCookie();
+      if (cookie != null) {
+        key = new Key(cookie);
+        val = manager.get(key);
+        if (val != null && val.needsCookieRefresh()) {
+          // Cookie is more than half old. Send the cookie again to the
+          // client with an updated expiration date.
+          val = manager.createVal(key, val);
+        }
 
-    final String cookie = readCookie();
-    if (cookie != null) {
-      key = new Key(cookie);
-      val = manager.get(key);
-    } else {
-      key = null;
-      val = null;
-    }
-    authMethod = isSignedIn() ? AuthMethod.COOKIE : AuthMethod.NONE;
-
-    if (isSignedIn() && val.needsCookieRefresh()) {
-      // Cookie is more than half old. Send the cookie again to the
-      // 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();
+        String token = request.getHeader("X-Gerrit-Auth");
+        if (val != null && token != null && token.equals(val.getAuth())) {
+          okPaths.add(AccessPath.REST_API);
+        }
+      }
     }
   }
 
@@ -128,33 +120,54 @@
     return null;
   }
 
+  @Override
   public boolean isSignedIn() {
     return val != null;
   }
 
-  public String getToken() {
-    return isSignedIn() ? val.getXsrfToken() : null;
+  @Override
+  public String getXGerritAuth() {
+    return isSignedIn() ? val.getAuth() : null;
   }
 
-  public boolean isTokenValid(final String inputToken) {
-    return isSignedIn() //
-        && val.getXsrfToken() != null //
-        && val.getXsrfToken().equals(inputToken);
+  @Override
+  public boolean isValidXGerritAuth(String keyIn) {
+    return keyIn.equals(getXGerritAuth());
   }
 
+  @Override
+  public boolean isAccessPathOk(AccessPath path) {
+    return okPaths.contains(path);
+  }
+
+  @Override
+  public void setAccessPathOk(AccessPath path, boolean ok) {
+    if (ok) {
+      okPaths.add(path);
+    } else {
+      okPaths.remove(path);
+    }
+  }
+
+  @Override
   public AccountExternalId.Key getLastLoginExternalId() {
     return val != null ? val.getExternalId() : null;
   }
 
+  @Override
   public CurrentUser getCurrentUser() {
-    if (isSignedIn()) {
-      return identified.create(accessPath, val.getAccountId());
+    if (user == null) {
+      if (isSignedIn()) {
+        user = identified.create(val.getAccountId());
+      } else {
+        user = anonymousProvider.get();
+      }
     }
-    return anonymousProvider.get();
+    return user;
   }
 
-  public void login(final AuthResult res, final AuthMethod meth,
-                    final boolean rememberMe) {
+  @Override
+  public void login(final AuthResult res, final boolean rememberMe) {
     final Account.Id id = res.getAccountId();
     final AccountExternalId.Key identity = res.getExternalId();
 
@@ -163,19 +176,18 @@
     }
 
     key = manager.createKey(id);
-    val = manager.createVal(key, id, rememberMe, identity, null);
+    val = manager.createVal(key, id, rememberMe, identity, null, null);
     saveCookie();
-
-    authMethod = meth;
   }
 
   /** Set the user account for this current request only. */
-  public void setUserAccountId(Account.Id id, AuthMethod method) {
+  @Override
+  public void setUserAccountId(Account.Id id) {
     key = new Key("id:" + id);
-    val = new Val(id, 0, false, null, "", 0);
-    authMethod = method;
+    val = new Val(id, 0, false, null, 0, null, null);
   }
 
+  @Override
   public void logout() {
     if (val != null) {
       manager.destroy(key);
@@ -185,6 +197,11 @@
     }
   }
 
+  @Override
+  public String getSessionId() {
+    return val != null ? val.getSessionId() : null;
+  }
+
   private void saveCookie() {
     final String token;
     final int ageSeconds;
@@ -220,8 +237,4 @@
   private static boolean isSecure(final HttpServletRequest req) {
     return req.isSecure() || "https".equals(req.getScheme());
   }
-
-  public AuthMethod getAuthMethod() {
-    return authMethod;
-  }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ContainerAuthFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ContainerAuthFilter.java
index 9ce2298..433b4f5 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ContainerAuthFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ContainerAuthFilter.java
@@ -17,9 +17,9 @@
 import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
 import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
 
+import com.google.gerrit.server.AccessPath;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
-import com.google.gerrit.server.account.AuthMethod;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -100,9 +100,10 @@
       rsp.sendError(SC_UNAUTHORIZED);
       return false;
     }
-    session.get().setUserAccountId(
-        who.getAccount().getId(),
-        AuthMethod.PASSWORD);
+    WebSession ws = session.get();
+    ws.setUserAccountId(who.getAccount().getId());
+    ws.setAccessPathOk(AccessPath.GIT, true);
+    ws.setAccessPathOk(AccessPath.REST_API, true);
     return true;
   }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritConfigProvider.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritConfigProvider.java
index c1f3ae4..aa56ae9 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
@@ -14,7 +14,6 @@
 
 package com.google.gerrit.httpd;
 
-import com.google.gerrit.common.data.ApprovalTypes;
 import com.google.gerrit.common.data.GerritConfig;
 import com.google.gerrit.common.data.GitwebConfig;
 import com.google.gerrit.reviewdb.client.Account;
@@ -52,7 +51,6 @@
   private final GitWebConfig gitWebConfig;
   private final AllProjectsName wildProject;
   private final SshInfo sshInfo;
-  private final ApprovalTypes approvalTypes;
 
   private EmailSender emailSender;
   private final ContactStore contactStore;
@@ -62,7 +60,7 @@
   @Inject
   GerritConfigProvider(final Realm r, @GerritServerConfig final Config gsc,
       final AuthConfig ac, final GitWebConfig gwc, final AllProjectsName wp,
-      final SshInfo si, final ApprovalTypes at, final ContactStore cs,
+      final SshInfo si, final ContactStore cs,
       final ServletContext sc, final DownloadConfig dc,
       final @AnonymousCowardName String acn) {
     realm = r;
@@ -72,7 +70,6 @@
     gitWebConfig = gwc;
     sshInfo = si;
     wildProject = wp;
-    approvalTypes = at;
     contactStore = cs;
     servletContext = sc;
     anonymousCowardName = acn;
@@ -86,25 +83,27 @@
   private GerritConfig create() throws MalformedURLException {
     final GerritConfig config = new GerritConfig();
     switch (authConfig.getAuthType()) {
-      case OPENID:
-        config.setAllowedOpenIDs(authConfig.getAllowedOpenIDs());
-        break;
-
-      case OPENID_SSO:
-        config.setOpenIdSsoUrl(authConfig.getOpenIdSsoUrl());
-        break;
-
       case LDAP:
       case LDAP_BIND:
         config.setRegisterUrl(cfg.getString("auth", null, "registerurl"));
+        config.setRegisterText(cfg.getString("auth", null, "registertext"));
         config.setEditFullNameUrl(cfg.getString("auth", null, "editFullNameUrl"));
         break;
 
       case CUSTOM_EXTENSION:
         config.setRegisterUrl(cfg.getString("auth", null, "registerurl"));
+        config.setRegisterText(cfg.getString("auth", null, "registertext"));
         config.setEditFullNameUrl(cfg.getString("auth", null, "editFullNameUrl"));
         config.setHttpPasswordUrl(cfg.getString("auth", null, "httpPasswordUrl"));
         break;
+
+      case CLIENT_SSL_CERT_LDAP:
+      case DEVELOPMENT_BECOME_ANY_ACCOUNT:
+      case HTTP:
+      case HTTP_LDAP:
+      case OPENID:
+      case OPENID_SSO:
+        break;
     }
     config.setUseContributorAgreements(cfg.getBoolean("auth",
         "contributoragreements", false));
@@ -115,12 +114,12 @@
     config.setDownloadCommands(downloadConfig.getDownloadCommands());
     config.setAuthType(authConfig.getAuthType());
     config.setWildProject(wildProject);
-    config.setApprovalTypes(approvalTypes);
     config.setDocumentationAvailable(servletContext
         .getResource("/Documentation/index.html") != null);
     config.setTestChangeMerge(cfg.getBoolean("changeMerge",
         "test", false));
     config.setAnonymousCowardName(anonymousCowardName);
+    config.setSuggestFrom(cfg.getInt("suggest", "from", 0));
 
     config.setReportBugUrl(cfg.getString("gerrit", null, "reportBugUrl"));
     if (config.getReportBugUrl() == null) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritUiOptions.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritUiOptions.java
new file mode 100644
index 0000000..cd07320
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GerritUiOptions.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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;
+
+public class GerritUiOptions {
+  private final boolean headless;
+
+  public GerritUiOptions(boolean headless) {
+    this.headless = headless;
+  }
+
+  public boolean enableDefaultUi() {
+    return !headless;
+  }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java
index 6bd35dd..dad9b80 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/GitOverHttpServlet.java
@@ -18,10 +18,13 @@
 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.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.git.AsyncReceiveCommits;
+import com.google.gerrit.server.git.ChangeCache;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.ReceiveCommits;
 import com.google.gerrit.server.git.TagCache;
@@ -156,8 +159,12 @@
       } catch (NoSuchProjectException err) {
         throw new RepositoryNotFoundException(projectName);
       }
+
+      CurrentUser user = pc.getCurrentUser();
+      user.setAccessPath(AccessPath.GIT);
+
       if (!pc.isVisible()) {
-        if (pc.getCurrentUser() instanceof AnonymousUser) {
+        if (user instanceof AnonymousUser) {
           throw new ServiceNotAuthorizedException();
         } else {
           throw new ServiceNotEnabledException();
@@ -176,12 +183,10 @@
 
   static class UploadFactory implements UploadPackFactory<HttpServletRequest> {
     private final TransferConfig config;
-    private final Provider<WebSession> session;
 
     @Inject
-    UploadFactory(TransferConfig tc, Provider<WebSession> session) {
+    UploadFactory(TransferConfig tc) {
       this.config = tc;
-      this.session = session;
     }
 
     @Override
@@ -196,11 +201,13 @@
   static class UploadFilter implements Filter {
     private final Provider<ReviewDb> db;
     private final TagCache tagCache;
+    private final ChangeCache changeCache;
 
     @Inject
-    UploadFilter(Provider<ReviewDb> db, TagCache tagCache) {
+    UploadFilter(Provider<ReviewDb> db, TagCache tagCache, ChangeCache changeCache) {
       this.db = db;
       this.tagCache = tagCache;
+      this.changeCache = changeCache;
     }
 
     @Override
@@ -219,7 +226,7 @@
       }
 
       if (!pc.allRefsAreVisible()) {
-        up.setAdvertiseRefsHook(new VisibleRefFilter(tagCache, repo, pc, db.get(), true));
+        up.setAdvertiseRefsHook(new VisibleRefFilter(tagCache, changeCache, repo, pc, db.get(), true));
       }
 
       next.doFilter(request, response);
@@ -236,14 +243,11 @@
 
   static class ReceiveFactory implements ReceivePackFactory<HttpServletRequest> {
     private final AsyncReceiveCommits.Factory factory;
-    private final Provider<WebSession> session;
     private final TransferConfig config;
 
     @Inject
-    ReceiveFactory(AsyncReceiveCommits.Factory factory,
-        Provider<WebSession> session, TransferConfig config) {
+    ReceiveFactory(AsyncReceiveCommits.Factory factory, TransferConfig config) {
       this.factory = factory;
-      this.session = session;
       this.config = config;
     }
 
@@ -285,6 +289,7 @@
 
       ReceiveCommits rc = (ReceiveCommits) request.getAttribute(ATT_RC);
       ReceivePack rp = rc.getReceivePack();
+      rp.getAdvertiseRefsHook().advertiseRefs(rp);
       ProjectControl pc = (ProjectControl) request.getAttribute(ATT_CONTROL);
       Project.NameKey projectName = pc.getProject().getNameKey();
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HtmlDomUtil.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HtmlDomUtil.java
index 0a93f80..c47552d 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HtmlDomUtil.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HtmlDomUtil.java
@@ -53,7 +53,7 @@
 
   /** DOCTYPE for a standards mode HTML document. */
   public static final String HTML_STRICT =
-      "-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/REC-html40/strict.dtd";
+      "-//W3C//DTD HTML 4.01//EN\" \"http://www.w3.org/TR/html4/strict.dtd";
 
   /** Convert a document to a UTF-8 byte sequence. */
   public static byte[] toUTF8(final Document hostDoc) throws IOException {
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 e9b3500..879dfa3 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
@@ -77,7 +77,7 @@
   protected void doGet(final HttpServletRequest req,
       final HttpServletResponse rsp) throws IOException {
 
-    final String sid = webSession.get().getToken();
+    final String sid = webSession.get().getSessionId();
     final CurrentUser currentUser = webSession.get().getCurrentUser();
     final String what = "sign out";
     final long when = System.currentTimeMillis();
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpRequestContext.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpRequestContext.java
index 8ef826b..2db3534 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpRequestContext.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/HttpRequestContext.java
@@ -14,20 +14,31 @@
 
 package com.google.gerrit.httpd;
 
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.config.RequestScopedReviewDbProvider;
 import com.google.gerrit.server.util.RequestContext;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 
 class HttpRequestContext implements RequestContext {
   private final WebSession session;
+  private final RequestScopedReviewDbProvider provider;
 
   @Inject
-  HttpRequestContext(final WebSession session) {
+  HttpRequestContext(WebSession session,
+      RequestScopedReviewDbProvider provider) {
     this.session = session;
+    this.provider = provider;
   }
 
   @Override
   public CurrentUser getCurrentUser() {
     return session.getCurrentUser();
   }
+
+  @Override
+  public Provider<ReviewDb> getReviewDbProvider() {
+    return provider;
+  }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java
index 5b39cb2..3d9f4c8 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/ProjectBasicAuthFilter.java
@@ -18,11 +18,11 @@
 
 import com.google.common.base.Objects;
 import com.google.common.base.Strings;
+import com.google.gerrit.server.AccessPath;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountException;
 import com.google.gerrit.server.account.AccountManager;
 import com.google.gerrit.server.account.AccountState;
-import com.google.gerrit.server.account.AuthMethod;
 import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.account.AuthResult;
 import com.google.gerrit.server.config.AuthConfig;
@@ -104,10 +104,9 @@
   private boolean verify(HttpServletRequest req, Response rsp)
       throws IOException {
     final String hdr = req.getHeader(AUTHORIZATION);
-    if (hdr == null) {
+    if (hdr == null || !hdr.startsWith(LIT_BASIC)) {
       // Allow an anonymous connection through, or it might be using a
       // session cookie instead of basic authentication.
-      //
       return true;
     }
 
@@ -143,8 +142,10 @@
 
     try {
       AuthResult whoAuthResult = accountManager.authenticate(whoAuth);
-      session.get().setUserAccountId(whoAuthResult.getAccountId(),
-          AuthMethod.PASSWORD);
+      WebSession ws = session.get();
+      ws.setUserAccountId(whoAuthResult.getAccountId());
+      ws.setAccessPathOk(AccessPath.GIT, true);
+      ws.setAccessPathOk(AccessPath.REST_API, true);
       return true;
     } catch (AccountException e) {
       log.warn("Authentication failed for " + username, e);
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 84aa532..c38425d 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
@@ -20,9 +20,9 @@
 import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
 import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
 
+import com.google.gerrit.server.AccessPath;
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.AccountState;
-import com.google.gerrit.server.account.AuthMethod;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gwtjsonrpc.server.SignedToken;
@@ -110,10 +110,9 @@
   private boolean verify(HttpServletRequest req, Response rsp)
       throws IOException {
     final String hdr = req.getHeader(AUTHORIZATION);
-    if (hdr == null) {
+    if (hdr == null || !hdr.startsWith("Digest ")) {
       // Allow an anonymous connection through, or it might be using a
       // session cookie instead of digest authentication.
-      //
       return true;
     }
 
@@ -165,9 +164,10 @@
     if (expect.equals(response)) {
       try {
         if (tokens.checkToken(nonce, "") != null) {
-          session.get().setUserAccountId(
-              who.getAccount().getId(),
-              AuthMethod.PASSWORD);
+          WebSession ws = session.get();
+          ws.setUserAccountId(who.getAccount().getId());
+          ws.setAccessPathOk(AccessPath.GIT, true);
+          ws.setAccessPathOk(AccessPath.REST_API, true);
           return true;
 
         } else {
@@ -232,12 +232,6 @@
   }
 
   private Map<String, String> parseAuthorization(String auth) {
-    if (!auth.startsWith("Digest ")) {
-      // We only support Digest authentication scheme, deny the rest.
-      //
-      return Collections.emptyMap();
-    }
-
     Map<String, String> p = new HashMap<String, String>();
     int next = "Digest ".length();
     while (next < auth.length()) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RestApiServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RestApiServlet.java
deleted file mode 100644
index 99db2f0..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RestApiServlet.java
+++ /dev/null
@@ -1,232 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.httpd;
-
-import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
-import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
-
-import com.google.common.base.Objects;
-import com.google.common.base.Strings;
-import com.google.gerrit.extensions.annotations.RequiresCapability;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.CapabilityControl;
-import com.google.gerrit.util.cli.CmdLineParser;
-import com.google.gwtjsonrpc.common.JsonConstants;
-import com.google.gwtjsonrpc.server.RPCServletUtils;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-import org.kohsuke.args4j.CmdLineException;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.IOException;
-import java.io.OutputStream;
-import java.io.StringWriter;
-import java.io.UnsupportedEncodingException;
-import java.util.Collections;
-import java.util.Map;
-import java.util.Set;
-
-import javax.annotation.Nullable;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-public abstract class RestApiServlet extends HttpServlet {
-  private static final long serialVersionUID = 1L;
-  private static final Logger log =
-      LoggerFactory.getLogger(RestApiServlet.class);
-
-  /** MIME type used for a JSON response body. */
-  protected static final String JSON_TYPE = JsonConstants.JSON_TYPE;
-
-  /**
-   * Garbage prefix inserted before JSON output to prevent XSSI.
-   * <p>
-   * This prefix is ")]}'\n" and is designed to prevent a web browser from
-   * executing the response body if the resource URI were to be referenced using
-   * a &lt;script src="...&gt; HTML tag from another web site. Clients using the
-   * HTTP interface will need to always strip the first line of response data to
-   * remove this magic header.
-   */
-  protected static final byte[] JSON_MAGIC;
-
-  static {
-    try {
-      JSON_MAGIC = ")]}'\n".getBytes("UTF-8");
-    } catch (UnsupportedEncodingException e) {
-      throw new RuntimeException("UTF-8 not supported", e);
-    }
-  }
-
-  private final Provider<CurrentUser> currentUser;
-
-  @Inject
-  protected RestApiServlet(final Provider<CurrentUser> currentUser) {
-    this.currentUser = currentUser;
-  }
-
-  @Override
-  protected void service(HttpServletRequest req, HttpServletResponse res)
-      throws ServletException, IOException {
-    res.setHeader("Expires", "Fri, 01 Jan 1980 00:00:00 GMT");
-    res.setHeader("Pragma", "no-cache");
-    res.setHeader("Cache-Control", "no-cache, must-revalidate");
-    res.setHeader("Content-Disposition", "attachment");
-
-    try {
-      checkRequiresCapability();
-      super.service(req, res);
-    } catch (RequireCapabilityException err) {
-      sendError(res, SC_FORBIDDEN, err.getMessage());
-    } catch (Error err) {
-      handleException(err, req, res);
-    } catch (RuntimeException err) {
-      handleException(err, req, res);
-    }
-  }
-
-  private void checkRequiresCapability() throws RequireCapabilityException {
-    RequiresCapability rc = getClass().getAnnotation(RequiresCapability.class);
-    if (rc != null) {
-      CurrentUser user = currentUser.get();
-      CapabilityControl ctl = user.getCapabilities();
-      if (!ctl.canPerform(rc.value()) && !ctl.canAdministrateServer()) {
-        String msg = String.format(
-          "fatal: %s does not have \"%s\" capability.",
-          Objects.firstNonNull(
-            user.getUserName(),
-            user instanceof IdentifiedUser
-              ? ((IdentifiedUser) user).getNameEmail()
-              : user.toString()),
-          rc.value());
-        throw new RequireCapabilityException(msg);
-      }
-    }
-  }
-
-  private static void handleException(Throwable err, HttpServletRequest req,
-      HttpServletResponse res) throws IOException {
-    String uri = req.getRequestURI();
-    if (!Strings.isNullOrEmpty(req.getQueryString())) {
-      uri += "?" + req.getQueryString();
-    }
-    log.error(String.format("Error in %s %s", req.getMethod(), uri), err);
-
-    if (!res.isCommitted()) {
-      res.reset();
-      sendError(res, SC_INTERNAL_SERVER_ERROR, "Internal Server Error");
-    }
-  }
-
-  protected static void sendError(HttpServletResponse res,
-      int statusCode, String msg) throws IOException {
-    res.setStatus(statusCode);
-    sendText(null, res, msg);
-  }
-
-  protected static boolean acceptsJson(HttpServletRequest req) {
-    String accept = req.getHeader("Accept");
-    if (accept == null) {
-      return false;
-    } else if (JSON_TYPE.equals(accept)) {
-      return true;
-    } else if (accept.startsWith(JSON_TYPE + ",")) {
-      return true;
-    }
-    for (String p : accept.split("[ ,;][ ,;]*")) {
-      if (JSON_TYPE.equals(p)) {
-        return true;
-      }
-    }
-    return false;
-  }
-
-  protected static void sendText(@Nullable HttpServletRequest req,
-      HttpServletResponse res, String data) throws IOException {
-    res.setContentType("text/plain");
-    res.setCharacterEncoding("UTF-8");
-    send(req, res, data.getBytes("UTF-8"));
-  }
-
-  protected static void send(@Nullable HttpServletRequest req,
-      HttpServletResponse res, byte[] data) throws IOException {
-    if (data.length > 256 && req != null
-        && RPCServletUtils.acceptsGzipEncoding(req)) {
-      res.setHeader("Content-Encoding", "gzip");
-      data = HtmlDomUtil.compress(data);
-    }
-    res.setContentLength(data.length);
-    OutputStream out = res.getOutputStream();
-    try {
-      out.write(data);
-    } finally {
-      out.close();
-    }
-  }
-
-  public static class ParameterParser {
-    private final CmdLineParser.Factory parserFactory;
-
-    @Inject
-    ParameterParser(CmdLineParser.Factory pf) {
-      this.parserFactory = pf;
-    }
-
-    public <T> boolean parse(T param, HttpServletRequest req,
-        HttpServletResponse res) throws IOException {
-      return parse(param, req, res, Collections.<String>emptySet());
-    }
-
-    public <T> boolean parse(T param, HttpServletRequest req,
-        HttpServletResponse res, Set<String> argNames) throws IOException {
-      CmdLineParser clp = parserFactory.create(param);
-      try {
-        @SuppressWarnings("unchecked")
-        Map<String, String[]> parameterMap = req.getParameterMap();
-        clp.parseOptionMap(parameterMap, argNames);
-      } catch (CmdLineException e) {
-        if (!clp.wasHelpRequestedByOption()) {
-          res.setStatus(HttpServletResponse.SC_BAD_REQUEST);
-          sendText(req, res, e.getMessage());
-          return false;
-        }
-      }
-
-      if (clp.wasHelpRequestedByOption()) {
-        StringWriter msg = new StringWriter();
-        clp.printQueryStringUsage(req.getRequestURI(), msg);
-        msg.write('\n');
-        msg.write('\n');
-        clp.printUsage(msg, null);
-        msg.write('\n');
-        sendText(req, res, msg.toString());
-        return false;
-      }
-
-      return true;
-    }
-  }
-
-  @SuppressWarnings("serial") // Never serialized or thrown out of this class.
-  private static class RequireCapabilityException extends Exception {
-    public RequireCapabilityException(String msg) {
-      super(msg);
-    }
-  }
-}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RestTokenVerifier.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RestTokenVerifier.java
deleted file mode 100644
index 783ebc7..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/RestTokenVerifier.java
+++ /dev/null
@@ -1,58 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.httpd;
-
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.server.mail.RegisterNewEmailSender;
-
-/** Verifies the token sent by {@link RegisterNewEmailSender}. */
-public interface RestTokenVerifier {
-  /**
-   * Construct a token to verify a REST PUT request.
-   *
-   * @param user the caller that wants to make a PUT request
-   * @param url the URL being requested
-   * @return an unforgeable string to send to the user as the body of a GET
-   *         request. Presenting the string in a follow-up POST request provides
-   *         proof the user has the ability to read messages sent to thier
-   *         browser and they likely aren't making the request via XSRF.
-   */
-  public String sign(Account.Id user, String url);
-
-  /**
-   * Decode a token previously created.
-   *
-   * @param user the user making the verify request.
-   * @param url the url user is attempting to access.
-   * @param token the string created by sign.
-   * @throws InvalidTokenException the token is invalid, expired, malformed,
-   *         etc.
-   */
-  public void verify(Account.Id user, String url, String token)
-      throws InvalidTokenException;
-
-  /** Exception thrown when a token does not parse correctly. */
-  public static class InvalidTokenException extends Exception {
-    private static final long serialVersionUID = 1L;
-
-    public InvalidTokenException() {
-      super("Invalid token");
-    }
-
-    public InvalidTokenException(Throwable cause) {
-      super("Invalid token", cause);
-    }
-  }
-}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/SignedTokenRestTokenVerifier.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/SignedTokenRestTokenVerifier.java
deleted file mode 100644
index 83d6caa..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/SignedTokenRestTokenVerifier.java
+++ /dev/null
@@ -1,97 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.httpd;
-
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.server.config.AuthConfig;
-import com.google.gwtjsonrpc.server.SignedToken;
-import com.google.gwtjsonrpc.server.ValidToken;
-import com.google.gwtjsonrpc.server.XsrfException;
-import com.google.inject.AbstractModule;
-import com.google.inject.Inject;
-
-import org.eclipse.jgit.util.Base64;
-
-import java.io.UnsupportedEncodingException;
-
-/** Verifies the token sent by {@link RestApiServlet}. */
-public class SignedTokenRestTokenVerifier implements RestTokenVerifier {
-  private final SignedToken restToken;
-
-  public static class Module extends AbstractModule {
-    @Override
-    protected void configure() {
-      bind(RestTokenVerifier.class).to(SignedTokenRestTokenVerifier.class);
-    }
-  }
-
-  @Inject
-  SignedTokenRestTokenVerifier(AuthConfig config) {
-    restToken = config.getRestToken();
-  }
-
-  @Override
-  public String sign(Account.Id user, String url) {
-    try {
-      String payload = String.format("%s:%s", user, url);
-      byte[] utf8 = payload.getBytes("UTF-8");
-      String base64 = Base64.encodeBytes(utf8);
-      return restToken.newToken(base64);
-    } catch (XsrfException e) {
-      throw new IllegalArgumentException(e);
-    } catch (UnsupportedEncodingException e) {
-      throw new IllegalArgumentException(e);
-    }
-  }
-
-  @Override
-  public void verify(Account.Id user, String url, String tokenString)
-      throws InvalidTokenException {
-    ValidToken token;
-    try {
-      token = restToken.checkToken(tokenString, null);
-    } catch (XsrfException err) {
-      throw new InvalidTokenException(err);
-    }
-    if (token == null || token.getData() == null || token.getData().isEmpty()) {
-      throw new InvalidTokenException();
-    }
-
-    String payload;
-    try {
-      payload = new String(Base64.decode(token.getData()), "UTF-8");
-    } catch (UnsupportedEncodingException err) {
-      throw new InvalidTokenException(err);
-    }
-
-    int colonPos = payload.indexOf(':');
-    if (colonPos == -1) {
-      throw new InvalidTokenException();
-    }
-
-    Account.Id tokenUser;
-    try {
-      tokenUser = Account.Id.parse(payload.substring(0, colonPos));
-    } catch (IllegalArgumentException err) {
-      throw new InvalidTokenException(err);
-    }
-
-    String tokenUrl = payload.substring(colonPos+1);
-
-    if (!tokenUser.equals(user) || !tokenUrl.equals(url)) {
-      throw new InvalidTokenException();
-    }
-  }
-}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/TokenVerifiedRestApiServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/TokenVerifiedRestApiServlet.java
deleted file mode 100644
index 98a1b57..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/TokenVerifiedRestApiServlet.java
+++ /dev/null
@@ -1,263 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.httpd;
-
-import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
-import static javax.servlet.http.HttpServletResponse.SC_UNAUTHORIZED;
-
-import com.google.common.base.Strings;
-import com.google.common.collect.Iterators;
-import com.google.common.collect.Maps;
-import com.google.gerrit.httpd.RestTokenVerifier.InvalidTokenException;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.OutputFormat;
-import com.google.gson.Gson;
-import com.google.gson.JsonElement;
-import com.google.gson.JsonObject;
-import com.google.gson.JsonParseException;
-import com.google.gson.JsonParser;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.net.URLDecoder;
-import java.net.URLEncoder;
-import java.util.Enumeration;
-import java.util.Map;
-
-import javax.annotation.Nullable;
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletRequestWrapper;
-import javax.servlet.http.HttpServletResponse;
-
-public abstract class TokenVerifiedRestApiServlet extends RestApiServlet {
-  private static final long serialVersionUID = 1L;
-  private static final String FORM_ENCODED = "application/x-www-form-urlencoded";
-  private static final String UTF_8 = "UTF-8";
-  private static final String AUTHKEY_NAME = "_authkey";
-  private static final String AUTHKEY_HEADER = "X-authkey";
-
-  private final Gson gson;
-  private final Provider<CurrentUser> userProvider;
-  private final RestTokenVerifier verifier;
-
-  @Inject
-  protected TokenVerifiedRestApiServlet(Provider<CurrentUser> userProvider,
-      RestTokenVerifier verifier) {
-    super(userProvider);
-    this.gson = OutputFormat.JSON_COMPACT.newGson();
-    this.userProvider = userProvider;
-    this.verifier = verifier;
-  }
-
-  /**
-   * Process the (possibly state changing) request.
-   *
-   * @param req incoming HTTP request.
-   * @param res outgoing response.
-   * @param requestData JSON object representing the HTTP request parameters.
-   *        Null if the request body was not supplied in JSON format.
-   * @throws IOException
-   * @throws ServletException
-   */
-  protected abstract void doRequest(HttpServletRequest req,
-      HttpServletResponse res,
-      @Nullable JsonObject requestData) throws IOException, ServletException;
-
-  @Override
-  protected final void doGet(HttpServletRequest req, HttpServletResponse res)
-      throws ServletException, IOException {
-    CurrentUser user = userProvider.get();
-    if (!(user instanceof IdentifiedUser)) {
-      sendError(res, SC_UNAUTHORIZED, "API requires authentication");
-      return;
-    }
-
-    TokenInfo info = new TokenInfo();
-    info._authkey = verifier.sign(
-        ((IdentifiedUser) user).getAccountId(),
-        computeUrl(req));
-
-    ByteArrayOutputStream buf = new ByteArrayOutputStream();
-    String type;
-    buf.write(JSON_MAGIC);
-    if (acceptsJson(req)) {
-      type = JSON_TYPE;
-      buf.write(gson.toJson(info).getBytes(UTF_8));
-    } else {
-      type = FORM_ENCODED;
-      buf.write(String.format("%s=%s",
-          AUTHKEY_NAME,
-          URLEncoder.encode(info._authkey, UTF_8)).getBytes(UTF_8));
-    }
-
-    res.setContentType(type);
-    res.setCharacterEncoding(UTF_8);
-    res.setHeader("Content-Disposition", "attachment");
-    send(req, res, buf.toByteArray());
-  }
-
-  @Override
-  protected final void doPost(HttpServletRequest req, HttpServletResponse res)
-      throws IOException, ServletException {
-    CurrentUser user = userProvider.get();
-    if (!(user instanceof IdentifiedUser)) {
-      sendError(res, SC_UNAUTHORIZED, "API requires authentication");
-      return;
-    }
-
-    ParsedBody body;
-    if (JSON_TYPE.equals(req.getContentType())) {
-      body = parseJson(req, res);
-    } else if (FORM_ENCODED.equals(req.getContentType())) {
-      body = parseForm(req, res);
-    } else {
-      sendError(res, SC_BAD_REQUEST, String.format(
-          "Expected Content-Type: %s or %s",
-          JSON_TYPE, FORM_ENCODED));
-      return;
-    }
-
-    if (body == null) {
-      return;
-    }
-
-    if (Strings.isNullOrEmpty(body._authkey)) {
-      String h = req.getHeader(AUTHKEY_HEADER);
-      if (Strings.isNullOrEmpty(h)) {
-        sendError(res, SC_BAD_REQUEST, String.format(
-            "Expected %s in request body or %s in HTTP headers",
-            AUTHKEY_NAME, AUTHKEY_HEADER));
-        return;
-      }
-      body._authkey = URLDecoder.decode(h, UTF_8);
-    }
-
-    try {
-      verifier.verify(
-          ((IdentifiedUser) user).getAccountId(),
-          computeUrl(req),
-          body._authkey);
-    } catch (InvalidTokenException err) {
-      sendError(res, SC_BAD_REQUEST,
-          String.format("Invalid or expired %s", AUTHKEY_NAME));
-      return;
-    }
-
-    doRequest(body.req, res, body.json);
-  }
-
-  private static ParsedBody parseJson(HttpServletRequest req,
-      HttpServletResponse res) throws IOException {
-    try {
-      JsonElement element = new JsonParser().parse(req.getReader());
-      if (!element.isJsonObject()) {
-        sendError(res, SC_BAD_REQUEST, "Expected JSON object in request body");
-        return null;
-      }
-
-      ParsedBody body = new ParsedBody();
-      body.req = req;
-      body.json = (JsonObject) element;
-      JsonElement authKey = body.json.remove(AUTHKEY_NAME);
-      if (authKey != null
-          && authKey.isJsonPrimitive()
-          && authKey.getAsJsonPrimitive().isString()) {
-        body._authkey = authKey.getAsString();
-      }
-      return body;
-    } catch (JsonParseException e) {
-      sendError(res, SC_BAD_REQUEST, "Invalid JSON object in request body");
-      return null;
-    }
-  }
-
-  private static ParsedBody parseForm(HttpServletRequest req,
-      HttpServletResponse res) throws IOException {
-    ParsedBody body = new ParsedBody();
-    body.req = new WrappedRequest(req);
-    body._authkey = req.getParameter(AUTHKEY_NAME);
-    return body;
-  }
-
-  private static String computeUrl(HttpServletRequest req) {
-    StringBuffer url = req.getRequestURL();
-    String qs = req.getQueryString();
-    if (!Strings.isNullOrEmpty(qs)) {
-      url.append('?').append(qs);
-    }
-    return url.toString();
-  }
-
-  private static class TokenInfo {
-    String _authkey;
-  }
-
-  private static class ParsedBody {
-    HttpServletRequest req;
-    String _authkey;
-    JsonObject json;
-  }
-
-  private static class WrappedRequest extends HttpServletRequestWrapper {
-    @SuppressWarnings("rawtypes")
-    private Map parameters;
-
-    WrappedRequest(HttpServletRequest req) {
-      super(req);
-    }
-
-    @Override
-    public String getParameter(String name) {
-      if (AUTHKEY_NAME.equals(name)) {
-        return null;
-      }
-      return super.getParameter(name);
-    }
-
-    @Override
-    public String[] getParameterValues(String name) {
-      if (AUTHKEY_NAME.equals(name)) {
-        return null;
-      }
-      return super.getParameterValues(name);
-    }
-
-    @SuppressWarnings({"rawtypes", "unchecked"})
-    @Override
-    public Map getParameterMap() {
-      Map m = parameters;
-      if (m == null) {
-        m = super.getParameterMap();
-        if (m.containsKey(AUTHKEY_NAME)) {
-          m = Maps.newHashMap(m);
-          m.remove(AUTHKEY_NAME);
-        }
-        parameters = m;
-      }
-      return m;
-    }
-
-    @SuppressWarnings({"rawtypes", "unchecked"})
-    @Override
-    public Enumeration getParameterNames() {
-      return Iterators.asEnumeration(getParameterMap().keySet().iterator());
-    }
-  }
-}
-
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
index a7fde9b..b93df43 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/UrlModule.java
@@ -24,10 +24,11 @@
 import com.google.gerrit.httpd.raw.SshInfoServlet;
 import com.google.gerrit.httpd.raw.StaticServlet;
 import com.google.gerrit.httpd.raw.ToolServlet;
-import com.google.gerrit.httpd.rpc.account.AccountCapabilitiesServlet;
+import com.google.gerrit.httpd.rpc.account.AccountsRestApiServlet;
+import com.google.gerrit.httpd.rpc.change.ChangesRestApiServlet;
 import com.google.gerrit.httpd.rpc.change.DeprecatedChangeQueryServlet;
-import com.google.gerrit.httpd.rpc.change.ListChangesServlet;
-import com.google.gerrit.httpd.rpc.project.ListProjectsServlet;
+import com.google.gerrit.httpd.rpc.group.GroupsRestApiServlet;
+import com.google.gerrit.httpd.rpc.project.ProjectsRestApiServlet;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.config.GerritServerConfig;
@@ -39,6 +40,7 @@
 import com.google.inject.servlet.ServletModule;
 
 import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Constants;
 
 import java.io.IOException;
 
@@ -57,9 +59,11 @@
   }
 
   private final UrlConfig cfg;
+  private GerritUiOptions uiOptions;
 
-  UrlModule(UrlConfig cfg) {
+  UrlModule(UrlConfig cfg, GerritUiOptions uiOptions) {
     this.cfg = cfg;
+    this.uiOptions = uiOptions;
   }
 
   @Override
@@ -67,9 +71,11 @@
     filter("/*").through(Key.get(CacheControlFilter.class));
     bind(Key.get(CacheControlFilter.class)).in(SINGLETON);
 
-    serve("/").with(HostPageServlet.class);
-    serve("/Gerrit").with(LegacyGerritServlet.class);
-    serve("/Gerrit/*").with(legacyGerritScreen());
+    if (uiOptions.enableDefaultUi()) {
+      serve("/").with(HostPageServlet.class);
+      serve("/Gerrit").with(LegacyGerritServlet.class);
+      serve("/Gerrit/*").with(legacyGerritScreen());
+    }
     serve("/cat/*").with(CatServlet.class);
     serve("/logout").with(HttpLogoutServlet.class);
     serve("/signout").with(HttpLogoutServlet.class);
@@ -94,9 +100,10 @@
     serveRegex("^/r/(.+)/?$").with(DirectChangeByCommit.class);
 
     filter("/a/*").through(RequireIdentifiedUserFilter.class);
-    serveRegex("^/(?:a/)?accounts/self/capabilities$").with(AccountCapabilitiesServlet.class);
-    serveRegex("^/(?:a/)?changes/$").with(ListChangesServlet.class);
-    serveRegex("^/(?:a/)?projects/(.*)?$").with(ListProjectsServlet.class);
+    serveRegex("^/(?:a/)?accounts/(.*)$").with(AccountsRestApiServlet.class);
+    serveRegex("^/(?:a/)?changes/(.*)$").with(ChangesRestApiServlet.class);
+    serveRegex("^/(?:a/)?groups/(.*)?$").with(GroupsRestApiServlet.class);
+    serveRegex("^/(?:a/)?projects/(.*)?$").with(ProjectsRestApiServlet.class);
 
     if (cfg.deprecatedQuery) {
       serve("/query").with(DeprecatedChangeQueryServlet.class);
@@ -148,7 +155,11 @@
       protected void doGet(final HttpServletRequest req,
           final HttpServletResponse rsp) throws IOException {
         try {
-          Change.Id id = Change.Id.parse(req.getPathInfo());
+          String idString = req.getPathInfo();
+          if (idString.endsWith("/")) {
+            idString = idString.substring(0, idString.length() - 1);
+          }
+          Change.Id id = Change.Id.parse(idString);
           toGerrit(PageLinks.toChange(id), req, rsp);
         } catch (IllegalArgumentException err) {
           rsp.sendError(HttpServletResponse.SC_NOT_FOUND);
@@ -173,8 +184,9 @@
         while (name.endsWith("/")) {
           name = name.substring(0, name.length() - 1);
         }
-        if (name.endsWith(".git")) {
-          name = name.substring(0, name.length() - 4);
+        if (name.endsWith(Constants.DOT_GIT_EXT)) {
+          name = name.substring(0, //
+              name.length() - Constants.DOT_GIT_EXT.length());
         }
         while (name.endsWith("/")) {
           name = name.substring(0, name.length() - 1);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebModule.java
index 1a48bb5..f7ba6df 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
@@ -14,11 +14,13 @@
 
 package com.google.gerrit.httpd;
 
-import static com.google.inject.Scopes.SINGLETON;
 import static com.google.gerrit.extensions.registration.PrivateInternals_DynamicTypes.registerInParentInjectors;
+import static com.google.inject.Scopes.SINGLETON;
 
 import com.google.gerrit.common.data.GerritConfig;
-import com.google.gerrit.httpd.auth.become.BecomeAnyAccountLoginServlet;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.webui.WebUiPlugin;
+import com.google.gerrit.httpd.auth.become.BecomeAnyAccountModule;
 import com.google.gerrit.httpd.auth.container.HttpAuthModule;
 import com.google.gerrit.httpd.auth.container.HttpsClientSslCertModule;
 import com.google.gerrit.httpd.auth.ldap.LdapAuthModule;
@@ -27,8 +29,6 @@
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.server.CmdLineParserModule;
 import com.google.gerrit.server.RemotePeer;
-import com.google.gerrit.server.account.AccountManager;
-import com.google.gerrit.server.account.ChangeUserName;
 import com.google.gerrit.server.account.ClearPassword;
 import com.google.gerrit.server.account.GeneratePassword;
 import com.google.gerrit.server.config.AuthConfig;
@@ -37,6 +37,7 @@
 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.git.AsyncReceiveCommits;
 import com.google.gerrit.server.util.GuiceRequestScopePropagator;
 import com.google.gerrit.server.util.RequestScopePropagator;
 import com.google.inject.AbstractModule;
@@ -44,7 +45,6 @@
 import com.google.inject.Injector;
 import com.google.inject.ProvisionException;
 import com.google.inject.servlet.RequestScoped;
-import com.google.inject.servlet.ServletModule;
 
 import java.net.SocketAddress;
 
@@ -55,15 +55,18 @@
   private final UrlModule.UrlConfig urlConfig;
   private final boolean wantSSL;
   private final GitWebConfig gitWebConfig;
+  private final GerritUiOptions uiOptions;
 
   @Inject
   WebModule(final AuthConfig authConfig,
       final UrlModule.UrlConfig urlConfig,
       @CanonicalWebUrl @Nullable final String canonicalUrl,
+      GerritUiOptions uiOptions,
       final Injector creatingInjector) {
     this.authConfig = authConfig;
     this.urlConfig = urlConfig;
     this.wantSSL = canonicalUrl != null && canonicalUrl.startsWith("https:");
+    this.uiOptions = uiOptions;
 
     this.gitWebConfig =
         creatingInjector.createChildInjector(new AbstractModule() {
@@ -99,12 +102,7 @@
         break;
 
       case DEVELOPMENT_BECOME_ANY_ACCOUNT:
-        install(new ServletModule() {
-          @Override
-          protected void configureServlets() {
-            serve("/become").with(BecomeAnyAccountLoginServlet.class);
-          }
-        });
+        install(new BecomeAnyAccountModule());
         break;
 
       case OPENID:
@@ -116,7 +114,7 @@
         throw new ProvisionException("Unsupported loginType: " + authConfig.getAuthType());
     }
 
-    install(new UrlModule(urlConfig));
+    install(new UrlModule(urlConfig, uiOptions));
     install(new UiRpcModule());
     install(new GerritRequestModule());
     install(new GitOverHttpServlet.Module());
@@ -130,11 +128,10 @@
         SINGLETON);
     bind(GerritConfigProvider.class);
     bind(GerritConfig.class).toProvider(GerritConfigProvider.class);
+    DynamicSet.setOf(binder(), WebUiPlugin.class);
 
-    bind(AccountManager.class);
-    bind(ChangeUserName.CurrentUser.class);
-    factory(ChangeUserName.Factory.class);
     factory(ClearPassword.Factory.class);
+    install(new AsyncReceiveCommits.Module());
     install(new CmdLineParserModule());
     factory(GeneratePassword.Factory.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 44920a48..3349cc1 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSession.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSession.java
@@ -16,27 +16,23 @@
 
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
+import com.google.gerrit.server.AccessPath;
 import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.account.AuthMethod;
 import com.google.gerrit.server.account.AuthResult;
 
 public interface WebSession {
-  public AuthMethod getAuthMethod();
-
   public boolean isSignedIn();
-
-  public String getToken();
-
-  public boolean isTokenValid(String inputToken);
-
+  public String getXGerritAuth();
+  public boolean isValidXGerritAuth(String keyIn);
   public AccountExternalId.Key getLastLoginExternalId();
-
   public CurrentUser getCurrentUser();
-
-  public void login(AuthResult res, AuthMethod meth, boolean rememberMe);
+  public void login(AuthResult res, boolean rememberMe);
 
   /** Set the user account for this current request only. */
-  public void setUserAccountId(Account.Id id, AuthMethod method);
+  public void setUserAccountId(Account.Id id);
+  public boolean isAccessPathOk(AccessPath path);
+  public void setAccessPathOk(AccessPath path, boolean ok);
 
   public void logout();
+  public String getSessionId();
 }
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 4b4edf4..a5338f8 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
@@ -25,6 +25,7 @@
 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 com.google.common.cache.Cache;
 import com.google.gerrit.reviewdb.client.Account;
@@ -36,6 +37,8 @@
 import com.google.inject.name.Named;
 
 import org.eclipse.jgit.lib.Config;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
 
 import java.io.ByteArrayOutputStream;
 import java.io.IOException;
@@ -47,6 +50,7 @@
 
 @Singleton
 class WebSessionManager {
+  private static final Logger log = LoggerFactory.getLogger(WebSessionManager.class);
   static final String CACHE_NAME = "web_sessions";
 
   static long now() {
@@ -63,12 +67,22 @@
     prng = new SecureRandom();
     self = cache;
 
-    sessionMaxAgeMillis = MINUTES.toMillis(ConfigUtil.getTimeUnit(cfg,
+    sessionMaxAgeMillis = SECONDS.toMillis(ConfigUtil.getTimeUnit(cfg,
         "cache", CACHE_NAME, "maxAge",
-        MAX_AGE_MINUTES, MINUTES));
+        SECONDS.convert(MAX_AGE_MINUTES, MINUTES), SECONDS));
+    if (sessionMaxAgeMillis < MINUTES.toMillis(5)) {
+      log.warn(String.format(
+          "cache.%s.maxAge is set to %d milliseconds;" +
+          " it should be at least 5 minutes.",
+          CACHE_NAME, sessionMaxAgeMillis));
+    }
   }
 
   Key createKey(final Account.Id who) {
+    return new Key(newUniqueToken(who));
+  }
+
+  private String newUniqueToken(final Account.Id who) {
     try {
       final int nonceLen = 20;
       final ByteArrayOutputStream buf;
@@ -80,7 +94,7 @@
       writeVarInt32(buf, who.get());
       writeBytes(buf, rnd);
 
-      return new Key(CookieBase64.encode(buf.toByteArray()));
+      return CookieBase64.encode(buf.toByteArray());
     } catch (IOException e) {
       throw new RuntimeException("Cannot produce new account cookie", e);
     }
@@ -90,13 +104,11 @@
     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, xsrfToken);
+    return createVal(key, who, remember, lastLogin, val.sessionId, val.auth);
   }
 
   Val createVal(final Key key, final Account.Id who, final boolean remember,
-      final AccountExternalId.Key lastLogin, String xsrfToken) {
+      final AccountExternalId.Key lastLogin, String sid, String auth) {
     // 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
@@ -108,18 +120,14 @@
     final long now = now();
     final long refreshCookieAt = now + refresh;
     final long expiresAt = now + sessionMaxAgeMillis;
-
-    if (xsrfToken == null) {
-      // If we don't yet have a token for this session, establish one.
-      //
-      final int nonceLen = 20;
-      final byte[] rnd = new byte[nonceLen];
-      prng.nextBytes(rnd);
-      xsrfToken = CookieBase64.encode(rnd);
+    if (sid == null) {
+      sid = newUniqueToken(who);
+    }
+    if (auth == null) {
+      auth = newUniqueToken(who);
     }
 
-    Val val = new Val(who, refreshCookieAt, remember,
-        lastLogin, xsrfToken, expiresAt);
+    Val val = new Val(who, refreshCookieAt, remember, lastLogin, expiresAt, sid, auth);
     self.put(key.token, val);
     return val;
   }
@@ -182,19 +190,20 @@
     private transient long refreshCookieAt;
     private transient boolean persistentCookie;
     private transient AccountExternalId.Key externalId;
-    private transient String xsrfToken;
     private transient long expiresAt;
+    private transient String sessionId;
+    private transient String auth;
 
     Val(final Account.Id accountId, final long refreshCookieAt,
         final boolean persistentCookie, final AccountExternalId.Key externalId,
-        final String xsrfToken,
-        final long expiresAt) {
+        final long expiresAt, final String sessionId, final String auth) {
       this.accountId = accountId;
       this.refreshCookieAt = refreshCookieAt;
       this.persistentCookie = persistentCookie;
       this.externalId = externalId;
-      this.xsrfToken = xsrfToken;
       this.expiresAt = expiresAt;
+      this.sessionId = sessionId;
+      this.auth = auth;
     }
 
     Account.Id getAccountId() {
@@ -205,6 +214,14 @@
       return externalId;
     }
 
+    String getSessionId() {
+      return sessionId;
+    }
+
+    String getAuth() {
+      return auth;
+    }
+
     boolean needsCookieRefresh() {
       return refreshCookieAt <= now();
     }
@@ -213,10 +230,6 @@
       return persistentCookie;
     }
 
-    String getXsrfToken() {
-      return xsrfToken;
-    }
-
     private void writeObject(final ObjectOutputStream out) throws IOException {
       writeVarInt32(out, 1);
       writeVarInt32(out, accountId.get());
@@ -232,12 +245,19 @@
         writeString(out, externalId.get());
       }
 
-      writeVarInt32(out, 5);
-      writeString(out, xsrfToken);
+      if (sessionId != null) {
+        writeVarInt32(out, 5);
+        writeString(out, sessionId);
+      }
 
       writeVarInt32(out, 6);
       writeFixInt64(out, expiresAt);
 
+      if (auth != null) {
+        writeVarInt32(out, 7);
+        writeString(out, auth);
+      }
+
       writeVarInt32(out, 0);
     }
 
@@ -260,11 +280,14 @@
             externalId = new AccountExternalId.Key(readString(in));
             continue;
           case 5:
-            xsrfToken = readString(in);
+            sessionId = readString(in);
             continue;
           case 6:
             expiresAt = readFixInt64(in);
             continue;
+          case 7:
+            auth = 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
index 82e9da7..eb06617 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSshGlueModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/WebSshGlueModule.java
@@ -15,7 +15,6 @@
 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;
@@ -29,18 +28,14 @@
  */
 public class WebSshGlueModule extends AbstractModule {
   private final Provider<SshInfo> sshInfoProvider;
-  private final Provider<SshKeyCache> sshKeyCacheProvider;
 
   @Inject
-  WebSshGlueModule(Provider<SshInfo> sshInfoProvider,
-      Provider<SshKeyCache> sshKeyCacheProvider) {
+  WebSshGlueModule(Provider<SshInfo> sshInfoProvider) {
     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 0821496..28e361c 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
@@ -16,17 +16,20 @@
 
 import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_USERNAME;
 
+import com.google.common.base.Objects;
+import com.google.common.base.Strings;
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.httpd.HtmlDomUtil;
 import com.google.gerrit.httpd.WebSession;
+import com.google.gerrit.httpd.template.SiteHeaderFooter;
 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.AuthMethod;
 import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.account.AuthResult;
+import com.google.gwtexpui.server.CacheHeaders;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.ResultSet;
 import com.google.gwtorm.server.SchemaFactory;
@@ -52,20 +55,24 @@
 
 @SuppressWarnings("serial")
 @Singleton
-public class BecomeAnyAccountLoginServlet extends HttpServlet {
+class BecomeAnyAccountLoginServlet extends HttpServlet {
   private static final boolean IS_DEV = Boolean.getBoolean("Gerrit.GwtDevMode");
 
   private final SchemaFactory<ReviewDb> schema;
   private final Provider<WebSession> webSession;
   private final AccountManager accountManager;
+  private final SiteHeaderFooter headers;
 
   @Inject
   BecomeAnyAccountLoginServlet(final Provider<WebSession> ws,
       final SchemaFactory<ReviewDb> sf,
-      final AccountManager am, final ServletContext servletContext) {
+      final AccountManager am,
+      final ServletContext servletContext,
+      SiteHeaderFooter shf) {
     webSession = ws;
     schema = sf;
     accountManager = am;
+    headers = shf;
   }
 
   @Override
@@ -77,9 +84,7 @@
   @Override
   protected void doPost(final HttpServletRequest req,
       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");
+    CacheHeaders.setNotCacheable(rsp);
 
     final AuthResult res;
     if ("create_account".equals(req.getParameter("action"))) {
@@ -114,9 +119,11 @@
     }
 
     if (res != null) {
-      webSession.get().login(res, AuthMethod.BACKDOOR, false);
+      webSession.get().login(res, false);
       final StringBuilder rdr = new StringBuilder();
-      rdr.append(req.getContextPath());
+      rdr.append(Objects.firstNonNull(
+          Strings.emptyToNull(req.getContextPath()),
+          "/"));
       if (IS_DEV && req.getParameter("gwt.codesvr") != null) {
         if (rdr.indexOf("?") < 0) {
           rdr.append("?");
@@ -147,12 +154,12 @@
 
   private byte[] prepareHtmlOutput() throws IOException, OrmException {
     final String pageName = "BecomeAnyAccount.html";
-    final Document doc = HtmlDomUtil.parseFile(getClass(), pageName);
+    Document doc = headers.parse(getClass(), pageName);
     if (doc == null) {
       throw new FileNotFoundException("No " + pageName + " in webapp");
     }
     if (!IS_DEV) {
-      final Element devmode = HtmlDomUtil.find(doc, "gerrit_gwtdevmode");
+      final Element devmode = HtmlDomUtil.find(doc, "gwtdevmode");
       if (devmode != null) {
         devmode.getParentNode().removeChild(devmode);
       }
@@ -166,7 +173,7 @@
         String displayName;
         if (a.getUserName() != null) {
           displayName = a.getUserName();
-        } else if (a.getFullName() != null) {
+        } else if (a.getFullName() != null && !a.getFullName().isEmpty()) {
           displayName = a.getFullName();
         } else if (a.getPreferredEmail() != null) {
           displayName = a.getPreferredEmail();
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountModule.java
new file mode 100644
index 0000000..5e2a425
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/become/BecomeAnyAccountModule.java
@@ -0,0 +1,24 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.auth.become;
+
+import com.google.inject.servlet.ServletModule;
+
+public class BecomeAnyAccountModule extends ServletModule {
+  @Override
+  protected void configureServlets() {
+    serve("/login", "/login/*").with(BecomeAnyAccountLoginServlet.class);
+  }
+}
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 eb8a76b..adca95e 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
@@ -14,14 +14,24 @@
 
 package com.google.gerrit.httpd.auth.container;
 
+import static com.google.common.base.Objects.firstNonNull;
+import static com.google.common.base.Strings.emptyToNull;
+import static com.google.common.net.HttpHeaders.AUTHORIZATION;
+import static com.google.gerrit.reviewdb.client.AccountExternalId.SCHEME_GERRIT;
+
 import com.google.gerrit.httpd.HtmlDomUtil;
 import com.google.gerrit.httpd.WebSession;
 import com.google.gerrit.httpd.raw.HostPageServlet;
+import com.google.gerrit.reviewdb.client.AccountExternalId;
+import com.google.gerrit.server.config.AuthConfig;
+import com.google.gwtexpui.server.CacheHeaders;
 import com.google.gwtjsonrpc.server.RPCServletUtils;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
+import org.eclipse.jgit.util.Base64;
+
 import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.OutputStream;
@@ -29,7 +39,6 @@
 import javax.servlet.Filter;
 import javax.servlet.FilterChain;
 import javax.servlet.FilterConfig;
-import javax.servlet.ServletContext;
 import javax.servlet.ServletException;
 import javax.servlet.ServletRequest;
 import javax.servlet.ServletResponse;
@@ -48,14 +57,15 @@
  */
 @Singleton
 class HttpAuthFilter implements Filter {
-  private final Provider<WebSession> webSession;
+  private final Provider<WebSession> sessionProvider;
   private final byte[] signInRaw;
   private final byte[] signInGzip;
+  private final String loginHeader;
 
   @Inject
   HttpAuthFilter(final Provider<WebSession> webSession,
-      final ServletContext servletContext) throws IOException {
-    this.webSession = webSession;
+      final AuthConfig authConfig) throws IOException {
+    this.sessionProvider = webSession;
 
     final String pageName = "LoginRedirect.html";
     final String doc = HtmlDomUtil.readFile(getClass(), pageName);
@@ -65,13 +75,18 @@
 
     signInRaw = doc.getBytes(HtmlDomUtil.ENC);
     signInGzip = HtmlDomUtil.compress(signInRaw);
+    loginHeader = firstNonNull(
+        emptyToNull(authConfig.getLoginHttpHeader()),
+        AUTHORIZATION);
   }
 
   @Override
   public void doFilter(final ServletRequest request,
       final ServletResponse response, final FilterChain chain)
       throws IOException, ServletException {
-    if (!webSession.get().isSignedIn()) {
+    if (isSessionValid((HttpServletRequest) request)) {
+      chain.doFilter(request, response);
+    } else {
       // Not signed in yet. Since the browser state might have an anchor
       // token which we want to capture and carry through the auth process
       // we send back JavaScript now to capture that, and do the real work
@@ -87,9 +102,7 @@
         tosend = signInRaw;
       }
 
-      rsp.setHeader("Expires", "Fri, 01 Jan 1980 00:00:00 GMT");
-      rsp.setHeader("Pragma", "no-cache");
-      rsp.setHeader("Cache-Control", "no-cache, must-revalidate");
+      CacheHeaders.setNotCacheable(rsp);
       rsp.setContentType("text/html");
       rsp.setCharacterEncoding(HtmlDomUtil.ENC);
       rsp.setContentLength(tosend.length);
@@ -99,13 +112,72 @@
       } finally {
         out.close();
       }
-    } else {
-      // Already signed in, forward the request.
-      //
-      chain.doFilter(request, response);
     }
   }
 
+  private boolean isSessionValid(HttpServletRequest req) {
+    WebSession session = sessionProvider.get();
+    if (session.isSignedIn()) {
+      String user = getRemoteUser(req);
+      return user == null || correctUser(user, session);
+    }
+    return false;
+  }
+
+  private static boolean correctUser(String user, WebSession session) {
+    AccountExternalId.Key id = session.getLastLoginExternalId();
+    return id != null
+        && id.equals(new AccountExternalId.Key(SCHEME_GERRIT, user));
+  }
+
+  String getRemoteUser(HttpServletRequest req) {
+    if (AUTHORIZATION.equals(loginHeader)) {
+      String user = emptyToNull(req.getRemoteUser());
+      if (user != null) {
+        // The container performed the authentication, and has the user
+        // identity already decoded for us. Honor that as we have been
+        // configured to honor HTTP authentication.
+        return user;
+      }
+
+      // If the container didn't do the authentication we might
+      // have done it in the front-end web server. Try to split
+      // the identity out of the Authorization header and honor it.
+      //
+      String auth = emptyToNull(req.getHeader(AUTHORIZATION));
+      if (auth == null) {
+        return null;
+
+      } else if (auth.startsWith("Basic ")) {
+        auth = auth.substring("Basic ".length());
+        auth = new String(Base64.decode(auth));
+        final int c = auth.indexOf(':');
+        return c > 0 ? auth.substring(0, c) : null;
+
+      } else if (auth.startsWith("Digest ")) {
+        int u = auth.indexOf("username=\"");
+        if (u <= 0) {
+          return null;
+        }
+        auth = auth.substring(u + 10);
+        int e = auth.indexOf('"');
+        return e > 0 ? auth.substring(0, auth.indexOf('"')) : null;
+
+      } else {
+        return null;
+      }
+    } else {
+      // Nonstandard HTTP header. We have been told to trust this
+      // header blindly as-is.
+      //
+      return emptyToNull(req.getHeader(loginHeader));
+    }
+  }
+
+  String getLoginHeader() {
+    return loginHeader;
+  }
+
   @Override
   public void init(final FilterConfig filterConfig) {
   }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpAuthModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpAuthModule.java
index 553b1f4..daaa7e2 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpAuthModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpAuthModule.java
@@ -21,6 +21,6 @@
   @Override
   protected void configureServlets() {
     filter("/").through(HttpAuthFilter.class);
-    serve("/login/*").with(HttpLoginServlet.class);
+    serve("/login", "/login/*").with(HttpLoginServlet.class);
   }
 }
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 9b7eaf5..696a585 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpLoginServlet.java
@@ -19,16 +19,14 @@
 import com.google.gerrit.httpd.WebSession;
 import com.google.gerrit.server.account.AccountException;
 import com.google.gerrit.server.account.AccountManager;
-import com.google.gerrit.server.account.AuthMethod;
 import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.account.AuthResult;
-import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gwtexpui.server.CacheHeaders;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
-import org.eclipse.jgit.util.Base64;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 import org.w3c.dom.Document;
@@ -58,23 +56,20 @@
   private static final Logger log =
       LoggerFactory.getLogger(HttpLoginServlet.class);
 
-  private static final String AUTHORIZATION = "Authorization";
   private final Provider<WebSession> webSession;
   private final Provider<String> urlProvider;
   private final AccountManager accountManager;
-  private final String loginHeader;
+  private final HttpAuthFilter authFilter;
 
   @Inject
-  HttpLoginServlet(final AuthConfig authConfig,
-      final Provider<WebSession> webSession,
+  HttpLoginServlet(final Provider<WebSession> webSession,
       @CanonicalWebUrl @Nullable final Provider<String> urlProvider,
-      final AccountManager accountManager) {
+      final AccountManager accountManager,
+      final HttpAuthFilter authFilter) {
     this.webSession = webSession;
     this.urlProvider = urlProvider;
     this.accountManager = accountManager;
-
-    final String hdr = authConfig.getLoginHttpHeader();
-    this.loginHeader = hdr != null && !hdr.equals("") ? hdr : AUTHORIZATION;
+    this.authFilter = authFilter;
   }
 
   @Override
@@ -86,19 +81,16 @@
       return;
     }
 
-    rsp.setHeader("Expires", "Fri, 01 Jan 1980 00:00:00 GMT");
-    rsp.setHeader("Pragma", "no-cache");
-    rsp.setHeader("Cache-Control", "no-cache, must-revalidate");
-
-    final String user = getRemoteUser(req);
+    CacheHeaders.setNotCacheable(rsp);
+    final String user = authFilter.getRemoteUser(req);
     if (user == null || "".equals(user)) {
-      log.error("Unable to authenticate user by " + loginHeader
+      log.error("Unable to authenticate user by " + authFilter.getLoginHeader()
           + " request header.  Check container or server configuration.");
 
       final Document doc = HtmlDomUtil.parseFile( //
           HttpLoginServlet.class, "ConfigurationError.html");
 
-      replace(doc, "loginHeader", loginHeader);
+      replace(doc, "loginHeader", authFilter.getLoginHeader());
       replace(doc, "ServerName", req.getServerName());
       replace(doc, "ServerPort", ":" + req.getServerPort());
       replace(doc, "ContextPath", req.getContextPath());
@@ -136,8 +128,7 @@
     }
     rdr.append(token);
 
-    webSession.get().login(arsp, AuthMethod.COOKIE,
-                           true /* persistent cookie */);
+    webSession.get().login(arsp, true /* persistent cookie */);
     rsp.sendRedirect(rdr.toString());
   }
 
@@ -173,50 +164,4 @@
     }
     return token;
   }
-
-  private String getRemoteUser(final HttpServletRequest req) {
-    if (AUTHORIZATION.equals(loginHeader)) {
-      final String user = req.getRemoteUser();
-      if (user != null && !"".equals(user)) {
-        // The container performed the authentication, and has the user
-        // identity already decoded for us. Honor that as we have been
-        // configured to honor HTTP authentication.
-        //
-        return user;
-      }
-
-      // If the container didn't do the authentication we might
-      // have done it in the front-end web server. Try to split
-      // the identity out of the Authorization header and honor it.
-      //
-      String auth = req.getHeader(AUTHORIZATION);
-      if (auth == null || "".equals(auth)) {
-        return null;
-
-      } else if (auth.startsWith("Basic ")) {
-        auth = auth.substring("Basic ".length());
-        auth = new String(Base64.decode(auth));
-        final int c = auth.indexOf(':');
-        return c > 0 ? auth.substring(0, c) : null;
-
-      } else if (auth.startsWith("Digest ")) {
-        final int u = auth.indexOf("username=\"");
-        if (u <= 0) {
-          return null;
-        }
-        auth = auth.substring(u + 10);
-        final int e = auth.indexOf('"');
-        return e > 0 ? auth.substring(0, auth.indexOf('"')) : null;
-
-      } else {
-        return null;
-      }
-    } else {
-      // Nonstandard HTTP header. We have been told to trust this
-      // header blindly as-is.
-      //
-      final String user = req.getHeader(loginHeader);
-      return user != null && !"".equals(user) ? user : null;
-    }
-  }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertAuthFilter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertAuthFilter.java
index ff0eb29..c892c9d 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertAuthFilter.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertAuthFilter.java
@@ -17,7 +17,6 @@
 import com.google.gerrit.httpd.WebSession;
 import com.google.gerrit.server.account.AccountException;
 import com.google.gerrit.server.account.AccountManager;
-import com.google.gerrit.server.account.AuthMethod;
 import com.google.gerrit.server.account.AuthRequest;
 import com.google.gerrit.server.account.AuthResult;
 import com.google.inject.Inject;
@@ -42,7 +41,7 @@
 @Singleton
 class HttpsClientSslCertAuthFilter implements Filter {
 
-  private static final Pattern REGEX_USERID = Pattern.compile("CN=([^,]*),.*");
+  private static final Pattern REGEX_USERID = Pattern.compile("CN=([^,]*)");
   private static final Logger log =
     LoggerFactory.getLogger(HttpsClientSslCertAuthFilter.class);
 
@@ -71,7 +70,7 @@
     String name = certs[0].getSubjectDN().getName();
     Matcher m = REGEX_USERID.matcher(name);
     String userName;
-    if (m.matches()) {
+    if (m.find()) {
       userName = m.group(1);
     } else {
       throw new ServletException("Couldn't extract username from your certificate");
@@ -85,7 +84,7 @@
       log.error(err, e);
       throw new ServletException(err, e);
     }
-    webSession.get().login(arsp, AuthMethod.COOKIE, true);
+    webSession.get().login(arsp, true);
     chain.doFilter(req, rsp);
   }
 
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 c5fa1ba..d9585b9 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
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gwtexpui.server.CacheHeaders;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -55,9 +56,7 @@
     rdr.append('#');
     rdr.append(getToken(req));
 
-    rsp.setHeader("Expires", "Fri, 01 Jan 1980 00:00:00 GMT");
-    rsp.setHeader("Pragma", "no-cache");
-    rsp.setHeader("Cache-Control", "no-cache, must-revalidate");
+    CacheHeaders.setNotCacheable(rsp);
     rsp.sendRedirect(rdr.toString());
   }
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertModule.java
index 7d32ac8..edf501b 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/container/HttpsClientSslCertModule.java
@@ -21,6 +21,6 @@
   @Override
   protected void configureServlets() {
     filter("/").through(HttpsClientSslCertAuthFilter.class);
-    serve("/login/*").with(HttpsClientSslCertLoginServlet.class);
+    serve("/login", "/login/*").with(HttpsClientSslCertLoginServlet.class);
   }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/ldap/LdapAuthModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/ldap/LdapAuthModule.java
index c46edc3..e7663bb 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/ldap/LdapAuthModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/ldap/LdapAuthModule.java
@@ -14,20 +14,12 @@
 
 package com.google.gerrit.httpd.auth.ldap;
 
-import com.google.gerrit.httpd.rpc.RpcServletModule;
-import com.google.gerrit.httpd.rpc.UiRpcModule;
 import com.google.inject.servlet.ServletModule;
 
-/** RPC support related to username/password LDAP authentication. */
+/** Configure username/password LDAP authentication. */
 public class LdapAuthModule extends ServletModule {
   @Override
   protected void configureServlets() {
-    serve("/login/*").with(LoginRedirectServlet.class);
-    install(new RpcServletModule(UiRpcModule.PREFIX) {
-      @Override
-      protected void configureServlets() {
-        rpc(UserPassAuthServiceImpl.class);
-      }
-    });
+    serve("/login", "/login/*").with(LdapLoginServlet.class);
   }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/ldap/LdapLoginServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/ldap/LdapLoginServlet.java
new file mode 100644
index 0000000..cfae86c
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/ldap/LdapLoginServlet.java
@@ -0,0 +1,169 @@
+// 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.auth.ldap;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Strings;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.httpd.HtmlDomUtil;
+import com.google.gerrit.httpd.WebSession;
+import com.google.gerrit.httpd.template.SiteHeaderFooter;
+import com.google.gerrit.server.account.AccountException;
+import com.google.gerrit.server.account.AccountManager;
+import com.google.gerrit.server.account.AccountUserNameException;
+import com.google.gerrit.server.account.AuthRequest;
+import com.google.gerrit.server.account.AuthResult;
+import com.google.gerrit.server.auth.AuthenticationUnavailableException;
+import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gwtexpui.server.CacheHeaders;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+import java.io.IOException;
+
+import javax.annotation.Nullable;
+import javax.servlet.ServletException;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/** Handles username/password based authentication against the directory. */
+@SuppressWarnings("serial")
+@Singleton
+class LdapLoginServlet extends HttpServlet {
+  private static final Logger log = LoggerFactory
+      .getLogger(LdapLoginServlet.class);
+
+  private final AccountManager accountManager;
+  private final Provider<WebSession> webSession;
+  private final Provider<String> urlProvider;
+  private final SiteHeaderFooter headers;
+
+  @Inject
+  LdapLoginServlet(AccountManager accountManager,
+      Provider<WebSession> webSession,
+      @CanonicalWebUrl @Nullable Provider<String> urlProvider,
+      SiteHeaderFooter headers) {
+    this.accountManager = accountManager;
+    this.webSession = webSession;
+    this.urlProvider = urlProvider;
+    this.headers = headers;
+
+    if (Strings.isNullOrEmpty(urlProvider.get())) {
+      log.error("gerrit.canonicalWebUrl must be set in gerrit.config");
+    }
+  }
+
+  private void sendForm(HttpServletRequest req, HttpServletResponse res,
+      @Nullable String errorMessage) throws IOException {
+    String self = req.getRequestURI();
+    String cancel = Objects.firstNonNull(urlProvider.get(), "/");
+    String token = getToken(req);
+    if (!token.equals("/")) {
+      cancel += "#" + token;
+    }
+
+    Document doc = headers.parse(LdapLoginServlet.class, "LoginForm.html");
+    HtmlDomUtil.find(doc, "hostName").setTextContent(req.getServerName());
+    HtmlDomUtil.find(doc, "login_form").setAttribute("action", self);
+    HtmlDomUtil.find(doc, "cancel_link").setAttribute("href", cancel);
+
+    Element emsg = HtmlDomUtil.find(doc, "error_message");
+    if (Strings.isNullOrEmpty(errorMessage)) {
+      emsg.getParentNode().removeChild(emsg);
+    } else {
+      emsg.setTextContent(errorMessage);
+    }
+
+    byte[] bin = HtmlDomUtil.toUTF8(doc);
+    res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+    res.setContentType("text/html");
+    res.setCharacterEncoding("UTF-8");
+    res.setContentLength(bin.length);
+    ServletOutputStream out = res.getOutputStream();
+    try {
+      out.write(bin);
+    } finally {
+      out.close();
+    }
+  }
+
+  @Override
+  protected void doGet(HttpServletRequest req, HttpServletResponse res)
+      throws IOException {
+    sendForm(req, res, null);
+  }
+
+  @Override
+  protected void doPost(HttpServletRequest req, HttpServletResponse res)
+      throws ServletException, IOException {
+    String username = Strings.nullToEmpty(req.getParameter("username")).trim();
+    String password = Strings.nullToEmpty(req.getParameter("password"));
+    String remember = Strings.nullToEmpty(req.getParameter("rememberme"));
+    if (username.isEmpty() || password.isEmpty()) {
+      sendForm(req, res, "Invalid username or password.");
+      return;
+    }
+
+    AuthRequest areq = AuthRequest.forUser(username);
+    areq.setPassword(password);
+
+    AuthResult ares;
+    try {
+      ares = accountManager.authenticate(areq);
+    } catch (AccountUserNameException e) {
+      sendForm(req, res, e.getMessage());
+      return;
+    } catch (AuthenticationUnavailableException e) {
+      sendForm(req, res, "Authentication unavailable at this time.");
+      return;
+    } catch (AccountException e) {
+      log.info(String.format("'%s' failed to sign in: %s", username, e.getMessage()));
+      sendForm(req, res, "Invalid username or password.");
+      return;
+    } catch (RuntimeException e) {
+      log.error("LDAP authentication failed", e);
+      sendForm(req, res, "Authentication unavailable at this time.");
+      return;
+    }
+
+    String token = getToken(req);
+    StringBuilder dest = new StringBuilder();
+    dest.append(urlProvider.get());
+    dest.append('#');
+    dest.append(token);
+
+    CacheHeaders.setNotCacheable(res);
+    webSession.get().login(ares, "1".equals(remember));
+    res.sendRedirect(dest.toString());
+  }
+
+  private static String getToken(final HttpServletRequest req) {
+    String token = req.getPathInfo();
+    if (token == null || token.isEmpty()) {
+      token = PageLinks.MINE;
+    } else if (!token.startsWith("/")) {
+      token = "/" + token;
+    }
+    return token;
+  }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/ldap/LoginRedirectServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/ldap/LoginRedirectServlet.java
deleted file mode 100644
index da6e227..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/ldap/LoginRedirectServlet.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.auth.ldap;
-
-import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.common.auth.SignInMode;
-import com.google.gerrit.httpd.WebSession;
-import com.google.gerrit.server.config.CanonicalWebUrl;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.Singleton;
-
-import java.io.IOException;
-
-import javax.annotation.Nullable;
-import javax.servlet.http.HttpServlet;
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-/**
- * Servlet bound to {@code /login/*} to redirect after user/pass sign-in.
- * <p>
- * This servlet is required because user authentication is done over RPC, but if
- * the RPC is successful we need to force the host page to fully reload to pick
- * up the account information, as we don't support updating the UI on the fly
- * after a sign-in.
- */
-@Singleton
-class LoginRedirectServlet extends HttpServlet {
-  private static final long serialVersionUID = 1L;
-
-  private final Provider<WebSession> webSession;
-  private final Provider<String> urlProvider;
-
-  @Inject
-  LoginRedirectServlet(final Provider<WebSession> webSession,
-      @CanonicalWebUrl @Nullable final Provider<String> urlProvider) {
-    this.webSession = webSession;
-    this.urlProvider = urlProvider;
-  }
-
-  @Override
-  protected void doGet(final HttpServletRequest req,
-      final HttpServletResponse rsp) throws IOException {
-    final String token;
-    if (webSession.get().isSignedIn()) {
-      token = getToken(req);
-    } else {
-      final String msg = "Session cookie not available.";
-      token = "/SignInFailure," + SignInMode.SIGN_IN + "," + msg;
-    }
-
-    final StringBuilder rdr = new StringBuilder();
-    rdr.append(urlProvider.get());
-    rdr.append('#');
-    rdr.append(token);
-
-    rsp.setHeader("Expires", "Fri, 01 Jan 1980 00:00:00 GMT");
-    rsp.setHeader("Pragma", "no-cache");
-    rsp.setHeader("Cache-Control", "no-cache, must-revalidate");
-    rsp.sendRedirect(rdr.toString());
-  }
-
-  private String getToken(final HttpServletRequest req) {
-    String token = req.getPathInfo();
-    if (token == null || token.isEmpty()) {
-      token = PageLinks.MINE;
-    } else if (!token.startsWith("/")) {
-      token = "/" + token;
-    }
-    return token;
-  }
-}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/ldap/UserPassAuthServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/ldap/UserPassAuthServiceImpl.java
deleted file mode 100644
index 348ecbb..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/auth/ldap/UserPassAuthServiceImpl.java
+++ /dev/null
@@ -1,94 +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.auth.ldap;
-
-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.AuthMethod;
-import com.google.gerrit.server.account.AuthRequest;
-import com.google.gerrit.server.account.AuthResult;
-import com.google.gerrit.server.auth.AuthenticationUnavailableException;
-import com.google.gerrit.server.config.AuthConfig;
-import com.google.gwtjsonrpc.common.AsyncCallback;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-class UserPassAuthServiceImpl implements UserPassAuthService {
-  private final Provider<WebSession> webSession;
-  private final AccountManager accountManager;
-  private final AuthType authType;
-
-  private static final Logger log = LoggerFactory
-      .getLogger(UserPassAuthServiceImpl.class);
-
-  @Inject
-  UserPassAuthServiceImpl(final Provider<WebSession> webSession,
-      final AccountManager accountManager, final AuthConfig authConfig) {
-    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(authType);
-    if (username == null || "".equals(username.trim()) //
-        || password == null || "".equals(password)) {
-      result.setError(LoginResult.Error.INVALID_LOGIN);
-      callback.onSuccess(result);
-      return;
-    }
-
-    username = username.trim();
-
-    final AuthRequest req = AuthRequest.forUser(username);
-    req.setPassword(password);
-
-    final AuthResult res;
-    try {
-      res = accountManager.authenticate(req);
-    } catch (AccountUserNameException e) {
-      // entered user name and password were correct, but user name could not be
-      // set for the newly created account and this is why the login fails,
-      // 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) {
-      log.info(String.format("'%s' failed to sign in: %s", username, e.getMessage()));
-      result.setError(LoginResult.Error.INVALID_LOGIN);
-      callback.onSuccess(result);
-      return;
-    }
-
-    result.success = true;
-    result.isNew = res.isNew();
-    webSession.get().login(res, AuthMethod.PASSWORD,
-                           true /* persistent cookie */);
-    callback.onSuccess(result);
-  }
-}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitLogoServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitLogoServlet.java
index 5ba1f09..41aa552 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitLogoServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitLogoServlet.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.httpd.gitweb;
 
 import com.google.gerrit.httpd.GitWebConfig;
+import com.google.gwtexpui.server.CacheHeaders;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -33,11 +34,6 @@
 @SuppressWarnings("serial")
 @Singleton
 class GitLogoServlet extends HttpServlet {
-  private static final long MAX_AGE =
-      TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES);
-  private static final String CACHE_CTRL =
-      "public, max-age=" + (MAX_AGE / 1000L);
-
   private final long modified;
   private final byte[] raw;
 
@@ -60,16 +56,18 @@
   }
 
   @Override
+  protected long getLastModified(final HttpServletRequest req) {
+    return modified;
+  }
+
+  @Override
   protected void doGet(final HttpServletRequest req,
       final HttpServletResponse rsp) throws IOException {
     if (raw != null) {
-      final long now = System.currentTimeMillis();
       rsp.setContentType("image/png");
       rsp.setContentLength(raw.length);
-      rsp.setHeader("Cache-Control", CACHE_CTRL);
-      rsp.setDateHeader("Date", now);
-      rsp.setDateHeader("Expires", now + MAX_AGE);
       rsp.setDateHeader("Last-Modified", modified);
+      CacheHeaders.setCacheable(req, rsp, 5, TimeUnit.MINUTES);
 
       final ServletOutputStream os = rsp.getOutputStream();
       try {
@@ -78,6 +76,7 @@
         os.close();
       }
     } else {
+      CacheHeaders.setNotCacheable(rsp);
       rsp.sendError(HttpServletResponse.SC_NOT_FOUND);
     }
   }
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 e397961..dcca106 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,6 +17,7 @@
 import com.google.gerrit.httpd.GitWebConfig;
 import com.google.gerrit.httpd.HtmlDomUtil;
 import com.google.gerrit.server.config.SitePaths;
+import com.google.gwtexpui.server.CacheHeaders;
 import com.google.gwtjsonrpc.server.RPCServletUtils;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -49,10 +50,6 @@
   }
 
   private static final String ENC = "UTF-8";
-  private static final long MAX_AGE =
-      TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES);
-  private static final String CACHE_CTRL =
-      "public, max-age=" + (MAX_AGE / 1000L);
 
   private final long modified;
   private final byte[] raw_css;
@@ -89,7 +86,6 @@
   protected void doGet(final HttpServletRequest req,
       final HttpServletResponse rsp) throws IOException {
     if (raw_css != null) {
-      final long now = System.currentTimeMillis();
       rsp.setContentType("text/css");
       rsp.setCharacterEncoding(ENC);
       final byte[] toSend;
@@ -100,10 +96,8 @@
         toSend = raw_css;
       }
       rsp.setContentLength(toSend.length);
-      rsp.setHeader("Cache-Control", CACHE_CTRL);
-      rsp.setDateHeader("Date", now);
-      rsp.setDateHeader("Expires", now + MAX_AGE);
       rsp.setDateHeader("Last-Modified", modified);
+      CacheHeaders.setCacheable(req, rsp, 5, TimeUnit.MINUTES);
 
       final ServletOutputStream os = rsp.getOutputStream();
       try {
@@ -112,6 +106,7 @@
         os.close();
       }
     } else {
+      CacheHeaders.setNotCacheable(rsp);
       rsp.sendError(HttpServletResponse.SC_NOT_FOUND);
     }
   }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitWebJavaScriptServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitWebJavaScriptServlet.java
index 61f32ac..d71732a 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitWebJavaScriptServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/gitweb/GitWebJavaScriptServlet.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.httpd.gitweb;
 
 import com.google.gerrit.httpd.GitWebConfig;
+import com.google.gwtexpui.server.CacheHeaders;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -33,11 +34,6 @@
 @SuppressWarnings("serial")
 @Singleton
 class GitWebJavaScriptServlet extends HttpServlet {
-  private static final long MAX_AGE =
-      TimeUnit.MILLISECONDS.convert(5, TimeUnit.MINUTES);
-  private static final String CACHE_CTRL =
-      "public, max-age=" + (MAX_AGE / 1000L);
-
   private final long modified;
   private final byte[] raw;
 
@@ -68,13 +64,10 @@
   protected void doGet(final HttpServletRequest req,
       final HttpServletResponse rsp) throws IOException {
     if (raw != null) {
-      final long now = System.currentTimeMillis();
       rsp.setContentType("text/javascript");
       rsp.setContentLength(raw.length);
-      rsp.setHeader("Cache-Control", CACHE_CTRL);
-      rsp.setDateHeader("Date", now);
-      rsp.setDateHeader("Expires", now + MAX_AGE);
       rsp.setDateHeader("Last-Modified", modified);
+      CacheHeaders.setCacheable(req, rsp, 5, TimeUnit.MINUTES);
 
       final ServletOutputStream os = rsp.getOutputStream();
       try {
@@ -83,6 +76,7 @@
         os.close();
       }
     } else {
+      CacheHeaders.setNotCacheable(rsp);
       rsp.sendError(HttpServletResponse.SC_NOT_FOUND);
     }
   }
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 6c37e43..ba7a5b8 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
@@ -30,6 +30,7 @@
 package com.google.gerrit.httpd.gitweb;
 
 import com.google.gerrit.common.data.GerritConfig;
+import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.httpd.GitWebConfig;
 import com.google.gerrit.launcher.GerritLauncher;
 import com.google.gerrit.reviewdb.client.Project;
@@ -39,6 +40,7 @@
 import com.google.gerrit.server.git.LocalDiskRepositoryManager;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectControl;
+import com.google.gwtexpui.server.CacheHeaders;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -58,10 +60,8 @@
 import java.io.InputStreamReader;
 import java.io.OutputStream;
 import java.io.PrintWriter;
-import java.io.UnsupportedEncodingException;
 import java.net.URI;
 import java.net.URISyntaxException;
-import java.net.URLDecoder;
 import java.util.Enumeration;
 import java.util.HashMap;
 import java.util.HashSet;
@@ -388,17 +388,14 @@
       return;
     }
     try {
-      rsp.setHeader("Expires", "Fri, 01 Jan 1980 00:00:00 GMT");
-      rsp.setHeader("Pragma", "no-cache");
-      rsp.setHeader("Cache-Control", "no-cache, must-revalidate");
+      CacheHeaders.setNotCacheable(rsp);
       exec(req, rsp, project, repo);
     } finally {
       repo.close();
     }
   }
 
-  private static Map<String, String> getParameters(final HttpServletRequest req)
-      throws UnsupportedEncodingException {
+  private static Map<String, String> getParameters(HttpServletRequest req) {
     final Map<String, String> params = new HashMap<String, String>();
     for (final String pair : req.getQueryString().split("[&;]")) {
       final int eq = pair.indexOf('=');
@@ -406,8 +403,8 @@
         String name = pair.substring(0, eq);
         String value = pair.substring(eq + 1);
 
-        name = URLDecoder.decode(name, "UTF-8");
-        value = URLDecoder.decode(value, "UTF-8");
+        name = Url.decode(name);
+        value = Url.decode(value);
         params.put(name, value);
       }
     }
@@ -419,7 +416,8 @@
       final Repository repo) throws IOException {
     final Process proc =
         Runtime.getRuntime().exec(new String[] {gitwebCgi.getAbsolutePath()},
-            makeEnv(req, project), repo.getDirectory());
+            makeEnv(req, project),
+            gitwebCgi.getAbsoluteFile().getParentFile());
 
     copyStderrToLog(proc.getErrorStream());
     if (0 < req.getContentLength()) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpAutoRegisterModuleGenerator.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpAutoRegisterModuleGenerator.java
index 2d957f2..d1c617f 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpAutoRegisterModuleGenerator.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpAutoRegisterModuleGenerator.java
@@ -14,14 +14,20 @@
 
 package com.google.gerrit.httpd.plugins;
 
+import static com.google.gerrit.server.plugins.AutoRegisterUtil.calculateBindAnnotation;
+
+import com.google.common.collect.LinkedListMultimap;
 import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
 import com.google.gerrit.extensions.annotations.Export;
 import com.google.gerrit.server.plugins.InvalidPluginException;
 import com.google.gerrit.server.plugins.ModuleGenerator;
 import com.google.inject.Module;
 import com.google.inject.Scopes;
+import com.google.inject.TypeLiteral;
 import com.google.inject.servlet.ServletModule;
 
+import java.lang.annotation.Annotation;
 import java.util.Map;
 
 import javax.servlet.http.HttpServlet;
@@ -29,6 +35,7 @@
 class HttpAutoRegisterModuleGenerator extends ServletModule
     implements ModuleGenerator {
   private final Map<String, Class<HttpServlet>> serve = Maps.newHashMap();
+  private final Multimap<TypeLiteral<?>, Class<?>> listeners = LinkedListMultimap.create();
 
   @Override
   protected void configureServlets() {
@@ -36,6 +43,16 @@
       bind(e.getValue()).in(Scopes.SINGLETON);
       serve(e.getKey()).with(e.getValue());
     }
+    for (Map.Entry<TypeLiteral<?>, Class<?>> e : listeners.entries()) {
+      @SuppressWarnings("unchecked")
+      TypeLiteral<Object> type = (TypeLiteral<Object>) e.getKey();
+
+      @SuppressWarnings("unchecked")
+      Class<Object> impl = (Class<Object>) e.getValue();
+
+      Annotation n = calculateBindAnnotation(impl);
+      bind(type).annotatedWith(n).to(impl);
+    }
   }
 
   @Override
@@ -63,6 +80,11 @@
   }
 
   @Override
+  public void listen(TypeLiteral<?> tl, Class<?> clazz) {
+    listeners.put(tl, clazz);
+  }
+
+  @Override
   public Module create() throws InvalidPluginException {
     return this;
   }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginModule.java
index 2bcaa30..5dc7e2e 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginModule.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginModule.java
@@ -27,7 +27,6 @@
   @Override
   protected void configureServlets() {
     bind(HttpPluginServlet.class);
-    serve("/plugins/*").with(HttpPluginServlet.class);
     serveRegex("^/(?:a/)?plugins/(.*)?$").with(HttpPluginServlet.class);
 
     bind(StartPluginListener.class)
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
index e737700..549c239 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/HttpPluginServlet.java
@@ -14,20 +14,24 @@
 
 package com.google.gerrit.httpd.plugins;
 
+import com.google.common.base.CharMatcher;
+import com.google.common.base.Splitter;
 import com.google.common.base.Strings;
 import com.google.common.cache.Cache;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
 import com.google.gerrit.extensions.registration.RegistrationHandle;
-import com.google.gerrit.httpd.rpc.plugin.ListPluginsServlet;
+import com.google.gerrit.httpd.restapi.RestApiServlet;
 import com.google.gerrit.server.MimeUtilFileTypeRegistry;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.documentation.MarkdownFormatter;
 import com.google.gerrit.server.plugins.Plugin;
+import com.google.gerrit.server.plugins.PluginsCollection;
 import com.google.gerrit.server.plugins.ReloadPluginListener;
 import com.google.gerrit.server.plugins.StartPluginListener;
 import com.google.gerrit.server.ssh.SshInfo;
+import com.google.gwtexpui.server.CacheHeaders;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
@@ -80,7 +84,7 @@
   private final Cache<ResourceKey, Resource> resourceCache;
   private final String sshHost;
   private final int sshPort;
-  private final ListPluginsServlet listServlet;
+  private final RestApiServlet managerApi;
 
   private List<Plugin> pending = Lists.newArrayList();
   private String base;
@@ -92,11 +96,13 @@
       @CanonicalWebUrl Provider<String> webUrl,
       @Named(HttpPluginModule.PLUGIN_RESOURCES) Cache<ResourceKey, Resource> cache,
       @GerritServerConfig Config cfg,
-      SshInfo sshInfo, ListPluginsServlet listServlet) {
+      SshInfo sshInfo,
+      RestApiServlet.Globals globals,
+      PluginsCollection plugins) {
     this.mimeUtil = mimeUtil;
     this.webUrl = webUrl;
     this.resourceCache = cache;
-    this.listServlet = listServlet;
+    this.managerApi = new RestApiServlet(globals, plugins);
 
     String sshHost = "review.example.com";
     int sshPort = 29418;
@@ -187,14 +193,19 @@
   @Override
   public void service(HttpServletRequest req, HttpServletResponse res)
       throws IOException, ServletException {
-    String name = extractName(req);
-    if (name.equals("")) {
-      listServlet.service(req, res);
+    List<String> parts = Lists.newArrayList(
+      Splitter.on('/').limit(3).omitEmptyStrings()
+        .split(Strings.nullToEmpty(req.getPathInfo())));
+
+    if (isApiCall(req, parts)) {
+      managerApi.service(req, res);
       return;
     }
+
+    String name = parts.get(0);
     final PluginHolder holder = plugins.get(name);
     if (holder == null) {
-      noCache(res);
+      CacheHeaders.setNotCacheable(res);
       res.sendError(HttpServletResponse.SC_NOT_FOUND);
       return;
     }
@@ -214,11 +225,19 @@
     }
   }
 
+  private static boolean isApiCall(HttpServletRequest req, List<String> parts) {
+    String method = req.getMethod();
+    int cnt = parts.size();
+    return cnt == 0
+        || (cnt == 1 && ("PUT".equals(method) || "DELETE".equals(method)))
+        || (cnt == 2 && parts.get(1).startsWith("gerrit~"));
+  }
+
   private void onDefault(PluginHolder holder,
       HttpServletRequest req,
       HttpServletResponse res) throws IOException {
     if (!"GET".equals(req.getMethod()) && !"HEAD".equals(req.getMethod())) {
-      noCache(res);
+      CacheHeaders.setNotCacheable(res);
       res.sendError(HttpServletResponse.SC_METHOD_NOT_ALLOWED);
       return;
     }
@@ -239,8 +258,11 @@
     }
 
     if ("".equals(file)) {
-      res.sendRedirect(uri + "Documentation/index.html");
-    } else if (file.startsWith("static/")) {
+      res.sendRedirect(uri + holder.docPrefix + "index.html");
+      return;
+    }
+
+    if (file.startsWith(holder.staticPrefix)) {
       JarFile jar = holder.plugin.getJarFile();
       JarEntry entry = jar.getJarEntry(file);
       if (exists(entry)) {
@@ -249,11 +271,12 @@
         resourceCache.put(key, Resource.NOT_FOUND);
         Resource.NOT_FOUND.send(req, res);
       }
-    } else if (file.equals("Documentation")) {
+    } else if (file.equals(
+        holder.docPrefix.substring(0, holder.docPrefix.length() - 1))) {
       res.sendRedirect(uri + "/index.html");
-    } else if (file.startsWith("Documentation/") && file.endsWith("/")) {
+    } else if (file.startsWith(holder.docPrefix) && file.endsWith("/")) {
       res.sendRedirect(uri + "index.html");
-    } else if (file.startsWith("Documentation/")) {
+    } else if (file.startsWith(holder.docPrefix)) {
       JarFile jar = holder.plugin.getJarFile();
       JarEntry entry = jar.getJarEntry(file);
       if (!exists(entry)) {
@@ -501,6 +524,10 @@
     }
     if (contentType == null) {
       contentType = mimeUtil.getMimeType(entry.getName(), data).toString();
+      if ("application/octet-stream".equals(contentType)
+          && entry.getName().endsWith(".js")) {
+        contentType = "application/javascript";
+      }
     }
 
     long time = entry.getTime();
@@ -549,29 +576,35 @@
     return data;
   }
 
-  private static String extractName(HttpServletRequest req) {
-    String path = req.getPathInfo();
-    if (Strings.isNullOrEmpty(path) || "/".equals(path)) {
-      return "";
-    }
-    int s = path.indexOf('/', 1);
-    return 0 <= s ? path.substring(1, s) : path.substring(1);
-  }
-
-  static void noCache(HttpServletResponse res) {
-    res.setHeader("Expires", "Fri, 01 Jan 1980 00:00:00 GMT");
-    res.setHeader("Pragma", "no-cache");
-    res.setHeader("Cache-Control", "no-cache, must-revalidate");
-    res.setHeader("Content-Disposition", "attachment");
-  }
-
   private static class PluginHolder {
     final Plugin plugin;
     final GuiceFilter filter;
+    final String staticPrefix;
+    final String docPrefix;
 
     PluginHolder(Plugin plugin, GuiceFilter filter) {
       this.plugin = plugin;
       this.filter = filter;
+      this.staticPrefix =
+        getPrefix(plugin, "Gerrit-HttpStaticPrefix", "static/");
+      this.docPrefix =
+        getPrefix(plugin, "Gerrit-HttpDocumentationPrefix", "Documentation/");
+    }
+
+    private static String getPrefix(Plugin plugin, String attr, String def) {
+      try {
+        String prefix = plugin.getJarFile().getManifest().getMainAttributes()
+            .getValue(attr);
+        if (prefix != null) {
+          return CharMatcher.is('/').trimFrom(prefix) + "/";
+        } else {
+          return def;
+        }
+      } catch (IOException e) {
+        log.warn(String.format("Error getting %s for plugin %s, using default",
+            attr, plugin.getName()), e);
+        return null;
+      }
     }
   }
 
@@ -590,7 +623,8 @@
 
     @Override
     public String getServletPath() {
-      return ((HttpServletRequest) getRequest()).getRequestURI();
+      return ((HttpServletRequest) getRequest()).getRequestURI().substring(
+          contextPath.length());
     }
   }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/Resource.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/Resource.java
index 05970af..f354fd5 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/Resource.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/plugins/Resource.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.httpd.plugins;
 
+import com.google.gwtexpui.server.CacheHeaders;
+
 import java.io.IOException;
 
 import javax.servlet.http.HttpServletRequest;
@@ -29,7 +31,7 @@
     @Override
     void send(HttpServletRequest req, HttpServletResponse res)
         throws IOException {
-      HttpPluginServlet.noCache(res);
+      CacheHeaders.setNotCacheable(res);
       res.sendError(HttpServletResponse.SC_NOT_FOUND);
     }
   };
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 adf4ad5..1ecd929 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,6 +14,7 @@
 
 package com.google.gerrit.httpd.raw;
 
+import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -23,6 +24,7 @@
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gwtexpui.server.CacheHeaders;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -44,7 +46,6 @@
 import java.io.IOException;
 import java.io.OutputStream;
 import java.io.UnsupportedEncodingException;
-import java.net.URLDecoder;
 import java.security.MessageDigest;
 import java.security.SecureRandom;
 import java.util.zip.ZipEntry;
@@ -95,7 +96,7 @@
     // rather than escaped as "%252F", which makes me feel really really
     // uncomfortable with a blind decode right here.
     //
-    keyStr = URLDecoder.decode(keyStr, "UTF-8");
+    keyStr = Url.decode(keyStr);
 
     if (!keyStr.startsWith("/")) {
       rsp.sendError(HttpServletResponse.SC_NOT_FOUND);
@@ -226,9 +227,7 @@
     final long when = fromCommit.getCommitTime() * 1000L;
 
     rsp.setDateHeader("Last-Modified", when);
-    rsp.setDateHeader("Expires", 0L);
-    rsp.setHeader("Pragma", "no-cache");
-    rsp.setHeader("Cache-Control", "no-cache, must-revalidate");
+    CacheHeaders.setNotCacheable(rsp);
 
     OutputStream out;
     ZipOutputStream zo;
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 6a43d95..6cdd9bd 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
@@ -14,25 +14,30 @@
 
 package com.google.gerrit.httpd.raw;
 
+import com.google.common.hash.Hasher;
+import com.google.common.hash.Hashing;
+import com.google.common.collect.Lists;
+import com.google.common.primitives.Bytes;
 import com.google.gerrit.common.data.GerritConfig;
 import com.google.gerrit.common.data.HostPageData;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.webui.WebUiPlugin;
 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.gwtjsonrpc.server.RPCServletUtils;
 import com.google.gwtexpui.linker.server.Permutation;
 import com.google.gwtexpui.linker.server.PermutationSelector;
+import com.google.gwtexpui.server.CacheHeaders;
 import com.google.gwtjsonrpc.server.JsonServlet;
+import com.google.gwtjsonrpc.server.RPCServletUtils;
 import com.google.inject.Inject;
 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;
 import org.slf4j.LoggerFactory;
 import org.w3c.dom.Document;
@@ -44,8 +49,8 @@
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.io.StringWriter;
-import java.security.MessageDigest;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 import javax.servlet.ServletContext;
@@ -66,6 +71,7 @@
   private final Provider<CurrentUser> currentUser;
   private final Provider<WebSession> session;
   private final GerritConfig config;
+  private final DynamicSet<WebUiPlugin> plugins;
   private final HostPageData.Theme signedOutTheme;
   private final HostPageData.Theme signedInTheme;
   private final SitePaths site;
@@ -79,15 +85,18 @@
   HostPageServlet(final Provider<CurrentUser> cu, final Provider<WebSession> w,
       final SitePaths sp, final ThemeFactory themeFactory,
       final GerritConfig gc, final ServletContext servletContext,
+      final DynamicSet<WebUiPlugin> webUiPlugins,
       @GerritServerConfig final Config cfg)
       throws IOException, ServletException {
     currentUser = cu;
     session = w;
     config = gc;
+    plugins = webUiPlugins;
     signedOutTheme = themeFactory.getSignedOutTheme();
     signedInTheme = themeFactory.getSignedInTheme();
     site = sp;
     refreshHeaderFooter = cfg.getBoolean("site", "refreshHeaderFooter", true);
+    boolean checkUserAgent = cfg.getBoolean("site", "checkUserAgent", true);
 
     final String pageName = "HostPage.html";
     template = HtmlDomUtil.parseFile(getClass(), pageName);
@@ -102,40 +111,40 @@
       throw new ServletException("No " + HPD_ID + " in " + pageName);
     }
 
-    final String src = "gerrit/gerrit.nocache.js";
-    selector = new PermutationSelector("gerrit");
-    if (IS_DEV || !cfg.getBoolean("site", "checkUserAgent", true)) {
-      noCacheName = src;
-    } else {
-      final Element devmode = HtmlDomUtil.find(template, "gerrit_gwtdevmode");
+    String src = "gerrit_ui/gerrit_ui.nocache.js";
+    if (!IS_DEV) {
+      Element devmode = HtmlDomUtil.find(template, "gwtdevmode");
       if (devmode != null) {
         devmode.getParentNode().removeChild(devmode);
       }
 
       InputStream in = servletContext.getResourceAsStream("/" + src);
-      if (in == null) {
-        throw new IOException("No " + src + " in webapp root");
-      }
-
-      final MessageDigest md = Constants.newMessageDigest();
-      try {
+      if (in != null) {
+        Hasher md = Hashing.md5().newHasher();
         try {
-          final byte[] buf = new byte[1024];
-          int n;
-          while ((n = in.read(buf)) > 0) {
-            md.update(buf, 0, n);
+          try {
+            final byte[] buf = new byte[1024];
+            int n;
+            while ((n = in.read(buf)) > 0) {
+              md.putBytes(buf, 0, n);
+            }
+          } finally {
+            in.close();
           }
-        } finally {
-          in.close();
+        } catch (IOException e) {
+          throw new IOException("Failed reading " + src, e);
         }
-      } catch (IOException e) {
-        throw new IOException("Failed reading " + src, e);
+        src += "?content=" + md.hash().toString();
+      } else {
+        log.debug("No " + src + " in webapp root; keeping noncache.js URL");
       }
-      final String id = ObjectId.fromRaw(md.digest()).name();
-      noCacheName = src + "?content=" + id;
-      selector.init(servletContext);
     }
 
+    noCacheName = src;
+    selector = new PermutationSelector("gerrit_ui");
+    if (checkUserAgent && !IS_DEV) {
+      selector.init(servletContext);
+    }
     page = new Page();
   }
 
@@ -162,18 +171,16 @@
   @Override
   protected void doGet(final HttpServletRequest req,
       final HttpServletResponse rsp) throws IOException {
-    final Page.Content page = get().get(select(req));
-    final byte[] raw;
-
+    final Page.Content page = select(req);
+    final StringWriter w = new StringWriter();
     final CurrentUser user = currentUser.get();
     if (user instanceof IdentifiedUser) {
-      final StringWriter w = new StringWriter();
       w.write(HPD_ID + ".account=");
       json(((IdentifiedUser) user).getAccount(), w);
       w.write(";");
 
-      w.write(HPD_ID + ".xsrfToken=");
-      json(session.get().getToken(), w);
+      w.write(HPD_ID + ".xGerritAuth=");
+      json(session.get().getXGerritAuth(), w);
       w.write(";");
 
       w.write(HPD_ID + ".accountDiffPref=");
@@ -183,24 +190,24 @@
       w.write(HPD_ID + ".theme=");
       json(signedInTheme, w);
       w.write(";");
-
-      final byte[] userData = w.toString().getBytes("UTF-8");
-      raw = concat(page.part1, userData, page.part2);
     } else {
-      raw = page.full;
+      w.write(HPD_ID + ".theme=");
+      json(signedOutTheme, w);
+      w.write(";");
     }
+    plugins(w);
 
+    final byte[] hpd = w.toString().getBytes("UTF-8");
+    final byte[] raw = Bytes.concat(page.part1, hpd, page.part2);
     final byte[] tosend;
     if (RPCServletUtils.acceptsGzipEncoding(req)) {
       rsp.setHeader("Content-Encoding", "gzip");
-      tosend = raw == page.full ? page.full_gz : HtmlDomUtil.compress(raw);
+      tosend = HtmlDomUtil.compress(raw);
     } else {
       tosend = raw;
     }
 
-    rsp.setHeader("Expires", "Fri, 01 Jan 1980 00:00:00 GMT");
-    rsp.setHeader("Pragma", "no-cache");
-    rsp.setHeader("Cache-Control", "no-cache, must-revalidate");
+    CacheHeaders.setNotCacheable(rsp);
     rsp.setContentType("text/html");
     rsp.setCharacterEncoding(HtmlDomUtil.ENC);
     rsp.setContentLength(tosend.length);
@@ -212,29 +219,31 @@
     }
   }
 
-  private Permutation select(final HttpServletRequest req) {
-    if ("0".equals(req.getParameter("s"))) {
+  private void plugins(StringWriter w) {
+    List<String> urls = Lists.newArrayList();
+    for (WebUiPlugin u : plugins) {
+      urls.add(String.format("plugins/%s/%s",
+          u.getPluginName(),
+          u.getJavaScriptResourcePath()));
+    }
+    if (!urls.isEmpty()) {
+      w.write(HPD_ID + ".plugins=");
+      json(urls, w);
+      w.write(";");
+    }
+  }
+
+  private Page.Content select(HttpServletRequest req) {
+    Page pg = get();
+    if ("1".equals(req.getParameter("dbg"))) {
+      return pg.debug;
+    } else if ("0".equals(req.getParameter("s"))) {
       // If s=0 is used in the URL, the user has explicitly asked us
       // to not perform selection on the server side, perhaps due to
       // it incorrectly guessing their user agent.
-      //
-      return null;
+      return pg.get(null);
     }
-    return selector.select(req);
-  }
-
-  private static byte[] concat(byte[] p1, byte[] p2, byte[] p3) {
-    final byte[] r = new byte[p1.length + p2.length + p3.length];
-    int p = 0;
-    p = append(p1, r, p);
-    p = append(p2, r, p);
-    p = append(p3, r, p);
-    return r;
-  }
-
-  private static int append(byte[] src, final byte[] dst, int p) {
-    System.arraycopy(src, 0, dst, p, src.length);
-    return p + src.length;
+    return pg.get(selector.select(req));
   }
 
   private static class FileInfo {
@@ -256,6 +265,7 @@
     private final FileInfo header;
     private final FileInfo footer;
     private final Map<Permutation, Content> permutations;
+    private final Content debug;
 
     Page() throws IOException {
       Document hostDoc = HtmlDomUtil.clone(template);
@@ -288,8 +298,12 @@
 
       Element nocache = HtmlDomUtil.find(hostDoc, "gerrit_module");
       asScript(nocache);
+      nocache.removeAttribute("id");
       nocache.setAttribute("src", noCacheName);
       permutations.put(null, new Content(hostDoc));
+
+      nocache.setAttribute("src", "gerrit_ui/gerrit_dbg.nocache.js");
+      debug = new Content(hostDoc);
     }
 
     Content get(Permutation p) {
@@ -305,7 +319,6 @@
     }
 
     private void asScript(final Element scriptNode) {
-      scriptNode.removeAttribute("id");
       scriptNode.setAttribute("type", "text/javascript");
       scriptNode.setAttribute("language", "javascript");
     }
@@ -313,8 +326,6 @@
     class Content {
       final byte[] part1;
       final byte[] part2;
-      final byte[] full;
-      final byte[] full_gz;
 
       Content(Document hostDoc) throws IOException {
         final String raw = HtmlDomUtil.toString(hostDoc);
@@ -324,15 +335,6 @@
         }
         part1 = raw.substring(0, p).getBytes("UTF-8");
         part2 = raw.substring(raw.indexOf('>', p) + 1).getBytes("UTF-8");
-
-        final StringWriter w = new StringWriter();
-        w.write(HPD_ID + ".theme=");
-        json(signedOutTheme, w);
-        w.write(";");
-
-        final byte[] themeData = w.toString().getBytes("UTF-8");
-        full = concat(part1, themeData, part2);
-        full_gz = HtmlDomUtil.compress(full);
       }
     }
 
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 d387f6e..79c2aeb 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,6 +15,7 @@
 package com.google.gerrit.httpd.raw;
 
 import com.google.gerrit.httpd.HtmlDomUtil;
+import com.google.gwtexpui.server.CacheHeaders;
 import com.google.gwtjsonrpc.server.RPCServletUtils;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -66,9 +67,7 @@
       tosend = raw;
     }
 
-    rsp.setHeader("Expires", "Fri, 01 Jan 1980 00:00:00 GMT");
-    rsp.setHeader("Pragma", "no-cache");
-    rsp.setHeader("Cache-Control", "no-cache, must-revalidate");
+    CacheHeaders.setNotCacheable(rsp);
     rsp.setContentType("text/html");
     rsp.setCharacterEncoding(HtmlDomUtil.ENC);
     rsp.setContentLength(tosend.length);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/SshInfoServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/SshInfoServlet.java
index 6078c4e..5756880 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/SshInfoServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/SshInfoServlet.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.httpd.raw;
 
 import com.google.gerrit.server.ssh.SshInfo;
+import com.google.gwtexpui.server.CacheHeaders;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -60,10 +61,6 @@
   @Override
   protected void doGet(final HttpServletRequest req,
       final HttpServletResponse rsp) throws IOException {
-    rsp.setHeader("Expires", "Fri, 01 Jan 1980 00:00:00 GMT");
-    rsp.setHeader("Pragma", "no-cache");
-    rsp.setHeader("Cache-Control", "no-cache, must-revalidate");
-
     final List<HostKey> hostKeys = sshd.getHostKeys();
     final String out;
     if (!hostKeys.isEmpty()) {
@@ -88,6 +85,7 @@
       out = "NOT_AVAILABLE";
     }
 
+    CacheHeaders.setNotCacheable(rsp);
     rsp.setCharacterEncoding("UTF-8");
     rsp.setContentType("text/plain");
     final PrintWriter w = rsp.getWriter();
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 988dadb..bc0a174 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
@@ -14,7 +14,9 @@
 
 package com.google.gerrit.httpd.raw;
 
+import com.google.common.collect.Maps;
 import com.google.gerrit.server.config.SitePaths;
+import com.google.gwtexpui.server.CacheHeaders;
 import com.google.gwtjsonrpc.server.RPCServletUtils;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -26,7 +28,8 @@
 import java.io.FileInputStream;
 import java.io.IOException;
 import java.io.OutputStream;
-import java.util.HashMap;
+import java.util.Map;
+import java.util.concurrent.TimeUnit;
 import java.util.zip.GZIPOutputStream;
 
 import javax.servlet.http.HttpServlet;
@@ -37,12 +40,7 @@
 @SuppressWarnings("serial")
 @Singleton
 public class StaticServlet extends HttpServlet {
-  private static final long MAX_AGE = 12 * 60 * 60 * 1000L/* milliseconds */;
-  private static final String CACHE_CTRL =
-      "public, max-age=" + (MAX_AGE / 1000L);
-
-  private static final HashMap<String, String> MIME_TYPES =
-      new HashMap<String, String>();
+  private static final Map<String, String> MIME_TYPES = Maps.newHashMap();
   static {
     MIME_TYPES.put("html", "text/html");
     MIME_TYPES.put("htm", "text/html");
@@ -147,6 +145,7 @@
       final HttpServletResponse rsp) throws IOException {
     final File p = local(req);
     if (p == null) {
+      CacheHeaders.setNotCacheable(rsp);
       rsp.setStatus(HttpServletResponse.SC_NOT_FOUND);
       return;
     }
@@ -161,8 +160,7 @@
       tosend = readFile(p);
     }
 
-    rsp.setHeader("Cache-Control", CACHE_CTRL);
-    rsp.setDateHeader("Expires", System.currentTimeMillis() + MAX_AGE);
+    CacheHeaders.setCacheable(req, rsp, 12, TimeUnit.HOURS);
     rsp.setDateHeader("Last-Modified", p.lastModified());
     rsp.setContentType(type);
     rsp.setContentLength(tosend.length);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ThemeFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ThemeFactory.java
index 9723674..9e89eee 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ThemeFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/raw/ThemeFactory.java
@@ -38,11 +38,11 @@
 
   private HostPageData.Theme getTheme(String name) {
     HostPageData.Theme theme = new HostPageData.Theme();
-    theme.backgroundColor = color(name, "backgroundColor", "#FCFEEF");
-    theme.textColor = color(name, "textColor", "#000000");
-    theme.trimColor = color(name, "trimColor", "#D4E9A9");
-    theme.selectionColor = color(name, "selectionColor", "#FFFFCC");
-    theme.topMenuColor = color(name, "topMenuColor", theme.trimColor);
+    theme.backgroundColor = color(name, "backgroundColor", "#FFFFFF");
+    theme.textColor = color(name, "textColor", "#353535");
+    theme.trimColor = color(name, "trimColor", "#EEEEEE");
+    theme.selectionColor = color(name, "selectionColor", "#D8EDF9");
+    theme.topMenuColor = color(name, "topMenuColor", "#FFFFFF");
     theme.changeTableOutdatedColor = color(name, "changeTableOutdatedColor", "#F08080");
     theme.tableOddRowColor = color(name, "tableOddRowColor", "transparent");
     theme.tableEvenRowColor = color(name, "tableEvenRowColor", "transparent");
@@ -54,11 +54,7 @@
     if (v == null || v.isEmpty()) {
       v = cfg.getString("theme", null, name);
       if (v == null || v.isEmpty()) {
-        if ("signed-in".equals(section) && "backgroundColor".equals(name)) {
-          v = "#FFFFFF";
-        } else {
-          v = defaultValue;
-        }
+        v = defaultValue;
       }
     }
     if (!v.startsWith("#") && v.matches("^[0-9a-fA-F]{2,6}$")) {
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/ParameterParser.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/ParameterParser.java
new file mode 100644
index 0000000..dabffc0
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/ParameterParser.java
@@ -0,0 +1,216 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.httpd.restapi;
+
+import static com.google.gerrit.httpd.restapi.RestApiServlet.replyBinaryResult;
+import static com.google.gerrit.httpd.restapi.RestApiServlet.replyError;
+import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
+
+import com.google.common.annotations.VisibleForTesting;
+import com.google.common.base.Splitter;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Sets;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.BinaryResult;
+import com.google.gerrit.extensions.restapi.Url;
+import com.google.gerrit.util.cli.CmdLineParser;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonPrimitive;
+import com.google.inject.Inject;
+
+import org.kohsuke.args4j.CmdLineException;
+
+import java.io.IOException;
+import java.io.StringWriter;
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+class ParameterParser {
+  private static final ImmutableSet<String> RESERVED_KEYS = ImmutableSet.of(
+      "pp", "prettyPrint", "strict", "callback", "alt", "fields");
+
+  private final CmdLineParser.Factory parserFactory;
+
+  @Inject
+  ParameterParser(CmdLineParser.Factory pf) {
+    this.parserFactory = pf;
+  }
+
+  <T> boolean parse(T param,
+      Multimap<String, String> in,
+      HttpServletRequest req,
+      HttpServletResponse res)
+      throws IOException {
+    CmdLineParser clp = parserFactory.create(param);
+    try {
+      clp.parseOptionMap(in);
+    } catch (CmdLineException e) {
+      if (!clp.wasHelpRequestedByOption()) {
+        replyError(res, SC_BAD_REQUEST, e.getMessage());
+        return false;
+      }
+    }
+
+    if (clp.wasHelpRequestedByOption()) {
+      StringWriter msg = new StringWriter();
+      clp.printQueryStringUsage(req.getRequestURI(), msg);
+      msg.write('\n');
+      msg.write('\n');
+      clp.printUsage(msg, null);
+      msg.write('\n');
+      replyBinaryResult(req, res,
+          BinaryResult.create(msg.toString()).setContentType("text/plain"));
+      return false;
+    }
+
+    return true;
+  }
+
+  static void splitQueryString(String queryString,
+      Multimap<String, String> config,
+      Multimap<String, String> params) {
+    if (!Strings.isNullOrEmpty(queryString)) {
+      for (String kvPair : Splitter.on('&').split(queryString)) {
+        Iterator<String> i = Splitter.on('=').limit(2).split(kvPair).iterator();
+        String key = Url.decode(i.next());
+        String val = i.hasNext() ? Url.decode(i.next()) : "";
+        if (RESERVED_KEYS.contains(key)) {
+          config.put(key, val);
+        } else {
+          params.put(key, val);
+        }
+      }
+    }
+  }
+
+  private static Set<String> query(HttpServletRequest req) {
+    Set<String> params = Sets.newHashSet();
+    if (!Strings.isNullOrEmpty(req.getQueryString())) {
+      for (String kvPair : Splitter.on('&').split(req.getQueryString())) {
+        params.add(Iterables.getFirst(
+            Splitter.on('=').limit(2).split(kvPair),
+            null));
+      }
+    }
+    return params;
+  }
+
+  /**
+   * Convert a standard URL encoded form input into a parsed JSON tree.
+   * <p>
+   * Given an input such as:
+   *
+   * <pre>
+   * message=Does+not+compile.&labels.Verified=-1
+   * </pre>
+   *
+   * which is easily created using the curl command line tool:
+   *
+   * <pre>
+   * curl --data 'message=Does not compile.' --data labels.Verified=-1
+   * </pre>
+   *
+   * converts to a JSON object structure that is normally expected:
+   *
+   * <pre>
+   * {
+   *   "message": "Does not compile.",
+   *   "labels": {
+   *     "Verified": "-1"
+   *   }
+   * }
+   * </pre>
+   *
+   * This input can then be further processed into the Java input type expected
+   * by a view using Gson. Here we rely on Gson to perform implicit conversion
+   * of a string {@code "-1"} to a number type when the Java input type expects
+   * a number.
+   * <p>
+   * Conversion assumes any field name that does not contain {@code "."} will be
+   * a property of the top level input object. Any field with a dot will use the
+   * first segment as the top level property name naming an object, and the rest
+   * of the field name as a property in the nested object.
+   *
+   * @param req request to parse form input from and create JSON tree.
+   * @return the converted JSON object tree.
+   * @throws BadRequestException the request cannot be cast, as there are
+   *         conflicting definitions for a nested object.
+   */
+  static JsonObject formToJson(HttpServletRequest req)
+      throws BadRequestException {
+    @SuppressWarnings("unchecked")
+    Map<String, String[]> map = req.getParameterMap();
+    return formToJson(map, query(req));
+  }
+
+  @VisibleForTesting
+  static JsonObject formToJson(Map<String, String[]> map, Set<String> query)
+      throws BadRequestException {
+    JsonObject inputObject = new JsonObject();
+    for (Map.Entry<String, String[]> ent : map.entrySet()) {
+      String key = ent.getKey();
+      String[] values = ent.getValue();
+
+      if (query.contains(key) || values.length == 0) {
+        // Disallow processing query parameters as input body fields.
+        // Implementations of views should avoid duplicate naming.
+        continue;
+      }
+
+      JsonObject obj = inputObject;
+      int dot = key.indexOf('.');
+      if (0 <= dot) {
+        String property = key.substring(0, dot);
+        JsonElement e = inputObject.get(property);
+        if (e == null) {
+          obj = new JsonObject();
+          inputObject.add(property, obj);
+        } else if (e.isJsonObject()) {
+          obj = e.getAsJsonObject();
+        } else {
+          throw new BadRequestException(String.format(
+              "key %s conflicts with %s",
+              key, property));
+        }
+        key = key.substring(dot + 1);
+      }
+
+      if (obj.get(key) != null) {
+        // This error should never happen. If all form values are handled
+        // together in a single pass properties are set only once. Setting
+        // again indicates something has gone very wrong.
+        throw new BadRequestException("invalid form input, use JSON instead");
+      } else if (values.length == 1) {
+        obj.addProperty(key, values[0]);
+      } else {
+        JsonArray list = new JsonArray();
+        for (String v : values) {
+          list.add(new JsonPrimitive(v));
+        }
+        obj.add(key, list);
+      }
+    }
+    return inputObject;
+  }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
new file mode 100644
index 0000000..a2aa191
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/restapi/RestApiServlet.java
@@ -0,0 +1,866 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.restapi;
+
+import static com.google.common.base.Charsets.UTF_8;
+import static com.google.common.base.Preconditions.checkNotNull;
+import static javax.servlet.http.HttpServletResponse.SC_BAD_REQUEST;
+import static javax.servlet.http.HttpServletResponse.SC_CONFLICT;
+import static javax.servlet.http.HttpServletResponse.SC_CREATED;
+import static javax.servlet.http.HttpServletResponse.SC_FORBIDDEN;
+import static javax.servlet.http.HttpServletResponse.SC_INTERNAL_SERVER_ERROR;
+import static javax.servlet.http.HttpServletResponse.SC_METHOD_NOT_ALLOWED;
+import static javax.servlet.http.HttpServletResponse.SC_NOT_FOUND;
+import static javax.servlet.http.HttpServletResponse.SC_OK;
+import static javax.servlet.http.HttpServletResponse.SC_PRECONDITION_FAILED;
+
+import com.google.common.base.Function;
+import com.google.common.base.Joiner;
+import com.google.common.base.Objects;
+import com.google.common.base.Splitter;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableMultimap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.LinkedHashMultimap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Sets;
+import com.google.common.net.HttpHeaders;
+import com.google.gerrit.audit.AuditService;
+import com.google.gerrit.audit.HttpAuditEvent;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.AcceptsCreate;
+import com.google.gerrit.extensions.restapi.AcceptsPost;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.BinaryResult;
+import com.google.gerrit.extensions.restapi.DefaultInput;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.extensions.restapi.PreconditionFailedException;
+import com.google.gerrit.extensions.restapi.PutInput;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestCollection;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.extensions.restapi.RestResource;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.extensions.restapi.StreamingResponse;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.httpd.WebSession;
+import com.google.gerrit.server.AccessPath;
+import com.google.gerrit.server.AnonymousUser;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.OutputFormat;
+import com.google.gerrit.server.account.CapabilityControl;
+import com.google.gson.ExclusionStrategy;
+import com.google.gson.FieldAttributes;
+import com.google.gson.FieldNamingPolicy;
+import com.google.gson.Gson;
+import com.google.gson.GsonBuilder;
+import com.google.gson.JsonElement;
+import com.google.gson.JsonParseException;
+import com.google.gson.JsonPrimitive;
+import com.google.gson.stream.JsonReader;
+import com.google.gson.stream.JsonToken;
+import com.google.gson.stream.MalformedJsonException;
+import com.google.gwtexpui.server.CacheHeaders;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.util.Providers;
+
+import org.eclipse.jgit.util.TemporaryBuffer;
+import org.eclipse.jgit.util.TemporaryBuffer.Heap;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.BufferedReader;
+import java.io.BufferedWriter;
+import java.io.EOFException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.Writer;
+import java.lang.reflect.Constructor;
+import java.lang.reflect.Field;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.ParameterizedType;
+import java.lang.reflect.Type;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.regex.Pattern;
+import java.util.zip.GZIPOutputStream;
+
+import javax.annotation.Nullable;
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+public class RestApiServlet extends HttpServlet {
+  private static final long serialVersionUID = 1L;
+  private static final Logger log = LoggerFactory
+      .getLogger(RestApiServlet.class);
+
+  /** MIME type used for a JSON response body. */
+  private static final String JSON_TYPE = "application/json";
+  private static final String FORM_TYPE = "application/x-www-form-urlencoded";
+
+  /**
+   * Garbage prefix inserted before JSON output to prevent XSSI.
+   * <p>
+   * This prefix is ")]}'\n" and is designed to prevent a web browser from
+   * executing the response body if the resource URI were to be referenced using
+   * a &lt;script src="...&gt; HTML tag from another web site. Clients using the
+   * HTTP interface will need to always strip the first line of response data to
+   * remove this magic header.
+   */
+  public static final byte[] JSON_MAGIC;
+
+  static {
+    JSON_MAGIC = ")]}'\n".getBytes(UTF_8);
+  }
+
+  public static class Globals {
+    final Provider<CurrentUser> currentUser;
+    final Provider<WebSession> webSession;
+    final Provider<ParameterParser> paramParser;
+    final AuditService auditService;
+
+    @Inject
+    Globals(Provider<CurrentUser> currentUser,
+        Provider<WebSession> webSession,
+        Provider<ParameterParser> paramParser,
+        AuditService auditService) {
+      this.currentUser = currentUser;
+      this.webSession = webSession;
+      this.paramParser = paramParser;
+      this.auditService = auditService;
+    }
+  }
+
+  private final Globals globals;
+  private final Provider<RestCollection<RestResource, RestResource>> members;
+
+  public RestApiServlet(Globals globals,
+      RestCollection<? extends RestResource, ? extends RestResource> members) {
+    this(globals, Providers.of(members));
+  }
+
+  public RestApiServlet(Globals globals,
+      Provider<? extends RestCollection<? extends RestResource, ? extends RestResource>> members) {
+    @SuppressWarnings("unchecked")
+    Provider<RestCollection<RestResource, RestResource>> n =
+        (Provider<RestCollection<RestResource, RestResource>>) checkNotNull((Object) members);
+    this.globals = globals;
+    this.members = n;
+  }
+
+  @Override
+  protected final void service(HttpServletRequest req, HttpServletResponse res)
+      throws ServletException, IOException {
+    long auditStartTs = System.currentTimeMillis();
+    CacheHeaders.setNotCacheable(res);
+    res.setHeader("Content-Disposition", "attachment");
+    res.setHeader("X-Content-Type-Options", "nosniff");
+    int status = SC_OK;
+    Object result = null;
+    Multimap<String, String> params = LinkedHashMultimap.create();
+    Object inputRequestBody = null;
+
+    try {
+      checkUserSession(req);
+
+      List<IdString> path = splitPath(req);
+      RestCollection<RestResource, RestResource> rc = members.get();
+      checkAccessAnnotations(rc.getClass());
+
+      RestResource rsrc = TopLevelResource.INSTANCE;
+      RestView<RestResource> view = null;
+      if (path.isEmpty()) {
+        if ("GET".equals(req.getMethod())) {
+          view = rc.list();
+        } else if (rc instanceof AcceptsPost && "POST".equals(req.getMethod())) {
+          @SuppressWarnings("unchecked")
+          AcceptsPost<RestResource> ac = (AcceptsPost<RestResource>) rc;
+          view = ac.post(rsrc);
+        } else {
+          throw new MethodNotAllowedException();
+        }
+      } else {
+        IdString id = path.remove(0);
+        try {
+          rsrc = rc.parse(rsrc, id);
+          checkPreconditions(req, rsrc);
+        } catch (ResourceNotFoundException e) {
+          if (rc instanceof AcceptsCreate
+              && path.isEmpty()
+              && ("POST".equals(req.getMethod())
+                  || "PUT".equals(req.getMethod()))) {
+            @SuppressWarnings("unchecked")
+            AcceptsCreate<RestResource> ac = (AcceptsCreate<RestResource>) rc;
+            view = ac.create(rsrc, id);
+            status = SC_CREATED;
+          } else {
+            throw e;
+          }
+        }
+        if (view == null) {
+          view = view(rc, req.getMethod(), path);
+        }
+      }
+      checkAccessAnnotations(view.getClass());
+
+      while (view instanceof RestCollection<?,?>) {
+        @SuppressWarnings("unchecked")
+        RestCollection<RestResource, RestResource> c =
+            (RestCollection<RestResource, RestResource>) view;
+
+        if (path.isEmpty()) {
+          if ("GET".equals(req.getMethod())) {
+            view = c.list();
+          } else if (c instanceof AcceptsPost && "POST".equals(req.getMethod())) {
+            @SuppressWarnings("unchecked")
+            AcceptsPost<RestResource> ac = (AcceptsPost<RestResource>) c;
+            view = ac.post(rsrc);
+          } else {
+            throw new MethodNotAllowedException();
+          }
+          break;
+        } else {
+          IdString id = path.remove(0);
+          try {
+            rsrc = c.parse(rsrc, id);
+            checkPreconditions(req, rsrc);
+            view = null;
+          } catch (ResourceNotFoundException e) {
+            if (c instanceof AcceptsCreate
+                && path.isEmpty()
+                && ("POST".equals(req.getMethod())
+                    || "PUT".equals(req.getMethod()))) {
+              @SuppressWarnings("unchecked")
+              AcceptsCreate<RestResource> ac = (AcceptsCreate<RestResource>) c;
+              view = ac.create(rsrc, id);
+              status = SC_CREATED;
+            } else {
+              throw e;
+            }
+          }
+          if (view == null) {
+            view = view(c, req.getMethod(), path);
+          }
+        }
+        checkAccessAnnotations(view.getClass());
+      }
+
+      Multimap<String, String> config = LinkedHashMultimap.create();
+      ParameterParser.splitQueryString(req.getQueryString(), config, params);
+      if (!globals.paramParser.get().parse(view, params, req, res)) {
+        return;
+      }
+
+      if (view instanceof RestModifyView<?, ?>) {
+        @SuppressWarnings("unchecked")
+        RestModifyView<RestResource, Object> m =
+            (RestModifyView<RestResource, Object>) view;
+
+        inputRequestBody = parseRequest(req, inputType(m));
+        result = m.apply(rsrc, inputRequestBody);
+      } else if (view instanceof RestReadView<?>) {
+        result = ((RestReadView<RestResource>) view).apply(rsrc);
+      } else {
+        throw new ResourceNotFoundException();
+      }
+
+      if (result instanceof Response) {
+        @SuppressWarnings("rawtypes")
+        Response r = (Response) result;
+        status = r.statusCode();
+      } else if (result instanceof Response.Redirect) {
+        res.sendRedirect(((Response.Redirect) result).location());
+        return;
+      }
+      res.setStatus(status);
+
+      if (result instanceof StreamingResponse) {
+        StreamingResponse r = (StreamingResponse) result;
+        res.setContentType(r.getContentType());
+        r.stream(res.getOutputStream());
+      } else if (result != Response.none()) {
+        result = Response.unwrap(result);
+        if (result instanceof BinaryResult) {
+          replyBinaryResult(req, res, (BinaryResult) result);
+        } else {
+          replyJson(req, res, config, result);
+        }
+      }
+    } catch (AuthException e) {
+      replyError(res, status = SC_FORBIDDEN, e.getMessage());
+    } catch (BadRequestException e) {
+      replyError(res, status = SC_BAD_REQUEST, e.getMessage());
+    } catch (MethodNotAllowedException e) {
+      replyError(res, status = SC_METHOD_NOT_ALLOWED, "Method not allowed");
+    } catch (ResourceConflictException e) {
+      replyError(res, status = SC_CONFLICT, e.getMessage());
+    } catch (PreconditionFailedException e) {
+      replyError(res, status = SC_PRECONDITION_FAILED,
+          Objects.firstNonNull(e.getMessage(), "Precondition failed"));
+    } catch (ResourceNotFoundException e) {
+      replyError(res, status = SC_NOT_FOUND, "Not found");
+    } catch (UnprocessableEntityException e) {
+      replyError(res, status = 422,
+          Objects.firstNonNull(e.getMessage(), "Unprocessable Entity"));
+    } catch (AmbiguousViewException e) {
+      replyError(res, status = SC_NOT_FOUND, e.getMessage());
+    } catch (MalformedJsonException e) {
+      replyError(res, status = SC_BAD_REQUEST, "Invalid " + JSON_TYPE + " in request");
+    } catch (JsonParseException e) {
+      replyError(res, status = SC_BAD_REQUEST, "Invalid " + JSON_TYPE + " in request");
+    } catch (Exception e) {
+      status = SC_INTERNAL_SERVER_ERROR;
+      handleException(e, req, res);
+    } finally {
+      globals.auditService.dispatch(new HttpAuditEvent(globals.webSession.get()
+          .getSessionId(), globals.currentUser.get(), req.getRequestURI(),
+          auditStartTs, params, req.getMethod(), inputRequestBody, status,
+          result));
+    }
+  }
+
+  private void checkPreconditions(HttpServletRequest req, RestResource rsrc)
+      throws PreconditionFailedException {
+    if ("*".equals(req.getHeader("If-None-Match"))) {
+      throw new PreconditionFailedException("Resource already exists");
+    }
+  }
+
+  private static Type inputType(RestModifyView<RestResource, Object> m) {
+    Type inputType = extractInputType(m.getClass());
+    if (inputType == null) {
+      throw new IllegalStateException(String.format(
+          "View %s does not correctly implement %s",
+          m.getClass(), RestModifyView.class.getSimpleName()));
+    }
+    return inputType;
+  }
+
+  @SuppressWarnings("rawtypes")
+  private static Type extractInputType(Class clazz) {
+    for (Type t : clazz.getGenericInterfaces()) {
+      if (t instanceof ParameterizedType
+          && ((ParameterizedType) t).getRawType() == RestModifyView.class) {
+        return ((ParameterizedType) t).getActualTypeArguments()[1];
+      }
+    }
+
+    if (clazz.getSuperclass() != null) {
+      Type i = extractInputType(clazz.getSuperclass());
+      if (i != null) {
+        return i;
+      }
+    }
+
+    for (Class t : clazz.getInterfaces()) {
+      Type i = extractInputType(t);
+      if (i != null) {
+        return i;
+      }
+    }
+
+    return null;
+  }
+
+  private Object parseRequest(HttpServletRequest req, Type type)
+      throws IOException, BadRequestException, SecurityException,
+      IllegalArgumentException, NoSuchMethodException, IllegalAccessException,
+      InstantiationException, InvocationTargetException, MethodNotAllowedException {
+    if (isType(JSON_TYPE, req.getContentType())) {
+      BufferedReader br = req.getReader();
+      try {
+        JsonReader json = new JsonReader(br);
+        json.setLenient(true);
+
+        JsonToken first;
+        try {
+          first = json.peek();
+        } catch (EOFException e) {
+          throw new BadRequestException("Expected JSON object");
+        }
+        if (first == JsonToken.STRING) {
+          return parseString(json.nextString(), type);
+        }
+        return OutputFormat.JSON.newGson().fromJson(json, type);
+      } finally {
+        br.close();
+      }
+    } else if ("PUT".equals(req.getMethod()) && acceptsPutInput(type)) {
+      return parsePutInput(req, type);
+    } else if ("DELETE".equals(req.getMethod()) && hasNoBody(req)) {
+      return null;
+    } else if (hasNoBody(req)) {
+      return createInstance(type);
+    } else if (isType("text/plain", req.getContentType())) {
+      BufferedReader br = req.getReader();
+      try {
+        char[] tmp = new char[256];
+        StringBuilder sb = new StringBuilder();
+        int n;
+        while (0 < (n = br.read(tmp))) {
+          sb.append(tmp, 0, n);
+        }
+        return parseString(sb.toString(), type);
+      } finally {
+        br.close();
+      }
+    } else if ("POST".equals(req.getMethod())
+        && isType(FORM_TYPE, req.getContentType())) {
+      return OutputFormat.JSON.newGson().fromJson(
+          ParameterParser.formToJson(req),
+          type);
+    } else {
+      throw new BadRequestException("Expected Content-Type: " + JSON_TYPE);
+    }
+  }
+
+  private static boolean hasNoBody(HttpServletRequest req) {
+    int len = req.getContentLength();
+    String type = req.getContentType();
+    return (len <= 0 && type == null)
+        || (len == 0 && isType(FORM_TYPE, type));
+  }
+
+  @SuppressWarnings("rawtypes")
+  private static boolean acceptsPutInput(Type type) {
+    if (type instanceof Class) {
+      for (Field f : ((Class) type).getDeclaredFields()) {
+        if (f.getType() == PutInput.class) {
+          return true;
+        }
+      }
+    }
+    return false;
+  }
+
+  private Object parsePutInput(final HttpServletRequest req, Type type)
+      throws SecurityException, NoSuchMethodException,
+      IllegalArgumentException, InstantiationException, IllegalAccessException,
+      InvocationTargetException, MethodNotAllowedException {
+    Object obj = createInstance(type);
+    for (Field f : obj.getClass().getDeclaredFields()) {
+      if (f.getType() == PutInput.class) {
+        f.setAccessible(true);
+        f.set(obj, new PutInput() {
+          @Override
+          public String getContentType() {
+            return req.getContentType();
+          }
+
+          @Override
+          public long getContentLength() {
+            return req.getContentLength();
+          }
+
+          @Override
+          public InputStream getInputStream() throws IOException {
+            return req.getInputStream();
+          }
+        });
+        return obj;
+      }
+    }
+    throw new MethodNotAllowedException();
+  }
+
+  private Object parseString(String value, Type type)
+      throws BadRequestException, SecurityException, NoSuchMethodException,
+      IllegalArgumentException, IllegalAccessException, InstantiationException,
+      InvocationTargetException {
+    if (type == String.class) {
+      return value;
+    }
+
+    Object obj = createInstance(type);
+    Field[] fields = obj.getClass().getDeclaredFields();
+    if (fields.length == 0 && Strings.isNullOrEmpty(value)) {
+      return obj;
+    }
+    for (Field f : fields) {
+      if (f.getAnnotation(DefaultInput.class) != null
+          && f.getType() == String.class) {
+        f.setAccessible(true);
+        f.set(obj, value);
+        return obj;
+      }
+    }
+    throw new BadRequestException("Expected JSON object");
+  }
+
+  private static Object createInstance(Type type)
+      throws NoSuchMethodException, InstantiationException,
+      IllegalAccessException, InvocationTargetException {
+    if (type instanceof Class) {
+      @SuppressWarnings("unchecked")
+      Class<Object> clazz = (Class<Object>) type;
+      Constructor<Object> c = clazz.getDeclaredConstructor();
+      c.setAccessible(true);
+      return c.newInstance();
+    }
+    throw new InstantiationException("Cannot make " + type);
+  }
+
+  private static void replyJson(@Nullable HttpServletRequest req,
+      HttpServletResponse res,
+      Multimap<String, String> config,
+      Object result)
+      throws IOException {
+    final TemporaryBuffer.Heap buf = heap(Integer.MAX_VALUE);
+    buf.write(JSON_MAGIC);
+    Writer w = new BufferedWriter(new OutputStreamWriter(buf, UTF_8));
+    Gson gson = newGson(config, req);
+    if (result instanceof JsonElement) {
+      gson.toJson((JsonElement) result, w);
+    } else {
+      gson.toJson(result, w);
+    }
+    w.write('\n');
+    w.flush();
+
+    replyBinaryResult(req, res, new BinaryResult() {
+      @Override
+      public long getContentLength() {
+        return buf.length();
+      }
+
+      @Override
+      public void writeTo(OutputStream os) throws IOException {
+        buf.writeTo(os, null);
+      }
+    }.setContentType(JSON_TYPE).setCharacterEncoding(UTF_8.name()));
+  }
+
+  private static Gson newGson(Multimap<String, String> config,
+      @Nullable HttpServletRequest req) {
+    GsonBuilder gb = OutputFormat.JSON_COMPACT.newGsonBuilder();
+
+    enablePrettyPrint(gb, config, req);
+    enablePartialGetFields(gb, config);
+
+    return gb.create();
+  }
+
+  private static void enablePrettyPrint(GsonBuilder gb,
+      Multimap<String, String> config,
+      @Nullable HttpServletRequest req) {
+    String pp = Iterables.getFirst(config.get("pp"), null);
+    if (pp == null) {
+      pp = Iterables.getFirst(config.get("prettyPrint"), null);
+      if (pp == null && req != null) {
+        pp = acceptsJson(req) ? "0" : "1";
+      }
+    }
+    if ("1".equals(pp) || "true".equals(pp)) {
+      gb.setPrettyPrinting();
+    }
+  }
+
+  private static void enablePartialGetFields(GsonBuilder gb,
+      Multimap<String, String> config) {
+    final Set<String> want = Sets.newHashSet();
+    for (String p : config.get("fields")) {
+      Iterables.addAll(want, Splitter.on(',')
+          .omitEmptyStrings().trimResults()
+          .split(p));
+    }
+    if (!want.isEmpty()) {
+      gb.addSerializationExclusionStrategy(new ExclusionStrategy() {
+        private final Map<String, String> names = Maps.newHashMap();
+
+        @Override
+        public boolean shouldSkipField(FieldAttributes field) {
+          String name = names.get(field.getName());
+          if (name == null) {
+            // Names are supplied by Gson in terms of Java source.
+            // Translate and cache the JSON lower_case_style used.
+            try {
+              name =
+                  FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES.translateName(//
+                      field.getDeclaringClass().getDeclaredField(field.getName()));
+              names.put(field.getName(), name);
+            } catch (SecurityException e) {
+              return true;
+            } catch (NoSuchFieldException e) {
+              return true;
+            }
+          }
+          return !want.contains(name);
+        }
+
+        @Override
+        public boolean shouldSkipClass(Class<?> clazz) {
+          return false;
+        }
+      });
+    }
+  }
+
+  static void replyBinaryResult(
+      @Nullable HttpServletRequest req,
+      HttpServletResponse res,
+      BinaryResult bin) throws IOException {
+    try {
+      res.setContentType(bin.getContentType());
+      OutputStream dst = res.getOutputStream();
+      try {
+        long len = bin.getContentLength();
+        boolean gzip = bin.canGzip() && acceptsGzip(req);
+        if (gzip && 256 <= len && len <= (10 << 20)) {
+          TemporaryBuffer.Heap buf = compress(bin);
+          if (buf.length() < len) {
+            res.setContentLength((int) buf.length());
+            res.setHeader("Content-Encoding", "gzip");
+            buf.writeTo(dst, null);
+          } else {
+            replyUncompressed(res, dst, bin, len);
+          }
+        } else if (gzip) {
+          res.setHeader("Content-Encoding", "gzip");
+          dst = new GZIPOutputStream(dst);
+          bin.writeTo(dst);
+        } else {
+          replyUncompressed(res, dst, bin, len);
+        }
+      } finally {
+        dst.close();
+      }
+    } finally {
+      bin.close();
+    }
+  }
+
+  private static void replyUncompressed(HttpServletResponse res,
+      OutputStream dst, BinaryResult bin, long len) throws IOException {
+    if (0 <= len && len < Integer.MAX_VALUE) {
+      res.setContentLength((int) len);
+    } else if (0 <= len) {
+      res.setHeader("Content-Length", Long.toString(len));
+    }
+    bin.writeTo(dst);
+  }
+
+  private RestView<RestResource> view(
+      RestCollection<RestResource, RestResource> rc,
+      String method, List<IdString> path) throws ResourceNotFoundException,
+      MethodNotAllowedException, AmbiguousViewException {
+    DynamicMap<RestView<RestResource>> views = rc.views();
+    final IdString projection = path.isEmpty()
+        ? IdString.fromUrl("/")
+        : path.remove(0);
+    if (!path.isEmpty()) {
+      // If there are path components still remaining after this projection
+      // is chosen, look for the projection based upon GET as the method as
+      // the client thinks it is a nested collection.
+      method = "GET";
+    }
+
+    List<String> p = splitProjection(projection);
+    if (p.size() == 2) {
+      RestView<RestResource> view =
+          views.get(p.get(0), method + "." + p.get(1));
+      if (view != null) {
+        return view;
+      }
+      throw new ResourceNotFoundException(projection);
+    }
+
+    String name = method + "." + p.get(0);
+    RestView<RestResource> core = views.get("gerrit", name);
+    if (core != null) {
+      return core;
+    }
+
+    Map<String, RestView<RestResource>> r = Maps.newTreeMap();
+    for (String plugin : views.plugins()) {
+      RestView<RestResource> action = views.get(plugin, name);
+      if (action != null) {
+        r.put(plugin, action);
+      }
+    }
+
+    if (r.size() == 1) {
+      return Iterables.getFirst(r.values(), null);
+    } else if (r.isEmpty()) {
+      throw new ResourceNotFoundException(projection);
+    } else {
+      throw new AmbiguousViewException(String.format(
+        "Projection %s is ambiguous: ",
+        name,
+        Joiner.on(", ").join(
+          Iterables.transform(r.keySet(), new Function<String, String>() {
+            @Override
+            public String apply(String in) {
+              return in + "~" + projection;
+            }
+          }))));
+    }
+  }
+
+  private static List<IdString> splitPath(HttpServletRequest req) {
+    String path = req.getPathInfo();
+    if (Strings.isNullOrEmpty(path)) {
+      return Collections.emptyList();
+    }
+    List<IdString> out = Lists.newArrayList();
+    for (String p : Splitter.on('/').split(path)) {
+      out.add(IdString.fromUrl(p));
+    }
+    if (out.size() > 0 && out.get(out.size() - 1).isEmpty()) {
+      out.remove(out.size() - 1);
+    }
+    return out;
+  }
+
+  private static List<String> splitProjection(IdString projection) {
+    List<String> p = Lists.newArrayListWithCapacity(2);
+    Iterables.addAll(p, Splitter.on('~').limit(2).split(projection.get()));
+    return p;
+  }
+
+  private void checkUserSession(HttpServletRequest req)
+      throws AuthException {
+    CurrentUser user = globals.currentUser.get();
+    if (isStateChange(req)) {
+      if (user instanceof AnonymousUser) {
+        throw new AuthException("Authentication required");
+      } else if (!globals.webSession.get().isAccessPathOk(AccessPath.REST_API)) {
+        throw new AuthException("Invalid authentication method. In order to authenticate, prefix the REST endpoint URL with /a/ (e.g. http://example.com/a/projects/).");
+      }
+    }
+    user.setAccessPath(AccessPath.REST_API);
+  }
+
+  private static boolean isStateChange(HttpServletRequest req) {
+    String method = req.getMethod();
+    return !("GET".equals(method) || "HEAD".equals(method));
+  }
+
+  private void checkAccessAnnotations(Class<? extends Object> clazz)
+      throws AuthException {
+    RequiresCapability rc = clazz.getAnnotation(RequiresCapability.class);
+    if (rc != null) {
+      CurrentUser user = globals.currentUser.get();
+      CapabilityControl ctl = user.getCapabilities();
+      if (!ctl.canPerform(rc.value()) && !ctl.canAdministrateServer()) {
+        throw new AuthException(String.format(
+            "Capability %s is required to access this resource",
+            rc.value()));
+      }
+    }
+  }
+
+  private static void handleException(Throwable err, HttpServletRequest req,
+      HttpServletResponse res) throws IOException {
+    String uri = req.getRequestURI();
+    if (!Strings.isNullOrEmpty(req.getQueryString())) {
+      uri += "?" + req.getQueryString();
+    }
+    log.error(String.format("Error in %s %s", req.getMethod(), uri), err);
+
+    if (!res.isCommitted()) {
+      res.reset();
+      replyError(res, SC_INTERNAL_SERVER_ERROR, "Internal server error");
+    }
+  }
+
+  static void replyError(HttpServletResponse res, int statusCode, String msg)
+      throws IOException {
+    res.setStatus(statusCode);
+    replyText(null, res, msg);
+  }
+
+  static void replyText(@Nullable HttpServletRequest req,
+      HttpServletResponse res, String text) throws IOException {
+    if ((req == null || "GET".equals(req.getMethod())) && isMaybeHTML(text)) {
+      replyJson(req, res, ImmutableMultimap.of("pp", "0"), new JsonPrimitive(text));
+    } else {
+      if (!text.endsWith("\n")) {
+        text += "\n";
+      }
+      replyBinaryResult(req, res,
+          BinaryResult.create(text).setContentType("text/plain"));
+    }
+  }
+
+  private static final Pattern IS_HTML = Pattern.compile("[<&]");
+  private static boolean isMaybeHTML(String text) {
+    return IS_HTML.matcher(text).find();
+  }
+
+  private static boolean acceptsJson(HttpServletRequest req) {
+    return req != null && isType(JSON_TYPE, req.getHeader(HttpHeaders.ACCEPT));
+  }
+
+  private static boolean acceptsGzip(HttpServletRequest req) {
+    if (req != null) {
+      String accepts = req.getHeader(HttpHeaders.ACCEPT_ENCODING);
+      return accepts != null && accepts.indexOf("gzip") != -1;
+    }
+    return false;
+  }
+
+  private static boolean isType(String expect, String given) {
+    if (given == null) {
+      return false;
+    } else if (expect.equals(given)) {
+      return true;
+    } else if (given.startsWith(expect + ",")) {
+      return true;
+    }
+    for (String p : given.split("[ ,;][ ,;]*")) {
+      if (expect.equals(p)) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  private static TemporaryBuffer.Heap compress(BinaryResult bin)
+      throws IOException {
+    TemporaryBuffer.Heap buf = heap(20 << 20);
+    GZIPOutputStream gz = new GZIPOutputStream(buf);
+    bin.writeTo(gz);
+    gz.finish();
+    gz.flush();
+    return buf;
+  }
+
+  private static Heap heap(int max) {
+    return new TemporaryBuffer.Heap(max);
+  }
+
+  @SuppressWarnings("serial")
+  private static class AmbiguousViewException extends Exception {
+    AmbiguousViewException(String message) {
+      super(message);
+    }
+  }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/AuditedHttpServletResponse.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/AuditedHttpServletResponse.java
new file mode 100644
index 0000000..059b54c
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/AuditedHttpServletResponse.java
@@ -0,0 +1,64 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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;
+
+import java.io.IOException;
+
+import javax.servlet.http.HttpServletResponse;
+import javax.servlet.http.HttpServletResponseWrapper;
+
+class AuditedHttpServletResponse
+    extends HttpServletResponseWrapper
+    implements HttpServletResponse {
+  private int status;
+
+  AuditedHttpServletResponse(HttpServletResponse response) {
+    super(response);
+  }
+
+  int getStatus() {
+    return status;
+  }
+
+  @Override
+  public void setStatus(int sc) {
+    super.setStatus(sc);
+    this.status = sc;
+  }
+
+  @Override
+  public void setStatus(int sc, String sm) {
+    super.setStatus(sc, sm);
+    this.status = sc;
+  }
+
+  @Override
+  public void sendError(int sc) throws IOException {
+    super.sendError(sc);
+    this.status = sc;
+  }
+
+  @Override
+  public void sendError(int sc, String msg) throws IOException {
+    super.sendError(sc, msg);
+    this.status = sc;
+  }
+
+  @Override
+  public void sendRedirect(String location) throws IOException {
+    super.sendRedirect(location);
+    this.status = SC_MOVED_TEMPORARILY;
+  }
+}
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 62506f0..3277992 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
@@ -48,6 +48,10 @@
     return null;
   }
 
+  protected CurrentUser getCurrentUser() {
+    return currentUser.get();
+  }
+
   /**
    * Executes <code>action.run</code> with an active ReviewDb connection.
    * <p>
@@ -68,7 +72,11 @@
     } catch (InvalidQueryException e) {
       callback.onFailure(e);
     } catch (NoSuchProjectException e) {
-      callback.onFailure(new NoSuchEntityException());
+      if (e.getMessage() != null) {
+        callback.onFailure(new NoSuchEntityException(e.getMessage()));
+      } else {
+        callback.onFailure(new NoSuchEntityException());
+      }
     } catch (NoSuchGroupException e) {
       callback.onFailure(new NoSuchEntityException());
     } catch (OrmRuntimeException e) {
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 1b24792..55ecb01 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/GerritJsonServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/GerritJsonServlet.java
@@ -14,13 +14,15 @@
 
 package com.google.gerrit.httpd.rpc;
 
-import com.google.common.collect.Lists;
-import com.google.gerrit.audit.AuditEvent;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.Multimap;
 import com.google.gerrit.audit.AuditService;
+import com.google.gerrit.audit.RpcAuditEvent;
 import com.google.gerrit.common.audit.Audit;
 import com.google.gerrit.common.auth.SignInRequired;
 import com.google.gerrit.common.errors.NotSignedInException;
 import com.google.gerrit.httpd.WebSession;
+import com.google.gerrit.server.AccessPath;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gson.GsonBuilder;
 import com.google.gwtjsonrpc.common.RemoteJsonService;
@@ -36,11 +38,11 @@
 
 import java.io.IOException;
 import java.lang.reflect.Field;
-import java.util.Arrays;
-import java.util.List;
+import java.lang.reflect.Method;
 
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
+
 /**
  * Base JSON servlet to ensure the current user is not forged.
  */
@@ -67,7 +69,7 @@
   @Override
   protected GerritCall createActiveCall(final HttpServletRequest req,
       final HttpServletResponse rsp) {
-    final GerritCall call = new GerritCall(session.get(), req, rsp);
+    final GerritCall call = new GerritCall(session.get(), req, new AuditedHttpServletResponse(rsp));
     currentCall.set(call);
     return call;
   }
@@ -131,64 +133,88 @@
       }
       Audit note = (Audit) method.getAnnotation(Audit.class);
       if (note != null) {
-        final String sid = call.getWebSession().getToken();
+        final String sid = call.getWebSession().getSessionId();
         final CurrentUser username = call.getWebSession().getCurrentUser();
-        final List<Object> args =
+        final Multimap<String, ?> args =
             extractParams(note, call);
-        final String what = extractWhat(note, method.getName());
+        final String what = extractWhat(note, call);
         final Object result = call.getResult();
 
-        audit.dispatch(new AuditEvent(sid, username, what, call.getWhen(), args,
-            result));
+        audit.dispatch(new RpcAuditEvent(sid, username, what, call.getWhen(),
+            args, call.getHttpServletRequest().getMethod(), call.getHttpServletRequest().getMethod(),
+            ((AuditedHttpServletResponse) (call.getHttpServletResponse()))
+                .getStatus(), result));
       }
     } catch (Throwable all) {
       log.error("Unable to log the call", all);
     }
   }
 
-  private List<Object> extractParams(final Audit note, final GerritCall call) {
-    List<Object> args = Lists.newArrayList(Arrays.asList(call.getParams()));
+  private Multimap<String, ?> extractParams(final Audit note, final GerritCall call) {
+    Multimap<String, Object> args = ArrayListMultimap.create();
+
+    Object[] params = call.getParams();
+    for (int i = 0; i < params.length; i++) {
+      args.put("$" + i, params[i]);
+    }
+
     for (int idx : note.obfuscate()) {
-      args.set(idx, "*****");
+      args.removeAll("$" + idx);
+      args.put("$" + idx, "*****");
     }
     return args;
   }
 
-  private String extractWhat(final Audit note, final String methodName) {
+  private String extractWhat(final Audit note, final GerritCall call) {
+    String methodClass = call.getMethodClass().getName();
+    methodClass = methodClass.substring(methodClass.lastIndexOf(".")+1);
     String what = note.action();
     if (what.length() == 0) {
-      boolean ccase = Character.isLowerCase(methodName.charAt(0));
-
-      StringBuilder sb = new StringBuilder();
-      for (int i = 0; i < methodName.length(); i++) {
-        char c = methodName.charAt(i);
-        if (ccase && !Character.isLowerCase(c)) {
-          sb.append(' ');
-        }
-        sb.append(Character.toLowerCase(c));
-      }
-      what = sb.toString();
+      what = call.getMethod().getName();
     }
 
-    return what;
+    return methodClass + "." + what;
   }
 
   static class GerritCall extends ActiveCall {
     private final WebSession session;
     private final long when;
     private static final Field resultField;
+    private static final Field methodField;
 
     // Needed to allow access to non-public result field in GWT/JSON-RPC
     static {
+      resultField = getPrivateField(ActiveCall.class, "result");
+      methodField = getPrivateField(MethodHandle.class, "method");
+    }
+
+    private static Field getPrivateField(Class<?> clazz, String fieldName) {
       Field declaredField = null;
       try {
-        declaredField = ActiveCall.class.getDeclaredField("result");
+        declaredField = clazz.getDeclaredField(fieldName);
         declaredField.setAccessible(true);
       } catch (Exception e) {
         log.error("Unable to expose RPS/JSON result field");
       }
+      return declaredField;
+    }
 
-      resultField = declaredField;
+    // Surrogate of the missing getMethodClass() in GWT/JSON-RPC
+    public Class<?> getMethodClass() {
+      if (methodField == null) {
+        return null;
+      }
+
+      try {
+        Method method = (Method) methodField.get(this.getMethod());
+        return method.getDeclaringClass();
+      } catch (IllegalArgumentException e) {
+        log.error("Cannot access result field");
+      } catch (IllegalAccessException e) {
+        log.error("No permissions to access result field");
+      }
+
+      return null;
     }
 
     // Surrogate of the missing getResult() in GWT/JSON-RPC
@@ -246,11 +272,13 @@
         //
         return !session.isSignedIn();
 
-      } else {
+      } else if (session.isSignedIn() && session.isValidXGerritAuth(keyIn)) {
         // The session must exist, and must be using this token.
         //
-        return session.isSignedIn() && session.isTokenValid(keyIn);
+        session.getCurrentUser().setAccessPath(AccessPath.JSON_RPC);
+        return true;
       }
+      return false;
     }
 
     public WebSession getWebSession() {
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 876fee3..953bc71 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
@@ -22,7 +22,7 @@
 
 /** Binds {@link RemoteJsonService} implementations to a JSON servlet. */
 public abstract class RpcServletModule extends ServletModule {
-  public static final String PREFIX = "/gerrit/rpc/";
+  public static final String PREFIX = "/gerrit_ui/rpc/";
 
   private final String prefix;
 
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 1baa49b..559c270 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
@@ -33,8 +33,8 @@
 import com.google.gerrit.server.account.AccountVisibility;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.GroupMembers;
+import com.google.gerrit.server.change.PostReviewers;
 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;
@@ -224,16 +224,27 @@
         } 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());
-            // we can't use changeControl directly as it won't suggest reviewers
-            // to drafts
-            return changeControl.forUser(who).isRefVisible();
-          }
-        };
+
+        VisibilityControl visibilityControl;
+        if (changeControl.getRefControl().isVisibleByRegisteredUsers()) {
+          visibilityControl = new VisibilityControl() {
+            @Override
+            public boolean isVisible(Account account) throws OrmException {
+              return true;
+            }
+          };
+        } else {
+          visibilityControl = new VisibilityControl() {
+            @Override
+            public boolean isVisible(Account account) throws OrmException {
+              IdentifiedUser who =
+                  identifiedUserFactory.create(reviewDbProvider, account.getId());
+              // we can't use changeControl directly as it won't suggest reviewers
+              // to drafts
+              return changeControl.forUser(who).isRefVisible();
+            }
+          };
+        }
 
         final List<AccountInfo> suggestedAccounts =
             suggestAccount(db, query, Boolean.TRUE, limit, visibilityControl);
@@ -262,13 +273,13 @@
 
   private boolean suggestGroupAsReviewer(final Project.NameKey project,
       final GroupReference group) throws OrmException {
-    if (!AddReviewer.isLegalReviewerGroup(group.getUUID())) {
+    if (!PostReviewers.isLegalReviewerGroup(group.getUUID())) {
       return false;
     }
 
     try {
-      final Set<Account> members =
-          groupMembersFactory.create().listAccounts(group.getUUID(), project);
+      final Set<Account> members = groupMembersFactory.create(getCurrentUser())
+          .listAccounts(group.getUUID(), project);
 
       if (members.isEmpty()) {
         return false;
@@ -276,7 +287,7 @@
 
       final int maxAllowed =
           cfg.getInt("addreviewer", "maxAllowed",
-              AddReviewer.DEFAULT_MAX_REVIEWERS);
+              PostReviewers.DEFAULT_MAX_REVIEWERS);
       if (maxAllowed > 0 && members.size() > maxAllowed) {
         return false;
       }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountCapabilitiesServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountCapabilitiesServlet.java
deleted file mode 100644
index a33c209..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountCapabilitiesServlet.java
+++ /dev/null
@@ -1,189 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.httpd.rpc.account;
-
-import static com.google.gerrit.common.data.GlobalCapability.CREATE_ACCOUNT;
-import static com.google.gerrit.common.data.GlobalCapability.CREATE_GROUP;
-import static com.google.gerrit.common.data.GlobalCapability.CREATE_PROJECT;
-import static com.google.gerrit.common.data.GlobalCapability.FLUSH_CACHES;
-import static com.google.gerrit.common.data.GlobalCapability.KILL_TASK;
-import static com.google.gerrit.common.data.GlobalCapability.PRIORITY;
-import static com.google.gerrit.common.data.GlobalCapability.START_REPLICATION;
-import static com.google.gerrit.common.data.GlobalCapability.VIEW_CACHES;
-import static com.google.gerrit.common.data.GlobalCapability.VIEW_CONNECTIONS;
-import static com.google.gerrit.common.data.GlobalCapability.VIEW_QUEUE;
-
-import com.google.common.collect.Maps;
-import com.google.common.collect.Sets;
-import com.google.gerrit.common.data.GlobalCapability;
-import com.google.gerrit.common.data.PermissionRange;
-import com.google.gerrit.httpd.RestApiServlet;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.OutputFormat;
-import com.google.gerrit.server.account.CapabilityControl;
-import com.google.gerrit.server.git.QueueProvider;
-import com.google.gson.reflect.TypeToken;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.Singleton;
-
-import org.kohsuke.args4j.Option;
-
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.OutputStreamWriter;
-import java.util.Iterator;
-import java.util.Map;
-import java.util.Set;
-
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-@Singleton
-public class AccountCapabilitiesServlet extends RestApiServlet {
-  private static final long serialVersionUID = 1L;
-  private final ParameterParser paramParser;
-  private final Provider<Impl> factory;
-
-  @Inject
-  AccountCapabilitiesServlet(final Provider<CurrentUser> currentUser,
-      ParameterParser paramParser, Provider<Impl> factory) {
-    super(currentUser);
-    this.paramParser = paramParser;
-    this.factory = factory;
-  }
-
-  @Override
-  protected void doGet(HttpServletRequest req, HttpServletResponse res)
-      throws IOException {
-    Impl impl = factory.get();
-    if (acceptsJson(req)) {
-      impl.format = OutputFormat.JSON_COMPACT;
-    }
-    if (paramParser.parse(impl, req, res)) {
-      impl.compute();
-
-      ByteArrayOutputStream buf = new ByteArrayOutputStream();
-      OutputStreamWriter out = new OutputStreamWriter(buf, "UTF-8");
-      if (impl.format.isJson()) {
-        res.setContentType(JSON_TYPE);
-        buf.write(JSON_MAGIC);
-        impl.format.newGson().toJson(
-            impl.have,
-            new TypeToken<Map<String, Object>>() {}.getType(),
-            out);
-        out.flush();
-        buf.write('\n');
-      } else {
-        res.setContentType("text/plain");
-        for (Map.Entry<String, Object> e : impl.have.entrySet()) {
-          out.write(e.getKey());
-          if (!(e.getValue() instanceof Boolean)) {
-            out.write(": ");
-            out.write(e.getValue().toString());
-          }
-          out.write('\n');
-        }
-        out.flush();
-      }
-      res.setCharacterEncoding("UTF-8");
-      send(req, res, buf.toByteArray());
-    }
-  }
-
-  static class Impl {
-    private final CapabilityControl cc;
-    private final Map<String, Object> have;
-
-    @Option(name = "--format", metaVar = "FMT", usage = "Output display format")
-    private OutputFormat format = OutputFormat.TEXT;
-
-    @Option(name = "-q", metaVar = "CAP", multiValued = true, usage = "Capability to inspect")
-    void addQuery(String name) {
-      if (query == null) {
-        query = Sets.newHashSet();
-      }
-      query.add(name.toLowerCase());
-    }
-    private Set<String> query;
-
-    @Inject
-    Impl(CurrentUser user) {
-      cc = user.getCapabilities();
-      have = Maps.newLinkedHashMap();
-    }
-
-    void compute() {
-      for (String name : GlobalCapability.getAllNames()) {
-        if (!name.equals(PRIORITY) && want(name) && cc.canPerform(name)) {
-          if (GlobalCapability.hasRange(name)) {
-            have.put(name, new Range(cc.getRange(name)));
-          } else {
-            have.put(name, true);
-          }
-        }
-      }
-
-      have.put(CREATE_ACCOUNT, cc.canCreateAccount());
-      have.put(CREATE_GROUP, cc.canCreateGroup());
-      have.put(CREATE_PROJECT, cc.canCreateProject());
-      have.put(KILL_TASK, cc.canKillTask());
-      have.put(VIEW_CACHES, cc.canViewCaches());
-      have.put(FLUSH_CACHES, cc.canFlushCaches());
-      have.put(VIEW_CONNECTIONS, cc.canViewConnections());
-      have.put(VIEW_QUEUE, cc.canViewQueue());
-      have.put(START_REPLICATION, cc.canStartReplication());
-
-      QueueProvider.QueueType queue = cc.getQueueType();
-      if (queue != QueueProvider.QueueType.INTERACTIVE
-          || (query != null && query.contains(PRIORITY))) {
-        have.put(PRIORITY, queue);
-      }
-
-      Iterator<Map.Entry<String, Object>> itr = have.entrySet().iterator();
-      while (itr.hasNext()) {
-        Map.Entry<String, Object> e = itr.next();
-        if (!want(e.getKey())) {
-          itr.remove();
-        } else if (e.getValue() instanceof Boolean && !((Boolean) e.getValue())) {
-          itr.remove();
-        }
-      }
-    }
-
-    private boolean want(String name) {
-      return query == null || query.contains(name.toLowerCase());
-    }
-  }
-
-  private static class Range {
-    private transient PermissionRange range;
-    @SuppressWarnings("unused")
-    private int min;
-    @SuppressWarnings("unused")
-    private int max;
-
-    Range(PermissionRange r) {
-      range = r;
-      min = r.getMin();
-      max = r.getMax();
-    }
-
-    @Override
-    public String toString() {
-      return range.toString();
-    }
-  }
-}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountModule.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountModule.java
index 957f339..bef6316 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
@@ -30,18 +30,12 @@
       @Override
       protected void configure() {
         factory(AgreementInfoFactory.Factory.class);
-        factory(CreateGroup.Factory.class);
         factory(DeleteExternalIds.Factory.class);
         factory(ExternalIdDetailFactory.Factory.class);
-        factory(GroupDetailHandler.Factory.class);
-        factory(MyGroupsFactory.Factory.class);
         factory(RegisterNewEmailSender.Factory.class);
-        factory(RenameGroup.Factory.class);
-        factory(VisibleGroupsHandler.Factory.class);
       }
     });
     rpc(AccountSecurityImpl.class);
     rpc(AccountServiceImpl.class);
-    rpc(GroupAdminServiceImpl.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 b62a10b..11846d3 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
@@ -18,10 +18,10 @@
 import com.google.gerrit.common.data.AccountSecurity;
 import com.google.gerrit.common.data.ContributorAgreement;
 import com.google.gerrit.common.errors.ContactInformationStoreException;
+import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.common.errors.InvalidSshKeyException;
-import com.google.gerrit.common.errors.NameAlreadyUsedException;
 import com.google.gerrit.common.errors.NoSuchEntityException;
-import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.common.errors.PermissionDeniedException;
 import com.google.gerrit.httpd.rpc.BaseServiceImplementation;
 import com.google.gerrit.httpd.rpc.Handler;
 import com.google.gerrit.reviewdb.client.Account;
@@ -47,7 +47,6 @@
 import com.google.gerrit.server.account.Realm;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.contact.ContactStore;
-import com.google.gerrit.server.mail.EmailException;
 import com.google.gerrit.server.mail.EmailTokenVerifier;
 import com.google.gerrit.server.mail.RegisterNewEmailSender;
 import com.google.gerrit.server.project.ProjectCache;
@@ -86,7 +85,6 @@
   private final ChangeUserName.CurrentUser changeUserNameFactory;
   private final DeleteExternalIds.Factory deleteExternalIdsFactory;
   private final ExternalIdDetailFactory.Factory externalIdDetailFactory;
-  private final MyGroupsFactory.Factory myGroupsFactory;
 
   private final ChangeHooks hooks;
   private final GroupCache groupCache;
@@ -104,7 +102,6 @@
       final ChangeUserName.CurrentUser changeUserNameFactory,
       final DeleteExternalIds.Factory deleteExternalIdsFactory,
       final ExternalIdDetailFactory.Factory externalIdDetailFactory,
-      final MyGroupsFactory.Factory myGroupsFactory,
       final ChangeHooks hooks, final GroupCache groupCache) {
     super(schema, currentUser);
     contactStore = cs;
@@ -126,7 +123,6 @@
     this.changeUserNameFactory = changeUserNameFactory;
     this.deleteExternalIdsFactory = deleteExternalIdsFactory;
     this.externalIdDetailFactory = externalIdDetailFactory;
-    this.myGroupsFactory = myGroupsFactory;
     this.hooks = hooks;
     this.groupCache = groupCache;
   }
@@ -191,7 +187,8 @@
     if (realm.allowsEdit(Account.FieldName.USER_NAME)) {
       Handler.wrap(changeUserNameFactory.create(newName)).to(callback);
     } else {
-      callback.onFailure(new NameAlreadyUsedException());
+      callback.onFailure(new PermissionDeniedException("Not allowed to change"
+          + " username"));
     }
   }
 
@@ -211,16 +208,6 @@
     externalIdDetailFactory.create().to(callback);
   }
 
-  @Override
-  public void myGroups(final AsyncCallback<List<AccountGroup>> callback) {
-    run(callback, new Action<List<AccountGroup>>() {
-      public List<AccountGroup> run(final ReviewDb db) throws OrmException,
-          NoSuchGroupException, Failure {
-        return myGroupsFactory.create().call();
-      }
-    });
-  }
-
   public void deleteExternalIds(final Set<AccountExternalId.Key> keys,
       final AsyncCallback<Set<AccountExternalId.Key>> callback) {
     deleteExternalIdsFactory.create(keys).to(callback);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountsRestApiServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountsRestApiServlet.java
new file mode 100644
index 0000000..351d563
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/AccountsRestApiServlet.java
@@ -0,0 +1,32 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.httpd.restapi.RestApiServlet;
+import com.google.gerrit.server.account.AccountsCollection;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+@Singleton
+public class AccountsRestApiServlet extends RestApiServlet {
+  private static final long serialVersionUID = 1L;
+
+  @Inject
+  AccountsRestApiServlet(RestApiServlet.Globals globals,
+      Provider<AccountsCollection> accounts) {
+    super(globals, accounts);
+  }
+}
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
deleted file mode 100644
index ebebd04..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/CreateGroup.java
+++ /dev/null
@@ -1,54 +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.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.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.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
-
-import java.util.Collections;
-
-class CreateGroup extends Handler<AccountGroup.Id> {
-  interface Factory {
-    CreateGroup create(String groupName);
-  }
-
-  private final PerformCreateGroup.Factory performCreateGroupFactory;
-  private final IdentifiedUser user;
-  private final String groupName;
-
-  @Inject
-  CreateGroup(final PerformCreateGroup.Factory performCreateGroupFactory,
-      final IdentifiedUser user, @Assisted final String groupName) {
-    this.performCreateGroupFactory = performCreateGroupFactory;
-    this.user = user;
-    this.groupName = groupName;
-  }
-
-  @Override
-  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/GroupAdminServiceImpl.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/GroupAdminServiceImpl.java
deleted file mode 100644
index aca2e05..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/GroupAdminServiceImpl.java
+++ /dev/null
@@ -1,421 +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.httpd.rpc.account;
-
-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.data.GroupReference;
-import com.google.gerrit.common.errors.InactiveAccountException;
-import com.google.gerrit.common.errors.NameAlreadyUsedException;
-import com.google.gerrit.common.errors.NoSuchAccountException;
-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.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.AuthType;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.AccountCache;
-import com.google.gerrit.server.account.AccountException;
-import com.google.gerrit.server.account.AccountManager;
-import com.google.gerrit.server.account.AccountResolver;
-import com.google.gerrit.server.account.AuthRequest;
-import com.google.gerrit.server.account.GroupBackend;
-import com.google.gerrit.server.account.GroupBackends;
-import com.google.gerrit.server.account.GroupCache;
-import com.google.gerrit.server.account.GroupControl;
-import com.google.gerrit.server.account.GroupIncludeCache;
-import com.google.gerrit.server.config.AuthConfig;
-import com.google.gwtjsonrpc.common.AsyncCallback;
-import com.google.gwtjsonrpc.common.VoidResult;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-import java.util.Collections;
-import java.util.HashSet;
-import java.util.Set;
-
-class GroupAdminServiceImpl extends BaseServiceImplementation implements
-    GroupAdminService {
-  private final AccountCache accountCache;
-  private final AccountResolver accountResolver;
-  private final AccountManager accountManager;
-  private final AuthType authType;
-  private final GroupCache groupCache;
-  private final GroupBackend groupBackend;
-  private final GroupIncludeCache groupIncludeCache;
-  private final GroupControl.Factory groupControlFactory;
-
-  private final CreateGroup.Factory createGroupFactory;
-  private final RenameGroup.Factory renameGroupFactory;
-  private final GroupDetailHandler.Factory groupDetailFactory;
-  private final VisibleGroupsHandler.Factory visibleGroupsFactory;
-
-  @Inject
-  GroupAdminServiceImpl(final Provider<ReviewDb> schema,
-      final Provider<IdentifiedUser> currentUser,
-      final AccountCache accountCache,
-      final GroupIncludeCache groupIncludeCache,
-      final AccountResolver accountResolver,
-      final AccountManager accountManager,
-      final AuthConfig authConfig,
-      final GroupCache groupCache,
-      final GroupBackend groupBackend,
-      final GroupControl.Factory groupControlFactory,
-      final CreateGroup.Factory createGroupFactory,
-      final RenameGroup.Factory renameGroupFactory,
-      final GroupDetailHandler.Factory groupDetailFactory,
-      final VisibleGroupsHandler.Factory visibleGroupsFactory) {
-    super(schema, currentUser);
-    this.accountCache = accountCache;
-    this.groupIncludeCache = groupIncludeCache;
-    this.accountResolver = accountResolver;
-    this.accountManager = accountManager;
-    this.authType = authConfig.getAuthType();
-    this.groupCache = groupCache;
-    this.groupBackend = groupBackend;
-    this.groupControlFactory = groupControlFactory;
-    this.createGroupFactory = createGroupFactory;
-    this.renameGroupFactory = renameGroupFactory;
-    this.groupDetailFactory = groupDetailFactory;
-    this.visibleGroupsFactory = visibleGroupsFactory;
-  }
-
-  public void visibleGroups(final AsyncCallback<GroupList> callback) {
-    visibleGroupsFactory.create().to(callback);
-  }
-
-  public void createGroup(final String newName,
-      final AsyncCallback<AccountGroup.Id> callback) {
-    createGroupFactory.create(newName).to(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);
-  }
-
-  public void changeGroupDescription(final AccountGroup.Id groupId,
-      final String description, final AsyncCallback<VoidResult> callback) {
-    run(callback, new Action<VoidResult>() {
-      public VoidResult run(final ReviewDb db) throws OrmException, Failure {
-        final AccountGroup group = db.accountGroups().get(groupId);
-        assertAmGroupOwner(db, group);
-        group.setDescription(description);
-        db.accountGroups().update(Collections.singleton(group));
-        groupCache.evict(group);
-        return VoidResult.INSTANCE;
-      }
-    });
-  }
-
-  public void changeGroupOptions(final AccountGroup.Id groupId,
-      final GroupOptions groupOptions, final AsyncCallback<VoidResult> callback) {
-    run(callback, new Action<VoidResult>() {
-      public VoidResult run(final ReviewDb db) throws OrmException, Failure {
-        final AccountGroup group = db.accountGroups().get(groupId);
-        assertAmGroupOwner(db, group);
-        group.setVisibleToAll(groupOptions.isVisibleToAll());
-        db.accountGroups().update(Collections.singleton(group));
-        groupCache.evict(group);
-        return VoidResult.INSTANCE;
-      }
-    });
-  }
-
-  public void changeGroupOwner(final AccountGroup.Id groupId,
-      final String newOwnerName, final AsyncCallback<VoidResult> callback) {
-    run(callback, new Action<VoidResult>() {
-      public VoidResult run(final ReviewDb db) throws OrmException, Failure {
-        final AccountGroup group = db.accountGroups().get(groupId);
-        assertAmGroupOwner(db, group);
-
-        GroupReference owner =
-            GroupBackends.findExactSuggestion(groupBackend, newOwnerName);
-        if (owner == null) {
-          throw new Failure(new NoSuchEntityException());
-        }
-
-        group.setOwnerGroupUUID(owner.getUUID());
-        db.accountGroups().update(Collections.singleton(group));
-        groupCache.evict(group);
-        return VoidResult.INSTANCE;
-      }
-    });
-  }
-
-  public void renameGroup(final AccountGroup.Id groupId, final String newName,
-      final AsyncCallback<GroupDetail> callback) {
-    renameGroupFactory.create(groupId, newName).to(callback);
-  }
-
-  public void changeGroupType(final AccountGroup.Id groupId,
-      final AccountGroup.Type newType, final AsyncCallback<VoidResult> callback) {
-    run(callback, new Action<VoidResult>() {
-      public VoidResult run(final ReviewDb db) throws OrmException, Failure {
-        final AccountGroup group = db.accountGroups().get(groupId);
-        assertAmGroupOwner(db, group);
-        group.setType(newType);
-        db.accountGroups().update(Collections.singleton(group));
-        groupCache.evict(group);
-        return VoidResult.INSTANCE;
-      }
-    });
-  }
-
-  public void addGroupMember(final AccountGroup.Id groupId,
-      final String nameOrEmail, final AsyncCallback<GroupDetail> callback) {
-    run(callback, new Action<GroupDetail>() {
-      public GroupDetail run(ReviewDb db) throws OrmException, Failure,
-          NoSuchGroupException {
-        final GroupControl control = groupControlFactory.validateFor(groupId);
-        if (groupCache.get(groupId).getType() != AccountGroup.Type.INTERNAL) {
-          throw new Failure(new NameAlreadyUsedException());
-        }
-
-        final Account a = findAccount(nameOrEmail);
-        if (!a.isActive()) {
-          throw new Failure(new InactiveAccountException(a.getFullName()));
-        }
-        if (!control.canAddMember(a.getId())) {
-          throw new Failure(new NoSuchEntityException());
-        }
-
-        final AccountGroupMember.Key key =
-            new AccountGroupMember.Key(a.getId(), groupId);
-        AccountGroupMember m = db.accountGroupMembers().get(key);
-        if (m == null) {
-          m = new AccountGroupMember(key);
-          db.accountGroupMembersAudit().insert(
-              Collections.singleton(new AccountGroupMemberAudit(m,
-                  getAccountId())));
-          db.accountGroupMembers().insert(Collections.singleton(m));
-          accountCache.evict(m.getAccountId());
-        }
-
-        return groupDetailFactory.create(groupId).call();
-      }
-    });
-  }
-
-  public void addGroupInclude(final AccountGroup.Id groupId,
-      final String groupName, final AsyncCallback<GroupDetail> callback) {
-    run(callback, new Action<GroupDetail>() {
-      public GroupDetail run(ReviewDb db) throws OrmException, Failure,
-          NoSuchGroupException {
-        final GroupControl control = groupControlFactory.validateFor(groupId);
-        if (groupCache.get(groupId).getType() != AccountGroup.Type.INTERNAL) {
-          throw new Failure(new NameAlreadyUsedException());
-        }
-
-        final AccountGroup a = findGroup(groupName);
-        if (!control.canAddGroup(a.getId())) {
-          throw new Failure(new NoSuchEntityException());
-        }
-
-        final AccountGroupInclude.Key key =
-            new AccountGroupInclude.Key(groupId, a.getId());
-        AccountGroupInclude m = db.accountGroupIncludes().get(key);
-        if (m == null) {
-          m = new AccountGroupInclude(key);
-          db.accountGroupIncludesAudit().insert(
-              Collections.singleton(new AccountGroupIncludeAudit(m,
-                  getAccountId())));
-          db.accountGroupIncludes().insert(Collections.singleton(m));
-          groupIncludeCache.evictInclude(a.getGroupUUID());
-        }
-
-        return groupDetailFactory.create(groupId).call();
-      }
-    });
-  }
-
-  public void deleteGroupMembers(final AccountGroup.Id groupId,
-      final Set<AccountGroupMember.Key> keys,
-      final AsyncCallback<VoidResult> callback) {
-    run(callback, new Action<VoidResult>() {
-      public VoidResult run(final ReviewDb db) throws OrmException,
-          NoSuchGroupException, Failure {
-        final GroupControl control = groupControlFactory.validateFor(groupId);
-        if (groupCache.get(groupId).getType() != AccountGroup.Type.INTERNAL) {
-          throw new Failure(new NameAlreadyUsedException());
-        }
-
-        for (final AccountGroupMember.Key k : keys) {
-          if (!groupId.equals(k.getAccountGroupId())) {
-            throw new Failure(new NoSuchEntityException());
-          }
-        }
-
-        final Account.Id me = getAccountId();
-        for (final AccountGroupMember.Key k : keys) {
-          final AccountGroupMember m = db.accountGroupMembers().get(k);
-          if (m != null) {
-            if (!control.canRemoveMember(m.getAccountId())) {
-              throw new Failure(new NoSuchEntityException());
-            }
-
-            AccountGroupMemberAudit audit = null;
-            for (AccountGroupMemberAudit a : db.accountGroupMembersAudit()
-                .byGroupAccount(m.getAccountGroupId(), m.getAccountId())) {
-              if (a.isActive()) {
-                audit = a;
-                break;
-              }
-            }
-
-            if (audit != null) {
-              audit.removed(me);
-              db.accountGroupMembersAudit()
-                  .update(Collections.singleton(audit));
-            } else {
-              audit = new AccountGroupMemberAudit(m, me);
-              audit.removedLegacy();
-              db.accountGroupMembersAudit()
-                  .insert(Collections.singleton(audit));
-            }
-
-            db.accountGroupMembers().delete(Collections.singleton(m));
-            accountCache.evict(m.getAccountId());
-          }
-        }
-        return VoidResult.INSTANCE;
-      }
-    });
-  }
-
-  public void deleteGroupIncludes(final AccountGroup.Id groupId,
-      final Set<AccountGroupInclude.Key> keys,
-      final AsyncCallback<VoidResult> callback) {
-    run(callback, new Action<VoidResult>() {
-      public VoidResult run(final ReviewDb db) throws OrmException,
-          NoSuchGroupException, Failure {
-        final GroupControl control = groupControlFactory.validateFor(groupId);
-        if (groupCache.get(groupId).getType() != AccountGroup.Type.INTERNAL) {
-          throw new Failure(new NameAlreadyUsedException());
-        }
-
-        for (final AccountGroupInclude.Key k : keys) {
-          if (!groupId.equals(k.getGroupId())) {
-            throw new Failure(new NoSuchEntityException());
-          }
-        }
-
-        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);
-          if (m != null) {
-            if (!control.canRemoveGroup(m.getIncludeId())) {
-              throw new Failure(new NoSuchEntityException());
-            }
-
-            AccountGroupIncludeAudit audit = null;
-            for (AccountGroupIncludeAudit a : db
-                .accountGroupIncludesAudit().byGroupInclude(
-                    m.getGroupId(), m.getIncludeId())) {
-              if (a.isActive()) {
-                audit = a;
-                break;
-              }
-            }
-
-            if (audit != null) {
-              audit.removed(me);
-              db.accountGroupIncludesAudit().update(
-                  Collections.singleton(audit));
-            }
-            db.accountGroupIncludes().delete(Collections.singleton(m));
-            groupsToEvict.add(k.getIncludeId());
-          }
-        }
-        for (AccountGroup group : db.accountGroups().get(groupsToEvict)) {
-          groupIncludeCache.evictInclude(group.getGroupUUID());
-        }
-        return VoidResult.INSTANCE;
-      }
-    });
-  }
-
-  private void assertAmGroupOwner(final ReviewDb db, final AccountGroup group)
-      throws Failure {
-    try {
-      if (!groupControlFactory.controlFor(group.getId()).isOwner()) {
-        throw new Failure(new NoSuchGroupException(group.getId()));
-      }
-    } catch (NoSuchGroupException e) {
-      throw new Failure(new NoSuchGroupException(group.getId()));
-    }
-  }
-
-  private Account findAccount(final String nameOrEmail) throws OrmException,
-      Failure {
-    Account r = accountResolver.find(nameOrEmail);
-    if (r == null) {
-      switch (authType) {
-        case HTTP_LDAP:
-        case CLIENT_SSL_CERT_LDAP:
-        case LDAP:
-          r = createAccountByLdap(nameOrEmail);
-          break;
-        default:
-      }
-      if (r == null) {
-        throw new Failure(new NoSuchAccountException(nameOrEmail));
-      }
-    }
-    return r;
-  }
-
-  private Account createAccountByLdap(String user) {
-    if (!user.matches(Account.USER_NAME_PATTERN)) {
-      return null;
-    }
-
-    try {
-      final AuthRequest req = AuthRequest.forUser(user);
-      req.setSkipAuthentication(true);
-      return accountCache.get(accountManager.authenticate(req).getAccountId())
-          .getAccount();
-    } catch (AccountException e) {
-      return null;
-    }
-  }
-
-  private AccountGroup findGroup(final String name) throws OrmException,
-      Failure {
-    final AccountGroup g = groupCache.get(new AccountGroup.NameKey(name));
-    if (g == null) {
-      throw new Failure(new NoSuchGroupException(name));
-    }
-    return g;
-  }
-
-}
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
deleted file mode 100644
index ce74218..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/GroupDetailHandler.java
+++ /dev/null
@@ -1,46 +0,0 @@
-// Copyright (C) 2011 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.httpd.rpc.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
deleted file mode 100644
index 33ce371..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/MyGroupsFactory.java
+++ /dev/null
@@ -1,46 +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.account;
-
-import com.google.gerrit.common.errors.NoSuchGroupException;
-import com.google.gerrit.httpd.rpc.Handler;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.account.VisibleGroups;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-
-import java.util.List;
-
-class MyGroupsFactory extends Handler<List<AccountGroup>> {
-  interface Factory {
-    MyGroupsFactory create();
-  }
-
-  private final VisibleGroups.Factory visibleGroupsFactory;
-  private final IdentifiedUser user;
-
-  @Inject
-  MyGroupsFactory(final VisibleGroups.Factory visibleGroupsFactory, final IdentifiedUser user) {
-    this.visibleGroupsFactory = visibleGroupsFactory;
-    this.user = user;
-  }
-
-  @Override
-  public List<AccountGroup> call() throws OrmException, NoSuchGroupException {
-    final VisibleGroups visibleGroups = visibleGroupsFactory.create();
-    return visibleGroups.get(user).getGroups();
-  }
-}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/RenameGroup.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/RenameGroup.java
deleted file mode 100644
index 24faf9e..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/RenameGroup.java
+++ /dev/null
@@ -1,51 +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.account;
-
-import com.google.gerrit.common.data.GroupDetail;
-import com.google.gerrit.common.errors.InvalidNameException;
-import com.google.gerrit.common.errors.NameAlreadyUsedException;
-import com.google.gerrit.common.errors.NoSuchGroupException;
-import com.google.gerrit.httpd.rpc.Handler;
-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;
-
-class RenameGroup extends Handler<GroupDetail> {
-  interface Factory {
-    RenameGroup create(AccountGroup.Id id, String newName);
-  }
-
-  private final PerformRenameGroup.Factory performRenameGroupFactory;
-
-  private final AccountGroup.Id groupId;
-  private final String newName;
-
-  @Inject
-  RenameGroup(final PerformRenameGroup.Factory performRenameGroupFactory,
-      @Assisted final AccountGroup.Id groupId, @Assisted final String newName) {
-    this.performRenameGroupFactory = performRenameGroupFactory;
-    this.groupId = groupId;
-    this.newName = newName;
-  }
-
-  @Override
-  public GroupDetail call() throws OrmException, NameAlreadyUsedException,
-      NoSuchGroupException, InvalidNameException {
-    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
deleted file mode 100644
index 54f91f7..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/account/VisibleGroupsHandler.java
+++ /dev/null
@@ -1,39 +0,0 @@
-// Copyright (C) 2011 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.httpd.rpc.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/change/ChangesRestApiServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/change/ChangesRestApiServlet.java
new file mode 100644
index 0000000..fe810b9
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/change/ChangesRestApiServlet.java
@@ -0,0 +1,32 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.httpd.rpc.change;
+
+import com.google.gerrit.httpd.restapi.RestApiServlet;
+import com.google.gerrit.server.change.ChangesCollection;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+@Singleton
+public class ChangesRestApiServlet extends RestApiServlet {
+  private static final long serialVersionUID = 1L;
+
+  @Inject
+  ChangesRestApiServlet(RestApiServlet.Globals globals,
+      Provider<ChangesCollection> changes) {
+    super(globals, changes);
+  }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/change/DeprecatedChangeQueryServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/change/DeprecatedChangeQueryServlet.java
index cf443e7..da21c51 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/change/DeprecatedChangeQueryServlet.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/change/DeprecatedChangeQueryServlet.java
@@ -73,6 +73,7 @@
     p.setIncludeCurrentPatchSet(get(req, "current-patch-set", false));
     p.setIncludePatchSets(get(req, "patch-sets", false));
     p.setIncludeApprovals(get(req, "all-approvals", false));
+    p.setIncludeFiles(get(req, "files", false));
     p.setOutput(rsp.getOutputStream(), format);
     p.query(get(req, "q", "status:open"));
   }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/change/ListChangesServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/change/ListChangesServlet.java
deleted file mode 100644
index b501d43..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/change/ListChangesServlet.java
+++ /dev/null
@@ -1,86 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.httpd.rpc.change;
-
-import com.google.gerrit.httpd.RestApiServlet;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.OutputFormat;
-import com.google.gerrit.server.query.QueryParseException;
-import com.google.gerrit.server.query.change.ListChanges;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.Singleton;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.BufferedWriter;
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.io.OutputStreamWriter;
-import java.io.Writer;
-
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-@Singleton
-public class ListChangesServlet extends RestApiServlet {
-  private static final long serialVersionUID = 1L;
-  private static final Logger log = LoggerFactory.getLogger(ListChangesServlet.class);
-  private final ParameterParser paramParser;
-  private final Provider<ListChanges> factory;
-
-  @Inject
-  ListChangesServlet(final Provider<CurrentUser> currentUser,
-      ParameterParser paramParser, Provider<ListChanges> ls) {
-    super(currentUser);
-    this.paramParser = paramParser;
-    this.factory = ls;
-  }
-
-  @Override
-  protected void doGet(HttpServletRequest req, HttpServletResponse res)
-      throws IOException {
-    ListChanges impl = factory.get();
-    if (acceptsJson(req)) {
-      impl.setFormat(OutputFormat.JSON_COMPACT);
-    }
-    if (paramParser.parse(impl, req, res)) {
-      ByteArrayOutputStream buf = new ByteArrayOutputStream();
-      if (impl.getFormat().isJson()) {
-        buf.write(JSON_MAGIC);
-      }
-
-      Writer out = new BufferedWriter(new OutputStreamWriter(buf, "UTF-8"));
-      try {
-        impl.query(out);
-      } catch (QueryParseException e) {
-        res.setStatus(HttpServletResponse.SC_BAD_REQUEST);
-        sendText(req, res, e.getMessage());
-        return;
-      } catch (OrmException e) {
-        log.error("Error querying /changes/", e);
-        res.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
-        return;
-      }
-      out.flush();
-
-      res.setContentType(impl.getFormat().isJson() ? JSON_TYPE : "text/plain");
-      res.setCharacterEncoding("UTF-8");
-      send(req, res, buf.toByteArray());
-    }
-  }
-}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/AbandonChangeHandler.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/AbandonChangeHandler.java
deleted file mode 100644
index 2110885..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/AbandonChangeHandler.java
+++ /dev/null
@@ -1,77 +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.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.Provider;
-import com.google.inject.assistedinject.Assisted;
-
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
-
-import java.io.IOException;
-
-import javax.annotation.Nullable;
-
-class AbandonChangeHandler extends Handler<ChangeDetail> {
-
-  interface Factory {
-    AbandonChangeHandler create(PatchSet.Id patchSetId, String message);
-  }
-
-  private final Provider<AbandonChange> abandonChangeProvider;
-  private final ChangeDetailFactory.Factory changeDetailFactory;
-
-  private final PatchSet.Id patchSetId;
-  @Nullable
-  private final String message;
-
-  @Inject
-  AbandonChangeHandler(final Provider<AbandonChange> abandonChangeProvider,
-      final ChangeDetailFactory.Factory changeDetailFactory,
-      @Assisted final PatchSet.Id patchSetId,
-      @Assisted @Nullable final String message) {
-    this.abandonChangeProvider = abandonChangeProvider;
-    this.changeDetailFactory = changeDetailFactory;
-
-    this.patchSetId = patchSetId;
-    this.message = message;
-  }
-
-  @Override
-  public ChangeDetail call() throws NoSuchChangeException, OrmException,
-      EmailException, NoSuchEntityException, InvalidChangeOperationException,
-      PatchSetInfoNotAvailableException, RepositoryNotFoundException,
-      IOException {
-    final AbandonChange abandonChange = abandonChangeProvider.get();
-    abandonChange.setChangeId(patchSetId.getParentKey());
-    abandonChange.setMessage(message);
-    final ReviewResult result = abandonChange.call();
-    if (result.getErrors().size() > 0) {
-      throw new NoSuchChangeException(result.getChangeId());
-    }
-    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 6660a3d..120b9af 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
@@ -14,9 +14,6 @@
 
 package com.google.gerrit.httpd.rpc.changedetail;
 
-import com.google.gerrit.common.data.ApprovalDetail;
-import com.google.gerrit.common.data.ApprovalType;
-import com.google.gerrit.common.data.ApprovalTypes;
 import com.google.gerrit.common.data.ChangeDetail;
 import com.google.gerrit.common.data.ChangeInfo;
 import com.google.gerrit.common.data.SubmitRecord;
@@ -27,7 +24,6 @@
 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;
@@ -36,15 +32,15 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.ProjectUtil;
 import com.google.gerrit.server.account.AccountInfoCacheFactory;
+import com.google.gerrit.server.changedetail.RebaseChange;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MergeOp;
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gerrit.server.workflow.CategoryFunction;
-import com.google.gerrit.server.workflow.FunctionState;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Inject;
@@ -69,9 +65,7 @@
     ChangeDetailFactory create(Change.Id id);
   }
 
-  private final ApprovalTypes approvalTypes;
   private final ChangeControl.Factory changeControlFactory;
-  private final FunctionState.Factory functionState;
   private final PatchSetDetailFactory.Factory patchSetDetail;
   private final AccountInfoCacheFactory aic;
   private final AnonymousUser anonymousUser;
@@ -87,9 +81,12 @@
   private final MergeOp.Factory opFactory;
   private boolean testMerge;
 
+  private List<PatchSetAncestor> currentPatchSetAncestors;
+  private List<PatchSet> currentDepPatchSets;
+  private List<Change> currentDepChanges;
+
   @Inject
-  ChangeDetailFactory(final ApprovalTypes approvalTypes,
-      final FunctionState.Factory functionState,
+  ChangeDetailFactory(
       final PatchSetDetailFactory.Factory patchSetDetail, final ReviewDb db,
       final GitRepositoryManager repoManager,
       final ChangeControl.Factory changeControlFactory,
@@ -98,8 +95,6 @@
       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.repoManager = repoManager;
@@ -116,7 +111,7 @@
   @Override
   public ChangeDetail call() throws OrmException, NoSuchEntityException,
       PatchSetInfoNotAvailableException, NoSuchChangeException,
-      RepositoryNotFoundException, IOException {
+      RepositoryNotFoundException, IOException, NoSuchProjectException {
     control = changeControlFactory.validateFor(changeId);
     final Change change = control.getChange();
     final PatchSet patch = db.patchSets().get(change.currentPatchSetId());
@@ -141,9 +136,9 @@
 
     detail.setCanRevert(change.getStatus() == Change.Status.MERGED && control.canAddPatchSet());
 
-    detail.setCanRebase(detail.getChange().getStatus().isOpen() && control.canRebase());
-
     detail.setCanEdit(control.getRefControl().canWrite());
+    detail.setCanEditCommitMessage(change.getStatus().isOpen() && control.canAddPatchSet());
+    detail.setCanEditTopicName(control.canEditTopicName());
 
     List<SubmitRecord> submitRecords = control.getSubmitRecords(db, patch);
     for (SubmitRecord rec : submitRecords) {
@@ -161,6 +156,8 @@
     }
     detail.setSubmitRecords(submitRecords);
 
+    detail.setSubmitTypeRecord(control.getSubmitTypeRecord(db, patch));
+
     patchsetsById = new HashMap<PatchSet.Id, PatchSet>();
     loadPatchSets();
     loadMessages();
@@ -168,6 +165,11 @@
       loadCurrentPatchSet();
     }
     load();
+
+    detail.setCanRebase(detail.getChange().getStatus().isOpen() &&
+        control.canRebase() &&
+        RebaseChange.canDoRebase(db, change, repoManager,
+            currentPatchSetAncestors, currentDepPatchSets, currentDepChanges));
     detail.setAccounts(aic.create());
     return detail;
   }
@@ -205,53 +207,13 @@
     }
   }
 
-  private void load() throws OrmException, NoSuchChangeException {
+  private void load() throws OrmException, NoSuchChangeException,
+      NoSuchProjectException {
     final Change.Status status = detail.getChange().getStatus();
     if ((status.equals(Change.Status.NEW) || status.equals(Change.Status.DRAFT)) &&
         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(control, psId, allApprovals);
-
-      for (final ApprovalType at : approvalTypes.getApprovalTypes()) {
-        CategoryFunction.forCategory(at.getCategory()).run(at, fs);
-      }
-    }
-
-    final boolean canRemoveReviewers = detail.getChange().getStatus().isOpen() //
-        && control.getCurrentUser() instanceof IdentifiedUser;
-    final HashMap<Account.Id, ApprovalDetail> ad =
-        new HashMap<Account.Id, ApprovalDetail>();
-    for (PatchSetApproval ca : allApprovals) {
-      ApprovalDetail d = ad.get(ca.getAccountId());
-      if (d == null) {
-        d = new ApprovalDetail(ca.getAccountId());
-        d.setCanRemove(canRemoveReviewers);
-        ad.put(d.getAccount(), d);
-      }
-      if (d.canRemove()) {
-        d.setCanRemove(control.canRemoveReviewer(ca));
-      }
-      if (ca.getPatchSetId().equals(psId)) {
-        d.add(ca);
-      }
-    }
-
-    final Account.Id owner = detail.getChange().getOwner();
-    if (ad.containsKey(owner)) {
-      // Ensure the owner always sorts to the top of the table
-      //
-      ad.get(owner).sortFirst();
-    }
-
-    aic.want(ad.keySet());
-    detail.setApprovals(ad.values());
   }
 
   private boolean isReviewer(Change change) {
@@ -266,6 +228,8 @@
   private void loadCurrentPatchSet() throws OrmException,
       NoSuchEntityException, PatchSetInfoNotAvailableException,
       NoSuchChangeException {
+    currentDepPatchSets = new ArrayList<PatchSet>();
+    currentDepChanges = new ArrayList<Change>();
     final PatchSet currentPatch = findCurrentOrLatestPatchSet();
     final PatchSet.Id psId = currentPatch.getId();
     final PatchSetDetailFactory loader = patchSetDetail.create(null, psId, null);
@@ -278,8 +242,10 @@
     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)) {
+    currentPatchSetAncestors = db.patchSetAncestors().ancestorsOf(psId).toList();
+    for (PatchSetAncestor a : currentPatchSetAncestors) {
       for (PatchSet p : db.patchSets().byRevision(a.getAncestorRevision())) {
+        currentDepPatchSets.add(p);
         final Change.Id ck = p.getId().getParentKey();
         if (changesToGet.add(ck)) {
           ancestorPatchIds.put(ck, p.getId());
@@ -313,6 +279,7 @@
     for (final Change.Id a : ancestorOrder) {
       final Change ac = m.get(a);
       if (ac != null && ac.getProject().equals(detail.getChange().getProject())) {
+        currentDepChanges.add(ac);
         if (ac.getStatus().getCode() != Change.STATUS_DRAFT
             || ac.getOwner().equals(currentUserId)
             || isReviewer(ac)) {
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 f99921b..53e0d9d 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
@@ -17,44 +17,27 @@
 import com.google.gerrit.common.data.ChangeDetail;
 import com.google.gerrit.common.data.ChangeManageService;
 import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSet.Id;
 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 AbandonChangeHandler.Factory abandonChangeHandlerFactory;
-  private final RebaseChange.Factory rebaseChangeFactory;
-  private final RestoreChangeHandler.Factory restoreChangeHandlerFactory;
-  private final RevertChange.Factory revertChangeFactory;
+  private final RebaseChangeHandler.Factory rebaseChangeFactory;
   private final PublishAction.Factory publishAction;
   private final DeleteDraftChange.Factory deleteDraftChangeFactory;
+  private final EditCommitMessageHandler.Factory editCommitMessageHandlerFactory;
 
   @Inject
-  ChangeManageServiceImpl(final SubmitAction.Factory patchSetAction,
-      final AbandonChangeHandler.Factory abandonChangeHandlerFactory,
-      final RebaseChange.Factory rebaseChangeFactory,
-      final RestoreChangeHandler.Factory restoreChangeHandlerFactory,
-      final RevertChange.Factory revertChangeFactory,
+  ChangeManageServiceImpl(
+      final RebaseChangeHandler.Factory rebaseChangeFactory,
       final PublishAction.Factory publishAction,
-      final DeleteDraftChange.Factory deleteDraftChangeFactory) {
-    this.submitAction = patchSetAction;
-    this.abandonChangeHandlerFactory = abandonChangeHandlerFactory;
+      final DeleteDraftChange.Factory deleteDraftChangeFactory,
+      final EditCommitMessageHandler.Factory editCommitMessageHandler) {
     this.rebaseChangeFactory = rebaseChangeFactory;
-    this.restoreChangeHandlerFactory = restoreChangeHandlerFactory;
-    this.revertChangeFactory = revertChangeFactory;
     this.publishAction = publishAction;
     this.deleteDraftChangeFactory = deleteDraftChangeFactory;
-  }
-
-  public void submit(final PatchSet.Id patchSetId,
-      final AsyncCallback<ChangeDetail> cb) {
-    submitAction.create(patchSetId).to(cb);
-  }
-
-  public void abandonChange(final PatchSet.Id patchSetId, final String message,
-      final AsyncCallback<ChangeDetail> callback) {
-    abandonChangeHandlerFactory.create(patchSetId, message).to(callback);
+    this.editCommitMessageHandlerFactory = editCommitMessageHandler;
   }
 
   public void rebaseChange(final PatchSet.Id patchSetId,
@@ -62,16 +45,6 @@
     rebaseChangeFactory.create(patchSetId).to(callback);
   }
 
-  public void revertChange(final PatchSet.Id patchSetId, final String message,
-      final AsyncCallback<ChangeDetail> callback) {
-    revertChangeFactory.create(patchSetId, message).to(callback);
-  }
-
-  public void restoreChange(final PatchSet.Id patchSetId, final String message,
-      final AsyncCallback<ChangeDetail> callback) {
-    restoreChangeHandlerFactory.create(patchSetId, message).to(callback);
-  }
-
   public void publish(final PatchSet.Id patchSetId,
       final AsyncCallback<ChangeDetail> callback) {
     publishAction.create(patchSetId).to(callback);
@@ -81,4 +54,9 @@
       final AsyncCallback<VoidResult> callback) {
     deleteDraftChangeFactory.create(patchSetId).to(callback);
   }
+
+  public void createNewPatchSet(Id patchSetId, String message,
+      AsyncCallback<ChangeDetail> callback) {
+    editCommitMessageHandlerFactory.create(patchSetId, message).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 b672a439..48c13c2 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,15 +28,12 @@
     install(new FactoryModule() {
       @Override
       protected void configure() {
-        factory(AbandonChangeHandler.Factory.class);
-        factory(RestoreChangeHandler.Factory.class);
-        factory(RevertChange.Factory.class);
-        factory(RebaseChange.Factory.class);
+        factory(EditCommitMessageHandler.Factory.class);
+        factory(RebaseChangeHandler.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);
       }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/DeleteDraftChange.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/DeleteDraftChange.java
index ecd8b54..63c22ce 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/DeleteDraftChange.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/DeleteDraftChange.java
@@ -38,7 +38,7 @@
   private final ChangeControl.Factory changeControlFactory;
   private final ReviewDb db;
   private final GitRepositoryManager gitManager;
-  private final GitReferenceUpdated replication;
+  private final GitReferenceUpdated gitRefUpdated;
 
   private final PatchSet.Id patchSetId;
 
@@ -46,12 +46,12 @@
   DeleteDraftChange(final ReviewDb db,
       final ChangeControl.Factory changeControlFactory,
       final GitRepositoryManager gitManager,
-      final GitReferenceUpdated replication,
+      final GitReferenceUpdated gitRefUpdated,
       @Assisted final PatchSet.Id patchSetId) {
     this.changeControlFactory = changeControlFactory;
     this.db = db;
     this.gitManager = gitManager;
-    this.replication = replication;
+    this.gitRefUpdated = gitRefUpdated;
 
     this.patchSetId = patchSetId;
   }
@@ -65,7 +65,7 @@
       throw new NoSuchChangeException(changeId);
     }
 
-    ChangeUtil.deleteDraftChange(patchSetId, gitManager, replication, db);
+    ChangeUtil.deleteDraftChange(patchSetId, gitManager, gitRefUpdated, db);
     return VoidResult.INSTANCE;
   }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/EditCommitMessageHandler.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/EditCommitMessageHandler.java
new file mode 100644
index 0000000..5b064c8
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/EditCommitMessageHandler.java
@@ -0,0 +1,146 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.ChangeHooks;
+import com.google.gerrit.common.data.ChangeDetail;
+import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.common.errors.NoSuchEntityException;
+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.ApprovalsUtil;
+import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.config.TrackingFooters;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.mail.CommitMessageEditedSender;
+import com.google.gerrit.server.git.validators.CommitValidators;
+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.project.NoSuchProjectException;
+import com.google.gerrit.server.ssh.NoSshInfo;
+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.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+
+import java.io.IOException;
+
+import javax.annotation.Nullable;
+
+class EditCommitMessageHandler extends Handler<ChangeDetail> {
+  interface Factory {
+    EditCommitMessageHandler create(PatchSet.Id patchSetId, String message);
+  }
+
+  private final ChangeControl.Factory changeControlFactory;
+  private final ReviewDb db;
+  private final IdentifiedUser currentUser;
+  private final ChangeDetailFactory.Factory changeDetailFactory;
+  private final CommitMessageEditedSender.Factory commitMessageEditedSenderFactory;
+
+  private final GitReferenceUpdated gitRefUpdated;
+
+  private final PatchSet.Id patchSetId;
+  @Nullable
+  private final String message;
+
+  private final ChangeHooks hooks;
+  private final CommitValidators.Factory commitValidatorsFactory;
+
+  private final GitRepositoryManager gitManager;
+  private final PatchSetInfoFactory patchSetInfoFactory;
+
+  private final PersonIdent myIdent;
+  private final ApprovalsUtil approvalsUtil;
+  private final TrackingFooters trackingFooters;
+
+  @Inject
+  EditCommitMessageHandler(final ChangeControl.Factory changeControlFactory,
+      final ReviewDb db, final IdentifiedUser currentUser,
+      final ChangeDetailFactory.Factory changeDetailFactory,
+      final CommitMessageEditedSender.Factory commitMessageEditedSenderFactory,
+      @Assisted final PatchSet.Id patchSetId,
+      @Assisted @Nullable final String message, final ChangeHooks hooks,
+      final CommitValidators.Factory commitValidatorsFactory,
+      final GitRepositoryManager gitManager,
+      final PatchSetInfoFactory patchSetInfoFactory,
+      final GitReferenceUpdated gitRefUpdated,
+      @GerritPersonIdent final PersonIdent myIdent,
+      final ApprovalsUtil approvalsUtil, TrackingFooters trackingFooters) {
+    this.changeControlFactory = changeControlFactory;
+    this.db = db;
+    this.currentUser = currentUser;
+    this.changeDetailFactory = changeDetailFactory;
+    this.commitMessageEditedSenderFactory = commitMessageEditedSenderFactory;
+
+    this.patchSetId = patchSetId;
+    this.message = message;
+    this.hooks = hooks;
+    this.commitValidatorsFactory = commitValidatorsFactory;
+    this.gitManager = gitManager;
+
+    this.patchSetInfoFactory = patchSetInfoFactory;
+    this.gitRefUpdated = gitRefUpdated;
+    this.myIdent = myIdent;
+    this.approvalsUtil = approvalsUtil;
+    this.trackingFooters = trackingFooters;
+  }
+
+  @Override
+  public ChangeDetail call() throws NoSuchChangeException, OrmException,
+      EmailException, NoSuchEntityException, PatchSetInfoNotAvailableException,
+      MissingObjectException, IncorrectObjectTypeException, IOException,
+      InvalidChangeOperationException, NoSuchProjectException {
+
+    final Change.Id changeId = patchSetId.getParentKey();
+    final ChangeControl control = changeControlFactory.validateFor(changeId);
+    if (!control.canAddPatchSet()) {
+      throw new InvalidChangeOperationException(
+          "Not allowed to add new Patch Sets to: " + changeId.toString());
+    }
+
+    final Repository git;
+    try {
+      git = gitManager.openRepository(db.changes().get(changeId).getProject());
+    } catch (RepositoryNotFoundException e) {
+      throw new NoSuchChangeException(changeId, e);
+    }
+    try {
+      CommitValidators commitValidators =
+          commitValidatorsFactory.create(control.getRefControl(), new NoSshInfo(), git);
+
+      ChangeUtil.editCommitMessage(patchSetId, control.getRefControl(), commitValidators, currentUser, message, db,
+          commitMessageEditedSenderFactory, hooks, git, patchSetInfoFactory, gitRefUpdated, myIdent,
+          approvalsUtil, trackingFooters);
+
+      return changeDetailFactory.create(changeId).call();
+    } finally {
+      git.close();
+    }
+  }
+}
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 50baf97..d0101a6 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,18 +14,14 @@
 
 package com.google.gerrit.httpd.rpc.changedetail;
 
-import com.google.gerrit.common.data.ApprovalDetail;
-import com.google.gerrit.common.data.ApprovalType;
-import com.google.gerrit.common.data.ApprovalTypes;
+import com.google.gerrit.common.data.LabelType;
 import com.google.gerrit.common.data.PatchSetPublishDetail;
 import com.google.gerrit.common.data.PermissionRange;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.httpd.rpc.Handler;
-import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
 import com.google.gerrit.reviewdb.client.PatchSet;
-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;
@@ -34,14 +30,10 @@
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
 import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.NoSuchChangeException;
-import com.google.gerrit.server.workflow.CategoryFunction;
-import com.google.gerrit.server.workflow.FunctionState;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
-import java.util.ArrayList;
-import java.util.Collections;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
@@ -53,9 +45,7 @@
 
   private final PatchSetInfoFactory infoFactory;
   private final ReviewDb db;
-  private final FunctionState.Factory functionState;
   private final ChangeControl.Factory changeControlFactory;
-  private final ApprovalTypes approvalTypes;
   private final AccountInfoCacheFactory aic;
   private final IdentifiedUser user;
 
@@ -69,15 +59,11 @@
   PatchSetPublishDetailFactory(final PatchSetInfoFactory infoFactory,
       final ReviewDb db,
       final AccountInfoCacheFactory.Factory accountInfoCacheFactory,
-      final FunctionState.Factory functionState,
       final ChangeControl.Factory changeControlFactory,
-      final ApprovalTypes approvalTypes,
       final IdentifiedUser user, @Assisted final PatchSet.Id patchSetId) {
     this.infoFactory = infoFactory;
     this.db = db;
-    this.functionState = functionState;
     this.changeControlFactory = changeControlFactory;
-    this.approvalTypes = approvalTypes;
     this.aic = accountInfoCacheFactory.create();
     this.user = user;
 
@@ -101,9 +87,6 @@
     detail.setChange(change);
     detail.setDrafts(drafts);
 
-    List<PermissionRange> allowed = Collections.emptyList();
-    List<PatchSetApproval> given = Collections.emptyList();
-
     if (change.getStatus().isOpen()
         && patchSetId.equals(change.currentPatchSetId())) {
       // TODO Push this selection of labels down into the Prolog interpreter.
@@ -120,11 +103,6 @@
           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, patchSet);
@@ -142,12 +120,8 @@
             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()) {
+              LabelType lt = control.getLabelTypes().byLabel(lbl.label);
+              if (lt != null && lt.getMax().getValue() == range.getMax()) {
                 canMakeOk = true;
               }
             }
@@ -163,6 +137,10 @@
                   ok++;
                 }
                 break;
+
+              case IMPOSSIBLE:
+              case REJECT:
+                break;
             }
           }
 
@@ -176,60 +154,11 @@
       if (couldSubmit && control.getRefControl().canSubmit()) {
         detail.setCanSubmit(true);
       }
-
-      detail.setSubmitRecords(submitRecords);
     }
 
-    detail.setLabels(allowed);
-    detail.setGiven(given);
-    loadApprovals(detail, control);
-
+    detail.setSubmitTypeRecord(control.getSubmitTypeRecord(db, patchSet));
     detail.setAccounts(aic.create());
 
     return detail;
   }
-
-  private void loadApprovals(final PatchSetPublishDetail detail,
-      final ChangeControl control) throws OrmException {
-    final PatchSet.Id psId = detail.getChange().currentPatchSetId();
-    final Change.Id changeId = patchSetId.getParentKey();
-    final List<PatchSetApproval> allApprovals =
-        db.patchSetApprovals().byChange(changeId).toList();
-
-    if (detail.getChange().getStatus().isOpen()) {
-      final FunctionState fs = functionState.create(control, psId, allApprovals);
-
-      for (final ApprovalType at : approvalTypes.getApprovalTypes()) {
-        CategoryFunction.forCategory(at.getCategory()).run(at, fs);
-      }
-    }
-
-    final boolean canRemoveReviewers = detail.getChange().getStatus().isOpen() //
-        && control.getCurrentUser() instanceof IdentifiedUser;
-    final HashMap<Account.Id, ApprovalDetail> ad =
-        new HashMap<Account.Id, ApprovalDetail>();
-    for (PatchSetApproval ca : allApprovals) {
-      ApprovalDetail d = ad.get(ca.getAccountId());
-      if (d == null) {
-        d = new ApprovalDetail(ca.getAccountId());
-        d.setCanRemove(canRemoveReviewers);
-        ad.put(d.getAccount(), d);
-      }
-      if (d.canRemove()) {
-        d.setCanRemove(control.canRemoveReviewer(ca));
-      }
-      if (ca.getPatchSetId().equals(psId)) {
-        d.add(ca);
-      }
-    }
-
-    final Account.Id owner = detail.getChange().getOwner();
-    if (ad.containsKey(owner)) {
-      // Ensure the owner always sorts to the top of the table
-      ad.get(owner).sortFirst();
-    }
-
-    aic.want(ad.keySet());
-    detail.setApprovals(ad.values());
-  }
 }
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PublishAction.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PublishAction.java
index f57b29c..38f8fc6 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PublishAction.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/PublishAction.java
@@ -22,6 +22,7 @@
 import com.google.gerrit.server.changedetail.PublishDraft;
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
 import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
@@ -53,7 +54,8 @@
   @Override
   public ChangeDetail call() throws OrmException, NoSuchEntityException,
       IllegalStateException, PatchSetInfoNotAvailableException,
-      NoSuchChangeException, RepositoryNotFoundException, IOException {
+      NoSuchChangeException, RepositoryNotFoundException, IOException,
+      NoSuchProjectException {
     final ReviewResult result = publishFactory.create(patchSetId).call();
     if (result.getErrors().size() > 0) {
       throw new IllegalStateException("Cannot publish patchset");
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RebaseChange.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RebaseChange.java
deleted file mode 100644
index e71e302..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RebaseChange.java
+++ /dev/null
@@ -1,110 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.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.client.PatchSet;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.ApprovalsUtil;
-import com.google.gerrit.server.ChangeUtil;
-import com.google.gerrit.server.GerritPersonIdent;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.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 GitReferenceUpdated replication;
-
-  private final PatchSet.Id patchSetId;
-
-  private final ChangeHookRunner hooks;
-
-  private final GitRepositoryManager gitManager;
-  private final PatchSetInfoFactory patchSetInfoFactory;
-
-  private final PersonIdent myIdent;
-
-  private final ApprovalsUtil approvalsUtil;
-
-  @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 GitReferenceUpdated replication,
-      @GerritPersonIdent final PersonIdent myIdent,
-      final ApprovalsUtil approvalsUtil) {
-    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.approvalsUtil = approvalsUtil;
-  }
-
-  @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, approvalsUtil);
-
-    return changeDetailFactory.create(patchSetId.getParentKey()).call();
-  }
-}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RebaseChangeHandler.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RebaseChangeHandler.java
new file mode 100644
index 0000000..b9acfa9
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RebaseChangeHandler.java
@@ -0,0 +1,68 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.httpd.rpc.changedetail;
+
+import com.google.gerrit.common.data.ChangeDetail;
+import com.google.gerrit.common.errors.EmailException;
+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.IdentifiedUser;
+import com.google.gerrit.server.changedetail.RebaseChange;
+import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.project.NoSuchProjectException;
+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 java.io.IOException;
+
+class RebaseChangeHandler extends Handler<ChangeDetail> {
+  interface Factory {
+    RebaseChangeHandler create(PatchSet.Id patchSetId);
+  }
+
+  private final RebaseChange rebaseChange;
+  private final IdentifiedUser currentUser;
+  private final ChangeDetailFactory.Factory changeDetailFactory;
+
+  private final PatchSet.Id patchSetId;
+
+  @Inject
+  RebaseChangeHandler(final RebaseChange rebaseChange,
+      final IdentifiedUser currentUser,
+      final ChangeDetailFactory.Factory changeDetailFactory,
+      @Assisted final PatchSet.Id patchSetId) {
+    this.rebaseChange = rebaseChange;
+    this.currentUser = currentUser;
+    this.changeDetailFactory = changeDetailFactory;
+
+    this.patchSetId = patchSetId;
+  }
+
+  @Override
+  public ChangeDetail call() throws NoSuchChangeException, OrmException,
+      EmailException, NoSuchEntityException, PatchSetInfoNotAvailableException,
+      MissingObjectException, IncorrectObjectTypeException, IOException,
+      InvalidChangeOperationException, NoSuchProjectException {
+    rebaseChange.rebase(patchSetId, currentUser.getAccountId());
+    return changeDetailFactory.create(patchSetId.getParentKey()).call();
+  }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RestoreChangeHandler.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RestoreChangeHandler.java
deleted file mode 100644
index e4571fd..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RestoreChangeHandler.java
+++ /dev/null
@@ -1,76 +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.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.Provider;
-import com.google.inject.assistedinject.Assisted;
-
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
-
-import java.io.IOException;
-
-import javax.annotation.Nullable;
-
-class RestoreChangeHandler extends Handler<ChangeDetail> {
-  interface Factory {
-    RestoreChangeHandler create(PatchSet.Id patchSetId, String message);
-  }
-
-  private final Provider<RestoreChange> restoreChangeProvider;
-  private final ChangeDetailFactory.Factory changeDetailFactory;
-
-  private final PatchSet.Id patchSetId;
-  @Nullable
-  private final String message;
-
-  @Inject
-  RestoreChangeHandler(final Provider<RestoreChange> restoreChangeProvider,
-      final ChangeDetailFactory.Factory changeDetailFactory,
-      @Assisted final PatchSet.Id patchSetId,
-      @Assisted @Nullable final String message) {
-    this.restoreChangeProvider = restoreChangeProvider;
-    this.changeDetailFactory = changeDetailFactory;
-
-    this.patchSetId = patchSetId;
-    this.message = message;
-  }
-
-  @Override
-  public ChangeDetail call() throws NoSuchChangeException, OrmException,
-      EmailException, NoSuchEntityException, InvalidChangeOperationException,
-      PatchSetInfoNotAvailableException, RepositoryNotFoundException,
-      IOException {
-    final RestoreChange restoreChange = restoreChangeProvider.get();
-    restoreChange.setChangeId(patchSetId.getParentKey());
-    restoreChange.setMessage(message);
-    final ReviewResult result = restoreChange.call();
-    if (result.getErrors().size() > 0) {
-      throw new NoSuchChangeException(result.getChangeId());
-    }
-    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
deleted file mode 100644
index 60a03bd..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/RevertChange.java
+++ /dev/null
@@ -1,114 +0,0 @@
-// Copyright (C) 2011 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.httpd.rpc.changedetail;
-
-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.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;
-import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.mail.EmailException;
-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.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;
-
-import javax.annotation.Nullable;
-
-class RevertChange extends Handler<ChangeDetail> {
-  interface Factory {
-    RevertChange create(PatchSet.Id patchSetId, String message);
-  }
-
-  private final ChangeControl.Factory changeControlFactory;
-  private final ReviewDb db;
-  private final IdentifiedUser currentUser;
-  private final RevertedSender.Factory revertedSenderFactory;
-  private final ChangeDetailFactory.Factory changeDetailFactory;
-  private final GitReferenceUpdated replication;
-
-  private final PatchSet.Id patchSetId;
-  @Nullable
-  private final String message;
-
-  private final ChangeHooks hooks;
-
-  private final GitRepositoryManager gitManager;
-  private final PatchSetInfoFactory patchSetInfoFactory;
-
-  private final PersonIdent myIdent;
-
-  @Inject
-  RevertChange(final ChangeControl.Factory changeControlFactory,
-      final ReviewDb db, final IdentifiedUser currentUser,
-      final RevertedSender.Factory revertedSenderFactory,
-      final ChangeDetailFactory.Factory changeDetailFactory,
-      @Assisted final PatchSet.Id patchSetId,
-      @Assisted @Nullable final String message, final ChangeHooks hooks,
-      final GitRepositoryManager gitManager,
-      final PatchSetInfoFactory patchSetInfoFactory,
-      final GitReferenceUpdated replication,
-      @GerritPersonIdent final PersonIdent myIdent) {
-    this.changeControlFactory = changeControlFactory;
-    this.db = db;
-    this.currentUser = currentUser;
-    this.revertedSenderFactory = revertedSenderFactory;
-    this.changeDetailFactory = changeDetailFactory;
-
-    this.patchSetId = patchSetId;
-    this.message = message;
-    this.hooks = hooks;
-    this.gitManager = gitManager;
-
-    this.patchSetInfoFactory = patchSetInfoFactory;
-    this.replication = replication;
-    this.myIdent = myIdent;
-  }
-
-  @Override
-  public ChangeDetail call() throws NoSuchChangeException, OrmException,
-      EmailException, NoSuchEntityException, PatchSetInfoNotAvailableException,
-      MissingObjectException, IncorrectObjectTypeException, IOException {
-
-    final Change.Id changeId = patchSetId.getParentKey();
-    final ChangeControl control = changeControlFactory.validateFor(changeId);
-    if (!control.canAddPatchSet()) {
-      throw new NoSuchChangeException(changeId);
-    }
-
-    Change.Id revertedChangeId = ChangeUtil.revert(patchSetId, currentUser, message, db,
-        revertedSenderFactory, hooks, gitManager, patchSetInfoFactory,
-        replication, myIdent);
-
-    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
deleted file mode 100644
index 23b21d5..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/changedetail/SubmitAction.java
+++ /dev/null
@@ -1,67 +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.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.Submit;
-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 org.eclipse.jgit.errors.RepositoryNotFoundException;
-
-import java.io.IOException;
-
-class SubmitAction extends Handler<ChangeDetail> {
-  interface Factory {
-    SubmitAction create(PatchSet.Id patchSetId);
-  }
-
-  private final Submit.Factory submitFactory;
-  private final ChangeDetailFactory.Factory changeDetailFactory;
-
-  private final PatchSet.Id patchSetId;
-
-  @Inject
-  SubmitAction(final Submit.Factory submitFactory,
-      final ChangeDetailFactory.Factory changeDetailFactory,
-      @Assisted final PatchSet.Id patchSetId) {
-    this.submitFactory = submitFactory;
-    this.changeDetailFactory = changeDetailFactory;
-
-    this.patchSetId = patchSetId;
-  }
-
-  @Override
-  public ChangeDetail call() throws OrmException, NoSuchEntityException,
-      IllegalStateException, InvalidChangeOperationException,
-      PatchSetInfoNotAvailableException, NoSuchChangeException,
-      RepositoryNotFoundException, IOException {
-    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/group/GroupsRestApiServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/group/GroupsRestApiServlet.java
new file mode 100644
index 0000000..04dc747
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/group/GroupsRestApiServlet.java
@@ -0,0 +1,32 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.group;
+
+import com.google.gerrit.httpd.restapi.RestApiServlet;
+import com.google.gerrit.server.group.GroupsCollection;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+@Singleton
+public class GroupsRestApiServlet extends RestApiServlet {
+  private static final long serialVersionUID = 1L;
+
+  @Inject
+  GroupsRestApiServlet(RestApiServlet.Globals globals,
+      Provider<GroupsCollection> groups) {
+    super(globals, groups);
+  }
+}
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
deleted file mode 100644
index 60d6e60..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/AddReviewerHandler.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.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 40c9b84..3174757 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,38 +14,24 @@
 
 package com.google.gerrit.httpd.rpc.patch;
 
-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.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.gerrit.server.project.NoSuchProjectException;
 import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.gwtjsonrpc.common.VoidResult;
 import com.google.gwtorm.server.OrmException;
@@ -56,22 +42,10 @@
 
 import java.io.IOException;
 import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
 
 class PatchDetailServiceImpl extends BaseServiceImplementation implements
     PatchDetailService {
-  private final ApprovalTypes approvalTypes;
-
-  private final AccountInfoCacheFactory.Factory accountInfoCacheFactory;
-  private final AddReviewerHandler.Factory addReviewerHandlerFactory;
-  private final ChangeControl.Factory changeControlFactory;
   private final DeleteDraftPatchSet.Factory deleteDraftPatchSetFactory;
-  private final RemoveReviewerHandler.Factory removeReviewerHandlerFactory;
-  private final FunctionState.Factory functionStateFactory;
-  private final PublishComments.Factory publishCommentsFactory;
   private final PatchScriptFactory.Factory patchScriptFactoryFactory;
   private final SaveDraft.Factory saveDraftFactory;
   private final ChangeDetailFactory.Factory changeDetailFactory;
@@ -79,28 +53,14 @@
   @Inject
   PatchDetailServiceImpl(final Provider<ReviewDb> schema,
       final Provider<CurrentUser> currentUser,
-      final ApprovalTypes approvalTypes,
-      final AccountInfoCacheFactory.Factory accountInfoCacheFactory,
-      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 ChangeDetailFactory.Factory changeDetailFactory) {
     super(schema, currentUser);
-    this.approvalTypes = approvalTypes;
 
-    this.accountInfoCacheFactory = accountInfoCacheFactory;
-    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;
   }
@@ -165,6 +125,8 @@
           return changeDetailFactory.create(result.getChangeId()).call();
         } catch (NoSuchChangeException e) {
           throw new Failure(new NoSuchChangeException(result.getChangeId()));
+        } catch (NoSuchProjectException e) {
+          throw new Failure(e);
         } catch (NoSuchEntityException e) {
           throw new Failure(e);
         } catch (PatchSetInfoNotAvailableException e) {
@@ -177,154 +139,4 @@
       }
     });
   }
-
-  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, false)).to(cb);
-  }
-
-  /**
-   * Update the reviewed status for the file by user @code{account}
-   */
-  public void setReviewedByCurrentUser(final Key patchKey,
-      final boolean reviewed, AsyncCallback<VoidResult> callback) {
-    run(callback, new Action<VoidResult>() {
-      public VoidResult run(ReviewDb db) throws OrmException {
-        Account.Id account = getAccountId();
-        AccountPatchReview.Key key =
-            new AccountPatchReview.Key(patchKey, account);
-        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();
-        }
-      }
-    });
-  }
-
-  public void addReviewers(final Change.Id id, final List<String> reviewers,
-      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) {
-    removeReviewerHandlerFactory.create(id, reviewerId).to(callback);
-  }
-
-  public void userApprovals(final Set<Change.Id> cids, final Account.Id aid,
-      final AsyncCallback<ApprovalSummarySet> callback) {
-    run(callback, new Action<ApprovalSummarySet>() {
-      public ApprovalSummarySet run(ReviewDb db) throws OrmException {
-        final Map<Change.Id, ApprovalSummary> approvals =
-            new HashMap<Change.Id, ApprovalSummary>();
-        final AccountInfoCacheFactory aicFactory =
-            accountInfoCacheFactory.create();
-
-        aicFactory.want(aid);
-        for (final Change.Id id : cids) {
-          try {
-            final ChangeControl cc = changeControlFactory.validateFor(id);
-            final Change change = cc.getChange();
-            final PatchSet.Id ps_id = change.currentPatchSetId();
-            final Map<ApprovalCategory.Id, PatchSetApproval> psas =
-                new HashMap<ApprovalCategory.Id, PatchSetApproval>();
-            final FunctionState fs =
-                functionStateFactory.create(cc, ps_id, psas.values());
-
-            for (final PatchSetApproval ca : db.patchSetApprovals()
-                .byPatchSetUser(ps_id, aid)) {
-              final ApprovalCategory.Id category = ca.getCategoryId();
-              if (ApprovalCategory.SUBMIT.equals(category)) {
-                continue;
-              }
-              if (change.getStatus().isOpen()) {
-                fs.normalize(approvalTypes.byId(category), ca);
-              }
-              if (ca.getValue() == 0) {
-                continue;
-              }
-              psas.put(category, ca);
-            }
-
-            approvals.put(id, new ApprovalSummary(psas.values()));
-          } catch (NoSuchChangeException nsce) {
-            /*
-             * The user has no access to see this change, so we simply do not
-             * provide any details about it.
-             */
-          }
-        }
-        return new ApprovalSummarySet(aicFactory.create(), approvals);
-      }
-    });
-  }
-
-  public void strongestApprovals(final Set<Change.Id> cids,
-      final AsyncCallback<ApprovalSummarySet> callback) {
-    run(callback, new Action<ApprovalSummarySet>() {
-      public ApprovalSummarySet run(ReviewDb db) throws OrmException {
-        final Map<Change.Id, ApprovalSummary> approvals =
-            new HashMap<Change.Id, ApprovalSummary>();
-        final AccountInfoCacheFactory aicFactory =
-            accountInfoCacheFactory.create();
-
-        for (final Change.Id id : cids) {
-          try {
-            final ChangeControl cc = changeControlFactory.validateFor(id);
-            final Change change = cc.getChange();
-            final PatchSet.Id ps_id = change.currentPatchSetId();
-            final Map<ApprovalCategory.Id, PatchSetApproval> psas =
-                new HashMap<ApprovalCategory.Id, PatchSetApproval>();
-            final FunctionState fs =
-                functionStateFactory.create(cc, ps_id, psas.values());
-
-            for (PatchSetApproval ca : db.patchSetApprovals().byPatchSet(ps_id)) {
-              final ApprovalCategory.Id category = ca.getCategoryId();
-              if (ApprovalCategory.SUBMIT.equals(category)) {
-                continue;
-              }
-              if (change.getStatus().isOpen()) {
-                fs.normalize(approvalTypes.byId(category), ca);
-              }
-              if (ca.getValue() == 0) {
-                continue;
-              }
-              boolean keep = true;
-              if (psas.containsKey(category)) {
-                final short oldValue = psas.get(category).getValue();
-                final short newValue = ca.getValue();
-                keep =
-                    (Math.abs(oldValue) < Math.abs(newValue))
-                        || ((Math.abs(oldValue) == Math.abs(newValue) && (newValue < oldValue)));
-              }
-              if (keep) {
-                aicFactory.want(ca.getAccountId());
-                psas.put(category, ca);
-              }
-            }
-
-            approvals.put(id, new ApprovalSummary(psas.values()));
-          } catch (NoSuchChangeException nsce) {
-            /*
-             * The user has no access to see this change, so we simply do not
-             * provide any details about it.
-             */
-          }
-        }
-
-        return new ApprovalSummarySet(aicFactory.create(), approvals);
-      }
-    });
-  }
 }
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 3e94b4c..d1f5b24 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,6 @@
     install(new FactoryModule() {
       @Override
       protected void configure() {
-        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 816bbef..5019403 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
@@ -133,6 +133,7 @@
       throws IOException {
     boolean intralineDifferenceIsPossible = true;
     boolean intralineFailure = false;
+    boolean intralineTimeout = false;
 
     a.path = oldName(content);
     b.path = newName(content);
@@ -160,10 +161,14 @@
             break;
 
           case ERROR:
-          case TIMEOUT:
             intralineDifferenceIsPossible = false;
             intralineFailure = true;
             break;
+
+          case TIMEOUT:
+            intralineDifferenceIsPossible = false;
+            intralineTimeout = true;
+            break;
         }
       } else {
         intralineDifferenceIsPossible = false;
@@ -198,10 +203,10 @@
         context = diffPrefs.getContext();
         hugeFile = true;
 
-      } else if (diffPrefs.isSyntaxHighlighting()) {
-        // In order to syntax highlight the file properly we need to
-        // give the client the complete file contents. So force our
-        // context temporarily to the complete file size.
+      } else {
+        // In order to expand the skipped common lines or syntax highlight the
+        // file properly we need to give the client the complete file contents.
+        // So force our context temporarily to the complete file size.
         //
         context = MAX_CONTEXT;
       }
@@ -212,7 +217,7 @@
         content.getOldName(), content.getNewName(), a.fileMode, b.fileMode,
         content.getHeaderLines(), diffPrefs, a.dst, b.dst, edits,
         a.displayMethod, b.displayMethod, comments, history, hugeFile,
-        intralineDifferenceIsPossible, intralineFailure);
+        intralineDifferenceIsPossible, intralineFailure, intralineTimeout);
   }
 
   private static boolean isModify(PatchListEntry content) {
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 97d850a..e0ec465 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
@@ -241,6 +241,12 @@
               name = oldName;
             }
             break;
+
+          case MODIFIED:
+          case DELETED:
+          case ADDED:
+          case REWRITE:
+            break;
         }
       }
 
@@ -266,6 +272,9 @@
         }
         loadPublished(byKey, aic, newName);
         break;
+
+      case REWRITE:
+        break;
     }
 
     final CurrentUser user = control.getCurrentUser();
@@ -288,6 +297,9 @@
           }
           loadDrafts(byKey, aic, me, newName);
           break;
+
+        case REWRITE:
+          break;
       }
     }
 
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
deleted file mode 100644
index 2b31c1c..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/patch/RemoveReviewerHandler.java
+++ /dev/null
@@ -1,59 +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.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 06c21df..18ab5ff5 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
@@ -71,8 +71,8 @@
 
       final Account.Id me = currentUser.getAccountId();
       if (comment.getKey().get() == null) {
-        if (comment.getLine() < 1) {
-          throw new IllegalStateException("Comment line must be >= 1, not "
+        if (comment.getLine() < 0) {
+          throw new IllegalStateException("Comment line must be >= 0, not "
               + comment.getLine());
         }
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/plugin/ListPluginsServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/plugin/ListPluginsServlet.java
deleted file mode 100644
index 5e8145c..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/plugin/ListPluginsServlet.java
+++ /dev/null
@@ -1,68 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.httpd.rpc.plugin;
-
-import com.google.gerrit.common.data.GlobalCapability;
-import com.google.gerrit.extensions.annotations.RequiresCapability;
-import com.google.gerrit.httpd.RestApiServlet;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.OutputFormat;
-import com.google.gerrit.server.plugins.ListPlugins;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.Singleton;
-
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-@Singleton
-@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
-public class ListPluginsServlet extends RestApiServlet {
-  private static final long serialVersionUID = 1L;
-  private final ParameterParser paramParser;
-  private final Provider<ListPlugins> factory;
-
-  @Inject
-  ListPluginsServlet(final Provider<CurrentUser> currentUser,
-      ParameterParser paramParser, Provider<ListPlugins> ls) {
-    super(currentUser);
-    this.paramParser = paramParser;
-    this.factory = ls;
-  }
-
-  @Override
-  protected void doGet(HttpServletRequest req, HttpServletResponse res)
-      throws IOException {
-    ListPlugins impl = factory.get();
-    if (acceptsJson(req)) {
-      impl.setFormat(OutputFormat.JSON_COMPACT);
-    }
-    if (paramParser.parse(impl, req, res)) {
-      ByteArrayOutputStream buf = new ByteArrayOutputStream();
-      if (impl.getFormat().isJson()) {
-        res.setContentType(JSON_TYPE);
-        buf.write(JSON_MAGIC);
-      } else {
-        res.setContentType("text/plain");
-      }
-      impl.display(buf);
-      res.setCharacterEncoding("UTF-8");
-      send(req, res, buf.toByteArray());
-    }
-  }
-}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/AddBranch.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/AddBranch.java
index 97e9bb4..d26501e 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
@@ -15,8 +15,7 @@
 package com.google.gerrit.httpd.rpc.project;
 
 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.data.AddBranchResult;
 import com.google.gerrit.common.errors.InvalidRevisionException;
 import com.google.gerrit.httpd.rpc.Handler;
 import com.google.gerrit.reviewdb.client.Branch;
@@ -46,7 +45,7 @@
 
 import java.io.IOException;
 
-class AddBranch extends Handler<ListBranchesResult> {
+class AddBranch extends Handler<AddBranchResult> {
   private static final Logger log = LoggerFactory.getLogger(AddBranch.class);
 
   interface Factory {
@@ -90,9 +89,7 @@
   }
 
   @Override
-  public ListBranchesResult call() throws NoSuchProjectException,
-      InvalidNameException, InvalidRevisionException, IOException,
-      BranchCreationNotAllowedException {
+  public AddBranchResult call() throws NoSuchProjectException, IOException {
     final ProjectControl projectControl =
         projectControlFactory.controlFor(projectName);
 
@@ -104,10 +101,14 @@
       refname = Constants.R_HEADS + refname;
     }
     if (!Repository.isValidRefName(refname)) {
-      throw new InvalidNameException();
+      return new AddBranchResult(new AddBranchResult.Error(
+          AddBranchResult.Error.Type.INVALID_NAME, refname));
     }
     if (MagicBranch.isMagicBranch(refname)) {
-      throw new BranchCreationNotAllowedException(refname);
+      return new AddBranchResult(
+          new AddBranchResult.Error(
+              AddBranchResult.Error.Type.BRANCH_CREATION_NOT_ALLOWED_UNDER_REFNAME_PREFIX,
+              MagicBranch.getMagicRefNamePrefix(refname)));
     }
 
     final Branch.NameKey name = new Branch.NameKey(projectName, refname);
@@ -144,9 +145,22 @@
           case FAST_FORWARD:
           case NEW:
           case NO_CHANGE:
-            referenceUpdated.fire(name.getParentKey(), refname);
+            referenceUpdated.fire(name.getParentKey(), u);
             hooks.doRefUpdatedHook(name, u, identifiedUser.getAccount());
             break;
+          case LOCK_FAILURE:
+            if (repo.getRef(refname) != null) {
+              return new AddBranchResult(new AddBranchResult.Error(
+                  AddBranchResult.Error.Type.BRANCH_ALREADY_EXISTS, refname));
+            }
+            String refPrefix = getRefPrefix(refname);
+            while (!Constants.R_HEADS.equals(refPrefix)) {
+              if (repo.getRef(refPrefix) != null) {
+                return new AddBranchResult(new AddBranchResult.Error(
+                    AddBranchResult.Error.Type.BRANCH_CREATION_CONFLICT, refPrefix));
+              }
+              refPrefix = getRefPrefix(refPrefix);
+            }
           default: {
             throw new IOException(result.name());
           }
@@ -155,11 +169,22 @@
         log.error("Cannot create branch " + name, err);
         throw err;
       }
+    } catch (InvalidRevisionException e) {
+      return new AddBranchResult(new AddBranchResult.Error(
+          AddBranchResult.Error.Type.INVALID_REVISION));
     } finally {
       repo.close();
     }
 
-    return listBranchesFactory.create(projectName).call();
+    return new AddBranchResult(listBranchesFactory.create(projectName).call());
+  }
+
+  private static String getRefPrefix(final String refName) {
+    final int i = refName.lastIndexOf('/');
+    if (i > Constants.R_HEADS.length() - 1) {
+      return refName.substring(0, i);
+    }
+    return Constants.R_HEADS;
   }
 
   private ObjectId parseStartingRevision(final Repository repo)
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/BranchCreationNotAllowedException.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/BranchCreationNotAllowedException.java
deleted file mode 100644
index 98cdc32..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/BranchCreationNotAllowedException.java
+++ /dev/null
@@ -1,26 +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;
-
-public class BranchCreationNotAllowedException extends Exception {
-
-  private static final long serialVersionUID = 1L;
-
-  public static final String MESSAGE = "Branch creation is not allowed under: ";
-
-  public BranchCreationNotAllowedException(final String refnamePrefix) {
-    super(MESSAGE + refnamePrefix);
-  }
-}
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
deleted file mode 100644
index 141f332..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/CreateProjectHandler.java
+++ /dev/null
@@ -1,85 +0,0 @@
-// Copyright (C) 2011 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License
-
-package com.google.gerrit.httpd.rpc.project;
-
-import com.google.gerrit.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 java.util.Collections;
-
-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 = Collections.emptyList();
-    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 f2b3ca3..8f5430d 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
@@ -50,7 +50,7 @@
 
   private final ProjectControl.Factory projectControlFactory;
   private final GitRepositoryManager repoManager;
-  private final GitReferenceUpdated replication;
+  private final GitReferenceUpdated gitRefUpdated;
   private final IdentifiedUser identifiedUser;
   private final ChangeHooks hooks;
   private final ReviewDb db;
@@ -61,7 +61,7 @@
   @Inject
   DeleteBranches(final ProjectControl.Factory projectControlFactory,
       final GitRepositoryManager repoManager,
-      final GitReferenceUpdated replication,
+      final GitReferenceUpdated gitRefUpdated,
       final IdentifiedUser identifiedUser,
       final ChangeHooks hooks,
       final ReviewDb db,
@@ -69,7 +69,7 @@
       @Assisted Project.NameKey name, @Assisted Set<Branch.NameKey> toRemove) {
     this.projectControlFactory = projectControlFactory;
     this.repoManager = repoManager;
-    this.replication = replication;
+    this.gitRefUpdated = gitRefUpdated;
     this.identifiedUser = identifiedUser;
     this.hooks = hooks;
     this.db = db;
@@ -121,7 +121,7 @@
           case FAST_FORWARD:
           case FORCED:
             deleted.add(branchKey);
-            replication.fire(projectName, refname);
+            gitRefUpdated.fire(projectName, u);
             hooks.doRefUpdatedHook(branchKey, u, identifiedUser.getAccount());
             break;
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ListProjectsServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ListProjectsServlet.java
deleted file mode 100644
index d327d35..0000000
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ListProjectsServlet.java
+++ /dev/null
@@ -1,70 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.httpd.rpc.project;
-
-import com.google.common.base.Strings;
-import com.google.gerrit.httpd.RestApiServlet;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.OutputFormat;
-import com.google.gerrit.server.project.ListProjects;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.Singleton;
-
-import java.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.net.URLDecoder;
-
-import javax.servlet.http.HttpServletRequest;
-import javax.servlet.http.HttpServletResponse;
-
-@Singleton
-public class ListProjectsServlet extends RestApiServlet {
-  private static final long serialVersionUID = 1L;
-  private final ParameterParser paramParser;
-  private final Provider<ListProjects> factory;
-
-  @Inject
-  ListProjectsServlet(final Provider<CurrentUser> currentUser,
-      ParameterParser paramParser, Provider<ListProjects> ls) {
-    super(currentUser);
-    this.paramParser = paramParser;
-    this.factory = ls;
-  }
-
-  @Override
-  protected void doGet(HttpServletRequest req, HttpServletResponse res)
-      throws IOException {
-    ListProjects impl = factory.get();
-    if (!Strings.isNullOrEmpty(req.getPathInfo())) {
-      impl.setMatchPrefix(URLDecoder.decode(req.getPathInfo(), "UTF-8"));
-    }
-    if (acceptsJson(req)) {
-      impl.setFormat(OutputFormat.JSON_COMPACT);
-    }
-    if (paramParser.parse(impl, req, res)) {
-      ByteArrayOutputStream buf = new ByteArrayOutputStream();
-      if (impl.getFormat().isJson()) {
-        res.setContentType(JSON_TYPE);
-        buf.write(JSON_MAGIC);
-      } else {
-        res.setContentType("text/plain");
-      }
-      impl.display(buf);
-      res.setCharacterEncoding("UTF-8");
-      send(req, res, buf.toByteArray());
-    }
-  }
-}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java
index 7b3b8e7..2c5681b 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessFactory.java
@@ -121,6 +121,9 @@
         if (pc.isOwner()) {
           local.add(section);
           ownerOf.add(name);
+
+        } else if (metaConfigControl.isVisible()) {
+          local.add(section);
         }
 
       } else if (RefConfigSection.isValid(name)) {
@@ -201,6 +204,7 @@
     detail.setCanUpload(pc.isOwner()
         || (metaConfigControl.isVisible() && metaConfigControl.canUpload()));
     detail.setConfigVisible(pc.isOwner() || metaConfigControl.isVisible());
+    detail.setLabelTypes(pc.getLabelTypes());
     return detail;
   }
 
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java
index 02b84b0..0a50a38 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectAccessHandler.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.httpd.rpc.project;
 
+import static com.google.gerrit.common.ProjectAccessUtil.mergeSections;
+
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.common.data.Permission;
@@ -36,11 +38,8 @@
 import org.eclipse.jgit.lib.ObjectId;
 
 import java.io.IOException;
-import java.util.ArrayList;
 import java.util.HashSet;
-import java.util.LinkedHashMap;
 import java.util.List;
-import java.util.Map;
 import java.util.Set;
 
 public abstract class ProjectAccessHandler<T> extends Handler<T> {
@@ -151,23 +150,6 @@
     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()) {
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 983f1fc..0e46bc3 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
@@ -15,6 +15,7 @@
 package com.google.gerrit.httpd.rpc.project;
 
 import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.AddBranchResult;
 import com.google.gerrit.common.data.ListBranchesResult;
 import com.google.gerrit.common.data.ProjectAccess;
 import com.google.gerrit.common.data.ProjectAdminService;
@@ -23,7 +24,6 @@
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gwtjsonrpc.common.AsyncCallback;
-import com.google.gwtjsonrpc.common.VoidResult;
 import com.google.inject.Inject;
 
 import org.eclipse.jgit.lib.ObjectId;
@@ -40,7 +40,6 @@
   private final ListBranches.Factory listBranchesFactory;
   private final VisibleProjectDetails.Factory visibleProjectDetailsFactory;
   private final ProjectAccessFactory.Factory projectAccessFactory;
-  private final CreateProjectHandler.Factory createProjectHandlerFactory;
   private final ProjectDetailFactory.Factory projectDetailFactory;
 
   @Inject
@@ -52,8 +51,7 @@
       final ListBranches.Factory listBranchesFactory,
       final VisibleProjectDetails.Factory visibleProjectDetailsFactory,
       final ProjectAccessFactory.Factory projectAccessFactory,
-      final ProjectDetailFactory.Factory projectDetailFactory,
-      final CreateProjectHandler.Factory createNewProjectFactory) {
+      final ProjectDetailFactory.Factory projectDetailFactory) {
     this.addBranchFactory = addBranchFactory;
     this.changeProjectAccessFactory = changeProjectAccessFactory;
     this.reviewProjectAccessFactory = reviewProjectAccessFactory;
@@ -63,7 +61,6 @@
     this.visibleProjectDetailsFactory = visibleProjectDetailsFactory;
     this.projectAccessFactory = projectAccessFactory;
     this.projectDetailFactory = projectDetailFactory;
-    this.createProjectHandlerFactory = createNewProjectFactory;
   }
 
   @Override
@@ -126,16 +123,8 @@
   @Override
   public void addBranch(final Project.NameKey projectName,
       final String branchName, final String startingRevision,
-      final AsyncCallback<ListBranchesResult> callback) {
+      final AsyncCallback<AddBranchResult> callback) {
     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 2741640..2533feb 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,8 +14,10 @@
 
 package com.google.gerrit.httpd.rpc.project;
 
+import com.google.common.collect.Iterables;
 import com.google.gerrit.common.data.ProjectDetail;
 import com.google.gerrit.httpd.rpc.Handler;
+import com.google.gerrit.reviewdb.client.InheritedBoolean;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.project.NoSuchProjectException;
@@ -68,6 +70,28 @@
     detail.setCanModifyMergeType(userIsOwner);
     detail.setCanModifyState(userIsOwner);
 
+    final InheritedBoolean useContributorAgreements = new InheritedBoolean();
+    final InheritedBoolean useSignedOffBy = new InheritedBoolean();
+    final InheritedBoolean useContentMerge = new InheritedBoolean();
+    final InheritedBoolean requireChangeID = new InheritedBoolean();
+    useContributorAgreements.setValue(projectState.getProject()
+        .getUseContributorAgreements());
+    useSignedOffBy.setValue(projectState.getProject().getUseSignedOffBy());
+    useContentMerge.setValue(projectState.getProject().getUseContentMerge());
+    requireChangeID.setValue(projectState.getProject().getRequireChangeID());
+    ProjectState parentState = Iterables.getFirst(projectState.parents(), null);
+    if (parentState != null) {
+      useContributorAgreements.setInheritedValue(parentState
+          .isUseContributorAgreements());
+      useSignedOffBy.setInheritedValue(parentState.isUseSignedOffBy());
+      useContentMerge.setInheritedValue(parentState.isUseContentMerge());
+      requireChangeID.setInheritedValue(parentState.isRequireChangeID());
+    }
+    detail.setUseContributorAgreements(useContributorAgreements);
+    detail.setUseSignedOffBy(useSignedOffBy);
+    detail.setUseContentMerge(useContentMerge);
+    detail.setRequireChangeID(requireChangeID);
+
     final Project.NameKey projectName = projectState.getProject().getNameKey();
     Repository git;
     try {
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 e943e3fc..2d4f210 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
@@ -31,7 +31,6 @@
         factory(AddBranch.Factory.class);
         factory(ChangeProjectAccess.Factory.class);
         factory(ReviewProjectAccess.Factory.class);
-        factory(CreateProjectHandler.Factory.class);
         factory(ChangeProjectSettings.Factory.class);
         factory(DeleteBranches.Factory.class);
         factory(ListBranches.Factory.class);
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectsRestApiServlet.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectsRestApiServlet.java
new file mode 100644
index 0000000..ea3b8b0
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ProjectsRestApiServlet.java
@@ -0,0 +1,32 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.restapi.RestApiServlet;
+import com.google.gerrit.server.project.ProjectsCollection;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+@Singleton
+public class ProjectsRestApiServlet extends RestApiServlet {
+  private static final long serialVersionUID = 1L;
+
+  @Inject
+  ProjectsRestApiServlet(RestApiServlet.Globals globals,
+      Provider<ProjectsCollection> projects) {
+    super(globals, projects);
+  }
+}
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java
index 69a283a..251249f 100644
--- a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/rpc/project/ReviewProjectAccess.java
@@ -27,14 +27,17 @@
 import com.google.gerrit.server.ChangeUtil;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.change.PostReviewers;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
-import com.google.gerrit.server.patch.AddReviewer;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
+import com.google.gerrit.server.project.ChangeControl;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 import com.google.inject.assistedinject.Assisted;
 
 import org.eclipse.jgit.lib.ObjectId;
@@ -58,14 +61,16 @@
   private final ReviewDb db;
   private final IdentifiedUser user;
   private final PatchSetInfoFactory patchSetInfoFactory;
-  private final AddReviewer.Factory addReviewerFactory;
+  private final Provider<PostReviewers> reviewersProvider;
+  private final ChangeControl.GenericFactory changeFactory;
 
   @Inject
   ReviewProjectAccess(final ProjectControl.Factory projectControlFactory,
       final GroupBackend groupBackend,
       final MetaDataUpdate.User metaDataUpdateFactory, final ReviewDb db,
       final IdentifiedUser user, final PatchSetInfoFactory patchSetInfoFactory,
-      final AddReviewer.Factory addReviewerFactory,
+      final Provider<PostReviewers> reviewersProvider,
+      final ChangeControl.GenericFactory changeFactory,
 
       @Assisted final Project.NameKey projectName,
       @Nullable @Assisted final ObjectId base,
@@ -76,14 +81,16 @@
     this.db = db;
     this.user = user;
     this.patchSetInfoFactory = patchSetInfoFactory;
-    this.addReviewerFactory = addReviewerFactory;
+    this.reviewersProvider = reviewersProvider;
+    this.changeFactory = changeFactory;
   }
 
   @Override
   protected Change.Id updateProjectConfig(ProjectConfig config, MetaDataUpdate md)
       throws IOException, OrmException {
     Change.Id changeId = new Change.Id(db.nextChangeId());
-    PatchSet ps = new PatchSet(new PatchSet.Id(changeId, 1));
+    PatchSet ps =
+        new PatchSet(new PatchSet.Id(changeId, Change.INITIAL_PATCH_SET_ID));
     RevCommit commit = config.commitToNewRef(md, ps.getRefName());
     if (commit.getId().equals(base)) {
       return null;
@@ -96,7 +103,6 @@
         new Branch.NameKey(
             config.getProject().getNameKey(),
             GitRepositoryManager.REF_CONFIG));
-    change.nextPatchSetId();
 
     ps.setCreatedOn(change.getCreatedOn());
     ps.setUploader(change.getOwner());
@@ -111,7 +117,7 @@
       insertAncestors(ps.getId(), commit);
       db.patchSets().insert(Collections.singleton(ps));
       db.changes().insert(Collections.singleton(change));
-      addProjectOwnersAsReviewers(changeId);
+      addProjectOwnersAsReviewers(change);
       db.commit();
     } finally {
       db.rollback();
@@ -133,12 +139,15 @@
     db.patchSetAncestors().insert(toInsert);
   }
 
-  private void addProjectOwnersAsReviewers(final Change.Id changeId) {
+  private void addProjectOwnersAsReviewers(final Change change) {
     final String projectOwners =
         groupBackend.get(AccountGroup.PROJECT_OWNERS).getName();
     try {
-      addReviewerFactory.create(changeId, Collections.singleton(projectOwners),
-          false).call();
+      ChangeResource rsrc =
+          new ChangeResource(changeFactory.controlFor(change, user));
+      PostReviewers.Input input = new PostReviewers.Input();
+      input.reviewer = projectOwners;
+      reviewersProvider.get().apply(rsrc, input);
     } catch (Exception e) {
       // one of the owner groups is not visible to the user and this it why it
       // can't be added as reviewer
diff --git a/gerrit-httpd/src/main/java/com/google/gerrit/httpd/template/SiteHeaderFooter.java b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/template/SiteHeaderFooter.java
new file mode 100644
index 0000000..321f032
--- /dev/null
+++ b/gerrit-httpd/src/main/java/com/google/gerrit/httpd/template/SiteHeaderFooter.java
@@ -0,0 +1,150 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.template;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.httpd.HtmlDomUtil;
+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 org.eclipse.jgit.lib.Config;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+import java.io.File;
+import java.io.IOException;
+
+@Singleton
+public class SiteHeaderFooter {
+  private static final Logger log = LoggerFactory.getLogger(SiteHeaderFooter.class);
+
+  private final boolean refreshHeaderFooter;
+  private final SitePaths sitePaths;
+  private volatile Template template;
+
+  @Inject
+  SiteHeaderFooter(@GerritServerConfig Config cfg, SitePaths sitePaths) {
+    this.refreshHeaderFooter = cfg.getBoolean("site", "refreshHeaderFooter", true);
+    this.sitePaths = sitePaths;
+
+    Template t = new Template(sitePaths);
+    try {
+      t.load();
+    } catch (IOException e) {
+      log.warn("Cannot load site header or footer", e);
+    }
+    template = t;
+  }
+
+  public Document parse(Class<?> clazz, String name) throws IOException {
+    Template t = template;
+    if (refreshHeaderFooter && t.isStale()) {
+      t = new Template(sitePaths);
+      try {
+        t.load();
+        template = t;
+      } catch (IOException e) {
+        log.warn("Cannot refresh site header or footer", e);
+        t = template;
+      }
+    }
+
+    Document doc = HtmlDomUtil.parseFile(clazz, name);
+    injectCss(doc, "gerrit_sitecss", t.css);
+    injectXml(doc, "gerrit_header", t.header);
+    injectXml(doc, "gerrit_footer", t.footer);
+    return doc;
+  }
+
+  private void injectCss(Document doc, String id, String content) {
+    Element e = HtmlDomUtil.find(doc, id);
+    if (e != null) {
+      if (!Strings.isNullOrEmpty(content)) {
+        while (e.getFirstChild() != null) {
+          e.removeChild(e.getFirstChild());
+        }
+        e.removeAttribute("id");
+        e.appendChild(doc.createCDATASection("\n" + content + "\n"));
+      } else {
+        e.getParentNode().removeChild(e);
+      }
+    }
+  }
+
+  private void injectXml(Document doc, String id, Element d) {
+    Element e = HtmlDomUtil.find(doc, id);
+    if (e != null) {
+      if (d != null) {
+        while (e.getFirstChild() != null) {
+          e.removeChild(e.getFirstChild());
+        }
+        e.appendChild(doc.importNode(d, true));
+      } else {
+        e.getParentNode().removeChild(e);
+      }
+    }
+  }
+
+  private static class Template {
+    private final FileInfo cssFile;
+    private final FileInfo headerFile;
+    private final FileInfo footerFile;
+
+    String css;
+    Element header;
+    Element footer;
+
+    Template(SitePaths site) {
+      cssFile = new FileInfo(site.site_css);
+      headerFile = new FileInfo(site.site_header);
+      footerFile = new FileInfo(site.site_footer);
+    }
+
+    void load() throws IOException {
+      css = HtmlDomUtil.readFile(
+          cssFile.path.getParentFile(),
+          cssFile.path.getName());
+      header = readXml(headerFile);
+      footer = readXml(footerFile);
+    }
+
+    boolean isStale() {
+      return cssFile.isStale() || headerFile.isStale() || footerFile.isStale();
+    }
+
+    private static Element readXml(FileInfo src) throws IOException {
+      Document d = HtmlDomUtil.parseFile(src.path);
+      return d != null ? d.getDocumentElement() : null;
+    }
+  }
+
+  private static class FileInfo {
+    final File path;
+    final long time;
+
+    FileInfo(File p) {
+      path = p;
+      time = path.lastModified();
+    }
+
+    boolean isStale() {
+      return time != path.lastModified();
+    }
+  }
+}
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 c3c7503..c660311 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
@@ -1,10 +1,10 @@
 <html>
   <head>
     <title>Gerrit Code Review</title>
-    <script id="gerrit_gwtdevmode">
+    <script id="gwtdevmode">
       (function () {
         var pn = 'gwt.codesvr';
-        var cn = 'gerrit.' + pn;
+        var cn = 'gerrit_ui.' + pn;
 
         var p_start = window.location.search.indexOf(pn + '=');
         if (p_start != -1) {
@@ -29,51 +29,59 @@
         }
       })();
     </script>
+    <style id="gerrit_sitecss" type="text/css"></style>
   </head>
   <body>
-    <h2>Sign In</h2>
-    <table border="0">
-      <tr>
-        <th>Username:</th>
-        <td>
-          <form method="GET">
-            <input type="text" size="30" name="user_name" />
-            <input type="submit" value="Become Account" />
-          </form>
-        </td>
-      </tr>
+    <div id="gerrit_topmenu" style="height:45px;" class="gerritTopMenu"></div>
+    <div id="gerrit_header"></div>
+    <div id="gerrit_body" class="gerritBody">
+      <h2>Sign In</h2>
+      <table border="0">
+        <tr>
+          <th>Username:</th>
+          <td>
+            <form method="GET">
+              <input type="text" size="30" name="user_name" />
+              <input type="submit" value="Become Account" />
+            </form>
+          </td>
+        </tr>
 
-      <tr>
-        <th>Email Address:</th>
-        <td>
-          <form method="GET">
-            <input type="text" size="30" name="preferred_email" />
-            <input type="submit" value="Become Account" />
-          </form>
-        </td>
-      </tr>
+        <tr>
+          <th>Email Address:</th>
+          <td>
+            <form method="GET">
+              <input type="text" size="30" name="preferred_email" />
+              <input type="submit" value="Become Account" />
+            </form>
+          </td>
+        </tr>
 
-      <tr>
-        <th>Account ID:</th>
-        <td>
-          <form method="GET">
-            <input type="text" size="12" name="account_id" />
-            <input type="submit" value="Become Account" />
-          </form>
-        </td>
-      </tr>
+        <tr>
+          <th>Account ID:</th>
+          <td>
+            <form method="GET">
+              <input type="text" size="12" name="account_id" />
+              <input type="submit" value="Become Account" />
+            </form>
+          </td>
+        </tr>
 
-      <tr>
-        <th>Choose:</th>
-        <td id="userlist"/>
-      </tr>
-    </table>
+        <tr>
+          <th style="vertical-align: top;">Choose:</th>
+          <td id="userlist"/>
+        </tr>
+      </table>
 
-    <hr />
-    <h2>Register</h2>
-    <form method="POST">
-      <input type="hidden" name="action" value="create_account" />
-      <input type="submit" value="New Account" />
-    </form>
+      <hr />
+      <h2>Register</h2>
+      <form method="POST">
+        <input type="hidden" name="action" value="create_account" />
+        <input type="submit" value="New Account" />
+      </form>
+    </div>
+    <div style="clear: both; margin-top: 15px; padding-top: 2px; margin-bottom: 15px;">
+      <div id="gerrit_footer"></div>
+    </div>
   </body>
 </html>
diff --git a/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/auth/container/ConfigurationError.html b/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/auth/container/ConfigurationError.html
index 7294012..0bc3369 100644
--- a/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/auth/container/ConfigurationError.html
+++ b/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/auth/container/ConfigurationError.html
@@ -49,22 +49,16 @@
 &lt;VirtualHost <span class='ServerName'>review.example.com</span><span class='ServerPort'>:80</span>&gt;
     ServerName <span class='ServerName'>review.example.com</span>
 
-    ProxyRequests Off
-    ProxyVia Off
-    ProxyPreserveHost On
-
-    &lt;Proxy *&gt;
-          Order deny,allow
-          Allow from all
-    &lt;/Proxy&gt;
-
 <div class='apache_auth'>    &lt;Location <span class='ContextPath'>/r</span>/login/&gt;
       AuthType Basic
       AuthName "Gerrit Code Review"
       Require valid-user
       ...
     &lt;/Location&gt;</div>
-    ProxyPass <span class='ContextPath'>/r</span>/ http://...<span class='ContextPath'>/r</span>/
+
+    AllowEncodedSlashes NoDecode
+    RewriteEngine On
+    RewriteRule ^<span class='ContextPath'>/r</span>/(.*) http://...<span class='ContextPath'>/r</span>/$1 [NE,P]
 &lt;/VirtualHost&gt;
     </pre>
   </body>
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 72b589f..433be20 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
@@ -1,4 +1,4 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
 <html>
   <head>
     <title>Gerrit Code Review</title>
diff --git a/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/auth/ldap/LoginForm.html b/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/auth/ldap/LoginForm.html
new file mode 100644
index 0000000..57bc7f4
--- /dev/null
+++ b/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/auth/ldap/LoginForm.html
@@ -0,0 +1,83 @@
+<html>
+  <head>
+    <title>Gerrit Code Review - Sign In</title>
+    <style type="text/css">
+      #error_message {
+        padding: 5px;
+        margin: 2em;
+        width: 20em;
+        background-color: rgb(255, 255, 116);
+        font-weight: bold;
+      }
+      #cancel_link {
+        margin-left: 45px;
+      }
+    </style>
+    <style id="gerrit_sitecss" type="text/css"></style>
+  </head>
+  <body>
+    <div id="gerrit_topmenu" style="height:45px;" class="gerritTopMenu"></div>
+    <div id="gerrit_header"></div>
+    <div id="gerrit_body" class="gerritBody">
+      <h1>Sign In to Gerrit Code Review at <span id="hostName">example.com</span></h1>
+      <div id="error_message">Invalid username or password.</div>
+      <form method="POST" action="#" id="login_form">
+        <table style="border: 0;">
+        <tr>
+          <th>Username</th>
+          <td><input name="username" id="f_user"
+                     type="text"
+                     size="25"
+                     tabindex="1" /></td>
+        </tr>
+        <tr>
+          <th>Password</th>
+          <td><input name="password" id="f_pass"
+                     type="password"
+                     size="25"
+                     tabindex="2" /></td>
+        </tr>
+        <tr>
+          <td></td>
+          <td>
+            <input name="rememberme" id="f_remember"
+                   type="checkbox"
+                   value="1"
+                   tabindex="3" />
+            <label for="f_remember">Remember me</label>
+          </td>
+        </tr>
+        <tr>
+          <td></td>
+          <td>
+            <input type="submit" value="Sign In" tabindex="4"/>
+            <a href="../" id="cancel_link">Cancel</a>
+          </td>
+        </tr>
+        </table>
+      </form>
+      <div style="clear: both; margin-top: 15px; padding-top: 2px; margin-bottom: 15px;">
+        <div id="gerrit_footer"></div>
+      </div>
+    </div>
+
+    <script type="text/javascript">
+      var login_form = document.getElementById('login_form');
+      var f_user = document.getElementById('f_user');
+      var f_pass = document.getElementById('f_pass');
+      f_user.onkeydown = function(e) {
+        if (e.keyCode == 13) {
+          f_pass.focus();
+          return false;
+        }
+      }
+      f_pass.onkeydown = function(e) {
+        if (e.keyCode == 13) {
+          login_form.submit();
+          return false;
+        }
+      }
+      f_user.focus();
+    </script>
+  </body>
+</html>
diff --git a/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/HostPage.html b/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/HostPage.html
index ff09f50..907414f 100644
--- a/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/HostPage.html
+++ b/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/HostPage.html
@@ -2,10 +2,10 @@
   <head>
     <title>Gerrit Code Review</title>
     <meta name="gwt:property" content="locale=en_US" />
-    <script id="gerrit_gwtdevmode">
+    <script id="gwtdevmode">
       (function () {
         var pn = 'gwt.codesvr';
-        var cn = 'gerrit.' + pn;
+        var cn = 'gerrit_ui.' + pn;
 
         var p_start = window.location.search.indexOf(pn + '=');
         if (p_start != -1) {
@@ -32,7 +32,7 @@
     </script>
     <script id="gerrit_hostpagedata"></script>
     <style  id="gerrit_sitecss" type="text/css"></style>
-    <link rel="icon" type="image/gif" href="favicon.ico" />
+    <link rel="shortcut icon" type="image/x-icon" href="favicon.ico" />
   </head>
   <body>
     <div id="gerrit_topmenu"></div>
diff --git a/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/LegacyGerrit.html b/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/LegacyGerrit.html
index 5050bf2..6639a5b 100644
--- a/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/LegacyGerrit.html
+++ b/gerrit-httpd/src/main/resources/com/google/gerrit/httpd/raw/LegacyGerrit.html
@@ -1,4 +1,4 @@
-<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/REC-html40/strict.dtd">
+<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01//EN" "http://www.w3.org/TR/html4/strict.dtd">
 <html>
   <head>
     <title>Gerrit Code Review</title>
diff --git a/gerrit-httpd/src/test/java/com/google/gerrit/httpd/restapi/ParameterParserTest.java b/gerrit-httpd/src/test/java/com/google/gerrit/httpd/restapi/ParameterParserTest.java
new file mode 100644
index 0000000..ffbc7f3
--- /dev/null
+++ b/gerrit-httpd/src/test/java/com/google/gerrit/httpd/restapi/ParameterParserTest.java
@@ -0,0 +1,49 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.httpd.restapi;
+
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gson.JsonArray;
+import com.google.gson.JsonObject;
+import com.google.gson.JsonPrimitive;
+
+import junit.framework.TestCase;
+
+public class ParameterParserTest extends TestCase {
+  public void testConvertFormToJson() throws BadRequestException {
+    JsonObject obj = ParameterParser.formToJson(
+        ImmutableMap.of(
+            "message", new String[]{"this.is.text"},
+            "labels.Verified", new String[]{"-1"},
+            "labels.Code-Review", new String[]{"2"},
+            "a_list", new String[]{"a", "b"}),
+        ImmutableSet.of("q"));
+
+    JsonObject labels = new JsonObject();
+    labels.addProperty("Verified", "-1");
+    labels.addProperty("Code-Review", "2");
+    JsonArray list = new JsonArray();
+    list.add(new JsonPrimitive("a"));
+    list.add(new JsonPrimitive("b"));
+    JsonObject exp = new JsonObject();
+    exp.addProperty("message", "this.is.text");
+    exp.add("labels", labels);
+    exp.add("a_list", list);
+
+    assertEquals(exp, obj);
+  }
+}
diff --git a/gerrit-launcher/pom.xml b/gerrit-launcher/pom.xml
index e700351..4f33ce8 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.5-SNAPSHOT</version>
+    <version>2.6-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 9cca559..d49c6c7 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
@@ -206,27 +206,10 @@
           final ZipEntry ze = e.nextElement();
           if (ze.isDirectory()) {
             continue;
-          }
-
-          if (ze.getName().startsWith("WEB-INF/lib/")) {
-            String name = ze.getName().substring("WEB-INF/lib/".length());
-            final File tmp = createTempFile(safeName(ze), ".jar");
-            final FileOutputStream out = new FileOutputStream(tmp);
-            try {
-              final InputStream in = zf.getInputStream(ze);
-              try {
-                final byte[] buf = new byte[4096];
-                int n;
-                while ((n = in.read(buf, 0, buf.length)) > 0) {
-                  out.write(buf, 0, n);
-                }
-              } finally {
-                in.close();
-              }
-            } finally {
-              out.close();
-            }
-            jars.put(name, tmp.toURI().toURL());
+          } else if (ze.getName().startsWith("WEB-INF/lib/")) {
+            extractJar(zf, ze, jars);
+          } else if (ze.getName().startsWith("WEB-INF/pgm-lib/")) {
+            extractJar(zf, ze, jars);
           }
         }
       } finally {
@@ -248,7 +231,7 @@
     move(jars, "javax.inject-1.jar", extapi);
     move(jars, "aopalliance-1.0.jar", extapi);
     move(jars, "guice-servlet-", extapi);
-    move(jars, "servlet-api-", extapi);
+    move(jars, "tomcat-servlet-api-", extapi);
 
     ClassLoader parent = ClassLoader.getSystemClassLoader();
     if (!extapi.isEmpty()) {
@@ -261,6 +244,31 @@
         parent);
   }
 
+  private static void extractJar(ZipFile zf, ZipEntry ze,
+      SortedMap<String, URL> jars) throws IOException {
+    File tmp = createTempFile(safeName(ze), ".jar");
+    FileOutputStream out = new FileOutputStream(tmp);
+    try {
+      InputStream in = zf.getInputStream(ze);
+      try {
+        byte[] buf = new byte[4096];
+        int n;
+        while ((n = in.read(buf, 0, buf.length)) > 0) {
+          out.write(buf, 0, n);
+        }
+      } finally {
+        in.close();
+      }
+    } finally {
+      out.close();
+    }
+
+    String name = ze.getName();
+    jars.put(
+        name.substring(name.lastIndexOf('/'), name.length()),
+        tmp.toURI().toURL());
+  }
+
   private static void move(SortedMap<String, URL> jars,
       String prefix,
       List<URL> extapi) {
diff --git a/gerrit-main/pom.xml b/gerrit-main/pom.xml
index bb2d763..d174af1 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.5-SNAPSHOT</version>
+    <version>2.6-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-main</artifactId>
diff --git a/gerrit-openid/.settings/org.eclipse.core.resources.prefs b/gerrit-openid/.settings/org.eclipse.core.resources.prefs
index f9fe345..839d647 100644
--- a/gerrit-openid/.settings/org.eclipse.core.resources.prefs
+++ b/gerrit-openid/.settings/org.eclipse.core.resources.prefs
@@ -1,4 +1,5 @@
 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-openid/pom.xml b/gerrit-openid/pom.xml
index 01e2e3e..dbcd6a8 100644
--- a/gerrit-openid/pom.xml
+++ b/gerrit-openid/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.5-SNAPSHOT</version>
+    <version>2.6-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-openid</artifactId>
diff --git a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/DiscoveryResult.java b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/DiscoveryResult.java
new file mode 100644
index 0000000..711f29a
--- /dev/null
+++ b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/DiscoveryResult.java
@@ -0,0 +1,47 @@
+// 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.auth.openid;
+
+import java.util.Map;
+
+final class DiscoveryResult {
+  static enum Status {
+    /** Provider was discovered and {@code providerUrl} is valid. */
+    VALID,
+
+    /** Identifier isn't for an OpenID provider. */
+    NO_PROVIDER,
+
+    /** The provider was discovered, but something else failed. */
+    ERROR;
+  }
+
+  Status status;
+  String providerUrl;
+  Map<String, String> providerArgs;
+
+  DiscoveryResult() {
+  }
+
+  DiscoveryResult(String redirect, Map<String, String> args) {
+    status = Status.VALID;
+    providerUrl = redirect;
+    providerArgs = args;
+  }
+
+  DiscoveryResult(Status s) {
+    status = s;
+  }
+}
diff --git a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/LoginForm.java b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/LoginForm.java
new file mode 100644
index 0000000..b8d31ee
--- /dev/null
+++ b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/LoginForm.java
@@ -0,0 +1,310 @@
+// 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.auth.openid;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Sets;
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.common.auth.openid.OpenIdUrls;
+import com.google.gerrit.extensions.restapi.Url;
+import com.google.gerrit.httpd.HtmlDomUtil;
+import com.google.gerrit.httpd.template.SiteHeaderFooter;
+import com.google.gerrit.reviewdb.client.AuthType;
+import com.google.gerrit.server.config.AuthConfig;
+import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.lib.Config;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.w3c.dom.Document;
+import org.w3c.dom.Element;
+
+import java.io.IOException;
+import java.util.Map;
+import java.util.Set;
+
+import javax.annotation.Nullable;
+import javax.servlet.ServletOutputStream;
+import javax.servlet.http.Cookie;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+/** Handles OpenID based login flow. */
+@SuppressWarnings("serial")
+@Singleton
+class LoginForm extends HttpServlet {
+  private static final Logger log = LoggerFactory.getLogger(LoginForm.class);
+  private static final ImmutableMap<String, String> ALL_PROVIDERS = ImmutableMap.of(
+      "google", OpenIdUrls.URL_GOOGLE,
+      "yahoo", OpenIdUrls.URL_YAHOO);
+
+  private final ImmutableSet<String> suggestProviders;
+  private final Provider<String> urlProvider;
+  private final OpenIdServiceImpl impl;
+  private final int maxRedirectUrlLength;
+  private final String ssoUrl;
+  private final SiteHeaderFooter header;
+
+  @Inject
+  LoginForm(
+      @CanonicalWebUrl @Nullable Provider<String> urlProvider,
+      @GerritServerConfig Config config,
+      AuthConfig authConfig,
+      OpenIdServiceImpl impl,
+      SiteHeaderFooter header) {
+    this.urlProvider = urlProvider;
+    this.impl = impl;
+    this.header = header;
+    this.maxRedirectUrlLength = config.getInt(
+        "openid", "maxRedirectUrlLength",
+        10);
+
+    if (Strings.isNullOrEmpty(urlProvider.get())) {
+      log.error("gerrit.canonicalWebUrl must be set in gerrit.config");
+    }
+
+    if (authConfig.getAuthType() == AuthType.OPENID_SSO) {
+      suggestProviders = ImmutableSet.of();
+      ssoUrl = authConfig.getOpenIdSsoUrl();
+    } else {
+      Set<String> providers = Sets.newHashSet();
+      for (Map.Entry<String, String> e : ALL_PROVIDERS.entrySet()) {
+        if (impl.isAllowedOpenID(e.getValue())) {
+          providers.add(e.getKey());
+        }
+      }
+      suggestProviders = ImmutableSet.copyOf(providers);
+      ssoUrl = null;
+    }
+  }
+
+  @Override
+  protected void doGet(HttpServletRequest req, HttpServletResponse res)
+      throws IOException {
+    if (ssoUrl != null) {
+      String token = getToken(req);
+      SignInMode mode;
+      if (PageLinks.REGISTER.equals(token)) {
+        mode = SignInMode.REGISTER;
+        token = PageLinks.MINE;
+      } else {
+        mode = SignInMode.SIGN_IN;
+      }
+      discover(req, res, false, ssoUrl, false, token, mode);
+    } else {
+      String id = Strings.nullToEmpty(req.getParameter("id")).trim();
+      if (!id.isEmpty()) {
+        doPost(req, res);
+      } else {
+        boolean link = req.getParameter("link") != null;
+        sendForm(req, res, link, null);
+      }
+    }
+  }
+
+  @Override
+  protected void doPost(HttpServletRequest req, HttpServletResponse res)
+      throws IOException {
+    boolean link = req.getParameter("link") != null;
+    String id = Strings.nullToEmpty(req.getParameter("id")).trim();
+    if (id.isEmpty()) {
+      sendForm(req, res, link, null);
+      return;
+    }
+    if (!id.startsWith("http://") && !id.startsWith("https://")) {
+      id = "http://" + id;
+    }
+    if ((ssoUrl != null && !ssoUrl.equals(id)) || !impl.isAllowedOpenID(id)) {
+      sendForm(req, res, link, "OpenID provider not permitted by site policy.");
+      return;
+    }
+
+    boolean remember = "1".equals(req.getParameter("rememberme"));
+    String token = getToken(req);
+    SignInMode mode;
+    if (link) {
+      mode = SignInMode.LINK_IDENTIY;
+    } else if (PageLinks.REGISTER.equals(token)) {
+      mode = SignInMode.REGISTER;
+      token = PageLinks.MINE;
+    } else {
+      mode = SignInMode.SIGN_IN;
+    }
+
+    discover(req, res, link, id, remember, token, mode);
+  }
+
+  private void discover(HttpServletRequest req, HttpServletResponse res,
+      boolean link, String id, boolean remember, String token, SignInMode mode)
+      throws IOException {
+    if (ssoUrl != null) {
+      remember = false;
+    }
+
+    DiscoveryResult r = impl.discover(id, mode, remember, token);
+    switch (r.status) {
+      case VALID:
+        redirect(r, res);
+        break;
+
+      case NO_PROVIDER:
+        sendForm(req, res, link,
+            "Provider is not supported, or was incorrectly entered.");
+        break;
+
+      case ERROR:
+        sendForm(req, res, link, "Unable to connect with OpenID provider.");
+        break;
+    }
+  }
+
+  private void redirect(DiscoveryResult r, HttpServletResponse res)
+      throws IOException {
+    StringBuilder url = new StringBuilder();
+    url.append(r.providerUrl);
+    if (r.providerArgs != null && !r.providerArgs.isEmpty()) {
+      boolean first = true;
+      for(Map.Entry<String, String> arg : r.providerArgs.entrySet()) {
+        if (first) {
+          url.append('?');
+          first = false;
+        } else {
+          url.append('&');
+        }
+        url.append(Url.encode(arg.getKey()))
+           .append('=')
+           .append(Url.encode(arg.getValue()));
+      }
+    }
+    if (url.length() <= maxRedirectUrlLength) {
+      res.sendRedirect(url.toString());
+      return;
+    }
+
+    Document doc = HtmlDomUtil.parseFile(LoginForm.class, "RedirectForm.html");
+    Element form = HtmlDomUtil.find(doc, "redirect_form");
+    form.setAttribute("action", r.providerUrl);
+    if (r.providerArgs != null && !r.providerArgs.isEmpty()) {
+      for (Map.Entry<String, String> arg : r.providerArgs.entrySet()) {
+        Element in = doc.createElement("input");
+        in.setAttribute("type", "hidden");
+        in.setAttribute("name", arg.getKey());
+        in.setAttribute("value", arg.getValue());
+        form.appendChild(in);
+      }
+    }
+    sendHtml(res, doc);
+  }
+
+  private static String getToken(HttpServletRequest req) {
+    String token = req.getPathInfo();
+    if (token == null || token.isEmpty()) {
+      token = PageLinks.MINE;
+    } else if (!token.startsWith("/")) {
+      token = "/" + token;
+    }
+    return token;
+  }
+
+  private void sendForm(HttpServletRequest req, HttpServletResponse res,
+      boolean link, @Nullable String errorMessage) throws IOException {
+    String self = req.getRequestURI();
+    String cancel = Objects.firstNonNull(urlProvider.get(), "/");
+    String token = getToken(req);
+    if (!token.equals("/")) {
+      cancel += "#" + token;
+    }
+
+    Document doc = header.parse(LoginForm.class, "LoginForm.html");
+    HtmlDomUtil.find(doc, "hostName").setTextContent(req.getServerName());
+    HtmlDomUtil.find(doc, "login_form").setAttribute("action", self);
+    HtmlDomUtil.find(doc, "cancel_link").setAttribute("href", cancel);
+
+    if (!link || ssoUrl != null) {
+      Element input = HtmlDomUtil.find(doc, "f_link");
+      input.getParentNode().removeChild(input);
+    }
+
+    String last = getLastId(req);
+    if (last != null) {
+      HtmlDomUtil.find(doc, "f_openid").setAttribute("value", last);
+    }
+
+    Element emsg = HtmlDomUtil.find(doc, "error_message");
+    if (Strings.isNullOrEmpty(errorMessage)) {
+      emsg.getParentNode().removeChild(emsg);
+    } else {
+      emsg.setTextContent(errorMessage);
+    }
+
+    for (String name : ALL_PROVIDERS.keySet()) {
+      Element div = HtmlDomUtil.find(doc, "provider_" + name);
+      if (div == null) {
+        continue;
+      }
+      if (!suggestProviders.contains(name)) {
+        div.getParentNode().removeChild(div);
+        continue;
+      }
+      Element a = HtmlDomUtil.find(div, "id_" + name);
+      if (a == null) {
+        div.getParentNode().removeChild(div);
+        continue;
+      }
+      StringBuilder u = new StringBuilder();
+      u.append(self).append(a.getAttribute("href"));
+      if (link) {
+        u.append("&link");
+      }
+      a.setAttribute("href", u.toString());
+    }
+    sendHtml(res, doc);
+  }
+
+  private void sendHtml(HttpServletResponse res, Document doc)
+      throws IOException {
+    byte[] bin = HtmlDomUtil.toUTF8(doc);
+    res.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
+    res.setContentType("text/html");
+    res.setCharacterEncoding("UTF-8");
+    res.setContentLength(bin.length);
+    ServletOutputStream out = res.getOutputStream();
+    try {
+      out.write(bin);
+    } finally {
+      out.close();
+    }
+  }
+
+  private static String getLastId(HttpServletRequest req) {
+    Cookie[] cookies = req.getCookies();
+    if (cookies != null) {
+      for (Cookie c : cookies) {
+        if (OpenIdUrls.LASTID_COOKIE.equals(c.getName())) {
+          return c.getValue();
+        }
+      }
+    }
+    return null;
+  }
+}
diff --git a/gerrit-openid/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
index cfb767d..4cbb22f 100644
--- a/gerrit-openid/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
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.httpd.auth.openid;
 
+import com.google.gwtexpui.server.CacheHeaders;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -44,9 +45,7 @@
   public void doPost(final HttpServletRequest req, final HttpServletResponse rsp)
       throws IOException {
     try {
-      rsp.setHeader("Expires", "Fri, 01 Jan 1980 00:00:00 GMT");
-      rsp.setHeader("Pragma", "no-cache");
-      rsp.setHeader("Cache-Control", "no-cache, must-revalidate");
+      CacheHeaders.setNotCacheable(rsp);
       impl.doAuth(req, rsp);
     } catch (Exception e) {
       getServletContext().log("Unexpected error during authentication", e);
diff --git a/gerrit-openid/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
index a928cb8..c87a0cf 100644
--- a/gerrit-openid/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
@@ -14,22 +14,16 @@
 
 package com.google.gerrit.httpd.auth.openid;
 
-import com.google.gerrit.httpd.rpc.RpcServletModule;
 import com.google.inject.servlet.ServletModule;
 
-/** Servlets and RPC support related to OpenID authentication. */
+/** Servlets related to OpenID authentication. */
 public class OpenIdModule extends ServletModule {
   @Override
   protected void configureServlets() {
+    serve("/login", "/login/*").with(LoginForm.class);
     serve("/" + OpenIdServiceImpl.RETURN_URL).with(OpenIdLoginServlet.class);
     serve("/" + XrdsServlet.LOCATION).with(XrdsServlet.class);
     filter("/").through(XrdsFilter.class);
-
-    install(new RpcServletModule(RpcServletModule.PREFIX) {
-      @Override
-      protected void configureServlets() {
-        rpc(OpenIdServiceImpl.class);
-      }
-    });
+    bind(OpenIdServiceImpl.class);
   }
 }
diff --git a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java
index 09a5d10..52f3b03 100644
--- a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java
+++ b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/OpenIdServiceImpl.java
@@ -15,10 +15,6 @@
 package com.google.gerrit.httpd.auth.openid;
 
 import com.google.gerrit.common.PageLinks;
-import com.google.gerrit.common.auth.SignInMode;
-import com.google.gerrit.common.auth.openid.DiscoveryResult;
-import com.google.gerrit.common.auth.openid.OpenIdProviderPattern;
-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.client.Account;
@@ -26,12 +22,11 @@
 import com.google.gerrit.server.UrlEncoded;
 import com.google.gerrit.server.account.AccountException;
 import com.google.gerrit.server.account.AccountManager;
-import com.google.gerrit.server.account.AuthMethod;
+import com.google.gerrit.server.auth.openid.OpenIdProviderPattern;
 import com.google.gerrit.server.config.AuthConfig;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gwtjsonrpc.common.AsyncCallback;
 import com.google.gwtorm.client.KeyUtil;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -74,7 +69,7 @@
 import javax.servlet.http.HttpServletResponse;
 
 @Singleton
-class OpenIdServiceImpl implements OpenIdService {
+class OpenIdServiceImpl {
   private static final Logger log =
       LoggerFactory.getLogger(OpenIdServiceImpl.class);
 
@@ -102,6 +97,7 @@
   private final AccountManager accountManager;
   private final ConsumerManager manager;
   private final List<OpenIdProviderPattern> allowedOpenIDs;
+  private final List<String> openIdDomains;
 
   /** Maximum age, in seconds, before forcing re-authentication of account. */
   private final int papeMaxAuthAge;
@@ -143,24 +139,18 @@
     accountManager = am;
     manager = new ConsumerManager();
     allowedOpenIDs = ac.getAllowedOpenIDs();
+    openIdDomains = ac.getOpenIdDomains();
     papeMaxAuthAge = (int) ConfigUtil.getTimeUnit(config, //
         "auth", null, "maxOpenIdSessionAge", -1, TimeUnit.SECONDS);
   }
 
   @SuppressWarnings("unchecked")
-  public void discover(final String openidIdentifier, final SignInMode mode,
-      final boolean remember, final String returnToken,
-      final AsyncCallback<DiscoveryResult> cb) {
-    if (!isAllowedOpenID(openidIdentifier)) {
-      cb.onSuccess(new DiscoveryResult(DiscoveryResult.Status.NOT_ALLOWED));
-      return;
-    }
-
+  DiscoveryResult discover(final String openidIdentifier, final SignInMode mode,
+      final boolean remember, final String returnToken) {
     final State state;
     state = init(openidIdentifier, mode, remember, returnToken);
     if (state == null) {
-      cb.onSuccess(new DiscoveryResult(DiscoveryResult.Status.NO_PROVIDER));
-      return;
+      return new DiscoveryResult(DiscoveryResult.Status.NO_PROVIDER);
     }
 
     final AuthRequest aReq;
@@ -188,16 +178,15 @@
       }
     } catch (MessageException e) {
       log.error("Cannot create OpenID redirect for " + openidIdentifier, e);
-      cb.onSuccess(new DiscoveryResult(DiscoveryResult.Status.ERROR));
-      return;
+      return new DiscoveryResult(DiscoveryResult.Status.ERROR);
     } catch (ConsumerException e) {
       log.error("Cannot create OpenID redirect for " + openidIdentifier, e);
-      cb.onSuccess(new DiscoveryResult(DiscoveryResult.Status.ERROR));
-      return;
+      return new DiscoveryResult(DiscoveryResult.Status.ERROR);
     }
 
-    cb.onSuccess(new DiscoveryResult(aReq.getDestinationUrl(false), //
-        aReq.getParameterMap()));
+    return new DiscoveryResult(
+        aReq.getDestinationUrl(false),
+        aReq.getParameterMap());
   }
 
   private boolean requestRegistration(final AuthRequest aReq) {
@@ -208,7 +197,6 @@
       // registration information, in case the identity is new to us.
       //
       return true;
-
     }
 
     // We might already have this account on file. Look for it.
@@ -221,7 +209,7 @@
     }
   }
 
-  /** Called by {@link OpenIdLoginServlet} doGet, doPost */
+  /** Called by {@link OpenIdLoginForm} doGet, doPost */
   void doAuth(final HttpServletRequest req, final HttpServletResponse rsp)
       throws Exception {
     if (OMODE_CANCEL.equals(req.getParameter(OPENID_MODE))) {
@@ -356,6 +344,32 @@
       areq.setEmailAddress(fetchRsp.getAttributeValue("Email"));
     }
 
+    if (openIdDomains != null && openIdDomains.size() > 0) {
+      // Administrator limited email domains, which can be used for OpenID.
+      // Login process will only work if the passed email matches one
+      // of these domains.
+      //
+      final String email = areq.getEmailAddress();
+      int emailAtIndex = email.lastIndexOf("@");
+      if (emailAtIndex >= 0 && emailAtIndex < email.length() - 1) {
+        final String emailDomain = email.substring(emailAtIndex);
+
+        boolean match = false;
+        for (String domain : openIdDomains) {
+          if (emailDomain.equalsIgnoreCase(domain)) {
+            match = true;
+            break;
+          }
+        }
+
+        if (!match) {
+          log.error("Domain disallowed: " + emailDomain);
+          cancelWithError(req, rsp, "Domain disallowed");
+          return;
+        }
+      }
+    }
+
     if (claimedIdentifier != null) {
       // The user used a claimed identity which has delegated to the verified
       // identity we have in our AuthRequest above. We still should have a
@@ -409,7 +423,7 @@
           arsp = accountManager.authenticate(areq);
 
           final Cookie lastId = new Cookie(OpenIdUrls.LASTID_COOKIE, "");
-          lastId.setPath(req.getContextPath() + "/");
+          lastId.setPath(req.getContextPath() + "/login/");
           if (remember) {
             lastId.setValue(rediscoverIdentifier);
             lastId.setMaxAge(LASTID_AGE);
@@ -417,7 +431,7 @@
             lastId.setMaxAge(0);
           }
           rsp.addCookie(lastId);
-          webSession.get().login(arsp, AuthMethod.COOKIE, remember);
+          webSession.get().login(arsp, remember);
           if (arsp.isNew() && claimedIdentifier != null) {
             final com.google.gerrit.server.account.AuthRequest linkReq =
                 new com.google.gerrit.server.account.AuthRequest(
@@ -431,7 +445,7 @@
 
         case LINK_IDENTIY: {
           arsp = accountManager.link(identifiedUser.get().getAccountId(), areq);
-          webSession.get().login(arsp, AuthMethod.COOKIE, remember);
+          webSession.get().login(arsp, remember);
           callback(false, req, rsp);
           break;
         }
@@ -532,7 +546,7 @@
     return new State(discovered, retTo, contextUrl);
   }
 
-  private boolean isAllowedOpenID(final String id) {
+  boolean isAllowedOpenID(final String id) {
     for (final OpenIdProviderPattern pattern : allowedOpenIDs) {
       if (pattern.matches(id)) {
         return true;
diff --git a/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/SignInMode.java b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/SignInMode.java
new file mode 100644
index 0000000..b6a9857
--- /dev/null
+++ b/gerrit-openid/src/main/java/com/google/gerrit/httpd/auth/openid/SignInMode.java
@@ -0,0 +1,19 @@
+// 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.httpd.auth.openid;
+
+enum SignInMode {
+  SIGN_IN, LINK_IDENTIY, REGISTER;
+}
diff --git a/gerrit-openid/src/main/resources/com/google/gerrit/httpd/auth/openid/LoginForm.html b/gerrit-openid/src/main/resources/com/google/gerrit/httpd/auth/openid/LoginForm.html
new file mode 100644
index 0000000..f5734ffe
--- /dev/null
+++ b/gerrit-openid/src/main/resources/com/google/gerrit/httpd/auth/openid/LoginForm.html
@@ -0,0 +1,90 @@
+<html>
+  <head>
+    <title>Gerrit Code Review - Sign In</title>
+    <style>
+      #error_message {
+        padding: 5px;
+        margin-left: 5px;
+        margin-bottom: 5px;
+        width: 20em;
+        background-color: rgb(255, 255, 116);
+        font-weight: bold;
+      }
+      #cancel_link {
+        margin-left: 45px;
+      }
+      #logo_box {
+        padding-left: 160px;
+      }
+      #logo_img {
+        width: 200px;
+        height: 80px;
+        background: url('data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAMgAAABQCAYAAABcbTqwAAANbUlEQVR42uxdeXBV1Rm/URYpuNDRFjtuZcaOZTpd7D+O07pi65QqFMQEE7KzhF2kQEUr1gUkIBQiIKDUBYRgUBCCIHEBBLKR5GUlKwlZSAKJtvUPZzr8+r73u80dfHnv3iyveeF9v5lvQsi53zvv3O93z/mWc66hUCgUCoVCoVAoFAqFQqFQKBQKhUKhUCgUCoVCoVAoFAoFQaClAtgQDhSmw1AoFBbwxRZgzgggwgBcShCFwgM0FAGrHgaiDEqcWwoPKEEUChxIBmZ+H5gkxBBRgigUBqozgeX3AU8YQLRb4kWUIAqFgT1LgSlDSI54ESWIQmH5G7EGMFkIoQRRKC4B6l3A1EFArBJEoeiEIIXA1MFKEIVCCaJQKEEUCiWIQqEEUSiUIApFcOdBlCAKhS9cPOtyRJCLWs2rCAVcuHBhakNDA2pqalBWVYOqI3twccogIM4XQfizdv8mlFXXea6T60WPoVD0d5w7dw6VlZUoLCxEbm4ucnJykJWV5ZHMUwUoPrTTEUHKd69DZn5Rx7XZ2dkefS6Xy6O/qalJZxhF/0BjYyNKSko8BmwatBBDRP6vQ7LzC1HySaojglR8kIJsF3Wa0qFTyCKfIf9XVFSEs2fPdoksLS0tOe5ZSQmmCBza2tpGnz59mqTgE96LEJReIoi3eJGlrKwM58+fX274gcw+J0+elKVbjwmCtjogOxVIXw58uJSy9wXgxLtAQ7ESMBQhBiizhRimLSm8CcIlVncIYi8dfZL+WUSxZrm8vLyOGe7MmTPoNjFKPwXWjgeShrJk/09uecSUsQa3DCcOBJbfD2TvuuyJguNvA4nXAnNvAM7XAuVHgZnXUmZcDSQN85YZbll4G/Dyb4B3ZgJZO/v/OMkT2jRCRwbLJZHlgwhBSg/vckSQyg9fcxOkVD7La4ZyShTpryynSktLuezj9T0iCLbNYQRuglmyv2gksH4i8O4sYPtcYHMUsPSXQLzZJtItKROA9st3SYcvNgHhBpBwBdBaDc8DJNqgJIQBiVd6S3wYEGWO0TiDD5qFtwOHU/rfONXV1dHwnBHDy0eQ5Uxzc3Ob6Pq2OtdRmPfb3D34n78gxlxcXIxTp06RLM774UXonhAEKx8CHvfMENwNWbAPPttWnQRem0AjeMwtz7lJc6EWlyVBjmwRA+c2htYaoOwz3t/EAcCpD4DGEqC+0JKGYuBMLpC3Fzi4Clg3Hpj+PWCiQaKt/gPwTdtooz/gO09ff9LRTpY4sqTpUaKw6ONOr5folfTJ/Dyny68eEwSbnhBycNZIewaOr/v8dSBhEG/+S3eFDkFiDGDKQOBcORzpaCoDNkbIA4XjvOqh4B+r/Px8J0/rjjZVVVX4f5aaVFdXW58fQILgy7doAJPckrqg6zPP0TeA6DA+HdMWI5QIIve7a9uxnwciTZJ88GxwjlV7e/tIy/Dso0jl5eXoy1osiaZJXwJGkIU/5g17+e7uO/ZbJlPHtKuAxlIoQXwD68bJw4RO/oW64Bsrifb4NziSR9qJj9DXxYrinwSKIO6QLW9+TFiP6sOEFJg2hCR535pFRCfKj+DSZdkm4PVIYMUDQPKDwJo/AmlLgLo8Z31uqQIOrABSJgLJD4gervPTV3icaK/2/2pNhSsdKP7E6ldzBbB7CbB6DHWsHA1sjoX0LeAEqcuTKCBJcvDV4CKIZKrF+O3IIUbZ19W8sqSziW71nCDrHzOd7J/bt7fXRV/kmVEgaUr4XZ++Ax1kWfgTCRuTSDEGxynCjPQkDga2z/PbDzEozBgOjDfY7xiK+3Opd9YNwOcbqcMiKX2rGdeAUamNwJSh/Mwosw+RnutptItHARXHEBiCEG5SMsK19pEgIQgNTozH1rjECe/Lcvf6+npPX06cOCH96bLIdeK/OOrvkyNoaDvm95wgR9+kIU0ZALRUAs0V/K5L7wRy0oCYK2ioshzL2QWcLZCID/8m5DIjYlgzBp3qf3+RkIlGvPxe4NhWoDYPkALRY//gbBRu+lJ7n4dFkHR+7pzrgf3L2GbezUyAVp4Amko9swu2z5FQLsn75I1A21kEhCD8LvyuC24KHoLYPY3l71Jj1Zf7QVpbW1PFuCV0LLNAN0TIwUSiDXAmlwYRZfRKIsttrEIO6svdLWFfhkLn/wiYeyOQdL3fQ7vx2UYglgYqy59L/pa9E4g0jf8932TGjqekDclYkmHNXHEG+xLnlmX3AO318JEUZJuJdKIDRBAubSOpUx4mwZIE9OuQy89Q2jAF137e6IQrgZos9IrOWdfRwD9ZI0bIsYg2TNKkwcGTldcnXQ20WFFD97KNT9x1Y+11bHicy5fl99DAiw7JmLMP824EvmqAjRPN61+5D+bM2PsEcaVTR/wVXM71NYQAdkur2traUCIIw7O88b1WW4Uld9A/2PeSZNdlLLgsWvmgI/34pn2ke8YhGT56ASaRadwJgxwZJOqLgISBQFwYPMGD019wzCeZOm2AjBSSdMEt/HxZvvUyQYQUMnuLHvl+Rp+Ba3pb30Oy2KG25RZHNsMiSBF6ReezPzMJ8vKlBMlYB8c63p5Gh/nV39NAd86nQ736Yec6XrmXs4As2yTrH+t8pkTeXvosScMCR5DK4+xPTF+/CoN5BL/LKzPXEXoEyU0TQ6DfUJffK/1xO8Jcv3+cLASh7jjDY1RdcPZJqgU300DXjCHp9ix1ruO9uSys3D5PwsxiiCwg/Lf9xjSpckC0tL86UAShXxRrcJYr+7RvbUEcb39LLJlduH8ixAhSeVzWwDSGvL097Q9zC9MG05hObJMkmDjGlHrHMxQNNIYGKpEkLP2VLLHo7C8Z5ZY7/Iv4K3NvYLQqZRxQ6NYXS30OjTfgBJEqaEymDi5v+7akxDZ6JdGjECMIE2izruV6+6MXe04Q3nRWstaeEie7OwTh+jzezMrX5kqY2NTrUOIMVtTGDgY2hgcnQQ6s4LjPHg7xu4KdIAyLhhRBCKy4nw5x8gM9J8hbU4GJVmxf8gsWQQq7OYPUsVI4QnIbfwOaykSXvTQU8edXjTLW1BdMBEkZz3F/8S4EQ/Zcl1g+4PYVaHwJA+39EPvoEwmyNQFmZbMYEY0t/6Ou+iBWFGnV7+i071/Wnf4xURgdPATBV03ArOGcQdKeDv4ciPytoqIiNAkifsJMLrOk5L27eiR8igiWfUhY9ZKxCKeD3fUoFqNW2DabUay1j6JLIeyUOHgSfyWHg2oGcZOCYxUvUbXsvrcDOfQgMzPTX5JQChNDkCCEO2NMI54cBmTt6PoNr84Cpg/j7LF+ArzGIpL1Wd3Og2Sn0kCnDwXO1zjT8+dbgDEGNyxVHg8WgjBBmHgVx2pjuHVtf0gUCpFCjyAEnv81b1rSdUC+44gWy1WeupUz0LwRUk3rTZAYJumk9sn+6bqYumZ8J5O+8Fb2b/Nkex2SxY8wdXzd3MYtsgEiSKPjuj2W0iSZs/X8mzhWQbRz0K7UhMnC0CQIK28X3MYnd8IAIO0vQFs9/N/wDfIGX5NYw4HSDHiNxZRBMh40iOgwSF2T330X8QNpQCw27IBU6CLCrLpNXehbR8E+YOowJgl3LQIN/uOAEUQeEH511eQAh1YDL97Nvj8m5LiN5SXBdgKigzJ3VvKGHkGsPMay34qB8kbO+yGwNRE4vI57rwv2S2aahzcsvl0Mlu2eHgVUfolOxyJxgBgafYHZ11P32rHAyW1ShetVzctl2kR02r83Yq0981K5e/wdqyJYiiPfjJPEG8mx8iHvat6koTBs4d0eR7cCk5hQJUE+tUr1F41k3mXJTztEfpcSf8y8RkLe/E7jDV7z90eB5nIYwQhxxB2UuzOrHsKnu8vJG1h0O594Yz03lwbyhEmIR82S8zk/AHYtBv7JAyt8EIRyvpoHGTx3p+ig0Mioa5wY4BDODn4g+RruB/GhIzYM2BzNQxGsGcEqHbGFd3scE4KYM4Z1aIMI20V1KrzP04aQNG9NB0qYMQ9qFBQUONowJZGvUH/9AfL3AbufATZEcMfdygf59N8xnzPC1+dgMxad5kGQlcqzopJHczfg+nAJ4TquB0NrFXBoDfB6FGeSZFPHh38FqjNtdhR6w6492us5exYd5O/uchUPiVxuKfQhBelA+TEuU/sX6LA7cdolfyL71/X9IDZwRhA9ibE/nchuOuaODoaTDUhKECVISJKEyy37Pd4S4ZLdekoQJUjogJW+QgBHx4ya7STrzuLGHr1hKl0JougHsE4PEXFCFLblsUDizEuCseMFOP+pzfdPkBgzGuQKAYLU5UskjFKXrwTp7687kHN2vY4htSeL9b4QVzHKMt73fXh1FAki8XzJ8BqXOSSCIxW+Iu5/K0H6P3g2rnl2VteP/Mxzdfb6A6vU4tlfAK59aiiK/g85rV2y6vQ9rFmlKy/QEeE5UVdJQk2Jobg8IdErc0+J17s8fBIkyiTHK/fLoQFKDkUogCekiGMuuxStdxaSNJmyxDq4AxeFHLNHABmvKTEUuhST87Qk9FtUVoHTGWm4uCVWjt1UcigUCoVCoVAoFAqFQqFQKBQKhUKhUCgUCsV/24MDEgAAAABB/1+3I1ABAGAilVZ2IKvvzEMAAAAASUVORK5CYII=') no-repeat 0px 0px;
+      }
+      #f_openid {
+        padding-left: 25px;
+        border: 1px solid #999;
+        background: #fff url('data:image/gif;base64,R0lGODlhEAAQAMQAAO3t7eHh4srKyvz8/P5pDP9rENLS0v/28P/17tXV1dHEvPDw8M3Nzfn5+d3d3f5jA97Syvnv6MfLzcfHx/1mCPx4Kc/S1Pf189C+tP+xgv/k1N3OxfHy9NLV1/39/f///yH5BAAAAAAALAAAAAAQABAAAAVq4CeOZGme6KhlSDoexdO6H0IUR+otwUYRkMDCUwIYJhLFTyGZJACAwQcgEAQ4kVuEE2AIGAOPQQAQwXCfS8KQGAwMjIYIUSi03B7iJ+AcnmclHg4TAh0QDzIpCw4WGBUZeikDFzk0lpcjIQA7') no-repeat scroll 5px 50%
+      }
+    </style>
+    <style id="gerrit_sitecss" type="text/css"></style>
+  </head>
+  <body>
+    <div id="gerrit_topmenu" style="height:45px;" class="gerritTopMenu"></div>
+    <div id="gerrit_header"></div>
+    <div id="gerrit_body" class="gerritBody">
+      <h1>Sign In to Gerrit Code Review at <span id="hostName">example.com</span></h1>
+      <form method="POST" action="#" id="login_form">
+        <input type="hidden" name="link" id="f_link" value="1" />
+        <div id="logo_box"><div id="logo_img"></div></div>
+        <div id="error_message">Invalid OpenID identifier.</div>
+        <div>
+          <input type="text"
+                 name="id"
+                 id="f_openid"
+                 size="60"
+                 tabindex="1" />
+        </div>
+        <div>
+          <input name="rememberme" id="f_remember"
+                 type="checkbox"
+                 value="1"
+                 tabindex="2" />
+          <label for="f_remember">Remember me</label>
+        </div>
+        <div style="margin-bottom: 25px;">
+          <input type="submit" value="Sign In" id="f_submit" tabindex="3" />
+          <a href="../" id="cancel_link">Cancel</a>
+        </div>
+
+        <div id="provider_google">
+          <img height="16" width="16" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAYAAAAf8/9hAAABsUlEQVR42rWSy0sbYRRH88fYhW5KQXThIkUQN6ISJFBBW6FFBN2UqlC78LVKFlHIoqWGakF8gUZ8gajVzKSKZpQmAWtK0pBmhkxmSgPuiuaYmRKhSEwU/ODyDZfzO8ydOxbLQx1NB0H4y8FBlmMpy9LyOavrKUoKB4IXvBsIIopRPJ441dY/2Fr8fAtFSxO0dkRy4eA1PDp+TkVlmGhMKS5IJOFpbYgj6fQalhV4UhVm0esvLojELimv1JiYOPwPflS2gM93WNoIXZ1nPC4/IRDQzYDTqTA0PM2dtuCZ8vGmV6SvP5srL/da5c+4TnNLFlvTMYmEVrpkazuM2x1naOQ7Dc1fqLOmaayXkaSz4hLHewm7fY+Z2R0iP+JmYGpym/q6DDb76u0CwR+g9+0FHz4u3wAdLh9VNWu5UX4VlqysfaXRDoODNz+aw7nDs9ZPpNPpwgJV1WhvF6m1/qb79T6fpwXm5kVGRnd5/mIdQSzwH2iahq7/27nxiq5xLy9fbdDWsUtPzyausRVC4VMMzmAMNv9syTdkWTZLVVUymYx5J5NJs5dKpVAUxaw8Z/SM7BV+fz9Uc4yYRAAAAABJRU5ErkJggg==" />
+          <a href="?id=https://www.google.com/accounts/o8/id" id="id_google">Sign in with a Google Account</a>
+        </div>
+        <div id="provider_yahoo">
+          <img height="16" width="16" src="data:image/gif;base64,R0lGODlhEAAQAPECAAAAAP8AAP///8zMzCH5BAEAAAMALAAAAAAQABAAAAIqnI+py30BY3AgAjCkfJDjiIAQlgUkNxqWkqrm0honKk7KhZOzw/f+fygAADs=" />
+          <a href="?id=https://me.yahoo.com" id="id_yahoo">Sign in with a Yahoo! ID</a>
+        </div>
+
+        <div style="margin-top: 25px;">
+          <h2>What is OpenID?</h2>
+          <p>OpenID provides secure single-sign-on, without revealing your passwords to this website.</p>
+          <p>There are many OpenID providers available.  You may already be member of one!</p>
+          <p><a href="http://openid.net/get/" target="_blank">Get OpenID</a></p>
+        </div>
+      </form>
+    </div>
+    <div style="clear: both; margin-top: 15px; padding-top: 2px; margin-bottom: 15px;">
+      <div id="gerrit_footer"></div>
+    </div>
+
+    <script type="text/javascript">
+      var f_openid = document.getElementById('f_openid');
+      var f_submit = document.getElementById('f_submit');
+      if (f_openid.value == '')
+        f_openid.focus();
+      else
+        f_submit.focus();
+    </script>
+  </body>
+</html>
diff --git a/gerrit-openid/src/main/resources/com/google/gerrit/httpd/auth/openid/RedirectForm.html b/gerrit-openid/src/main/resources/com/google/gerrit/httpd/auth/openid/RedirectForm.html
new file mode 100644
index 0000000..9d34ef6
--- /dev/null
+++ b/gerrit-openid/src/main/resources/com/google/gerrit/httpd/auth/openid/RedirectForm.html
@@ -0,0 +1,16 @@
+<html>
+  <head>
+    <title>Gerrit Code Review - Redirecting ...</title>
+  </head>
+  <body>
+    <div>Redirecting ...</div>
+	<form method="POST" action="#" id="redirect_form">
+	  <input type="submit" value="Continue" />
+    </form>
+    <script type="text/javascript" language="javascript">
+      var r = document.getElementById('redirect_form');
+      r.style.display = 'none';
+      r.submit();
+    </script>
+  </body>
+</html>
diff --git a/gerrit-package-plugins/.gitignore b/gerrit-package-plugins/.gitignore
deleted file mode 100644
index c96b05c..0000000
--- a/gerrit-package-plugins/.gitignore
+++ /dev/null
@@ -1,6 +0,0 @@
-/target
-/.classpath
-/.project
-/.settings/org.maven.ide.eclipse.prefs
-/.settings/org.eclipse.m2e.core.prefs
-/gerrit-package-plugins.iml
diff --git a/gerrit-package-plugins/pom.xml b/gerrit-package-plugins/pom.xml
deleted file mode 100644
index c072719..0000000
--- a/gerrit-package-plugins/pom.xml
+++ /dev/null
@@ -1,90 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!--
-Copyright (C) 2012 The Android Open Source Project
-
-Licensed under the Apache License, Version 2.0 (the "License");
-you may not use this file except in compliance with the License.
-You may obtain a copy of the License at
-
-http://www.apache.org/licenses/LICENSE-2.0
-
-Unless required by applicable law or agreed to in writing, software
-distributed under the License is distributed on an "AS IS" BASIS,
-WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-See the License for the specific language governing permissions and
-limitations under the License.
--->
-<project xmlns="http://maven.apache.org/POM/4.0.0"
-        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
-        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
-  <modelVersion>4.0.0</modelVersion>
-
-  <groupId>com.google.gerrit</groupId>
-  <artifactId>gerrit-package-plugins</artifactId>
-  <packaging>war</packaging>
-  <version>2.5-SNAPSHOT</version>
-
-  <name>Gerrit Code Review - Package Plugins</name>
-  <url>http://code.google.com/p/gerrit/</url>
-
-  <properties>
-    <project.build.sourceEncoding>
-      UTF-8
-    </project.build.sourceEncoding>
-  </properties>
-
-  <dependencies>
-    <dependency>
-      <groupId>com.google.gerrit</groupId>
-      <artifactId>gerrit-war</artifactId>
-      <version>${project.version}</version>
-      <type>war</type>
-    </dependency>
-    <dependency>
-      <groupId>com.googlesource.gerrit.plugins.replication</groupId>
-      <artifactId>replication</artifactId>
-      <version>1.0</version>
-      <scope>provided</scope>
-    </dependency>
-  </dependencies>
-
-  <build>
-    <plugins>
-      <plugin>
-        <groupId>org.apache.maven.plugins</groupId>
-        <artifactId>maven-dependency-plugin</artifactId>
-        <version>2.1</version>
-        <executions>
-          <execution>
-            <phase>process-resources</phase>
-            <goals>
-              <goal>copy-dependencies</goal>
-            </goals>
-            <configuration>
-              <includeTypes>jar</includeTypes>
-              <stripVersion>true</stripVersion>
-              <outputDirectory>${project.build.directory}/${project.build.finalName}/WEB-INF/plugins</outputDirectory>
-            </configuration>
-          </execution>
-        </executions>
-      </plugin>
-
-      <plugin>
-        <groupId>org.apache.maven.plugins</groupId>
-        <artifactId>maven-war-plugin</artifactId>
-        <version>2.1.1</version>
-        <configuration>
-          <warName>gerrit-full-${project.version}</warName>
-          <archive>
-            <addMavenDescriptor>false</addMavenDescriptor>
-            <manifestEntries>
-              <Main-Class>Main</Main-Class>
-              <Implementation-Title>Gerrit Code Review</Implementation-Title>
-              <Implementation-Version>${project.version}</Implementation-Version>
-            </manifestEntries>
-          </archive>
-        </configuration>
-      </plugin>
-    </plugins>
-  </build>
-</project>
diff --git a/gerrit-patch-commonsnet/pom.xml b/gerrit-patch-commonsnet/pom.xml
index f1a8b3e..78ca61c 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.5-SNAPSHOT</version>
+    <version>2.6-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-patch-commonsnet</artifactId>
diff --git a/gerrit-patch-jgit/pom.xml b/gerrit-patch-jgit/pom.xml
index 65223fb..7e50af8 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.5-SNAPSHOT</version>
+    <version>2.6-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-patch-jgit</artifactId>
diff --git a/gerrit-patch-jgit/src/main/java/org/eclipse/jgit/internal/storage/file/WindowCacheStatAccessor.java b/gerrit-patch-jgit/src/main/java/org/eclipse/jgit/internal/storage/file/WindowCacheStatAccessor.java
new file mode 100644
index 0000000..f241daa
--- /dev/null
+++ b/gerrit-patch-jgit/src/main/java/org/eclipse/jgit/internal/storage/file/WindowCacheStatAccessor.java
@@ -0,0 +1,33 @@
+// 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 org.eclipse.jgit.internal.storage.file;
+
+import org.eclipse.jgit.internal.storage.file.WindowCache;
+
+// Hack to obtain visibility to package level methods only.
+// These aren't yet part of the public JGit API.
+
+public class WindowCacheStatAccessor {
+  public static int getOpenFiles() {
+    return WindowCache.getInstance().getOpenFiles();
+  }
+
+  public static long getOpenBytes() {
+    return WindowCache.getInstance().getOpenBytes();
+  }
+
+  private WindowCacheStatAccessor() {
+  }
+}
diff --git a/gerrit-patch-jgit/src/main/java/org/eclipse/jgit/storage/file/WindowCacheStatAccessor.java b/gerrit-patch-jgit/src/main/java/org/eclipse/jgit/storage/file/WindowCacheStatAccessor.java
deleted file mode 100644
index 7e29536..0000000
--- a/gerrit-patch-jgit/src/main/java/org/eclipse/jgit/storage/file/WindowCacheStatAccessor.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 org.eclipse.jgit.storage.file;
-
-// Hack to obtain visibility to package level methods only.
-// These aren't yet part of the public JGit API.
-
-public class WindowCacheStatAccessor {
-  public static int getOpenFiles() {
-    return WindowCache.getInstance().getOpenFiles();
-  }
-
-  public static long getOpenBytes() {
-    return WindowCache.getInstance().getOpenBytes();
-  }
-
-  private WindowCacheStatAccessor() {
-  }
-}
diff --git a/gerrit-pgm/pom.xml b/gerrit-pgm/pom.xml
index a015219..f73a2b0 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.5-SNAPSHOT</version>
+    <version>2.6-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-pgm</artifactId>
@@ -65,6 +65,12 @@
       <groupId>com.google.gerrit</groupId>
       <artifactId>gerrit-server</artifactId>
       <version>${project.version}</version>
+      <exclusions>
+        <exclusion>
+          <groupId>org.apache.tomcat</groupId>
+          <artifactId>servlet-api</artifactId>
+        </exclusion>
+      </exclusions>
     </dependency>
 
     <dependency>
@@ -92,7 +98,7 @@
 
     <dependency>
       <groupId>org.apache.tomcat</groupId>
-      <artifactId>servlet-api</artifactId>
+      <artifactId>tomcat-servlet-api</artifactId>
     </dependency>
 
     <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 a826c88..ca98a84 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
@@ -19,10 +19,10 @@
 import com.google.gerrit.common.ChangeHookRunner;
 import com.google.gerrit.httpd.AllRequestFilter;
 import com.google.gerrit.httpd.CacheBasedWebSession;
+import com.google.gerrit.httpd.GerritUiOptions;
 import com.google.gerrit.httpd.GitOverHttpModule;
 import com.google.gerrit.httpd.HttpCanonicalWebUrlProvider;
 import com.google.gerrit.httpd.RequestContextFilter;
-import com.google.gerrit.httpd.SignedTokenRestTokenVerifier;
 import com.google.gerrit.httpd.WebModule;
 import com.google.gerrit.httpd.WebSshGlueModule;
 import com.google.gerrit.httpd.auth.openid.OpenIdModule;
@@ -33,6 +33,7 @@
 import com.google.gerrit.pgm.http.jetty.JettyModule;
 import com.google.gerrit.pgm.http.jetty.ProjectQoSFilter;
 import com.google.gerrit.pgm.util.ErrorLogFile;
+import com.google.gerrit.pgm.util.GarbageCollectionLogFile;
 import com.google.gerrit.pgm.util.LogFileCompressor;
 import com.google.gerrit.pgm.util.RuntimeShutdown;
 import com.google.gerrit.pgm.util.SiteProgram;
@@ -51,12 +52,15 @@
 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.patch.IntraLineWorkerPool;
 import com.google.gerrit.server.plugins.PluginGuiceEnvironment;
 import com.google.gerrit.server.plugins.PluginModule;
 import com.google.gerrit.server.schema.SchemaUpdater;
 import com.google.gerrit.server.schema.SchemaVersionCheck;
 import com.google.gerrit.server.schema.UpdateUI;
+import com.google.gerrit.server.ssh.NoSshKeyCache;
 import com.google.gerrit.server.ssh.NoSshModule;
+import com.google.gerrit.sshd.SshKeyCacheImpl;
 import com.google.gerrit.sshd.SshModule;
 import com.google.gerrit.sshd.commands.MasterCommandModule;
 import com.google.gerrit.sshd.commands.SlaveCommandModule;
@@ -65,6 +69,7 @@
 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.Inject;
 import com.google.inject.Injector;
 import com.google.inject.Module;
@@ -113,6 +118,9 @@
   @Option(name = "--run-id", usage = "Cookie to store in $site_path/logs/gerrit.run")
   private String runId;
 
+  @Option(name = "--headless", usage = "Don't start the UI frontend")
+  private boolean headless;
+
   private final LifecycleManager manager = new LifecycleManager();
   private Injector dbInjector;
   private Injector cfgInjector;
@@ -122,6 +130,15 @@
   private Injector httpdInjector;
   private File runFile;
 
+  private Runnable serverStarted;
+
+  public Daemon() {
+  }
+
+  public Daemon(Runnable serverStarted) {
+    this.serverStarted = serverStarted;
+  }
+
   @Override
   public int run() throws Exception {
     mustHaveValidSite();
@@ -147,6 +164,7 @@
       throw die("Cannot combine --slave and --enable-httpd");
     }
 
+    manager.add(GarbageCollectionLogFile.start(getSitePath()));
     if (consoleLog) {
     } else {
       manager.add(ErrorLogFile.start(getSitePath()));
@@ -198,6 +216,10 @@
         }
       }
 
+      if (serverStarted != null) {
+        serverStarted.run();
+      }
+
       RuntimeShutdown.waitFor();
       return 0;
     } catch (Throwable err) {
@@ -291,11 +313,11 @@
     modules.add(new WorkQueue.Module());
     modules.add(new ChangeHookRunner.Module());
     modules.add(new ReceiveCommitsExecutorModule());
+    modules.add(new IntraLineWorkerPool.Module());
     modules.add(cfgInjector.getInstance(GerritGlobalModule.class));
     modules.add(new DefaultCacheFactory.Module());
     modules.add(new SmtpEmailSender.Module());
     modules.add(new SignedTokenEmailTokenVerifier.Module());
-    modules.add(new SignedTokenRestTokenVerifier.Module());
     modules.add(new PluginModule());
     if (httpd) {
       modules.add(new CanonicalWebUrlModule() {
@@ -312,9 +334,20 @@
         }
       });
     }
+    if (sshd) {
+      modules.add(SshKeyCacheImpl.module());
+    } else {
+      modules.add(NoSshKeyCache.module());
+    }
     if (!slave) {
       modules.add(new MasterNodeStartup());
     }
+    modules.add(new AbstractModule() {
+      @Override
+      protected void configure() {
+        bind(GerritUiOptions.class).toInstance(new GerritUiOptions(headless));
+      }
+    });
     return cfgInjector.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
deleted file mode 100644
index 525360d..0000000
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ExportReviewNotes.java
+++ /dev/null
@@ -1,244 +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.pgm;
-
-import static com.google.gerrit.server.schema.DataSourceProvider.Context.MULTI_USER;
-
-import com.google.gerrit.common.data.ApprovalTypes;
-import com.google.gerrit.lifecycle.LifecycleManager;
-import com.google.gerrit.lifecycle.LifecycleModule;
-import com.google.gerrit.pgm.util.SiteProgram;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.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.h2.DefaultCacheFactory;
-import com.google.gerrit.server.config.ApprovalTypesProvider;
-import com.google.gerrit.server.config.CanonicalWebUrl;
-import com.google.gerrit.server.config.CanonicalWebUrlProvider;
-import com.google.gerrit.server.config.FactoryModule;
-import com.google.gerrit.server.git.CodeReviewNoteCreationException;
-import com.google.gerrit.server.git.CreateCodeReviewNotes;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.LocalDiskRepositoryManager;
-import com.google.gerrit.server.git.NotesBranchUtil;
-import com.google.gerrit.server.schema.SchemaVersionCheck;
-import com.google.gwtorm.server.OrmException;
-import com.google.gwtorm.server.SchemaFactory;
-import com.google.inject.AbstractModule;
-import com.google.inject.Inject;
-import com.google.inject.Injector;
-import com.google.inject.Scopes;
-
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.lib.TextProgressMonitor;
-import org.eclipse.jgit.lib.ThreadSafeProgressMonitor;
-import org.eclipse.jgit.util.BlockList;
-import org.kohsuke.args4j.Option;
-
-import java.io.IOException;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-import java.util.Map.Entry;
-
-/** Export review notes for all submitted changes in all projects. */
-public class ExportReviewNotes 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 textMonitor = new TextProgressMonitor();
-  private final ThreadSafeProgressMonitor monitor =
-      new ThreadSafeProgressMonitor(textMonitor);
-
-  private Injector dbInjector;
-  private Injector gitInjector;
-
-  @Inject
-  private GitRepositoryManager gitManager;
-
-  @Inject
-  private SchemaFactory<ReviewDb> database;
-
-  @Inject
-  private CreateCodeReviewNotes.Factory codeReviewNotesFactory;
-
-  private Map<Project.NameKey, List<Change>> changes;
-
-  @Override
-  public int run() throws Exception {
-    if (threads <= 0) {
-      threads = 1;
-    }
-
-    dbInjector = createDbInjector(MULTI_USER);
-    gitInjector = dbInjector.createChildInjector(new AbstractModule() {
-      @Override
-      protected void configure() {
-        install(SchemaVersionCheck.module());
-        bind(ApprovalTypes.class).toProvider(ApprovalTypesProvider.class).in(
-            Scopes.SINGLETON);
-        bind(String.class).annotatedWith(CanonicalWebUrl.class)
-            .toProvider(CanonicalWebUrlProvider.class).in(Scopes.SINGLETON);
-
-        install(AccountCacheImpl.module());
-        install(GroupCacheImpl.module());
-        install(new DefaultCacheFactory.Module());
-        install(new FactoryModule() {
-          @Override
-          protected void configure() {
-            factory(CreateCodeReviewNotes.Factory.class);
-            factory(NotesBranchUtil.Factory.class);
-          }
-        });
-        install(new LifecycleModule() {
-          @Override
-          protected void configure() {
-            listener().to(LocalDiskRepositoryManager.Lifecycle.class);
-          }
-        });
-      }
-    });
-
-    manager.add(dbInjector, gitInjector);
-    manager.start();
-    gitInjector.injectMembers(this);
-
-    List<Change> allChangeList = allChanges();
-    monitor.beginTask("Scanning changes", allChangeList.size());
-    changes = cluster(allChangeList);
-    allChangeList = null;
-
-    monitor.startWorkers(threads);
-    for (int tid = 0; tid < threads; tid++) {
-      new Worker().start();
-    }
-    monitor.waitForCompletion();
-    monitor.endTask();
-    manager.stop();
-    return 0;
-  }
-
-  private List<Change> allChanges() throws OrmException {
-    final ReviewDb db = database.open();
-    try {
-      return db.changes().all().toList();
-    } finally {
-      db.close();
-    }
-  }
-
-  private Map<Project.NameKey, List<Change>> cluster(List<Change> changes) {
-    HashMap<Project.NameKey, List<Change>> m =
-        new HashMap<Project.NameKey, List<Change>>();
-    for (Change change : changes) {
-      if (change.getStatus() == Change.Status.MERGED) {
-        List<Change> l = m.get(change.getProject());
-        if (l == null) {
-          l = new BlockList<Change>();
-          m.put(change.getProject(), l);
-        }
-        l.add(change);
-      } else {
-        monitor.update(1);
-      }
-    }
-    return m;
-  }
-
-  private void export(ReviewDb db, Project.NameKey project, List<Change> changes)
-      throws IOException, OrmException, CodeReviewNoteCreationException,
-      InterruptedException {
-    final Repository git;
-    try {
-      git = gitManager.openRepository(project);
-    } catch (RepositoryNotFoundException e) {
-      return;
-    }
-    try {
-      CreateCodeReviewNotes notes = codeReviewNotesFactory.create(db, git);
-      notes.create(changes, null,
-          "Exported prior reviews from Gerrit Code Review\n", monitor);
-    } finally {
-      git.close();
-    }
-  }
-
-  private Map.Entry<Project.NameKey, List<Change>> next() {
-    synchronized (changes) {
-      if (changes.isEmpty()) {
-        return null;
-      }
-
-      final Project.NameKey name = changes.keySet().iterator().next();
-      final List<Change> list = changes.remove(name);
-      return new Map.Entry<Project.NameKey, List<Change>>() {
-        @Override
-        public Project.NameKey getKey() {
-          return name;
-        }
-
-        @Override
-        public List<Change> getValue() {
-          return list;
-        }
-
-        @Override
-        public List<Change> setValue(List<Change> value) {
-          throw new UnsupportedOperationException();
-        }
-      };
-    }
-  }
-
-  private class Worker extends Thread {
-    @Override
-    public void run() {
-      ReviewDb db;
-      try {
-        db = database.open();
-      } catch (OrmException e) {
-        e.printStackTrace();
-        return;
-      }
-      try {
-        for (;;) {
-          Entry<Project.NameKey, List<Change>> next = next();
-          if (next != null) {
-            try {
-              export(db, next.getKey(), next.getValue());
-            } catch (IOException e) {
-              e.printStackTrace();
-            } catch (OrmException e) {
-              e.printStackTrace();
-            } catch (CodeReviewNoteCreationException e) {
-              e.printStackTrace();
-            } catch (InterruptedException e) {
-              e.printStackTrace();
-            }
-          } else {
-            break;
-          }
-        }
-      } finally {
-        monitor.endWorker();
-        db.close();
-      }
-    }
-  }
-}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Init.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/Init.java
index 95b8487f..3c7822c 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
@@ -21,7 +21,6 @@
 import com.google.gerrit.pgm.init.Browser;
 import com.google.gerrit.pgm.init.InitFlags;
 import com.google.gerrit.pgm.init.InitModule;
-import com.google.gerrit.pgm.init.ReloadSiteLibrary;
 import com.google.gerrit.pgm.init.SitePathInitializer;
 import com.google.gerrit.pgm.util.ConsoleUI;
 import com.google.gerrit.pgm.util.Die;
@@ -121,12 +120,6 @@
       protected void configure() {
         bind(ConsoleUI.class).toInstance(ui);
         bind(File.class).annotatedWith(SitePath.class).toInstance(sitePath);
-        bind(ReloadSiteLibrary.class).toInstance(new ReloadSiteLibrary() {
-          @Override
-          public void reload() {
-            Init.super.loadSiteLib();
-          }
-        });
       }
     });
 
@@ -140,8 +133,9 @@
         throw (Die) why;
       }
 
-      final StringBuilder buf = new StringBuilder();
+      final StringBuilder buf = new StringBuilder(ce.getMessage());
       while (why != null) {
+        buf.append("\n");
         buf.append(why.getMessage());
         why = why.getCause();
         if (why != null) {
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
index 17b7017..4512078 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ProtoGen.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/ProtoGen.java
@@ -18,7 +18,7 @@
 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.internal.storage.file.LockFile;
 import org.eclipse.jgit.util.FS;
 import org.eclipse.jgit.util.IO;
 import org.kohsuke.args4j.Option;
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 5823940..3a7a874 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
@@ -42,10 +42,10 @@
 import org.eclipse.jetty.server.ssl.SslSelectChannelConnector;
 import org.eclipse.jetty.servlet.DefaultServlet;
 import org.eclipse.jetty.servlet.FilterHolder;
-import org.eclipse.jetty.servlet.FilterMapping;
 import org.eclipse.jetty.servlet.ServletContextHandler;
 import org.eclipse.jetty.servlet.ServletHolder;
 import org.eclipse.jetty.util.resource.Resource;
+import org.eclipse.jetty.util.ssl.SslContextFactory;
 import org.eclipse.jetty.util.thread.QueuedThreadPool;
 import org.eclipse.jetty.util.thread.ThreadPool;
 import org.eclipse.jgit.lib.Config;
@@ -60,6 +60,7 @@
 import java.net.URISyntaxException;
 import java.net.URL;
 import java.util.ArrayList;
+import java.util.EnumSet;
 import java.util.Enumeration;
 import java.util.HashSet;
 import java.util.List;
@@ -67,6 +68,8 @@
 import java.util.zip.ZipEntry;
 import java.util.zip.ZipFile;
 
+import javax.servlet.DispatcherType;
+
 @Singleton
 public class JettyServer {
   static class Lifecycle implements LifecycleListener {
@@ -161,23 +164,23 @@
         defaultPort = 80;
         c = new SelectChannelConnector();
       } else if ("https".equals(u.getScheme())) {
-        final SslSelectChannelConnector ssl = new SslSelectChannelConnector();
+        SslContextFactory ssl = new SslContextFactory();
         final File keystore = getFile(cfg, "sslkeystore", "etc/keystore");
         String password = cfg.getString("httpd", null, "sslkeypassword");
         if (password == null) {
           password = "gerrit";
         }
-        ssl.setKeystore(keystore.getAbsolutePath());
-        ssl.setTruststore(keystore.getAbsolutePath());
-        ssl.setKeyPassword(password);
-        ssl.setTrustPassword(password);
+        ssl.setKeyStorePath(keystore.getAbsolutePath());
+        ssl.setTrustStore(keystore.getAbsolutePath());
+        ssl.setKeyStorePassword(password);
+        ssl.setTrustStorePassword(password);
 
         if (AuthType.CLIENT_SSL_CERT_LDAP.equals(authType)) {
           ssl.setNeedClientAuth(true);
         }
 
         defaultPort = 443;
-        c = ssl;
+        c = new SslSelectChannelConnector(ssl);
 
       } else if ("proxy-http".equals(u.getScheme())) {
         defaultPort = 8080;
@@ -336,7 +339,9 @@
     // already have built.
     //
     GuiceFilter filter = env.webInjector.getInstance(GuiceFilter.class);
-    app.addFilter(new FilterHolder(filter), "/*", FilterMapping.DEFAULT);
+    app.addFilter(new FilterHolder(filter), "/*", EnumSet.of(
+        DispatcherType.REQUEST,
+        DispatcherType.ASYNC));
     app.addEventListener(new GuiceServletContextListener() {
       @Override
       protected Injector getInjector() {
@@ -491,7 +496,7 @@
     for (File e : entries) {
       if (e.isDirectory() /* must be a directory */
           && e.getName().startsWith("gerrit-gwtui-")
-          && new File(e, "gerrit/gerrit.nocache.js").isFile()) {
+          && new File(e, "gerrit_ui/gerrit_ui.nocache.js").isFile()) {
         return Resource.newResource(e.toURI());
       }
     }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Browser.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Browser.java
index ea13043..8e3948e 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Browser.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Browser.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.pgm.init;
 
+import com.google.common.base.Strings;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.inject.Inject;
 
@@ -42,7 +43,6 @@
     if (url == null) {
       return;
     }
-
     if (url.startsWith("proxy-")) {
       url = url.substring("proxy-".length());
     }
@@ -54,15 +54,19 @@
       System.err.println("error: invalid httpd.listenUrl: " + url);
       return;
     }
-    final String hostname = uri.getHost();
-    final int port = InitUtil.portOf(uri);
+    waitForServer(uri);
+    openBrowser(uri, link);
+  }
 
-    System.err.print("Waiting for server to start ... ");
+  private void waitForServer(URI uri) throws IOException {
+    String host = uri.getHost();
+    int port = InitUtil.portOf(uri);
+    System.err.format("Waiting for server on %s:%d ... ", host, port);
     System.err.flush();
     for (;;) {
-      final Socket s;
+      Socket s;
       try {
-        s = new Socket(hostname, port);
+        s = new Socket(host, port);
       } catch (IOException e) {
         try {
           Thread.sleep(100);
@@ -74,18 +78,33 @@
       break;
     }
     System.err.println("OK");
+  }
 
-    url = cfg.getString("gerrit", null, "canonicalWebUrl");
-    if (url == null || url.isEmpty()) {
+  private String resolveUrl(URI uri, String link) {
+    String url = cfg.getString("gerrit", null, "canonicalWebUrl");
+    if (Strings.isNullOrEmpty(url)) {
       url = uri.toString();
     }
     if (!url.endsWith("/")) {
       url += "/";
     }
-    if (link != null && !link.isEmpty()) {
+    if (!Strings.isNullOrEmpty(link)) {
       url += "#" + link;
     }
-    System.err.println("Opening browser ...");
-    org.h2.tools.Server.openBrowser(url);
+    return url;
+  }
+
+  private void openBrowser(URI uri, String link) {
+    String url = resolveUrl(uri, link);
+    System.err.format("Opening %s ...", url);
+    System.err.flush();
+    try {
+      org.h2.tools.Server.openBrowser(url);
+      System.err.println("OK");
+    } catch (Exception e) {
+      System.err.println("FAILED");
+      System.err.println("Open Gerrit with a JavaScript capable browser:");
+      System.err.println("  " + url);
+    }
   }
 }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/DatabaseConfigInitializer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/DatabaseConfigInitializer.java
new file mode 100644
index 0000000..7ac1ed6
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/DatabaseConfigInitializer.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.pgm.init;
+
+/** Abstraction of initializer for the database section */
+interface DatabaseConfigInitializer {
+
+  /**
+   * Performs database platform specific configuration steps and writes
+   * configuration parameters into the given database section
+   */
+  public void initConfig(Section databaseSection);
+}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/DatabaseConfigModule.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/DatabaseConfigModule.java
new file mode 100644
index 0000000..32f8c2e
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/DatabaseConfigModule.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.pgm.init;
+
+import com.google.gerrit.server.config.SitePaths;
+import com.google.inject.AbstractModule;
+import com.google.inject.name.Names;
+
+public class DatabaseConfigModule extends AbstractModule {
+
+  private final SitePaths site;
+
+  public DatabaseConfigModule(final SitePaths site) {
+    this.site = site;
+  }
+
+  @Override
+  protected void configure() {
+    bind(SitePaths.class).toInstance(site);
+    bind(DatabaseConfigInitializer.class).annotatedWith(
+        Names.named("h2")).to(H2Initializer.class);
+    bind(DatabaseConfigInitializer.class).annotatedWith(
+        Names.named("jdbc")).to(JDBCInitializer.class);
+    bind(DatabaseConfigInitializer.class).annotatedWith(
+        Names.named("mysql")).to(MySqlInitializer.class);
+    bind(DatabaseConfigInitializer.class).annotatedWith(
+        Names.named("postgresql")).to(PostgreSQLInitializer.class);
+  }
+}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/H2Initializer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/H2Initializer.java
new file mode 100644
index 0000000..0ea3ff0
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/H2Initializer.java
@@ -0,0 +1,48 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.pgm.init;
+
+import com.google.gerrit.server.config.SitePaths;
+import com.google.inject.Inject;
+
+import java.io.File;
+
+class H2Initializer implements DatabaseConfigInitializer {
+
+  private final SitePaths site;
+
+  @Inject
+  H2Initializer(final SitePaths site) {
+    this.site = site;
+  }
+
+  @Override
+  public void initConfig(Section databaseSection) {
+    String path = databaseSection.get("database");
+    if (path == null) {
+      path = "db/ReviewDB";
+      databaseSection.set("database", path);
+    }
+    File db = site.resolve(path);
+    if (db == null) {
+      throw InitUtil.die("database.database must be supplied for H2");
+    }
+    db = db.getParentFile();
+    if (!db.exists() && !db.mkdirs()) {
+      throw InitUtil.die("cannot create database.database "
+          + db.getAbsolutePath());
+    }
+  }
+}
\ No newline at end of file
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 fa4dc14..1524707 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
@@ -32,8 +32,8 @@
   @Inject
   InitAuth(final ConsoleUI ui, final Section.Factory sections) {
     this.ui = ui;
-    this.auth = sections.get("auth");
-    this.ldap = sections.get("ldap");
+    this.auth = sections.get("auth", null);
+    this.ldap = sections.get("ldap", null);
   }
 
   public void run() {
@@ -54,6 +54,15 @@
         auth.string("SSO logout URL", "logoutUrl", null);
         break;
       }
+
+      case CLIENT_SSL_CERT_LDAP:
+      case CUSTOM_EXTENSION:
+      case DEVELOPMENT_BECOME_ANY_ACCOUNT:
+      case LDAP:
+      case LDAP_BIND:
+      case OPENID:
+      case OPENID_SSO:
+        break;
     }
 
     switch (auth_type) {
@@ -80,6 +89,14 @@
         ldap.string("Group BaseDN", "groupBase", aBase);
         break;
       }
+
+      case CLIENT_SSL_CERT_LDAP:
+      case CUSTOM_EXTENSION:
+      case DEVELOPMENT_BECOME_ANY_ACCOUNT:
+      case HTTP:
+      case OPENID:
+      case OPENID_SSO:
+        break;
     }
 
     if (auth.getSecure("registerEmailPrivateKey") == null) {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitCache.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitCache.java
index fb1a924..2b5729f 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitCache.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitCache.java
@@ -31,7 +31,7 @@
   @Inject
   InitCache(final SitePaths site, final Section.Factory sections) {
     this.site = site;
-    this.cache = sections.get("cache");
+    this.cache = sections.get("cache", null);
   }
 
   public void run() {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitContainer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitContainer.java
index 7063f54..fbd543d 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitContainer.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitContainer.java
@@ -23,7 +23,7 @@
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
 
-import org.eclipse.jgit.storage.file.LockFile;
+import org.eclipse.jgit.internal.storage.file.LockFile;
 import org.eclipse.jgit.util.FS;
 
 import java.io.File;
@@ -44,7 +44,7 @@
       final Section.Factory sections) {
     this.ui = ui;
     this.site = site;
-    this.container = sections.get("container");
+    this.container = sections.get("container", null);
   }
 
   public void run() throws FileNotFoundException, IOException {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitDatabase.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitDatabase.java
index d501ea5..0336dda 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitDatabase.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitDatabase.java
@@ -14,17 +14,26 @@
 
 package com.google.gerrit.pgm.init;
 
-import static com.google.gerrit.pgm.init.InitUtil.die;
-import static com.google.gerrit.pgm.init.InitUtil.username;
-import static com.google.gerrit.server.schema.DataSourceProvider.Type.H2;
+import static com.google.inject.Stage.PRODUCTION;
 
+import com.google.common.base.Strings;
+import com.google.common.collect.Sets;
 import com.google.gerrit.pgm.util.ConsoleUI;
 import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.schema.DataSourceProvider;
+import com.google.inject.Binding;
+import com.google.inject.Guice;
 import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.Key;
 import com.google.inject.Singleton;
+import com.google.inject.TypeLiteral;
+import com.google.inject.name.Named;
+import com.google.inject.name.Names;
 
-import java.io.File;
+import java.lang.annotation.Annotation;
+import java.util.List;
+import java.util.Set;
+import java.util.TreeSet;
 
 /** Initialize the {@code database} configuration section. */
 @Singleton
@@ -40,65 +49,39 @@
     this.ui = ui;
     this.site = site;
     this.libraries = libraries;
-    this.database = sections.get("database");
+    this.database = sections.get("database", null);
   }
 
   public void run() {
     ui.header("SQL Database");
 
-    final DataSourceProvider.Type db_type =
-        database.select("Database server type", "type", H2);
+    Set<String> allowedValues = Sets.newTreeSet();
+    Injector i = Guice.createInjector(PRODUCTION, new DatabaseConfigModule(site));
+    List<Binding<DatabaseConfigInitializer>> dbConfigBindings =
+        i.findBindingsByType(new TypeLiteral<DatabaseConfigInitializer>() {});
+    for (Binding<DatabaseConfigInitializer> binding : dbConfigBindings) {
+      Annotation annotation = binding.getKey().getAnnotation();
+      if (annotation instanceof Named) {
+        allowedValues.add(((Named) annotation).value());
+      }
+    }
 
-    switch (db_type) {
-      case MYSQL:
+    if (!Strings.isNullOrEmpty(database.get("url"))
+        && Strings.isNullOrEmpty(database.get("type"))) {
+      database.set("type", "jdbc");
+    }
+
+    String dbType =
+        database.select("Database server type", "type", "h2", allowedValues);
+
+    DatabaseConfigInitializer dci =
+        i.getInstance(Key.get(DatabaseConfigInitializer.class,
+            Names.named(dbType.toLowerCase())));
+
+    if (dci instanceof MySqlInitializer) {
         libraries.mysqlDriver.downloadRequired();
-        break;
     }
 
-    final boolean userPassAuth;
-    switch (db_type) {
-      case H2: {
-        userPassAuth = false;
-        String path = database.get("database");
-        if (path == null) {
-          path = "db/ReviewDB";
-          database.set("database", path);
-        }
-        File db = site.resolve(path);
-        if (db == null) {
-          throw die("database.database must be supplied for H2");
-        }
-        db = db.getParentFile();
-        if (!db.exists() && !db.mkdirs()) {
-          throw die("cannot create database.database " + db.getAbsolutePath());
-        }
-        break;
-      }
-
-      case JDBC: {
-        userPassAuth = true;
-        database.string("Driver class name", "driver", null);
-        database.string("URL", "url", null);
-        break;
-      }
-
-      case POSTGRESQL:
-      case MYSQL: {
-        userPassAuth = true;
-        final String defPort = "(" + db_type.toString() + " default)";
-        database.string("Server hostname", "hostname", "localhost");
-        database.string("Server port", "port", defPort, true);
-        database.string("Database name", "database", "reviewdb");
-        break;
-      }
-
-      default:
-        throw die("internal bug, database " + db_type + " not supported");
-    }
-
-    if (userPassAuth) {
-      database.string("Database username", "username", username());
-      database.password("username", "password");
-    }
+    dci.initConfig(database);
   }
 }
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 f0cd31f..b6f8519 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
@@ -31,7 +31,7 @@
   @Inject
   InitGitManager(final ConsoleUI ui, final Section.Factory sections) {
     this.ui = ui;
-    this.gerrit = sections.get("gerrit");
+    this.gerrit = sections.get("gerrit", null);
   }
 
   public void run() {
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 98d2e47..3e7901a 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,8 +21,6 @@
 import static com.google.gerrit.pgm.init.InitUtil.toURI;
 
 import com.google.gerrit.pgm.util.ConsoleUI;
-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;
 import com.google.inject.Inject;
@@ -48,8 +46,8 @@
     this.ui = ui;
     this.site = site;
     this.flags = flags;
-    this.httpd = sections.get("httpd");
-    this.gerrit = sections.get("gerrit");
+    this.httpd = sections.get("httpd", null);
+    this.gerrit = sections.get("gerrit", null);
   }
 
   public void run() throws IOException, InterruptedException {
@@ -121,12 +119,7 @@
     } catch (URISyntaxException e) {
       throw die("invalid httpd.listenUrl");
     }
-    if (gerrit.get("canonicalWebUrl") != null //
-        || (!proxy && ssl) //
-        || getAuthType() == AuthType.OPENID) {
-      gerrit.string("Canonical URL", "canonicalWebUrl", uri.toString());
-    }
-
+    gerrit.string("Canonical URL", "canonicalWebUrl", uri.toString());
     generateSslCertificate();
   }
 
@@ -196,8 +189,4 @@
       throw die("Cannot delete " + tmpdir);
     }
   }
-
-  private AuthType getAuthType() {
-    return ConfigUtil.getEnum(flags.cfg, "auth", null, "type", AuthType.OPENID);
-  }
 }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPluginStepsLoader.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPluginStepsLoader.java
new file mode 100644
index 0000000..7658701
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPluginStepsLoader.java
@@ -0,0 +1,129 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.pgm.init;
+
+import com.google.gerrit.extensions.annotations.PluginName;
+import com.google.gerrit.pgm.util.ConsoleUI;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import com.google.inject.Injector;
+import com.google.inject.Singleton;
+
+import java.io.File;
+import java.io.FileFilter;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.jar.Attributes;
+import java.util.jar.JarFile;
+
+@Singleton
+public class InitPluginStepsLoader {
+  private final File pluginsDir;
+  private final Injector initInjector;
+  final ConsoleUI ui;
+
+  @Inject
+  public InitPluginStepsLoader(final ConsoleUI ui, final SitePaths sitePaths,
+      final Injector initInjector) {
+    this.pluginsDir = sitePaths.plugins_dir;
+    this.initInjector = initInjector;
+    this.ui = ui;
+  }
+
+  public Collection<InitStep> getInitSteps() {
+    List<File> jars = scanJarsInPluginsDirectory();
+    ArrayList<InitStep> pluginsInitSteps = new ArrayList<InitStep>();
+
+    for (File jar : jars) {
+      InitStep init = loadInitStep(jar);
+      if (init != null) {
+        pluginsInitSteps.add(init);
+      }
+    }
+    return pluginsInitSteps;
+  }
+
+  private InitStep loadInitStep(File jar) {
+    try {
+      ClassLoader pluginLoader =
+          new URLClassLoader(new URL[] {jar.toURI().toURL()},
+              InitPluginStepsLoader.class.getClassLoader());
+      JarFile jarFile = new JarFile(jar);
+      Attributes jarFileAttributes = jarFile.getManifest().getMainAttributes();
+      String initClassName = jarFileAttributes.getValue("Gerrit-InitStep");
+      if (initClassName == null) {
+        return null;
+      }
+      @SuppressWarnings("unchecked")
+      Class<? extends InitStep> initStepClass =
+          (Class<? extends InitStep>) pluginLoader.loadClass(initClassName);
+      return getPluginInjector(jar).getInstance(initStepClass);
+    } catch (ClassCastException e) {
+      ui.message(
+          "WARN: InitStep from plugin %s does not implement %s (Exception: %s)",
+          jar.getName(), InitStep.class.getName(), e.getMessage());
+      return null;
+    } catch (Exception e) {
+      ui.message(
+          "WARN: Cannot load and get plugin init step for %s (Exception: %s)",
+          jar, e.getMessage());
+      return null;
+    }
+  }
+
+  private Injector getPluginInjector(File jarFile) {
+    String jarFileName = jarFile.getName();
+    final String pluginName =
+        jarFileName.substring(0, jarFileName.lastIndexOf('.'));
+    return initInjector.createChildInjector(new AbstractModule() {
+      @Override
+      protected void configure() {
+        bind(String.class).annotatedWith(PluginName.class).toInstance(
+            pluginName);
+      }
+    });
+  }
+
+  private List<File> scanJarsInPluginsDirectory() {
+    if (pluginsDir == null || !pluginsDir.exists()) {
+      return Collections.emptyList();
+    }
+    File[] matches = pluginsDir.listFiles(new FileFilter() {
+      @Override
+      public boolean accept(File pathname) {
+        String n = pathname.getName();
+        return (n.endsWith(".jar") && pathname.isFile());
+      }
+    });
+    if (matches == null) {
+      ui.message("WARN: Cannot list %s", pluginsDir.getAbsolutePath());
+      return Collections.emptyList();
+    }
+    Arrays.sort(matches, new Comparator<File>() {
+      @Override
+      public int compare(File o1, File o2) {
+        return o1.getName().compareTo(o2.getName());
+      }
+    });
+    return Arrays.asList(matches);
+  }
+}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPlugins.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPlugins.java
index 155fe4c..b82df64 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPlugins.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitPlugins.java
@@ -39,17 +39,24 @@
 
   private final ConsoleUI ui;
   private final SitePaths site;
+  private InitPluginStepsLoader pluginLoader;
 
   @Inject
-  InitPlugins(final ConsoleUI ui, final SitePaths site) {
+  InitPlugins(final ConsoleUI ui, final SitePaths site, InitPluginStepsLoader pluginLoader) {
     this.ui = ui;
     this.site = site;
+    this.pluginLoader = pluginLoader;
   }
 
   @Override
   public void run() throws Exception {
     ui.header("Plugins");
 
+    installPlugins();
+    initPlugins();
+  }
+
+  private void installPlugins() throws IOException {
     final File myWar;
     try {
       myWar = GerritLauncher.getDistributionArchive();
@@ -127,6 +134,12 @@
     }
   }
 
+  private void initPlugins() throws Exception {
+    for (InitStep initStep : pluginLoader.getInitSteps()) {
+      initStep.run();
+    }
+  }
+
   private static String getVersion(final File plugin) throws IOException {
     final JarFile jarFile = new JarFile(plugin);
     try {
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 c5732e9..e4b827d1 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
@@ -34,7 +34,7 @@
   InitSendEmail(final ConsoleUI ui, final SitePaths site,
       final Section.Factory sections) {
     this.ui = ui;
-    this.sendemail = sections.get("sendemail");
+    this.sendemail = sections.get("sendemail", null);
     this.site = site;
   }
 
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 bfc0eaf..0c2a3c6 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
@@ -45,7 +45,7 @@
     this.ui = ui;
     this.site = site;
     this.libraries = libraries;
-    this.sshd = sections.get("sshd");
+    this.sshd = sections.get("sshd", null);
   }
 
   public void run() throws Exception {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitUtil.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitUtil.java
index 1382f65..0ad7560 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitUtil.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/InitUtil.java
@@ -16,9 +16,9 @@
 
 import com.google.gerrit.pgm.util.Die;
 
+import org.eclipse.jgit.internal.storage.file.LockFile;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.storage.file.FileBasedConfig;
-import org.eclipse.jgit.storage.file.LockFile;
 import org.eclipse.jgit.util.FS;
 import org.eclipse.jgit.util.IO;
 import org.eclipse.jgit.util.SystemReader;
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/JDBCInitializer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/JDBCInitializer.java
new file mode 100644
index 0000000..20034c1
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/JDBCInitializer.java
@@ -0,0 +1,44 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.pgm.init;
+
+import static com.google.gerrit.pgm.init.InitUtil.username;
+
+import com.google.common.base.Strings;
+
+class JDBCInitializer implements DatabaseConfigInitializer {
+  @Override
+  public void initConfig(Section database) {
+    boolean hasUrl = Strings.emptyToNull(database.get("url")) != null;
+    database.string("URL", "url", null);
+    guessDriver(database);
+    database.string("Driver class name", "driver", null);
+    database.string("Database username", "username", hasUrl ? null : username());
+    database.password("username", "password");
+  }
+
+  private void guessDriver(Section database) {
+    String url = Strings.emptyToNull(database.get("url"));
+    if (url != null && Strings.isNullOrEmpty(database.get("driver"))) {
+      if (url.startsWith("jdbc:h2:")) {
+        database.set("driver", "org.h2.Driver");
+      } else if (url.startsWith("jdbc:mysql:")) {
+        database.set("driver", "com.mysql.jdbc.Driver");
+      } else if (url.startsWith("jdbc:postgresql:")) {
+        database.set("driver", "org.postgresql.Driver");
+      }
+    }
+  }
+}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Libraries.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Libraries.java
index ff1eddf..b1fa0c3 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Libraries.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/Libraries.java
@@ -78,6 +78,7 @@
     dl.setName(get(cfg, n, "name"));
     dl.setJarUrl(get(cfg, n, "url"));
     dl.setSHA1(get(cfg, n, "sha1"));
+    dl.setRemove(get(cfg, n, "remove"));
     field.set(this, dl);
   }
 
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/LibraryDownloader.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/LibraryDownloader.java
index ea1b515..bf358f4 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/LibraryDownloader.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/LibraryDownloader.java
@@ -14,8 +14,10 @@
 
 package com.google.gerrit.pgm.init;
 
+import com.google.common.base.Strings;
 import com.google.gerrit.pgm.util.ConsoleUI;
 import com.google.gerrit.pgm.util.Die;
+import com.google.gerrit.pgm.util.IoUtil;
 import com.google.gerrit.server.config.SitePaths;
 import com.google.inject.Inject;
 
@@ -26,6 +28,7 @@
 import java.io.FileInputStream;
 import java.io.FileNotFoundException;
 import java.io.FileOutputStream;
+import java.io.FilenameFilter;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
@@ -40,20 +43,18 @@
 class LibraryDownloader {
   private final ConsoleUI ui;
   private final File lib_dir;
-  private final ReloadSiteLibrary reload;
 
   private boolean required;
   private String name;
   private String jarUrl;
   private String sha1;
+  private String remove;
   private File dst;
 
   @Inject
-  LibraryDownloader(final ReloadSiteLibrary reload, final ConsoleUI ui,
-      final SitePaths site) {
+  LibraryDownloader(ConsoleUI ui, SitePaths site) {
     this.ui = ui;
     this.lib_dir = site.lib_dir;
-    this.reload = reload;
   }
 
   void setName(final String name) {
@@ -68,6 +69,10 @@
     this.sha1 = sha1;
   }
 
+  void setRemove(String remove) {
+    this.remove = remove;
+  }
+
   void downloadRequired() {
     this.required = true;
     download();
@@ -123,6 +128,7 @@
     }
 
     try {
+      removeStaleVersions();
       doGetByHttp();
       verifyFileChecksum();
     } catch (IOException err) {
@@ -155,7 +161,29 @@
       }
     }
 
-    reload.reload();
+    if (dst.exists()) {
+      IoUtil.loadJARs(dst);
+    }
+  }
+
+  private void removeStaleVersions() {
+    if (!Strings.isNullOrEmpty(remove)) {
+      String[] names = lib_dir.list(new FilenameFilter() {
+        @Override
+        public boolean accept(File dir, String name) {
+          return name.matches("^" + remove + "$");
+        }
+      });
+      if (names != null) {
+        for (String old : names) {
+          String bak = "." + old + ".backup";
+          ui.message("Renaming %s to %s", old, bak);
+          if (!new File(lib_dir, old).renameTo(new File(lib_dir, bak))) {
+            throw new Die("cannot rename " + old);
+          }
+        }
+      }
+    }
   }
 
   private void doGetByHttp() throws IOException {
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/MySqlInitializer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/MySqlInitializer.java
new file mode 100644
index 0000000..fe6a4d9
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/MySqlInitializer.java
@@ -0,0 +1,30 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.pgm.init;
+
+import static com.google.gerrit.pgm.init.InitUtil.username;
+
+class MySqlInitializer implements DatabaseConfigInitializer {
+
+  @Override
+  public void initConfig(Section databaseSection) {
+    final String defPort = "(mysql default)";
+    databaseSection.string("Server hostname", "hostname", "localhost");
+    databaseSection.string("Server port", "port", defPort, true);
+    databaseSection.string("Database name", "database", "reviewdb");
+    databaseSection.string("Database username", "username", username());
+    databaseSection.password("username", "password");
+  }
+}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/PostgreSQLInitializer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/PostgreSQLInitializer.java
new file mode 100644
index 0000000..1425663
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/PostgreSQLInitializer.java
@@ -0,0 +1,30 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.pgm.init;
+
+import static com.google.gerrit.pgm.init.InitUtil.username;
+
+class PostgreSQLInitializer implements DatabaseConfigInitializer {
+
+  @Override
+  public void initConfig(Section databaseSection) {
+    final String defPort = "(postgresql default)";
+    databaseSection.string("Server hostname", "hostname", "localhost");
+    databaseSection.string("Server port", "port", defPort, true);
+    databaseSection.string("Database name", "database", "reviewdb");
+    databaseSection.string("Database username", "username", username());
+    databaseSection.password("username", "password");
+  }
+}
\ No newline at end of file
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/ReloadSiteLibrary.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/ReloadSiteLibrary.java
deleted file mode 100644
index b21d3c0..0000000
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/ReloadSiteLibrary.java
+++ /dev/null
@@ -1,20 +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.pgm.init;
-
-/** Requests the site's {@code lib/} directory be scanned again. */
-public interface ReloadSiteLibrary {
-  public void reload();
-}
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 02ed991..387b93a 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
@@ -23,53 +23,60 @@
 import java.io.File;
 import java.util.ArrayList;
 import java.util.Arrays;
+import java.util.Set;
+
+import javax.annotation.Nullable;
 
 /** Helper to edit a section of the configuration files. */
-class Section {
-  interface Factory {
-    Section get(String name);
+public class Section {
+  public interface Factory {
+    Section get(@Assisted("section") String section,
+        @Assisted("subsection") String subsection);
   }
 
   private final InitFlags flags;
   private final SitePaths site;
   private final ConsoleUI ui;
   private final String section;
+  private final String subsection;
 
   @Inject
-  Section(final InitFlags flags, final SitePaths site, final ConsoleUI ui,
-      @Assisted final String section) {
+  public Section(final InitFlags flags, final SitePaths site,
+      final ConsoleUI ui, @Assisted("section") final String section,
+      @Assisted("subsection") @Nullable final String subsection) {
     this.flags = flags;
     this.site = site;
     this.ui = ui;
     this.section = section;
+    this.subsection = subsection;
   }
 
   String get(String name) {
     return flags.cfg.getString(section, null, name);
   }
 
-  void set(final String name, final String value) {
+  public void set(final String name, final String value) {
     final ArrayList<String> all = new ArrayList<String>();
-    all.addAll(Arrays.asList(flags.cfg.getStringList(section, null, name)));
+    all.addAll(Arrays.asList(flags.cfg.getStringList(section, subsection, name)));
 
     if (value != null) {
       if (all.size() == 0 || all.size() == 1) {
-        flags.cfg.setString(section, null, name, value);
+        flags.cfg.setString(section, subsection, name, value);
       } else {
         all.set(0, value);
-        flags.cfg.setStringList(section, null, name, all);
+        flags.cfg.setStringList(section, subsection, name, all);
       }
 
     } else if (all.size() == 0) {
     } else if (all.size() == 1) {
-      flags.cfg.unset(section, null, name);
+      flags.cfg.unset(section, subsection, name);
     } else {
       all.remove(0);
-      flags.cfg.setStringList(section, null, name, all);
+      flags.cfg.setStringList(section, subsection, name, all);
     }
   }
 
-  <T extends Enum<?>> void set(final String name, final T value) {
+  public <T extends Enum<?>> void set(final String name, final T value) {
     if (value != null) {
       set(name, value.name());
     } else {
@@ -77,15 +84,15 @@
     }
   }
 
-  void unset(String name) {
+  public void unset(String name) {
     set(name, (String) null);
   }
 
-  String string(final String title, final String name, final String dv) {
+  public String string(final String title, final String name, final String dv) {
     return string(title, name, dv, false);
   }
 
-  String string(final String title, final String name, final String dv,
+  public String string(final String title, final String name, final String dv,
       final boolean nullIfDefault) {
     final String ov = get(name);
     String nv = ui.readString(ov != null ? ov : dv, "%s", title);
@@ -98,19 +105,19 @@
     return nv;
   }
 
-  File path(final String title, final String name, final String defValue) {
+  public File path(final String title, final String name, final String defValue) {
     return site.resolve(string(title, name, defValue));
   }
 
-  <T extends Enum<?>> T select(final String title, final String name,
+  public <T extends Enum<?>> T select(final String title, final String name,
       final T defValue) {
     return select(title, name, defValue, false);
   }
 
-  <T extends Enum<?>> T select(final String title, final String name,
+  public <T extends Enum<?>> T select(final String title, final String name,
       final T defValue, final boolean nullIfDefault) {
     final boolean set = get(name) != null;
-    T oldValue = ConfigUtil.getEnum(flags.cfg, section, null, name, defValue);
+    T oldValue = ConfigUtil.getEnum(flags.cfg, section, subsection, name, defValue);
     T newValue = ui.readEnum(oldValue, "%s", title);
     if (nullIfDefault && newValue == defValue) {
       newValue = null;
@@ -125,16 +132,26 @@
     return newValue;
   }
 
-  String password(final String username, final String password) {
+  public String select(final String title, final String name, final String dv,
+      Set<String> allowedValues) {
+    final String ov = get(name);
+    String nv = ui.readString(ov != null ? ov : dv, allowedValues, "%s", title);
+    if (!eq(ov, nv)) {
+      set(name, nv);
+    }
+    return nv;
+  }
+
+  public String password(final String username, final String password) {
     final String ov = getSecure(password);
 
-    String user = flags.sec.getString(section, null, username);
+    String user = flags.sec.getString(section, subsection, username);
     if (user == null) {
       user = get(username);
     }
 
     if (user == null) {
-      flags.sec.unset(section, null, password);
+      flags.sec.unset(section, subsection, password);
       return null;
     }
 
@@ -154,18 +171,22 @@
     return nv;
   }
 
-  String getSecure(String name) {
-    return flags.sec.getString(section, null, name);
+  public String getSecure(String name) {
+    return flags.sec.getString(section, subsection, name);
   }
 
-  void setSecure(String name, String value) {
+  public void setSecure(String name, String value) {
     if (value != null) {
-      flags.sec.setString(section, null, name, value);
+      flags.sec.setString(section, subsection, name, value);
     } else {
-      flags.sec.unset(section, null, name);
+      flags.sec.unset(section, subsection, name);
     }
   }
 
+  String getName() {
+    return section;
+  }
+
   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/init/SitePathInitializer.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java
index f8ef637..bf0af7f 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/SitePathInitializer.java
@@ -89,6 +89,8 @@
     extractMailExample("ChangeFooter.vm");
     extractMailExample("ChangeSubject.vm");
     extractMailExample("Comment.vm");
+    extractMailExample("CommentFooter.vm");
+    extractMailExample("CommitMessageEdited.vm");
     extractMailExample("Merged.vm");
     extractMailExample("MergeFail.vm");
     extractMailExample("NewChange.vm");
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_x.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_x.java
index 9f62fc5..b982ae1 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_x.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_x.java
@@ -20,7 +20,6 @@
 
 import com.google.gerrit.pgm.util.ConsoleUI;
 import com.google.gerrit.server.config.SitePaths;
-import com.google.gerrit.server.schema.DataSourceProvider;
 import com.google.gerrit.server.util.SocketUtil;
 import com.google.inject.Inject;
 import com.google.inject.Singleton;
@@ -118,11 +117,11 @@
 
     final Properties oldprop = readGerritServerProperties();
     if (oldprop != null) {
-      final Section database = sections.get("database");
+      final Section database = sections.get("database", null);
 
       String url = oldprop.getProperty("url");
       if (url != null && !convertUrl(database, url)) {
-        database.set("type", DataSourceProvider.Type.JDBC);
+        database.set("type", "jdbc");
         database.set("driver", oldprop.getProperty("driver"));
         database.set("url", url);
       }
@@ -189,7 +188,7 @@
 
     if (url.startsWith("jdbc:h2:file:")) {
       url = url.substring("jdbc:h2:file:".length());
-      database.set("type", DataSourceProvider.Type.H2);
+      database.set("type", "h2");
       database.set("database", url);
       return true;
     }
@@ -202,7 +201,7 @@
       }
 
       final InetSocketAddress addr = SocketUtil.parse(url.substring(0, sl), 0);
-      database.set("type", DataSourceProvider.Type.POSTGRESQL);
+      database.set("type", "postgresql");
       sethost(database, addr);
       database.set("database", url.substring(sl + 1));
       setuser(database, username, password);
@@ -211,7 +210,7 @@
 
     if (url.startsWith("jdbc:postgresql:")) {
       url = url.substring("jdbc:postgresql:".length());
-      database.set("type", DataSourceProvider.Type.POSTGRESQL);
+      database.set("type", "postgresql");
       database.set("hostname", "localhost");
       database.set("database", url);
       setuser(database, username, password);
@@ -226,7 +225,7 @@
       }
 
       final InetSocketAddress addr = SocketUtil.parse(url.substring(0, sl), 0);
-      database.set("type", DataSourceProvider.Type.MYSQL);
+      database.set("type", "mysql");
       sethost(database, addr);
       database.set("database", url.substring(sl + 1));
       setuser(database, username, password);
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ConsoleUI.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ConsoleUI.java
index b4e0fad..e8cf0ab 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ConsoleUI.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/ConsoleUI.java
@@ -18,6 +18,7 @@
 
 import java.io.Console;
 import java.lang.reflect.InvocationTargetException;
+import java.util.Set;
 
 /** Console based interaction with the invoking user. */
 public abstract class ConsoleUI {
@@ -73,6 +74,10 @@
   /** Prompt the user for a string, suggesting a default, and returning choice. */
   public abstract String readString(String def, String fmt, Object... args);
 
+  /** Prompt the user to make a choice from an allowed list of values. */
+  public abstract String readString(String def, Set<String> allowedValues,
+      String fmt, Object... args);
+
   /** Prompt the user for an integer value, suggesting a default. */
   public int readInt(int def, String fmt, Object... args) {
     for (;;) {
@@ -162,6 +167,24 @@
     }
 
     @Override
+    public String readString(String def, Set<String> allowedValues, String fmt,
+        Object... args) {
+      for (;;) {
+        String r = readString(def, fmt, args);
+        if (allowedValues.contains(r.toLowerCase())) {
+          return r.toLowerCase();
+        }
+        if (!"?".equals(r)) {
+          console.printf("error: '%s' is not a valid choice\n", r);
+        }
+        console.printf("       Supported options are:\n");
+        for (final String v : allowedValues) {
+          console.printf("         %s\n", v.toString().toLowerCase());
+        }
+      }
+    }
+
+    @Override
     public String password(String fmt, Object... args) {
       final String prompt = String.format(fmt, args);
       for (;;) {
@@ -242,6 +265,12 @@
     }
 
     @Override
+    public String readString(String def, Set<String> allowedValues, String fmt,
+        Object... args) {
+      return def;
+    }
+
+    @Override
     public void waitForUser() {
     }
 
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/GarbageCollectionLogFile.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/GarbageCollectionLogFile.java
new file mode 100644
index 0000000..95e5763
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/GarbageCollectionLogFile.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.pgm.util;
+
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.git.GarbageCollection;
+
+import org.apache.log4j.Appender;
+import org.apache.log4j.DailyRollingFileAppender;
+import org.apache.log4j.Level;
+import org.apache.log4j.LogManager;
+import org.apache.log4j.Logger;
+import org.apache.log4j.PatternLayout;
+import org.apache.log4j.helpers.OnlyOnceErrorHandler;
+import org.apache.log4j.spi.ErrorHandler;
+import org.apache.log4j.spi.LoggingEvent;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+
+public class GarbageCollectionLogFile {
+  private static final org.slf4j.Logger log = LoggerFactory.getLogger(GarbageCollectionLogFile.class);
+
+  public static LifecycleListener start(File sitePath)
+      throws FileNotFoundException {
+    File logdir = new SitePaths(sitePath).logs_dir;
+    if (!logdir.exists() && !logdir.mkdirs()) {
+      throw new Die("Cannot create log directory: " + logdir);
+    }
+
+    PatternLayout layout = new PatternLayout();
+    layout.setConversionPattern("[%d] %-5p %x: %m%n");
+
+    DailyRollingFileAppender dst = new DailyRollingFileAppender();
+    dst.setName(GarbageCollection.LOG_NAME);
+    dst.setLayout(layout);
+    dst.setEncoding("UTF-8");
+    dst.setFile(new File(resolve(logdir), GarbageCollection.LOG_NAME).getPath());
+    dst.setImmediateFlush(true);
+    dst.setAppend(true);
+    dst.setThreshold(Level.INFO);
+    dst.setErrorHandler(new LogErrorHandler());
+    dst.activateOptions();
+    dst.setErrorHandler(new OnlyOnceErrorHandler());
+
+    Logger gcLogger = LogManager.getLogger(GarbageCollection.LOG_NAME);
+    gcLogger.removeAllAppenders();
+    gcLogger.addAppender(dst);
+    gcLogger.setAdditivity(false);
+
+    return new LifecycleListener() {
+      @Override
+      public void start() {
+      }
+
+      @Override
+      public void stop() {
+        LogManager.getLogger(GarbageCollection.LOG_NAME).removeAllAppenders();
+      }
+    };
+  }
+
+  private static File resolve(File logs_dir) {
+    try {
+      return logs_dir.getCanonicalFile();
+    } catch (IOException e) {
+      return logs_dir.getAbsoluteFile();
+    }
+  }
+
+  private GarbageCollectionLogFile() {
+  }
+
+  private static final class LogErrorHandler implements ErrorHandler {
+    @Override
+    public void error(String message, Exception e, int errorCode,
+        LoggingEvent event) {
+      error(e != null ? e.getMessage() : message);
+    }
+
+    @Override
+    public void error(String message, Exception e, int errorCode) {
+      error(e != null ? e.getMessage() : message);
+    }
+
+    @Override
+    public void error(String message) {
+      log.error("Cannot open '" + GarbageCollection.LOG_NAME + "' log file: "
+          + message);
+    }
+
+    @Override
+    public void activateOptions() {
+    }
+
+    @Override
+    public void setAppender(Appender appender) {
+    }
+
+    @Override
+    public void setBackupAppender(Appender appender) {
+    }
+
+    @Override
+    public void setLogger(Logger logger) {
+    }
+  }
+}
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/IoUtil.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/IoUtil.java
index 9398851..f750748a 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/IoUtil.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/IoUtil.java
@@ -14,9 +14,19 @@
 
 package com.google.gerrit.pgm.util;
 
+import com.google.common.collect.Sets;
+
+import java.io.File;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.OutputStream;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.util.Arrays;
+import java.util.Set;
 
 public final class IoUtil {
   public static void copyWithThread(final InputStream src,
@@ -42,6 +52,47 @@
     }.start();
   }
 
+  public static void loadJARs(File... jars) {
+    ClassLoader cl = IoUtil.class.getClassLoader();
+    if (!(cl instanceof URLClassLoader)) {
+      throw noAddURL("Not loaded by URLClassLoader", null);
+    }
+    @SuppressWarnings("resource")
+    URLClassLoader urlClassLoader = (URLClassLoader) cl;
+
+    Method addURL;
+    try {
+      addURL = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
+      addURL.setAccessible(true);
+    } catch (SecurityException e) {
+      throw noAddURL("Method addURL not available", e);
+    } catch (NoSuchMethodException e) {
+      throw noAddURL("Method addURL not available", e);
+    }
+
+    Set<URL> have = Sets.newHashSet(Arrays.asList(urlClassLoader.getURLs()));
+    for (File path : jars) {
+      try {
+        URL url = path.toURI().toURL();
+        if (have.add(url)) {
+          addURL.invoke(cl, url);
+        }
+      } catch (MalformedURLException e) {
+        throw noAddURL("addURL " + path + " failed", e);
+      } catch (IllegalArgumentException e) {
+        throw noAddURL("addURL " + path + " failed", e);
+      } catch (IllegalAccessException e) {
+        throw noAddURL("addURL " + path + " failed", e);
+      } catch (InvocationTargetException e) {
+        throw noAddURL("addURL " + path + " failed", e.getCause());
+      }
+    }
+  }
+
+  private static UnsupportedOperationException noAddURL(String m, Throwable why) {
+    String prefix = "Cannot extend classpath: ";
+    return new UnsupportedOperationException(prefix + m, why);
+  }
   private IoUtil() {
   }
 }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/LogFileCompressor.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/LogFileCompressor.java
index 57cc7c4..cda653e 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/LogFileCompressor.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/LogFileCompressor.java
@@ -19,6 +19,7 @@
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.git.GarbageCollection;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.inject.Inject;
 
@@ -97,6 +98,7 @@
   private boolean isLive(final File entry) {
     final String name = entry.getName();
     return ErrorLogFile.LOG_NAME.equals(name) //
+        || GarbageCollection.LOG_NAME.equals(name) //
         || "sshd_log".equals(name) //
         || "httpd_log".equals(name) //
         || "gerrit.run".equals(name) //
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/RuntimeShutdown.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/RuntimeShutdown.java
index 18b7064..c00ad7f 100644
--- a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/RuntimeShutdown.java
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/RuntimeShutdown.java
@@ -103,9 +103,7 @@
           try {
             wait();
           } catch (InterruptedException e) {
-            log.warn("Thread " + Thread.currentThread().getName()
-                + " interrupted while waiting for graceful shutdown;"
-                + " ignoring interrupt request");
+            return;
           }
         }
       }
diff --git a/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteLibraryBasedDataSourceProvider.java b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteLibraryBasedDataSourceProvider.java
new file mode 100644
index 0000000..6ab7395
--- /dev/null
+++ b/gerrit-pgm/src/main/java/com/google/gerrit/pgm/util/SiteLibraryBasedDataSourceProvider.java
@@ -0,0 +1,81 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF 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.util;
+
+import com.google.common.primitives.Longs;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.gerrit.server.schema.DataSourceProvider;
+import com.google.gerrit.server.schema.DataSourceType;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.lib.Config;
+
+import java.io.File;
+import java.io.FileFilter;
+import java.util.Arrays;
+import java.util.Comparator;
+
+import javax.sql.DataSource;
+
+/** Loads the site library if not yet loaded. */
+@Singleton
+public class SiteLibraryBasedDataSourceProvider extends DataSourceProvider {
+  private final File libdir;
+  private boolean init;
+
+  @Inject
+  SiteLibraryBasedDataSourceProvider(SitePaths site,
+      @GerritServerConfig Config cfg,
+      DataSourceProvider.Context ctx,
+      DataSourceType dst) {
+    super(site, cfg, ctx, dst);
+    libdir = site.lib_dir;
+  }
+
+  public synchronized DataSource get() {
+    if (!init) {
+      loadSiteLib();
+      init = true;
+    }
+    return super.get();
+  }
+
+  private void loadSiteLib() {
+    File[] jars = libdir.listFiles(new FileFilter() {
+      @Override
+      public boolean accept(File path) {
+        String name = path.getName();
+        return (name.endsWith(".jar") || name.endsWith(".zip"))
+            && path.isFile();
+      }
+    });
+    if (jars != null && 0 < jars.length) {
+      Arrays.sort(jars, new Comparator<File>() {
+        @Override
+        public int compare(File a, File b) {
+          // Sort by reverse last-modified time so newer JARs are first.
+          int cmp = Longs.compare(b.lastModified(), a.lastModified());
+          if (cmp != 0) {
+            return cmp;
+          }
+          return a.getName().compareTo(b.getName());
+        }
+      });
+      IoUtil.loadJARs(jars);
+    }
+  }
+}
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 4893579..aae5b48 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
@@ -18,10 +18,13 @@
 import static com.google.inject.Stage.PRODUCTION;
 
 import com.google.gerrit.lifecycle.LifecycleModule;
+import com.google.gerrit.server.config.GerritServerConfig;
 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.DataSourceModule;
 import com.google.gerrit.server.schema.DataSourceProvider;
+import com.google.gerrit.server.schema.DataSourceType;
 import com.google.gerrit.server.schema.DatabaseModule;
 import com.google.gerrit.server.schema.SchemaModule;
 import com.google.gwtorm.server.OrmException;
@@ -34,22 +37,13 @@
 import com.google.inject.name.Names;
 import com.google.inject.spi.Message;
 
+import org.eclipse.jgit.lib.Config;
 import org.kohsuke.args4j.Option;
 
 import java.io.File;
-import java.io.FileFilter;
-import java.lang.reflect.InvocationTargetException;
-import java.lang.reflect.Method;
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.net.URLClassLoader;
 import java.sql.SQLException;
 import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.Comparator;
-import java.util.HashSet;
 import java.util.List;
-import java.util.Set;
 
 import javax.sql.DataSource;
 
@@ -74,95 +68,44 @@
     }
   }
 
-  /** Load extra JARs from {@code lib/} subdirectory of {@link #getSitePath()} */
-  protected void loadSiteLib() {
-    final File libdir = new File(getSitePath(), "lib");
-    final File[] list = libdir.listFiles(new FileFilter() {
-      @Override
-      public boolean accept(File path) {
-        if (!path.isFile()) {
-          return false;
-        }
-        return path.getName().endsWith(".jar") //
-            || path.getName().endsWith(".zip");
-      }
-    });
-    if (list != null && 0 < list.length) {
-      Arrays.sort(list, new Comparator<File>() {
-        @Override
-        public int compare(File a, File b) {
-          return a.getName().compareTo(b.getName());
-        }
-      });
-      addToClassLoader(list);
-    }
-  }
-
-  private void addToClassLoader(final File[] additionalLocations) {
-    final ClassLoader cl = getClass().getClassLoader();
-    if (!(cl instanceof URLClassLoader)) {
-      throw noAddURL("Not loaded by URLClassLoader", null);
-    }
-
-    final URLClassLoader ucl = (URLClassLoader) cl;
-    final Set<URL> have = new HashSet<URL>();
-    have.addAll(Arrays.asList(ucl.getURLs()));
-
-    final Method m;
-    try {
-      m = URLClassLoader.class.getDeclaredMethod("addURL", URL.class);
-      m.setAccessible(true);
-    } catch (SecurityException e) {
-      throw noAddURL("Method addURL not available", e);
-    } catch (NoSuchMethodException e) {
-      throw noAddURL("Method addURL not available", e);
-    }
-
-    for (final File path : additionalLocations) {
-      try {
-        final URL url = path.toURI().toURL();
-        if (have.add(url)) {
-          m.invoke(cl, url);
-        }
-      } catch (MalformedURLException e) {
-        throw noAddURL("addURL " + path + " failed", e);
-      } catch (IllegalArgumentException e) {
-        throw noAddURL("addURL " + path + " failed", e);
-      } catch (IllegalAccessException e) {
-        throw noAddURL("addURL " + path + " failed", e);
-      } catch (InvocationTargetException e) {
-        throw noAddURL("addURL " + path + " failed", e.getCause());
-      }
-    }
-  }
-
-  private static UnsupportedOperationException noAddURL(String m, Throwable why) {
-    final String prefix = "Cannot extend classpath: ";
-    return new UnsupportedOperationException(prefix + m, why);
-  }
-
   /** @return provides database connectivity and site path. */
   protected Injector createDbInjector(final DataSourceProvider.Context context) {
-    loadSiteLib();
-
     final File sitePath = getSitePath();
     final List<Module> modules = new ArrayList<Module>();
-    modules.add(new AbstractModule() {
+
+    Module sitePathModule = new AbstractModule() {
       @Override
       protected void configure() {
         bind(File.class).annotatedWith(SitePath.class).toInstance(sitePath);
       }
-    });
+    };
+    modules.add(sitePathModule);
+
     modules.add(new LifecycleModule() {
       @Override
       protected void configure() {
         bind(DataSourceProvider.Context.class).toInstance(context);
-        bind(Key.get(DataSource.class, Names.named("ReviewDb"))).toProvider(
-            DataSourceProvider.class).in(SINGLETON);
-        listener().to(DataSourceProvider.class);
+        bind(Key.get(DataSource.class, Names.named("ReviewDb")))
+          .toProvider(SiteLibraryBasedDataSourceProvider.class)
+          .in(SINGLETON);
+        listener().to(SiteLibraryBasedDataSourceProvider.class);
       }
     });
-    modules.add(new GerritServerConfigModule());
+    Module configModule = new GerritServerConfigModule();
+    modules.add(configModule);
+    Injector cfgInjector = Guice.createInjector(sitePathModule, configModule);
+    Config cfg = cfgInjector.getInstance(Key.get(Config.class, GerritServerConfig.class));
+    String dbType = cfg.getString("database", null, "type");
+
+    final DataSourceType dst = Guice.createInjector(new DataSourceModule(), configModule,
+            sitePathModule).getInstance(
+            Key.get(DataSourceType.class, Names.named(dbType.toLowerCase())));
+
+    modules.add(new AbstractModule() {
+      @Override
+      protected void configure() {
+        bind(DataSourceType.class).toInstance(dst);
+      }});
     modules.add(new DatabaseModule());
     modules.add(new SchemaModule());
     modules.add(new LocalDiskRepositoryManager.Module());
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 3857ebd..9aa4ea3 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
@@ -9,6 +9,17 @@
 # processname: gerrit
 # ========================
 
+### BEGIN INIT INFO
+# Provides:          gerrit
+# Required-Start:    $named $remote_fs $syslog
+# Required-Stop:     $named $remote_fs $syslog
+# Default-Start:     2 3 4 5
+# Default-Stop:      0 1 6
+# Short-Description: Start/stop Gerrit Code Review
+# Description:       Gerrit is a web based code review system, facilitating online code reviews
+#                    for projects using the Git version control system.
+### END INIT INFO
+
 # Configuration files:
 #
 # /etc/default/gerritcodereview
@@ -36,7 +47,7 @@
 
 usage() {
     me=`basename "$0"`
-    echo >&2 "Usage: $me {start|stop|restart|check|run|supervise} [-d site]"
+    echo >&2 "Usage: $me {start|stop|restart|check|status|run|supervise} [-d site]"
     exit 1
 }
 
@@ -108,7 +119,7 @@
 ##################################################
 # See if there's a default configuration file
 ##################################################
-if test -f /etc/default/gerritcodereview ; then 
+if test -f /etc/default/gerritcodereview ; then
   . /etc/default/gerritcodereview
 fi
 
@@ -126,7 +137,7 @@
 GERRIT_INSTALL_TRACE_FILE=etc/gerrit.config
 
 ##################################################
-# No git in PATH? Needed for gerrit.confg parsing
+# No git in PATH? Needed for gerrit.config parsing
 ##################################################
 if type git >/dev/null 2>&1 ; then
   : OK
@@ -139,10 +150,9 @@
 # Try to determine GERRIT_SITE if not set
 ##################################################
 if test -z "$GERRIT_SITE" ; then
-  GERRIT_SITE_1=`dirname "$0"`
-  GERRIT_SITE_1=`dirname "$GERRIT_SITE_1"`
-  if test -f "${GERRIT_SITE_1}/${GERRIT_INSTALL_TRACE_FILE}" ; then 
-    GERRIT_SITE=${GERRIT_SITE_1} 
+  GERRIT_SITE_1=`dirname "$0"`/..
+  if test -f "${GERRIT_SITE_1}/${GERRIT_INSTALL_TRACE_FILE}" ; then
+    GERRIT_SITE=${GERRIT_SITE_1}
   fi
 fi
 
@@ -150,10 +160,11 @@
 # No GERRIT_SITE yet? We're out of luck!
 ##################################################
 if test -z "$GERRIT_SITE" ; then
-    echo >&2 "** ERROR: GERRIT_SITE not set" 
+    echo >&2 "** ERROR: GERRIT_SITE not set"
     exit 1
 fi
 
+INITIAL_DIR=`pwd`
 if cd "$GERRIT_SITE" ; then
   GERRIT_SITE=`pwd`
 else
@@ -206,7 +217,7 @@
     "
     for N in java jdk jre ; do
       for L in $JAVA_LOCATIONS ; do
-        test -d "$L" || continue 
+        test -d "$L" || continue
         find $L -name "$N" ! -type d | grep -v threads | while read J ; do
           test -x "$J" || continue
           VERSION=`eval "$J" -version 2>&1`
@@ -241,7 +252,9 @@
 fi
 
 if test -z "$JAVA" ; then
-  echo >&2 "Cannot find a JRE or JDK. Please set JAVA_HOME to a >=1.6 JRE"
+  echo >&2 "Cannot find a JRE or JDK. Please set JAVA_HOME or"
+  echo >&2 "container.javaHome in $GERRIT_SITE/etc/gerrit.config"
+  echo >&2 "to a >=1.6 JRE"
   exit 1
 fi
 
@@ -340,7 +353,7 @@
   start)
     printf '%s' "Starting Gerrit Code Review: "
 
-    if test 1 = "$NO_START" ; then 
+    if test 1 = "$NO_START" ; then
       echo "Not starting gerrit - NO_START=1 in /etc/default/gerritcodereview"
       exit 0
     fi
@@ -365,22 +378,22 @@
           echo >&2 "fatal: start-stop-daemon failed"
           rc=1
         fi
-        exit $rc 
+        exit $rc
       fi
     else
       if test -f "$GERRIT_PID" ; then
         if running "$GERRIT_PID" ; then
           echo "Already Running!!"
-          exit 1
+          exit 0
         else
           rm -f "$GERRIT_PID" "$GERRIT_RUN"
         fi
       fi
 
-      if test $UID = 0 -a -n "$GERRIT_USER" ; then 
+      if test $UID = 0 -a -n "$GERRIT_USER" ; then
         touch "$GERRIT_PID"
         chown $GERRIT_USER "$GERRIT_PID"
-        su - $GERRIT_USER -c "
+        su - $GERRIT_USER -s /bin/sh -c "
           JAVA='$JAVA' ; export JAVA ;
           $RUN_EXEC $RUN_Arg1 '$RUN_Arg2' $RUN_Arg3 $RUN_ARGS </dev/null >/dev/null 2>&1 &
           PID=\$! ;
@@ -426,7 +439,7 @@
 
     if test 1 = "$START_STOP_DAEMON" && type start-stop-daemon >/dev/null 2>&1
     then
-      start-stop-daemon -K -p "$GERRIT_PID" -s HUP 
+      start-stop-daemon -K -p "$GERRIT_PID" -s HUP
       sleep 1
       if running "$GERRIT_PID" ; then
         sleep 3
@@ -458,8 +471,13 @@
     if test -f "$GERRIT_SH" ; then
       : OK
     else
-      echo >&2 "** ERROR: Cannot locate gerrit.sh"
-      exit 1
+      GERRIT_SH="$INITIAL_DIR/$GERRIT_SH"
+      if test -f "$GERRIT_SH" ; then
+        : OK
+      else
+        echo >&2 "** ERROR: Cannot locate gerrit.sh"
+        exit 1
+      fi
     fi
     $GERRIT_SH stop $*
     sleep 5
@@ -480,7 +498,7 @@
     if test -f "$GERRIT_PID" ; then
         if running "$GERRIT_PID" ; then
           echo "Already Running!!"
-          exit 1
+          exit 0
         else
           rm -f "$GERRIT_PID"
         fi
@@ -489,7 +507,7 @@
     exec "$RUN_EXEC" $RUN_Arg1 "$RUN_Arg2" $RUN_Arg3 $RUN_ARGS --console-log
   ;;
 
-  check)
+  check|status)
     echo "Checking arguments to Gerrit Code Review:"
     echo "  GERRIT_SITE     =  $GERRIT_SITE"
     echo "  GERRIT_CONFIG   =  $GERRIT_CONFIG"
@@ -510,7 +528,7 @@
             exit 0
         fi
     fi
-    exit 1
+    exit 3
   ;;
 
   *)
diff --git a/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/libraries.config b/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/libraries.config
index b99267c..f1ecadd 100644
--- a/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/libraries.config
+++ b/gerrit-pgm/src/main/resources/com/google/gerrit/pgm/libraries.config
@@ -17,8 +17,10 @@
   name = Bouncy Castle Crypto v144
   url = http://www.bouncycastle.org/download/bcprov-jdk16-144.jar
   sha1 = 6327a5f7a3dc45e0fd735adb5d08c5a74c05c20c
+  remove = bcprov-.*[.]jar
 
 [library "mysqlDriver"]
-  name = MySQL Connector/J 5.1.10
-  url = http://repo2.maven.org/maven2/mysql/mysql-connector-java/5.1.10/mysql-connector-java-5.1.10.jar
-  sha1 = b83574124f1a00d6f70d56ba64aa52b8e1588e6d
+  name = MySQL Connector/J 5.1.21
+  url = http://repo2.maven.org/maven2/mysql/mysql-connector-java/5.1.21/mysql-connector-java-5.1.21.jar
+  sha1 = 7abbd19fc2e2d5b92c0895af8520f7fa30266be9
+  remove = mysql-connector-java-.*[.]jar
diff --git a/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/LibrariesTest.java b/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/LibrariesTest.java
index e723463..df1f447 100644
--- a/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/LibrariesTest.java
+++ b/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/LibrariesTest.java
@@ -30,16 +30,14 @@
 public class LibrariesTest extends TestCase {
   public void testCreate() throws FileNotFoundException {
     final SitePaths site = new SitePaths(new File("."));
-    final ReloadSiteLibrary reload = createStrictMock(ReloadSiteLibrary.class);
     final ConsoleUI ui = createStrictMock(ConsoleUI.class);
 
     replay(ui);
-    replay(reload);
 
     Libraries lib = new Libraries(new Provider<LibraryDownloader>() {
       @Override
       public LibraryDownloader get() {
-        return new LibraryDownloader(reload, ui, site);
+        return new LibraryDownloader(ui, site);
       }
     });
 
@@ -47,6 +45,5 @@
     assertNotNull(lib.mysqlDriver);
 
     verify(ui);
-    verify(reload);
   }
 }
diff --git a/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_xTest.java b/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_xTest.java
index 493a440..86f0260 100644
--- a/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_xTest.java
+++ b/gerrit-pgm/src/test/java/com/google/gerrit/pgm/init/UpgradeFrom2_0_xTest.java
@@ -72,8 +72,8 @@
     final ConsoleUI ui = createStrictMock(ConsoleUI.class);
     Section.Factory sections = new Section.Factory() {
       @Override
-      public Section get(String name) {
-        return new Section(flags, site, ui, name);
+      public Section get(String name, String subsection) {
+        return new Section(flags, site, ui, name, subsection);
       }
     };
 
diff --git a/gerrit-plugin-api/pom.xml b/gerrit-plugin-api/pom.xml
index 84f6f7b..81a50984 100644
--- a/gerrit-plugin-api/pom.xml
+++ b/gerrit-plugin-api/pom.xml
@@ -22,7 +22,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.5-SNAPSHOT</version>
+    <version>2.6-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-plugin-api</artifactId>
@@ -46,6 +46,18 @@
     </dependency>
 
     <dependency>
+      <groupId>com.google.gerrit</groupId>
+      <artifactId>gerrit-pgm</artifactId>
+      <version>${project.version}</version>
+      <exclusions>
+        <exclusion>
+          <groupId>org.eclipse.jetty</groupId>
+          <artifactId>jetty-servlet</artifactId>
+        </exclusion>
+      </exclusions>
+    </dependency>
+
+    <dependency>
       <groupId>org.apache.tomcat</groupId>
       <artifactId>servlet-api</artifactId>
     </dependency>
@@ -66,8 +78,8 @@
               <exclude>com.google.gerrit:gerrit-patch-commonsnet</exclude>
               <exclude>com.google.gerrit:gerrit-patch-jgit</exclude>
               <exclude>com.google.gerrit:gerrit-util-ssl</exclude>
-              <exclude>com.google.gerrit:juniversalchardet</exclude>
 
+              <exclude>com.googlecode.juniversalchardet:juniversalchardet</exclude>
               <exclude>com.googlecode.prolog-cafe:PrologCafe</exclude>
               <exclude>org.slf4j:slf4j-log4j12</exclude>
               <exclude>log4j:log4j</exclude>
diff --git a/gerrit-plugin-archetype/pom.xml b/gerrit-plugin-archetype/pom.xml
index dd1794b..71feabc 100644
--- a/gerrit-plugin-archetype/pom.xml
+++ b/gerrit-plugin-archetype/pom.xml
@@ -21,7 +21,7 @@
   <parent>
     <groupId>com.google.gerrit</groupId>
     <artifactId>gerrit-parent</artifactId>
-    <version>2.5-SNAPSHOT</version>
+    <version>2.6-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-plugin-archetype</artifactId>
diff --git a/gerrit-plugin-gwt-archetype/.gitignore b/gerrit-plugin-gwt-archetype/.gitignore
new file mode 100644
index 0000000..7075a2f
--- /dev/null
+++ b/gerrit-plugin-gwt-archetype/.gitignore
@@ -0,0 +1,4 @@
+/target
+/.classpath
+/.project
+/.settings
diff --git a/gerrit-plugin-gwt-archetype/pom.xml b/gerrit-plugin-gwt-archetype/pom.xml
new file mode 100644
index 0000000..91cd142
--- /dev/null
+++ b/gerrit-plugin-gwt-archetype/pom.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2012 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>com.google.gerrit</groupId>
+    <artifactId>gerrit-parent</artifactId>
+    <version>2.6-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>gerrit-plugin-gwt-archetype</artifactId>
+  <name>Gerrit Code Review - Web Ui GWT Plugin Archetype</name>
+
+  <properties>
+    <defaultGerritApiVersion>${project.version}</defaultGerritApiVersion>
+  </properties>
+
+  <build>
+    <resources>
+      <resource>
+        <directory>src/main/resources</directory>
+        <filtering>true</filtering>
+        <includes>
+          <include>META-INF/maven/archetype-metadata.xml</include>
+        </includes>
+      </resource>
+      <resource>
+        <directory>src/main/resources</directory>
+        <filtering>false</filtering>
+        <excludes>
+          <exclude>META-INF/maven/archetype-metadata.xml</exclude>
+        </excludes>
+      </resource>
+    </resources>
+  </build>
+
+</project>
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml b/gerrit-plugin-gwt-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml
new file mode 100644
index 0000000..78f2941
--- /dev/null
+++ b/gerrit-plugin-gwt-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml
@@ -0,0 +1,56 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2012 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<archetype-descriptor name="Gerrit Plugin">
+  <requiredProperties>
+    <requiredProperty key="pluginName"/>
+
+    <requiredProperty key="Implementation-Vendor"/>
+    <requiredProperty key="Implementation-Url"/>
+    <requiredProperty key="Gwt-Version"/>
+
+    <requiredProperty key="gerritApiVersion">
+      <defaultValue>${defaultGerritApiVersion}</defaultValue>
+    </requiredProperty>
+  </requiredProperties>
+
+  <fileSets>
+    <fileSet filtered="true" packaged="true">
+      <directory>src/main/java</directory>
+      <includes>
+        <include>**/*.css</include>
+        <include>**/*.png</include>
+        <include>**/*.java</include>
+        <include>**/*.gwt.xml</include>
+      </includes>
+    </fileSet>
+
+    <fileSet filtered="true">
+      <directory>src/main/resources/Documentation</directory>
+      <includes>
+        <include>**/*.md</include>
+      </includes>
+    </fileSet>
+
+    <fileSet>
+      <directory></directory>
+      <includes>
+        <include>.gitignore</include>
+        <include>LICENSE</include>
+      </includes>
+    </fileSet>
+  </fileSets>
+</archetype-descriptor>
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/.gitignore b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/.gitignore
new file mode 100644
index 0000000..80d6257
--- /dev/null
+++ b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/.gitignore
@@ -0,0 +1,5 @@
+/target
+/.classpath
+/.project
+/.settings/org.maven.ide.eclipse.prefs
+/.settings/org.eclipse.m2e.core.prefs
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/LICENSE b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/LICENSE
new file mode 100644
index 0000000..11069ed
--- /dev/null
+++ b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/LICENSE
@@ -0,0 +1,201 @@
+                              Apache License
+                        Version 2.0, January 2004
+                     http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+   "License" shall mean the terms and conditions for use, reproduction,
+   and distribution as defined by Sections 1 through 9 of this document.
+
+   "Licensor" shall mean the copyright owner or entity authorized by
+   the copyright owner that is granting the License.
+
+   "Legal Entity" shall mean the union of the acting entity and all
+   other entities that control, are controlled by, or are under common
+   control with that entity. For the purposes of this definition,
+   "control" means (i) the power, direct or indirect, to cause the
+   direction or management of such entity, whether by contract or
+   otherwise, or (ii) ownership of fifty percent (50%) or more of the
+   outstanding shares, or (iii) beneficial ownership of such entity.
+
+   "You" (or "Your") shall mean an individual or Legal Entity
+   exercising permissions granted by this License.
+
+   "Source" form shall mean the preferred form for making modifications,
+   including but not limited to software source code, documentation
+   source, and configuration files.
+
+   "Object" form shall mean any form resulting from mechanical
+   transformation or translation of a Source form, including but
+   not limited to compiled object code, generated documentation,
+   and conversions to other media types.
+
+   "Work" shall mean the work of authorship, whether in Source or
+   Object form, made available under the License, as indicated by a
+   copyright notice that is included in or attached to the work
+   (an example is provided in the Appendix below).
+
+   "Derivative Works" shall mean any work, whether in Source or Object
+   form, that is based on (or derived from) the Work and for which the
+   editorial revisions, annotations, elaborations, or other modifications
+   represent, as a whole, an original work of authorship. For the purposes
+   of this License, Derivative Works shall not include works that remain
+   separable from, or merely link (or bind by name) to the interfaces of,
+   the Work and Derivative Works thereof.
+
+   "Contribution" shall mean any work of authorship, including
+   the original version of the Work and any modifications or additions
+   to that Work or Derivative Works thereof, that is intentionally
+   submitted to Licensor for inclusion in the Work by the copyright owner
+   or by an individual or Legal Entity authorized to submit on behalf of
+   the copyright owner. For the purposes of this definition, "submitted"
+   means any form of electronic, verbal, or written communication sent
+   to the Licensor or its representatives, including but not limited to
+   communication on electronic mailing lists, source code control systems,
+   and issue tracking systems that are managed by, or on behalf of, the
+   Licensor for the purpose of discussing and improving the Work, but
+   excluding communication that is conspicuously marked or otherwise
+   designated in writing by the copyright owner as "Not a Contribution."
+
+   "Contributor" shall mean Licensor and any individual or Legal Entity
+   on behalf of whom a Contribution has been received by Licensor and
+   subsequently incorporated within the Work.
+
+2. Grant of Copyright License. Subject to the terms and conditions of
+   this License, each Contributor hereby grants to You a perpetual,
+   worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+   copyright license to reproduce, prepare Derivative Works of,
+   publicly display, publicly perform, sublicense, and distribute the
+   Work and such Derivative Works in Source or Object form.
+
+3. Grant of Patent License. Subject to the terms and conditions of
+   this License, each Contributor hereby grants to You a perpetual,
+   worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+   (except as stated in this section) patent license to make, have made,
+   use, offer to sell, sell, import, and otherwise transfer the Work,
+   where such license applies only to those patent claims licensable
+   by such Contributor that are necessarily infringed by their
+   Contribution(s) alone or by combination of their Contribution(s)
+   with the Work to which such Contribution(s) was submitted. If You
+   institute patent litigation against any entity (including a
+   cross-claim or counterclaim in a lawsuit) alleging that the Work
+   or a Contribution incorporated within the Work constitutes direct
+   or contributory patent infringement, then any patent licenses
+   granted to You under this License for that Work shall terminate
+   as of the date such litigation is filed.
+
+4. Redistribution. You may reproduce and distribute copies of the
+   Work or Derivative Works thereof in any medium, with or without
+   modifications, and in Source or Object form, provided that You
+   meet the following conditions:
+
+   (a) You must give any other recipients of the Work or
+       Derivative Works a copy of this License; and
+
+   (b) You must cause any modified files to carry prominent notices
+       stating that You changed the files; and
+
+   (c) You must retain, in the Source form of any Derivative Works
+       that You distribute, all copyright, patent, trademark, and
+       attribution notices from the Source form of the Work,
+       excluding those notices that do not pertain to any part of
+       the Derivative Works; and
+
+   (d) If the Work includes a "NOTICE" text file as part of its
+       distribution, then any Derivative Works that You distribute must
+       include a readable copy of the attribution notices contained
+       within such NOTICE file, excluding those notices that do not
+       pertain to any part of the Derivative Works, in at least one
+       of the following places: within a NOTICE text file distributed
+       as part of the Derivative Works; within the Source form or
+       documentation, if provided along with the Derivative Works; or,
+       within a display generated by the Derivative Works, if and
+       wherever such third-party notices normally appear. The contents
+       of the NOTICE file are for informational purposes only and
+       do not modify the License. You may add Your own attribution
+       notices within Derivative Works that You distribute, alongside
+       or as an addendum to the NOTICE text from the Work, provided
+       that such additional attribution notices cannot be construed
+       as modifying the License.
+
+   You may add Your own copyright statement to Your modifications and
+   may provide additional or different license terms and conditions
+   for use, reproduction, or distribution of Your modifications, or
+   for any such Derivative Works as a whole, provided Your use,
+   reproduction, and distribution of the Work otherwise complies with
+   the conditions stated in this License.
+
+5. Submission of Contributions. Unless You explicitly state otherwise,
+   any Contribution intentionally submitted for inclusion in the Work
+   by You to the Licensor shall be under the terms and conditions of
+   this License, without any additional terms or conditions.
+   Notwithstanding the above, nothing herein shall supersede or modify
+   the terms of any separate license agreement you may have executed
+   with Licensor regarding such Contributions.
+
+6. Trademarks. This License does not grant permission to use the trade
+   names, trademarks, service marks, or product names of the Licensor,
+   except as required for reasonable and customary use in describing the
+   origin of the Work and reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty. Unless required by applicable law or
+   agreed to in writing, Licensor provides the Work (and each
+   Contributor provides its Contributions) on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+   implied, including, without limitation, any warranties or conditions
+   of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+   PARTICULAR PURPOSE. You are solely responsible for determining the
+   appropriateness of using or redistributing the Work and assume any
+   risks associated with Your exercise of permissions under this License.
+
+8. Limitation of Liability. In no event and under no legal theory,
+   whether in tort (including negligence), contract, or otherwise,
+   unless required by applicable law (such as deliberate and grossly
+   negligent acts) or agreed to in writing, shall any Contributor be
+   liable to You for damages, including any direct, indirect, special,
+   incidental, or consequential damages of any character arising as a
+   result of this License or out of the use or inability to use the
+   Work (including but not limited to damages for loss of goodwill,
+   work stoppage, computer failure or malfunction, or any and all
+   other commercial damages or losses), even if such Contributor
+   has been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability. While redistributing
+   the Work or Derivative Works thereof, You may choose to offer,
+   and charge a fee for, acceptance of support, warranty, indemnity,
+   or other liability obligations and/or rights consistent with this
+   License. However, in accepting such obligations, You may act only
+   on Your own behalf and on Your sole responsibility, not on behalf
+   of any other Contributor, and only if You agree to indemnify,
+   defend, and hold each Contributor harmless for any liability
+   incurred by, or claims asserted against, such Contributor by reason
+   of your accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+APPENDIX: How to apply the Apache License to your work.
+
+   To apply the Apache License to your work, attach the following
+   boilerplate notice, with the fields enclosed by brackets "[]"
+   replaced with your own identifying information. (Don't include
+   the brackets!)  The text should be enclosed in the appropriate
+   comment syntax for the file format. We also recommend that a
+   file or class name and description of purpose be included on the
+   same "printed page" as the copyright notice for easier
+   identification within third-party archives.
+
+Copyright [yyyy] [name of copyright owner]
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/pom.xml b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/pom.xml
new file mode 100644
index 0000000..6e1ba21
--- /dev/null
+++ b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/pom.xml
@@ -0,0 +1,129 @@
+<!--
+Copyright (C) 2012 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <groupId>${groupId}</groupId>
+  <artifactId>${artifactId}</artifactId>
+  <packaging>jar</packaging>
+  <version>${version}</version>
+  <name>${pluginName}</name>
+
+  <properties>
+    <Gerrit-ApiType>extension</Gerrit-ApiType>
+    <Gerrit-ApiVersion>${gerritApiVersion}</Gerrit-ApiVersion>
+  </properties>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-jar-plugin</artifactId>
+        <version>2.4</version>
+        <configuration>
+          <includes>
+            <include>**/*.*</include>
+          </includes>
+          <archive>
+            <manifestEntries>
+              <Implementation-Vendor>${Implementation-Vendor}</Implementation-Vendor>
+              <Implementation-URL>${Implementation-Url}</Implementation-URL>
+
+              <Implementation-Title>${Gerrit-ApiType} ${project.artifactId}</Implementation-Title>
+              <Implementation-Version>${project.version}</Implementation-Version>
+
+              <Gerrit-ApiType>${Gerrit-ApiType}</Gerrit-ApiType>
+              <Gerrit-ApiVersion>${Gerrit-ApiVersion}</Gerrit-ApiVersion>
+            </manifestEntries>
+          </archive>
+        </configuration>
+      </plugin>
+
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-compiler-plugin</artifactId>
+        <version>2.3.2</version>
+        <configuration>
+          <source>1.6</source>
+          <target>1.6</target>
+          <encoding>UTF-8</encoding>
+        </configuration>
+      </plugin>
+
+      <plugin>
+        <groupId>org.codehaus.mojo</groupId>
+        <artifactId>gwt-maven-plugin</artifactId>
+        <version>${Gwt-Version}</version>
+        <configuration>
+          <module>${package}.HelloPlugins</module>
+          <disableClassMetadata>true</disableClassMetadata>
+          <disableCastChecking>true</disableCastChecking>
+          <webappDirectory>${project.build.directory}/classes/static</webappDirectory>
+        </configuration>
+        <executions>
+          <execution>
+            <goals>
+              <goal>compile</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+
+    </plugins>
+  </build>
+
+  <dependencies>
+    <dependency>
+      <groupId>com.google.gerrit</groupId>
+      <artifactId>gerrit-${Gerrit-ApiType}-api</artifactId>
+      <version>${Gerrit-ApiVersion}</version>
+      <scope>provided</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>com.google.gerrit</groupId>
+      <artifactId>gerrit-plugin-gwtui</artifactId>
+      <version>${Gerrit-ApiVersion}</version>
+    </dependency>
+
+    <dependency>
+      <groupId>com.google.gwt</groupId>
+      <artifactId>gwt-user</artifactId>
+      <version>${Gwt-Version}</version>
+      <scope>provided</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <version>4.8.1</version>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+
+  <repositories>
+    <repository>
+      <id>gerrit-api-repository</id>
+#if ($gerritApiVersion.endsWith("SNAPSHOT"))
+      <url>https://gerrit-api.commondatastorage.googleapis.com/snapshot/</url>
+#else
+      <url>https://gerrit-api.commondatastorage.googleapis.com/release/</url>
+#end
+    </repository>
+  </repositories>
+</project>
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/HelloPlugins.gwt.xml b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/HelloPlugins.gwt.xml
new file mode 100644
index 0000000..4c70fcc
--- /dev/null
+++ b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/HelloPlugins.gwt.xml
@@ -0,0 +1,29 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+ Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<module rename-to="hello_gwt_plugins">
+  <!-- Inherit the core Web Toolkit stuff.                        -->
+  <inherits name="com.google.gwt.user.User"/>
+  <!-- Other module inherits                                      -->
+  <inherits name="com.google.gerrit.Plugin"/>
+  <inherits name="com.google.gwt.http.HTTP"/>
+  <!-- Using GWT built-in themes adds a number of static          -->
+  <!-- resources to the plugin. No theme inherits lines were      -->
+  <!-- added in order to make this plugin as simple as possible   -->
+  <!-- Specify the app entry point class.                         -->
+  <entry-point class="${package}.client.HelloPlugins"/>
+  <stylesheet src="hello.css"/>
+</module>
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/MyExtension.java b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/MyExtension.java
new file mode 100644
index 0000000..ebdbb26
--- /dev/null
+++ b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/MyExtension.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2012 Google
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package ${package};
+
+import com.google.gerrit.extensions.webui.GwtPlugin;
+import com.google.gerrit.extensions.annotations.Listen;
+
+@Listen
+public class MyExtension extends GwtPlugin {
+  public MyExtension() {
+    super("hello_gwt_plugins");
+  }
+}
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/client/HelloPlugins.java b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/client/HelloPlugins.java
new file mode 100644
index 0000000..5584d85
--- /dev/null
+++ b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/client/HelloPlugins.java
@@ -0,0 +1,72 @@
+// Copyright (C) 2012 Google Inc
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package ${package}.client;
+
+import com.google.gerrit.client.Plugin;
+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.DialogBox;
+import com.google.gwt.user.client.ui.Image;
+import com.google.gwt.user.client.ui.RootPanel;
+import com.google.gwt.user.client.ui.VerticalPanel;
+
+/**
+ * HelloWorld Plugins.
+ */
+public class HelloPlugins extends Plugin {
+
+  @Override
+  public void onModuleLoad() {
+    Image img = new Image("http://code.google.com/webtoolkit/logo-185x175.png");
+    Button button = new Button("Click me");
+
+    VerticalPanel vPanel = new VerticalPanel();
+    vPanel.setWidth("100%");
+    vPanel.setHorizontalAlignment(VerticalPanel.ALIGN_CENTER);
+    vPanel.add(img);
+    vPanel.add(button);
+
+    RootPanel.get().add(vPanel);
+
+    // Create the dialog box
+    final DialogBox dialogBox = new DialogBox();
+
+    // The content of the dialog comes from a User specified Preference
+    dialogBox.setText("Hello from GWT Gerrit UI plugin");
+    dialogBox.setAnimationEnabled(true);
+    Button closeButton = new Button("Close");
+    VerticalPanel dialogVPanel = new VerticalPanel();
+    dialogVPanel.setWidth("100%");
+    dialogVPanel.setHorizontalAlignment(VerticalPanel.ALIGN_CENTER);
+    dialogVPanel.add(closeButton);
+
+    closeButton.addClickHandler(new ClickHandler() {
+      public void onClick(ClickEvent event) {
+        dialogBox.hide();
+      }
+    });
+
+    // Set the contents of the Widget
+    dialogBox.setWidget(dialogVPanel);
+
+    button.addClickHandler(new ClickHandler() {
+      public void onClick(ClickEvent event) {
+        dialogBox.center();
+        dialogBox.show();
+      }
+    });
+  }
+}
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/public/gwt-hello-gadgets-igoogle-thumb.png b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/public/gwt-hello-gadgets-igoogle-thumb.png
new file mode 100644
index 0000000..e970774
--- /dev/null
+++ b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/public/gwt-hello-gadgets-igoogle-thumb.png
Binary files differ
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/public/gwt-hello-gadgets-igoogle.png b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/public/gwt-hello-gadgets-igoogle.png
new file mode 100644
index 0000000..c041149
--- /dev/null
+++ b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/public/gwt-hello-gadgets-igoogle.png
Binary files differ
diff --git a/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/public/hello.css b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/public/hello.css
new file mode 100644
index 0000000..a88059d
--- /dev/null
+++ b/gerrit-plugin-gwt-archetype/src/main/resources/archetype-resources/src/main/java/public/hello.css
@@ -0,0 +1,103 @@
+/**
+ * The file contains styles for a sample plugin derived from the
+ * GWT standard theme.
+ * Images in the stylesheet have been removed, as well as styles for widgets
+ * not currently in use.
+ */
+
+body, table td, select {
+  font-family: Arial Unicode MS, Arial, sans-serif;
+  font-size: small;
+}
+pre {
+  font-family: "courier new", courier;
+  font-size: small;
+}
+body {
+  color: black;
+  margin: 0px;
+  border: 0px;
+  padding: 0px;
+  background: #fff;
+  direction: ltr;
+}
+a, a:visited, a:hover {
+  color: #0000AA;
+}
+
+/**
+ * The reference theme can be used to determine when this style sheet has
+ * loaded.  Create a hidden div element with absolute position, assign the style
+ * name below, and attach it to the DOM.  Use a timer to detect when the
+ * element's height and width are set to 5px.
+ */
+.gwt-Reference-standard {
+  height: 5px;
+  width: 5px;
+  zoom: 1;
+}
+
+.gwt-Button {
+  margin: 0;
+  padding: 3px 5px;
+  text-decoration: none;
+  font-size: small;
+  cursor: pointer;
+  cursor: hand;
+  border: 1px outset #ccc;
+}
+.gwt-Button:active {
+  border: 1px inset #ccc;
+}
+.gwt-Button:hover {
+  border-color: #9cf #69e #69e #7af;
+}
+.gwt-Button[disabled] {
+  cursor: default;
+  color: #888;
+}
+.gwt-Button[disabled]:hover {
+  border: 1px outset #ccc;
+}
+
+.gwt-DialogBox .Caption {
+  background: #e3e8f3;
+  padding: 4px 4px 4px 8px;
+  cursor: default;
+  border-bottom: 1px solid #bbbbbb;
+  border-top: 5px solid #d0e4f6;
+  border-left: 5px solid #d0e4f6;
+  border-right: 5px solid #d0e4f6;
+}
+
+.gwt-DialogBox .dialogContent {
+}
+
+.gwt-DialogBox .dialogMiddleCenter {
+  padding: 3px;
+  background: white;
+  border-left: 5px solid #d0e4f6;
+  border-right: 5px solid #d0e4f6;
+  border-bottom: 5px solid #d0e4f6;
+}
+
+.gwt-DialogBox .dialogTopLeftInner {
+  width: 5px;
+  zoom: 1;
+}
+.gwt-DialogBox .dialogTopRightInner {
+  width: 8px;
+  zoom: 1;
+}
+.gwt-DialogBox .dialogBottomLeftInner {
+  width: 5px;
+  height: 8px;
+  zoom: 1;
+}
+
+.gwt-DialogBox .dialogBottomRightInner {
+  width: 5px;
+  height: 8px;
+  zoom: 1;
+}
+
diff --git a/gerrit-plugin-gwtui/.gitignore b/gerrit-plugin-gwtui/.gitignore
new file mode 100644
index 0000000..2dcf1ed
--- /dev/null
+++ b/gerrit-plugin-gwtui/.gitignore
@@ -0,0 +1,6 @@
+/target
+/.classpath
+/.project
+/.settings/org.maven.ide.eclipse.prefs
+/.settings/org.eclipse.m2e.core.prefs
+/gerrit-gwtui-plugin.iml
diff --git a/gerrit-plugin-gwtui/.settings/org.eclipse.core.resources.prefs b/gerrit-plugin-gwtui/.settings/org.eclipse.core.resources.prefs
new file mode 100644
index 0000000..839d647
--- /dev/null
+++ b/gerrit-plugin-gwtui/.settings/org.eclipse.core.resources.prefs
@@ -0,0 +1,5 @@
+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-plugin-gwtui/.settings/org.eclipse.jdt.core.prefs b/gerrit-plugin-gwtui/.settings/org.eclipse.jdt.core.prefs
new file mode 100644
index 0000000..955e208
--- /dev/null
+++ b/gerrit-plugin-gwtui/.settings/org.eclipse.jdt.core.prefs
@@ -0,0 +1,295 @@
+eclipse.preferences.version=1
+org.eclipse.jdt.core.codeComplete.argumentPrefixes=
+org.eclipse.jdt.core.codeComplete.argumentSuffixes=
+org.eclipse.jdt.core.codeComplete.fieldPrefixes=
+org.eclipse.jdt.core.codeComplete.fieldSuffixes=
+org.eclipse.jdt.core.codeComplete.localPrefixes=
+org.eclipse.jdt.core.codeComplete.localSuffixes=
+org.eclipse.jdt.core.codeComplete.staticFieldPrefixes=
+org.eclipse.jdt.core.codeComplete.staticFieldSuffixes=
+org.eclipse.jdt.core.codeComplete.staticFinalFieldPrefixes=
+org.eclipse.jdt.core.codeComplete.staticFinalFieldSuffixes=
+org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6
+org.eclipse.jdt.core.compiler.compliance=1.6
+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_annotation=0
+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_method_declaration=0
+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_resources_in_try=80
+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.alignment_for_union_type_in_multicatch=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.comment.new_lines_at_block_boundaries=true
+org.eclipse.jdt.core.formatter.comment.new_lines_at_javadoc_boundaries=true
+org.eclipse.jdt.core.formatter.comment.preserve_white_space_between_code_and_line_comments=false
+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.disabling_tag=@formatter\:off
+org.eclipse.jdt.core.formatter.enabling_tag=@formatter\:on
+org.eclipse.jdt.core.formatter.format_guardian_clause_on_one_line=false
+org.eclipse.jdt.core.formatter.format_line_comment_starting_on_first_column=true
+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_field=insert
+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_method=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_annotation_on_package=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_annotation_on_type=insert
+org.eclipse.jdt.core.formatter.insert_new_line_after_label=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_try=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_semicolon_in_try_resources=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_try=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_try=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_semicolon_in_try_resources=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.join_lines_in_comments=true
+org.eclipse.jdt.core.formatter.join_wrapped_lines=true
+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_on_off_tags=false
+org.eclipse.jdt.core.formatter.use_tabs_only_for_leading_indentations=false
+org.eclipse.jdt.core.formatter.wrap_before_binary_operator=true
+org.eclipse.jdt.core.formatter.wrap_before_or_operator_multicatch=true
+org.eclipse.jdt.core.formatter.wrap_outer_expressions_when_nested=true
diff --git a/gerrit-plugin-gwtui/.settings/org.eclipse.jdt.ui.prefs b/gerrit-plugin-gwtui/.settings/org.eclipse.jdt.ui.prefs
new file mode 100644
index 0000000..c018821
--- /dev/null
+++ b/gerrit-plugin-gwtui/.settings/org.eclipse.jdt.ui.prefs
@@ -0,0 +1,7 @@
+eclipse.preferences.version=1
+formatter_profile=_Google Format
+formatter_settings_version=12
+org.eclipse.jdt.ui.exception.name=e
+org.eclipse.jdt.ui.gettersetter.use.is=true
+org.eclipse.jdt.ui.keywordthis=false
+org.eclipse.jdt.ui.overrideannotation=true
diff --git a/gerrit-plugin-gwtui/pom.xml b/gerrit-plugin-gwtui/pom.xml
new file mode 100644
index 0000000..1460870
--- /dev/null
+++ b/gerrit-plugin-gwtui/pom.xml
@@ -0,0 +1,125 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2012 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+        xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>com.google.gerrit</groupId>
+    <artifactId>gerrit-parent</artifactId>
+    <version>2.6-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>gerrit-plugin-gwtui</artifactId>
+  <name>Gerrit Code Review - Plugin GWT UI</name>
+
+  <description>
+    API for UI plugins to build with GWT and integrate with Gerrit
+  </description>
+
+  <dependencies>
+    <dependency>
+      <groupId>com.google.gwt</groupId>
+      <artifactId>gwt-user</artifactId>
+    </dependency>
+
+    <dependency>
+      <groupId>com.google.gwt</groupId>
+      <artifactId>gwt-dev</artifactId>
+    </dependency>
+  </dependencies>
+
+   <build>
+     <pluginManagement>
+       <plugins>
+         <plugin>
+           <groupId>org.eclipse.m2e</groupId>
+           <artifactId>lifecycle-mapping</artifactId>
+           <version>1.0.0</version>
+           <configuration>
+             <lifecycleMappingMetadata>
+               <pluginExecutions>
+                 <pluginExecution>
+                   <pluginExecutionFilter>
+                     <groupId>org.codehaus.mojo</groupId>
+                     <artifactId>gwt-maven-plugin</artifactId>
+                     <versionRange>[2.5.0,)</versionRange>
+                     <goals>
+                       <goal>resources</goal>
+                       <goal>compile</goal>
+                       <goal>i18n</goal>
+                       <goal>generateAsync</goal>
+                     </goals>
+                   </pluginExecutionFilter>
+                   <action>
+                     <execute />
+                   </action>
+                 </pluginExecution>
+                 <pluginExecution>
+                   <pluginExecutionFilter>
+                     <groupId>org.apache.maven.plugins</groupId>
+                     <artifactId>maven-war-plugin</artifactId>
+                     <versionRange>[2.1.1,)</versionRange>
+                     <goals>
+                       <goal>exploded</goal>
+                     </goals>
+                   </pluginExecutionFilter>
+                   <action>
+                     <execute />
+                   </action>
+                 </pluginExecution>
+               </pluginExecutions>
+             </lifecycleMappingMetadata>
+          </configuration>
+        </plugin>
+      </plugins>
+    </pluginManagement>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-source-plugin</artifactId>
+        <executions>
+          <execution>
+            <goals>
+              <goal>jar</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+
+      <plugin>
+        <groupId>org.codehaus.mojo</groupId>
+        <artifactId>gwt-maven-plugin</artifactId>
+        <configuration>
+          <module>com.google.gerrit.Plugin</module>
+          <disableClassMetadata>true</disableClassMetadata>
+          <disableCastChecking>true</disableCastChecking>
+        </configuration>
+        <executions>
+          <execution>
+            <goals>
+              <goal>resources</goal>
+              <goal>compile</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+    </plugins>
+  </build>
+</project>
+
diff --git a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/Plugin.gwt.xml b/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/Plugin.gwt.xml
new file mode 100644
index 0000000..03edf67
--- /dev/null
+++ b/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/Plugin.gwt.xml
@@ -0,0 +1,23 @@
+<!--
+ Copyright (C) 2012 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+<module>
+  <define-linker name="gerrit_plugin" class="com.google.gerrit.linker.GerritPluginLinker"/>
+  <add-linker name="gerrit_plugin"/>
+  <generate-with class="com.google.gerrit.rebind.PluginGenerator">
+    <when-type-assignable class="com.google.gerrit.client.Plugin"/>
+  </generate-with>
+  <source path="client"/>
+</module>
diff --git a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/client/Plugin.java b/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/client/Plugin.java
new file mode 100644
index 0000000..1291b79
--- /dev/null
+++ b/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/client/Plugin.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.client;
+
+import com.google.gwt.core.client.EntryPoint;
+
+/**
+ * Base class for writing Gerrit Web UI plugins
+ *
+ * Writing a plugin:
+ * <ol>
+ * <li>Declare subtype of Plugin</li>
+ * <li>Bind WebUiPlugin to GwtPlugin implementation in Gerrit-Module</li>
+ * </ol>
+ */
+public abstract class Plugin implements EntryPoint {
+}
diff --git a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/linker/GerritPluginLinker.java b/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/linker/GerritPluginLinker.java
new file mode 100644
index 0000000..e50334e
--- /dev/null
+++ b/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/linker/GerritPluginLinker.java
@@ -0,0 +1,31 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.linker;
+
+import com.google.gwt.core.ext.LinkerContext;
+import com.google.gwt.core.linker.CrossSiteIframeLinker;
+
+/** Finalizes the module manifest file with the selection script. */
+public final class GerritPluginLinker extends CrossSiteIframeLinker {
+  @Override
+  public String getDescription() {
+    return "Gerrit GWT UI plugin";
+  }
+
+  @Override
+  protected String getJsComputeUrlForResource(LinkerContext context) {
+    return "com/google/gerrit/linker/computeUrlForPluginResource.js";
+  }
+}
diff --git a/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/rebind/PluginGenerator.java b/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/rebind/PluginGenerator.java
new file mode 100644
index 0000000..37c3e96
--- /dev/null
+++ b/gerrit-plugin-gwtui/src/main/java/com/google/gerrit/rebind/PluginGenerator.java
@@ -0,0 +1,89 @@
+// Copyright (C) 2012 The Android Open Source Project
+// Copyright 2008 Google Inc.
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.rebind;
+
+import java.io.PrintWriter;
+
+import com.google.gwt.core.client.GWT;
+import com.google.gwt.core.ext.Generator;
+import com.google.gwt.core.ext.GeneratorContext;
+import com.google.gwt.core.ext.TreeLogger;
+import com.google.gwt.core.ext.UnableToCompleteException;
+import com.google.gwt.core.ext.typeinfo.JClassType;
+import com.google.gwt.core.ext.typeinfo.TypeOracle;
+import com.google.gwt.user.rebind.ClassSourceFileComposerFactory;
+import com.google.gwt.user.rebind.SourceWriter;
+
+/**
+ * Write the top layer in the Gadget bootstrap sandwich and generate a stub
+ * manifest that will be completed by the linker.
+ *
+ * Based on gwt-gadgets GadgetGenerator class
+ */
+public class PluginGenerator extends Generator {
+  @Override
+  public String generate(TreeLogger logger, GeneratorContext context,
+      String typeName) throws UnableToCompleteException {
+
+    // The TypeOracle knows about all types in the type system
+    TypeOracle typeOracle = context.getTypeOracle();
+
+    // Get a reference to the type that the generator should implement
+    JClassType sourceType = typeOracle.findType(typeName);
+
+    // Ensure that the requested type exists
+    if (sourceType == null) {
+      logger.log(TreeLogger.ERROR, "Could not find requested typeName", null);
+      throw new UnableToCompleteException();
+    }
+
+    // Make sure the Gadget type is correctly defined
+    validateType(logger, sourceType);
+
+    // Pick a name for the generated class to not conflict.
+    String generatedSimpleSourceName = sourceType.getSimpleSourceName()
+        + "PluginImpl";
+
+    // Begin writing the generated source.
+    ClassSourceFileComposerFactory f = new ClassSourceFileComposerFactory(
+        sourceType.getPackage().getName(), generatedSimpleSourceName);
+    f.addImport(GWT.class.getName());
+    f.setSuperclass(typeName);
+
+    // All source gets written through this Writer
+    PrintWriter out = context.tryCreate(logger,
+        sourceType.getPackage().getName(), generatedSimpleSourceName);
+
+    // If an implementation already exists, we don't need to do any work
+    if (out != null) {
+
+      // We really use a SourceWriter since it's convenient
+      SourceWriter sw = f.createSourceWriter(context, out);
+      sw.commit(logger);
+    }
+
+    return f.getCreatedClassName();
+  }
+
+  protected void validateType(TreeLogger logger, JClassType type)
+      throws UnableToCompleteException {
+    if (!type.isDefaultInstantiable()) {
+      logger.log(TreeLogger.ERROR, "Plugin types must be default instantiable",
+          null);
+      throw new UnableToCompleteException();
+    }
+  }
+}
diff --git a/gerrit-plugin-gwtui/src/main/resources/com/google/gerrit/linker/computeUrlForPluginResource.js b/gerrit-plugin-gwtui/src/main/resources/com/google/gerrit/linker/computeUrlForPluginResource.js
new file mode 100644
index 0000000..3e20e94
--- /dev/null
+++ b/gerrit-plugin-gwtui/src/main/resources/com/google/gerrit/linker/computeUrlForPluginResource.js
@@ -0,0 +1,3 @@
+function computeUrlForResource(resource) {
+  return __MODULE_FUNC__.__moduleBase + resource;
+}
diff --git a/gerrit-plugin-js-archetype/.gitignore b/gerrit-plugin-js-archetype/.gitignore
new file mode 100644
index 0000000..7075a2f
--- /dev/null
+++ b/gerrit-plugin-js-archetype/.gitignore
@@ -0,0 +1,4 @@
+/target
+/.classpath
+/.project
+/.settings
diff --git a/gerrit-plugin-js-archetype/pom.xml b/gerrit-plugin-js-archetype/pom.xml
new file mode 100644
index 0000000..1f0f53a
--- /dev/null
+++ b/gerrit-plugin-js-archetype/pom.xml
@@ -0,0 +1,53 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2012 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <parent>
+    <groupId>com.google.gerrit</groupId>
+    <artifactId>gerrit-parent</artifactId>
+    <version>2.6-SNAPSHOT</version>
+  </parent>
+
+  <artifactId>gerrit-plugin-js-archetype</artifactId>
+  <name>Gerrit Code Review - Web UI JavaScript Plugin Archetype</name>
+
+  <properties>
+    <defaultGerritApiVersion>${project.version}</defaultGerritApiVersion>
+  </properties>
+
+  <build>
+    <resources>
+      <resource>
+        <directory>src/main/resources</directory>
+        <filtering>true</filtering>
+        <includes>
+          <include>META-INF/maven/archetype-metadata.xml</include>
+        </includes>
+      </resource>
+      <resource>
+        <directory>src/main/resources</directory>
+        <filtering>false</filtering>
+        <excludes>
+          <exclude>META-INF/maven/archetype-metadata.xml</exclude>
+        </excludes>
+      </resource>
+    </resources>
+  </build>
+
+</project>
diff --git a/gerrit-plugin-js-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml b/gerrit-plugin-js-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml
new file mode 100644
index 0000000..054caae
--- /dev/null
+++ b/gerrit-plugin-js-archetype/src/main/resources/META-INF/maven/archetype-metadata.xml
@@ -0,0 +1,62 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+Copyright (C) 2012 The Android Open Source Project
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+-->
+<archetype-descriptor name="Gerrit Plugin">
+  <requiredProperties>
+    <requiredProperty key="pluginName"/>
+
+    <requiredProperty key="Implementation-Vendor"/>
+    <requiredProperty key="Implementation-Url"/>
+
+    <requiredProperty key="gerritApiType">
+      <defaultValue>js</defaultValue>
+    </requiredProperty>
+    <requiredProperty key="gerritApiVersion">
+      <defaultValue>${defaultGerritApiVersion}</defaultValue>
+    </requiredProperty>
+  </requiredProperties>
+
+  <fileSets>
+    <fileSet filtered="true" packaged="true">
+      <directory>src/main/java</directory>
+      <includes>
+        <include>**/*.java</include>
+      </includes>
+    </fileSet>
+
+    <fileSet>
+      <directory>src/main/js</directory>
+      <includes>
+        <include>**/*.js</include>
+      </includes>
+    </fileSet>
+
+    <fileSet filtered="true">
+      <directory>src/main/resources/Documentation</directory>
+      <includes>
+        <include>**/*.md</include>
+      </includes>
+    </fileSet>
+
+    <fileSet>
+      <directory></directory>
+      <includes>
+        <include>.gitignore</include>
+        <include>LICENSE</include>
+      </includes>
+    </fileSet>
+  </fileSets>
+</archetype-descriptor>
diff --git a/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/.gitignore b/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/.gitignore
new file mode 100644
index 0000000..80d6257
--- /dev/null
+++ b/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/.gitignore
@@ -0,0 +1,5 @@
+/target
+/.classpath
+/.project
+/.settings/org.maven.ide.eclipse.prefs
+/.settings/org.eclipse.m2e.core.prefs
diff --git a/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/LICENSE b/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/LICENSE
new file mode 100644
index 0000000..11069ed
--- /dev/null
+++ b/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/LICENSE
@@ -0,0 +1,201 @@
+                              Apache License
+                        Version 2.0, January 2004
+                     http://www.apache.org/licenses/
+
+TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
+
+1. Definitions.
+
+   "License" shall mean the terms and conditions for use, reproduction,
+   and distribution as defined by Sections 1 through 9 of this document.
+
+   "Licensor" shall mean the copyright owner or entity authorized by
+   the copyright owner that is granting the License.
+
+   "Legal Entity" shall mean the union of the acting entity and all
+   other entities that control, are controlled by, or are under common
+   control with that entity. For the purposes of this definition,
+   "control" means (i) the power, direct or indirect, to cause the
+   direction or management of such entity, whether by contract or
+   otherwise, or (ii) ownership of fifty percent (50%) or more of the
+   outstanding shares, or (iii) beneficial ownership of such entity.
+
+   "You" (or "Your") shall mean an individual or Legal Entity
+   exercising permissions granted by this License.
+
+   "Source" form shall mean the preferred form for making modifications,
+   including but not limited to software source code, documentation
+   source, and configuration files.
+
+   "Object" form shall mean any form resulting from mechanical
+   transformation or translation of a Source form, including but
+   not limited to compiled object code, generated documentation,
+   and conversions to other media types.
+
+   "Work" shall mean the work of authorship, whether in Source or
+   Object form, made available under the License, as indicated by a
+   copyright notice that is included in or attached to the work
+   (an example is provided in the Appendix below).
+
+   "Derivative Works" shall mean any work, whether in Source or Object
+   form, that is based on (or derived from) the Work and for which the
+   editorial revisions, annotations, elaborations, or other modifications
+   represent, as a whole, an original work of authorship. For the purposes
+   of this License, Derivative Works shall not include works that remain
+   separable from, or merely link (or bind by name) to the interfaces of,
+   the Work and Derivative Works thereof.
+
+   "Contribution" shall mean any work of authorship, including
+   the original version of the Work and any modifications or additions
+   to that Work or Derivative Works thereof, that is intentionally
+   submitted to Licensor for inclusion in the Work by the copyright owner
+   or by an individual or Legal Entity authorized to submit on behalf of
+   the copyright owner. For the purposes of this definition, "submitted"
+   means any form of electronic, verbal, or written communication sent
+   to the Licensor or its representatives, including but not limited to
+   communication on electronic mailing lists, source code control systems,
+   and issue tracking systems that are managed by, or on behalf of, the
+   Licensor for the purpose of discussing and improving the Work, but
+   excluding communication that is conspicuously marked or otherwise
+   designated in writing by the copyright owner as "Not a Contribution."
+
+   "Contributor" shall mean Licensor and any individual or Legal Entity
+   on behalf of whom a Contribution has been received by Licensor and
+   subsequently incorporated within the Work.
+
+2. Grant of Copyright License. Subject to the terms and conditions of
+   this License, each Contributor hereby grants to You a perpetual,
+   worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+   copyright license to reproduce, prepare Derivative Works of,
+   publicly display, publicly perform, sublicense, and distribute the
+   Work and such Derivative Works in Source or Object form.
+
+3. Grant of Patent License. Subject to the terms and conditions of
+   this License, each Contributor hereby grants to You a perpetual,
+   worldwide, non-exclusive, no-charge, royalty-free, irrevocable
+   (except as stated in this section) patent license to make, have made,
+   use, offer to sell, sell, import, and otherwise transfer the Work,
+   where such license applies only to those patent claims licensable
+   by such Contributor that are necessarily infringed by their
+   Contribution(s) alone or by combination of their Contribution(s)
+   with the Work to which such Contribution(s) was submitted. If You
+   institute patent litigation against any entity (including a
+   cross-claim or counterclaim in a lawsuit) alleging that the Work
+   or a Contribution incorporated within the Work constitutes direct
+   or contributory patent infringement, then any patent licenses
+   granted to You under this License for that Work shall terminate
+   as of the date such litigation is filed.
+
+4. Redistribution. You may reproduce and distribute copies of the
+   Work or Derivative Works thereof in any medium, with or without
+   modifications, and in Source or Object form, provided that You
+   meet the following conditions:
+
+   (a) You must give any other recipients of the Work or
+       Derivative Works a copy of this License; and
+
+   (b) You must cause any modified files to carry prominent notices
+       stating that You changed the files; and
+
+   (c) You must retain, in the Source form of any Derivative Works
+       that You distribute, all copyright, patent, trademark, and
+       attribution notices from the Source form of the Work,
+       excluding those notices that do not pertain to any part of
+       the Derivative Works; and
+
+   (d) If the Work includes a "NOTICE" text file as part of its
+       distribution, then any Derivative Works that You distribute must
+       include a readable copy of the attribution notices contained
+       within such NOTICE file, excluding those notices that do not
+       pertain to any part of the Derivative Works, in at least one
+       of the following places: within a NOTICE text file distributed
+       as part of the Derivative Works; within the Source form or
+       documentation, if provided along with the Derivative Works; or,
+       within a display generated by the Derivative Works, if and
+       wherever such third-party notices normally appear. The contents
+       of the NOTICE file are for informational purposes only and
+       do not modify the License. You may add Your own attribution
+       notices within Derivative Works that You distribute, alongside
+       or as an addendum to the NOTICE text from the Work, provided
+       that such additional attribution notices cannot be construed
+       as modifying the License.
+
+   You may add Your own copyright statement to Your modifications and
+   may provide additional or different license terms and conditions
+   for use, reproduction, or distribution of Your modifications, or
+   for any such Derivative Works as a whole, provided Your use,
+   reproduction, and distribution of the Work otherwise complies with
+   the conditions stated in this License.
+
+5. Submission of Contributions. Unless You explicitly state otherwise,
+   any Contribution intentionally submitted for inclusion in the Work
+   by You to the Licensor shall be under the terms and conditions of
+   this License, without any additional terms or conditions.
+   Notwithstanding the above, nothing herein shall supersede or modify
+   the terms of any separate license agreement you may have executed
+   with Licensor regarding such Contributions.
+
+6. Trademarks. This License does not grant permission to use the trade
+   names, trademarks, service marks, or product names of the Licensor,
+   except as required for reasonable and customary use in describing the
+   origin of the Work and reproducing the content of the NOTICE file.
+
+7. Disclaimer of Warranty. Unless required by applicable law or
+   agreed to in writing, Licensor provides the Work (and each
+   Contributor provides its Contributions) on an "AS IS" BASIS,
+   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
+   implied, including, without limitation, any warranties or conditions
+   of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
+   PARTICULAR PURPOSE. You are solely responsible for determining the
+   appropriateness of using or redistributing the Work and assume any
+   risks associated with Your exercise of permissions under this License.
+
+8. Limitation of Liability. In no event and under no legal theory,
+   whether in tort (including negligence), contract, or otherwise,
+   unless required by applicable law (such as deliberate and grossly
+   negligent acts) or agreed to in writing, shall any Contributor be
+   liable to You for damages, including any direct, indirect, special,
+   incidental, or consequential damages of any character arising as a
+   result of this License or out of the use or inability to use the
+   Work (including but not limited to damages for loss of goodwill,
+   work stoppage, computer failure or malfunction, or any and all
+   other commercial damages or losses), even if such Contributor
+   has been advised of the possibility of such damages.
+
+9. Accepting Warranty or Additional Liability. While redistributing
+   the Work or Derivative Works thereof, You may choose to offer,
+   and charge a fee for, acceptance of support, warranty, indemnity,
+   or other liability obligations and/or rights consistent with this
+   License. However, in accepting such obligations, You may act only
+   on Your own behalf and on Your sole responsibility, not on behalf
+   of any other Contributor, and only if You agree to indemnify,
+   defend, and hold each Contributor harmless for any liability
+   incurred by, or claims asserted against, such Contributor by reason
+   of your accepting any such warranty or additional liability.
+
+END OF TERMS AND CONDITIONS
+
+APPENDIX: How to apply the Apache License to your work.
+
+   To apply the Apache License to your work, attach the following
+   boilerplate notice, with the fields enclosed by brackets "[]"
+   replaced with your own identifying information. (Don't include
+   the brackets!)  The text should be enclosed in the appropriate
+   comment syntax for the file format. We also recommend that a
+   file or class name and description of purpose be included on the
+   same "printed page" as the copyright notice for easier
+   identification within third-party archives.
+
+Copyright [yyyy] [name of copyright owner]
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
diff --git a/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/pom.xml b/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/pom.xml
new file mode 100644
index 0000000..2a8b469
--- /dev/null
+++ b/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/pom.xml
@@ -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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0"
+  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
+  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+  <modelVersion>4.0.0</modelVersion>
+
+  <groupId>${groupId}</groupId>
+  <artifactId>${artifactId}</artifactId>
+  <packaging>jar</packaging>
+  <version>${version}</version>
+  <name>${pluginName}</name>
+
+  <properties>
+    <Gerrit-ApiType>${gerritApiType}</Gerrit-ApiType>
+    <Gerrit-ApiVersion>${gerritApiVersion}</Gerrit-ApiVersion>
+  </properties>
+
+  <build>
+    <plugins>
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-jar-plugin</artifactId>
+        <version>2.4</version>
+        <configuration>
+          <includes>
+            <include>**/*.js</include>
+            <include>**/*.class</include>
+          </includes>
+          <archive>
+            <manifestEntries>
+              <Implementation-Vendor>${Implementation-Vendor}</Implementation-Vendor>
+              <Implementation-URL>${Implementation-Url}</Implementation-URL>
+
+              <Implementation-Title>${Gerrit-ApiType} ${project.artifactId}</Implementation-Title>
+              <Implementation-Version>${project.version}</Implementation-Version>
+
+              <Gerrit-ApiType>${Gerrit-ApiType}</Gerrit-ApiType>
+              <Gerrit-ApiVersion>${Gerrit-ApiVersion}</Gerrit-ApiVersion>
+            </manifestEntries>
+          </archive>
+        </configuration>
+      </plugin>
+
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-compiler-plugin</artifactId>
+        <version>2.3.2</version>
+        <configuration>
+          <source>1.6</source>
+          <target>1.6</target>
+          <encoding>UTF-8</encoding>
+        </configuration>
+      </plugin>
+
+      <plugin>
+        <artifactId>maven-resources-plugin</artifactId>
+        <version>2.6</version>
+        <executions>
+          <execution>
+            <id>copy-resources</id>
+            <phase>process-resources</phase>
+            <goals>
+              <goal>copy-resources</goal>
+            </goals>
+            <configuration>
+              <outputDirectory>${basedir}/target/classes/static</outputDirectory>
+              <resources>
+                <resource>
+                  <directory>src/main/js</directory>
+                  <filtering>true</filtering>
+                </resource>
+              </resources>
+            </configuration>
+          </execution>
+        </executions>
+      </plugin>
+
+    </plugins>
+  </build>
+
+  <dependencies>
+    <dependency>
+      <groupId>com.google.gerrit</groupId>
+      <artifactId>gerrit-extension-api</artifactId>
+      <version>${Gerrit-ApiVersion}</version>
+      <scope>provided</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>junit</groupId>
+      <artifactId>junit</artifactId>
+      <version>4.8.1</version>
+      <scope>test</scope>
+    </dependency>
+  </dependencies>
+
+  <repositories>
+    <repository>
+      <id>gerrit-api-repository</id>
+#if ($gerritApiVersion.endsWith("SNAPSHOT"))
+      <url>https://gerrit-api.commondatastorage.googleapis.com/snapshot/</url>
+#else
+      <url>https://gerrit-api.commondatastorage.googleapis.com/release/</url>
+#end
+    </repository>
+  </repositories>
+</project>
diff --git a/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/src/main/java/MyJsExtension.java b/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/src/main/java/MyJsExtension.java
new file mode 100644
index 0000000..bec914dd
--- /dev/null
+++ b/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/src/main/java/MyJsExtension.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package ${package};
+
+import com.google.gerrit.extensions.annotations.Listen;
+import com.google.gerrit.extensions.webui.JavaScriptPlugin;
+
+@Listen
+public class MyJsExtension extends JavaScriptPlugin {
+  public MyJsExtension() {
+    super("hello-js-plugins.js");
+  }
+}
diff --git a/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/src/main/js/hello-js-plugins.js b/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/src/main/js/hello-js-plugins.js
new file mode 100644
index 0000000..fd51a42
--- /dev/null
+++ b/gerrit-plugin-js-archetype/src/main/resources/archetype-resources/src/main/js/hello-js-plugins.js
@@ -0,0 +1 @@
+alert("Greeting from JavaScript Gerrit plugin!");
\ No newline at end of file
diff --git a/gerrit-prettify/pom.xml b/gerrit-prettify/pom.xml
index 9354274..4db1056 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.5-SNAPSHOT</version>
+    <version>2.6-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-prettify</artifactId>
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/ClientSideFormatter.java b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/ClientSideFormatter.java
index 4bce954..1436bba 100644
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/ClientSideFormatter.java
+++ b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/ClientSideFormatter.java
@@ -39,15 +39,25 @@
     RootPanel.get().add(prettify);
 
     prettify.compile(Resources.I.core());
+    prettify.compile(Resources.I.lang_apollo());
+    prettify.compile(Resources.I.lang_clj());
     prettify.compile(Resources.I.lang_css());
+    prettify.compile(Resources.I.lang_dart());
+    prettify.compile(Resources.I.lang_go());
     prettify.compile(Resources.I.lang_hs());
     prettify.compile(Resources.I.lang_lisp());
     prettify.compile(Resources.I.lang_lua());
     prettify.compile(Resources.I.lang_ml());
+    prettify.compile(Resources.I.lang_n());
     prettify.compile(Resources.I.lang_proto());
+    prettify.compile(Resources.I.lang_scala());
     prettify.compile(Resources.I.lang_sql());
+    prettify.compile(Resources.I.lang_tex());
     prettify.compile(Resources.I.lang_vb());
+    prettify.compile(Resources.I.lang_vhdl());
     prettify.compile(Resources.I.lang_wiki());
+    prettify.compile(Resources.I.lang_xq());
+    prettify.compile(Resources.I.lang_yaml());
   }
 
   @Override
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/Resources.java b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/Resources.java
index 745c921..f53c91c 100644
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/Resources.java
+++ b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/client/Resources.java
@@ -32,33 +32,23 @@
   @Source("prettify.js")
   TextResource core();
 
-  @Source("lang-apollo.js")
-  TextResource lang_apollo();
-
-  @Source("lang-css.js")
-  TextResource lang_css();
-
-  @Source("lang-hs.js")
-  TextResource lang_hs();
-
-  @Source("lang-lisp.js")
-  TextResource lang_lisp();
-
-  @Source("lang-lua.js")
-  TextResource lang_lua();
-
-  @Source("lang-ml.js")
-  TextResource lang_ml();
-
-  @Source("lang-proto.js")
-  TextResource lang_proto();
-
-  @Source("lang-sql.js")
-  TextResource lang_sql();
-
-  @Source("lang-vb.js")
-  TextResource lang_vb();
-
-  @Source("lang-wiki.js")
-  TextResource lang_wiki();
+  @Source("lang-apollo.js") TextResource lang_apollo();
+  @Source("lang-clj.js") TextResource lang_clj();
+  @Source("lang-css.js") TextResource lang_css();
+  @Source("lang-dart.js") TextResource lang_dart();
+  @Source("lang-go.js") TextResource lang_go();
+  @Source("lang-hs.js") TextResource lang_hs();
+  @Source("lang-lisp.js") TextResource lang_lisp();
+  @Source("lang-lua.js") TextResource lang_lua();
+  @Source("lang-ml.js") TextResource lang_ml();
+  @Source("lang-n.js") TextResource lang_n();
+  @Source("lang-proto.js") TextResource lang_proto();
+  @Source("lang-scala.js") TextResource lang_scala();
+  @Source("lang-sql.js") TextResource lang_sql();
+  @Source("lang-tex.js") TextResource lang_tex();
+  @Source("lang-vb.js") TextResource lang_vb();
+  @Source("lang-vhdl.js") TextResource lang_vhdl();
+  @Source("lang-wiki.js") TextResource lang_wiki();
+  @Source("lang-xq.js") TextResource lang_xq();
+  @Source("lang-yaml.js") TextResource lang_yaml();
 }
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 151149b..511b056 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
@@ -135,11 +135,21 @@
       // confuse the parser.
       //
       html = html.replaceAll("&#39;", "'");
-      html = prettify(html, getFileType());
 
+      // If a line is modified at its end and the line ending is changed from
+      // '\n' to '\r\n' then the '\r' of the new line is part of the modified
+      // text. If intraline diffs are highlighted the modified text is
+      // surrounded by a 'span' tag. As result '\r' and '\n' of the new line get
+      // separated by '</span>'. For the prettify parser this now looks like two
+      // separate line endings. This messes up the line counting below.
+      // Drop any '\r' to avoid this problem.
+      html = html.replace("\r</span>\n", "</span>\n");
+
+      html = html.replace("\n", " \n");
+      html = prettify(html, getFileType());
+      html = html.replace(" \n", "\n");
     } else {
       html = expandTabs(html);
-      html = html.replaceAll("\n", "<br />");
     }
 
     int pos = 0;
@@ -152,8 +162,9 @@
     buf = new StringBuilder();
     while (pos <= html.length()) {
       int tagStart = html.indexOf('<', pos);
+      int lf = html.indexOf('\n', pos);
 
-      if (tagStart < 0) {
+      if (tagStart < 0 && lf < 0) {
         // No more tags remaining. What's left is plain text.
         //
         assert lastTag == Tag.NULL;
@@ -167,6 +178,22 @@
         break;
       }
 
+      // Line end occurs before the next HTML tag. Break the line.
+      if (0 <= lf && (lf < tagStart || tagStart < 0)) {
+        if (textChunkStart < lf) {
+          lastTag.open(buf, html);
+          htmlText(html.substring(textChunkStart, lf));
+        }
+        pos = lf + 1;
+        textChunkStart = pos;
+
+        lastTag.close(buf, html);
+        content.addLine(src.mapIndexToLine(lineIdx++), buf.toString());
+        buf = new StringBuilder();
+        col = 0;
+        continue;
+      }
+
       // Assume no attribute contains '>' and that all tags
       // within the HTML will be well-formed.
       //
@@ -183,14 +210,7 @@
       }
       textChunkStart = pos;
 
-      if (isBR(html, tagStart, tagEnd)) {
-        lastTag.close(buf, html);
-        content.addLine(src.mapIndexToLine(lineIdx), buf.toString());
-        buf = new StringBuilder();
-        col = 0;
-        lineIdx++;
-
-      } else if (html.charAt(tagStart + 1) == '/') {
+      if (html.charAt(tagStart + 1) == '/') {
         lastTag = lastTag.pop(buf, html);
 
       } else if (html.charAt(tagEnd - 1) != '/') {
@@ -245,13 +265,6 @@
   /** Run the prettify engine over the text and return the result. */
   protected abstract String prettify(String html, String type);
 
-  private static boolean isBR(String html, int tagStart, int tagEnd) {
-    return tagEnd - tagStart == 5 //
-        && html.charAt(tagStart + 1) == 'b' //
-        && html.charAt(tagStart + 2) == 'r' //
-        && html.charAt(tagStart + 3) == ' ';
-  }
-
   private static class Tag {
     static final Tag NULL = new Tag(null, 0, 0) {
       @Override
diff --git a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/SparseFileContent.java b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/SparseFileContent.java
index 1a5468c..609f091 100644
--- a/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/SparseFileContent.java
+++ b/gerrit-prettify/src/main/java/com/google/gerrit/prettify/common/SparseFileContent.java
@@ -296,6 +296,7 @@
 
     @Override
     public String toString() {
+      // Usage of [ and ) is intentional to denote inclusive/exclusive range
       return "Range[" + base + "," + end() + ")";
     }
   }
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-apollo.js b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-apollo.js
index 4042030..99e4a97 100644
--- a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-apollo.js
+++ b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-apollo.js
@@ -1 +1,2 @@
-PR.registerLangHandler(PR.createSimpleLexer([[PR.PR_COMMENT,/^#[^\r\n]*/,null,'#'],[PR.PR_PLAIN,/^[\t\n\r \xA0]+/,null,'	\n\r \xa0'],[PR.PR_STRING,/^\"(?:[^\"\\]|\\[\s\S])*(?:\"|$)/,null,'\"']],[[PR.PR_KEYWORD,/^(?:ADS|AD|AUG|BZF|BZMF|CAE|CAF|CA|CCS|COM|CS|DAS|DCA|DCOM|DCS|DDOUBL|DIM|DOUBLE|DTCB|DTCF|DV|DXCH|EDRUPT|EXTEND|INCR|INDEX|NDX|INHINT|LXCH|MASK|MSK|MP|MSU|NOOP|OVSK|QXCH|RAND|READ|RELINT|RESUME|RETURN|ROR|RXOR|SQUARE|SU|TCR|TCAA|OVSK|TCF|TC|TS|WAND|WOR|WRITE|XCH|XLQ|XXALQ|ZL|ZQ|ADD|ADZ|SUB|SUZ|MPY|MPR|MPZ|DVP|COM|ABS|CLA|CLZ|LDQ|STO|STQ|ALS|LLS|LRS|TRA|TSQ|TMI|TOV|AXT|TIX|DLY|INP|OUT)\s/,null],[PR.PR_TYPE,/^(?:-?GENADR|=MINUS|2BCADR|VN|BOF|MM|-?2CADR|-?[1-6]DNADR|ADRES|BBCON|[SE]?BANK\=?|BLOCK|BNKSUM|E?CADR|COUNT\*?|2?DEC\*?|-?DNCHAN|-?DNPTR|EQUALS|ERASE|MEMORY|2?OCT|REMADR|SETLOC|SUBRO|ORG|BSS|BES|SYN|EQU|DEFINE|END)\s/,null],[PR.PR_LITERAL,/^\'(?:-*(?:\w|\\[\x21-\x7e])(?:[\w-]*|\\[\x21-\x7e])[=!?]?)?/],[PR.PR_PLAIN,/^-*(?:[!-z_]|\\[\x21-\x7e])(?:[\w-]*|\\[\x21-\x7e])[=!?]?/i],[PR.PR_PUNCTUATION,/^[^\w\t\n\r \xA0()\"\\\';]+/]]),['apollo','agc','aea'])
\ No newline at end of file
+PR.registerLangHandler(PR.createSimpleLexer([["com",/^#[^\n\r]*/,null,"#"],["pln",/^[\t\n\r \xa0]+/,null,"\t\n\r \u00a0"],["str",/^"(?:[^"\\]|\\[\S\s])*(?:"|$)/,null,'"']],[["kwd",/^(?:ADS|AD|AUG|BZF|BZMF|CAE|CAF|CA|CCS|COM|CS|DAS|DCA|DCOM|DCS|DDOUBL|DIM|DOUBLE|DTCB|DTCF|DV|DXCH|EDRUPT|EXTEND|INCR|INDEX|NDX|INHINT|LXCH|MASK|MSK|MP|MSU|NOOP|OVSK|QXCH|RAND|READ|RELINT|RESUME|RETURN|ROR|RXOR|SQUARE|SU|TCR|TCAA|OVSK|TCF|TC|TS|WAND|WOR|WRITE|XCH|XLQ|XXALQ|ZL|ZQ|ADD|ADZ|SUB|SUZ|MPY|MPR|MPZ|DVP|COM|ABS|CLA|CLZ|LDQ|STO|STQ|ALS|LLS|LRS|TRA|TSQ|TMI|TOV|AXT|TIX|DLY|INP|OUT)\s/,
+null],["typ",/^(?:-?GENADR|=MINUS|2BCADR|VN|BOF|MM|-?2CADR|-?[1-6]DNADR|ADRES|BBCON|[ES]?BANK=?|BLOCK|BNKSUM|E?CADR|COUNT\*?|2?DEC\*?|-?DNCHAN|-?DNPTR|EQUALS|ERASE|MEMORY|2?OCT|REMADR|SETLOC|SUBRO|ORG|BSS|BES|SYN|EQU|DEFINE|END)\s/,null],["lit",/^'(?:-*(?:\w|\\[!-~])(?:[\w-]*|\\[!-~])[!=?]?)?/],["pln",/^-*(?:[!-z]|\\[!-~])(?:[\w-]*|\\[!-~])[!=?]?/],["pun",/^[^\w\t\n\r "'-);\\\xa0]+/]]),["apollo","agc","aea"]);
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-clj.js b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-clj.js
new file mode 100644
index 0000000..1bb539c
--- /dev/null
+++ b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-clj.js
@@ -0,0 +1,18 @@
+/*
+ Copyright (C) 2011 Google Inc.
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+*/
+var a=null;
+PR.registerLangHandler(PR.createSimpleLexer([["opn",/^[([{]+/,a,"([{"],["clo",/^[)\]}]+/,a,")]}"],["com",/^;[^\n\r]*/,a,";"],["pln",/^[\t\n\r \xa0]+/,a,"\t\n\r \u00a0"],["str",/^"(?:[^"\\]|\\[\S\s])*(?:"|$)/,a,'"']],[["kwd",/^(?:def|if|do|let|quote|var|fn|loop|recur|throw|try|monitor-enter|monitor-exit|defmacro|defn|defn-|macroexpand|macroexpand-1|for|doseq|dosync|dotimes|and|or|when|not|assert|doto|proxy|defstruct|first|rest|cons|defprotocol|deftype|defrecord|reify|defmulti|defmethod|meta|with-meta|ns|in-ns|create-ns|import|intern|refer|alias|namespace|resolve|ref|deref|refset|new|set!|memfn|to-array|into-array|aset|gen-class|reduce|map|filter|find|nil?|empty?|hash-map|hash-set|vec|vector|seq|flatten|reverse|assoc|dissoc|list|list?|disj|get|union|difference|intersection|extend|extend-type|extend-protocol|prn)\b/,a],
+["typ",/^:[\dA-Za-z-]+/]]),["clj"]);
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-css.js b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-css.js
index c650d8f..2086bd5 100644
--- a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-css.js
+++ b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-css.js
@@ -1 +1,2 @@
-PR.registerLangHandler(PR.createSimpleLexer([[PR.PR_PLAIN,/^[ \t\r\n\f]+/,null,' 	\r\n']],[[PR.PR_STRING,/^\"(?:[^\n\r\f\\\"]|\\(?:\r\n?|\n|\f)|\\[\s\S])*\"/,null],[PR.PR_STRING,/^\'(?:[^\n\r\f\\\']|\\(?:\r\n?|\n|\f)|\\[\s\S])*\'/,null],['lang-css-str',/^url\(([^\)\"\']*)\)/i],[PR.PR_KEYWORD,/^(?:url|rgb|\!important|@import|@page|@media|@charset|inherit)(?=[^\-\w]|$)/i,null],['lang-css-kw',/^(-?(?:[_a-z]|(?:\\[0-9a-f]+ ?))(?:[_a-z0-9\-]|\\(?:\\[0-9a-f]+ ?))*)\s*:/i],[PR.PR_COMMENT,/^\/\*[^*]*\*+(?:[^\/*][^*]*\*+)*\//],[PR.PR_COMMENT,/^(?:<!--|-->)/],[PR.PR_LITERAL,/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],[PR.PR_LITERAL,/^#(?:[0-9a-f]{3}){1,2}/i],[PR.PR_PLAIN,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i],[PR.PR_PUNCTUATION,/^[^\s\w\'\"]+/]]),['css']),PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_KEYWORD,/^-?(?:[_a-z]|(?:\\[\da-f]+ ?))(?:[_a-z\d\-]|\\(?:\\[\da-f]+ ?))*/i]]),['css-kw']),PR.registerLangHandler(PR.createSimpleLexer([],[[PR.PR_STRING,/^[^\)\"\']+/]]),['css-str'])
\ No newline at end of file
+PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\f\r ]+/,null," \t\r\n\u000c"]],[["str",/^"(?:[^\n\f\r"\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*"/,null],["str",/^'(?:[^\n\f\r'\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*'/,null],["lang-css-str",/^url\(([^"')]+)\)/i],["kwd",/^(?:url|rgb|!important|@import|@page|@media|@charset|inherit)(?=[^\w-]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*)\s*:/i],["com",/^\/\*[^*]*\*+(?:[^*/][^*]*\*+)*\//],
+["com",/^(?:<\!--|--\>)/],["lit",/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],["lit",/^#[\da-f]{3,6}/i],["pln",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i],["pun",/^[^\s\w"']+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[["kwd",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[["str",/^[^"')]+/]]),["css-str"]);
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-dart.js b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-dart.js
new file mode 100644
index 0000000..eefccc9
--- /dev/null
+++ b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-dart.js
@@ -0,0 +1,3 @@
+PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\r \xa0]+/,null,"\t\n\r \u00a0"]],[["com",/^#!.*/],["kwd",/^\b(?:import|library|part of|part|as|show|hide)\b/i],["com",/^\/\/.*/],["com",/^\/\*[^*]*\*+(?:[^*/][^*]*\*+)*\//],["kwd",/^\b(?:class|interface)\b/i],["kwd",/^\b(?:assert|break|case|catch|continue|default|do|else|finally|for|if|in|is|new|return|super|switch|this|throw|try|while)\b/i],["kwd",/^\b(?:abstract|const|extends|factory|final|get|implements|native|operator|set|static|typedef|var)\b/i],
+["typ",/^\b(?:bool|double|dynamic|int|num|object|string|void)\b/i],["kwd",/^\b(?:false|null|true)\b/i],["str",/^r?'''[\S\s]*?[^\\]'''/],["str",/^r?"""[\S\s]*?[^\\]"""/],["str",/^r?'('|[^\n\f\r]*?[^\\]')/],["str",/^r?"("|[^\n\f\r]*?[^\\]")/],["pln",/^[$_a-z]\w*/i],["pun",/^[!%&*+/:<-?^|~-]/],["lit",/^\b0x[\da-f]+/i],["lit",/^\b\d+(?:\.\d*)?(?:e[+-]?\d+)?/i],["lit",/^\b\.\d+(?:e[+-]?\d+)?/i],["pun",/^[(),.;[\]{}]/]]),
+["dart"]);
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-go.js b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-go.js
new file mode 100644
index 0000000..1caca23
--- /dev/null
+++ b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-go.js
@@ -0,0 +1 @@
+PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\r \xa0]+/,null,"\t\n\r \u00a0"],["pln",/^(?:"(?:[^"\\]|\\[\S\s])*(?:"|$)|'(?:[^'\\]|\\[\S\s])+(?:'|$)|`[^`]*(?:`|$))/,null,"\"'"]],[["com",/^(?:\/\/[^\n\r]*|\/\*[\S\s]*?\*\/)/],["pln",/^(?:[^"'/`]|\/(?![*/]))+/]]),["go"]);
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-hs.js b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-hs.js
index 27b221a..ff3729b 100644
--- a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-hs.js
+++ b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-hs.js
@@ -1 +1,2 @@
-PR.registerLangHandler(PR.createSimpleLexer([[PR.PR_PLAIN,/^[\t\n\x0B\x0C\r ]+/,null,'	\n\r '],[PR.PR_STRING,/^\"(?:[^\"\\\n\x0C\r]|\\[\s\S])*(?:\"|$)/,null,'\"'],[PR.PR_STRING,/^\'(?:[^\'\\\n\x0C\r]|\\[^&])\'?/,null,'\''],[PR.PR_LITERAL,/^(?:0o[0-7]+|0x[\da-f]+|\d+(?:\.\d+)?(?:e[+\-]?\d+)?)/i,null,'0123456789']],[[PR.PR_COMMENT,/^(?:(?:--+(?:[^\r\n\x0C]*)?)|(?:\{-(?:[^-]|-+[^-\}])*-\}))/],[PR.PR_KEYWORD,/^(?:case|class|data|default|deriving|do|else|if|import|in|infix|infixl|infixr|instance|let|module|newtype|of|then|type|where|_)(?=[^a-zA-Z0-9\']|$)/,null],[PR.PR_PLAIN,/^(?:[A-Z][\w\']*\.)*[a-zA-Z][\w\']*/],[PR.PR_PUNCTUATION,/^[^\t\n\x0B\x0C\r a-zA-Z0-9\'\"]+/]]),['hs'])
\ No newline at end of file
+PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t-\r ]+/,null,"\t\n\u000b\u000c\r "],["str",/^"(?:[^\n\f\r"\\]|\\[\S\s])*(?:"|$)/,null,'"'],["str",/^'(?:[^\n\f\r'\\]|\\[^&])'?/,null,"'"],["lit",/^(?:0o[0-7]+|0x[\da-f]+|\d+(?:\.\d+)?(?:e[+-]?\d+)?)/i,null,"0123456789"]],[["com",/^(?:--+[^\n\f\r]*|{-(?:[^-]|-+[^}-])*-})/],["kwd",/^(?:case|class|data|default|deriving|do|else|if|import|in|infix|infixl|infixr|instance|let|module|newtype|of|then|type|where|_)(?=[^\d'A-Za-z]|$)/,
+null],["pln",/^(?:[A-Z][\w']*\.)*[A-Za-z][\w']*/],["pun",/^[^\d\t-\r "'A-Za-z]+/]]),["hs"]);
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-lisp.js b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-lisp.js
index 85c6c23..9c8cfa5 100644
--- a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-lisp.js
+++ b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-lisp.js
@@ -1 +1,3 @@
-PR.registerLangHandler(PR.createSimpleLexer([['opn',/^\(/,null,'('],['clo',/^\)/,null,')'],[PR.PR_COMMENT,/^;[^\r\n]*/,null,';'],[PR.PR_PLAIN,/^[\t\n\r \xA0]+/,null,'	\n\r \xa0'],[PR.PR_STRING,/^\"(?:[^\"\\]|\\[\s\S])*(?:\"|$)/,null,'\"']],[[PR.PR_KEYWORD,/^(?:block|c[ad]+r|catch|cons|defun|do|eq|eql|equal|equalp|eval-when|flet|format|go|if|labels|lambda|let|load-time-value|locally|macrolet|multiple-value-call|nil|progn|progv|quote|require|return-from|setq|symbol-macrolet|t|tagbody|the|throw|unwind)\b/,null],[PR.PR_LITERAL,/^[+\-]?(?:0x[0-9a-f]+|\d+\/\d+|(?:\.\d+|\d+(?:\.\d*)?)(?:[ed][+\-]?\d+)?)/i],[PR.PR_LITERAL,/^\'(?:-*(?:\w|\\[\x21-\x7e])(?:[\w-]*|\\[\x21-\x7e])[=!?]?)?/],[PR.PR_PLAIN,/^-*(?:[a-z_]|\\[\x21-\x7e])(?:[\w-]*|\\[\x21-\x7e])[=!?]?/i],[PR.PR_PUNCTUATION,/^[^\w\t\n\r \xA0()\"\\\';]+/]]),['cl','el','lisp','scm'])
\ No newline at end of file
+var a=null;
+PR.registerLangHandler(PR.createSimpleLexer([["opn",/^\(+/,a,"("],["clo",/^\)+/,a,")"],["com",/^;[^\n\r]*/,a,";"],["pln",/^[\t\n\r \xa0]+/,a,"\t\n\r \u00a0"],["str",/^"(?:[^"\\]|\\[\S\s])*(?:"|$)/,a,'"']],[["kwd",/^(?:block|c[ad]+r|catch|con[ds]|def(?:ine|un)|do|eq|eql|equal|equalp|eval-when|flet|format|go|if|labels|lambda|let|load-time-value|locally|macrolet|multiple-value-call|nil|progn|progv|quote|require|return-from|setq|symbol-macrolet|t|tagbody|the|throw|unwind)\b/,a],
+["lit",/^[+-]?(?:[#0]x[\da-f]+|\d+\/\d+|(?:\.\d+|\d+(?:\.\d*)?)(?:[de][+-]?\d+)?)/i],["lit",/^'(?:-*(?:\w|\\[!-~])(?:[\w-]*|\\[!-~])[!=?]?)?/],["pln",/^-*(?:[_a-z]|\\[!-~])(?:[\w-]*|\\[!-~])[!=?]?/i],["pun",/^[^\w\t\n\r "'-);\\\xa0]+/]]),["cl","el","lisp","lsp","scm","ss","rkt"]);
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-lua.js b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-lua.js
index d107bab..7e44cca 100644
--- a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-lua.js
+++ b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-lua.js
@@ -1 +1,2 @@
-PR.registerLangHandler(PR.createSimpleLexer([[PR.PR_PLAIN,/^[\t\n\r \xA0]+/,null,'	\n\r \xa0'],[PR.PR_STRING,/^(?:\"(?:[^\"\\]|\\[\s\S])*(?:\"|$)|\'(?:[^\'\\]|\\[\s\S])*(?:\'|$))/,null,'\"\'']],[[PR.PR_COMMENT,/^--(?:\[(=*)\[[\s\S]*?(?:\]\1\]|$)|[^\r\n]*)/],[PR.PR_STRING,/^\[(=*)\[[\s\S]*?(?:\]\1\]|$)/],[PR.PR_KEYWORD,/^(?:and|break|do|else|elseif|end|false|for|function|if|in|local|nil|not|or|repeat|return|then|true|until|while)\b/,null],[PR.PR_LITERAL,/^[+-]?(?:0x[\da-f]+|(?:(?:\.\d+|\d+(?:\.\d*)?)(?:e[+\-]?\d+)?))/i],[PR.PR_PLAIN,/^[a-z_]\w*/i],[PR.PR_PUNCTUATION,/^[^\w\t\n\r \xA0][^\w\t\n\r \xA0\"\'\-\+=]*/]]),['lua'])
\ No newline at end of file
+PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\r \xa0]+/,null,"\t\n\r \u00a0"],["str",/^(?:"(?:[^"\\]|\\[\S\s])*(?:"|$)|'(?:[^'\\]|\\[\S\s])*(?:'|$))/,null,"\"'"]],[["com",/^--(?:\[(=*)\[[\S\s]*?(?:]\1]|$)|[^\n\r]*)/],["str",/^\[(=*)\[[\S\s]*?(?:]\1]|$)/],["kwd",/^(?:and|break|do|else|elseif|end|false|for|function|if|in|local|nil|not|or|repeat|return|then|true|until|while)\b/,null],["lit",/^[+-]?(?:0x[\da-f]+|(?:\.\d+|\d+(?:\.\d*)?)(?:e[+-]?\d+)?)/i],
+["pln",/^[_a-z]\w*/i],["pun",/^[^\w\t\n\r \xa0][^\w\t\n\r "'+=\xa0-]*/]]),["lua"]);
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-ml.js b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-ml.js
index 698d6de..8ed2b0c 100644
--- a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-ml.js
+++ b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-ml.js
@@ -1 +1,2 @@
-PR.registerLangHandler(PR.createSimpleLexer([[PR.PR_PLAIN,/^[\t\n\r \xA0]+/,null,'	\n\r \xa0'],[PR.PR_COMMENT,/^#(?:if[\t\n\r \xA0]+(?:[a-z_$][\w\']*|``[^\r\n\t`]*(?:``|$))|else|endif|light)/i,null,'#'],[PR.PR_STRING,/^(?:\"(?:[^\"\\]|\\[\s\S])*(?:\"|$)|\'(?:[^\'\\]|\\[\s\S])*(?:\'|$))/,null,'\"\'']],[[PR.PR_COMMENT,/^(?:\/\/[^\r\n]*|\(\*[\s\S]*?\*\))/],[PR.PR_KEYWORD,/^(?:abstract|and|as|assert|begin|class|default|delegate|do|done|downcast|downto|elif|else|end|exception|extern|false|finally|for|fun|function|if|in|inherit|inline|interface|internal|lazy|let|match|member|module|mutable|namespace|new|null|of|open|or|override|private|public|rec|return|static|struct|then|to|true|try|type|upcast|use|val|void|when|while|with|yield|asr|land|lor|lsl|lsr|lxor|mod|sig|atomic|break|checked|component|const|constraint|constructor|continue|eager|event|external|fixed|functor|global|include|method|mixin|object|parallel|process|protected|pure|sealed|trait|virtual|volatile)\b/],[PR.PR_LITERAL,/^[+\-]?(?:0x[\da-f]+|(?:(?:\.\d+|\d+(?:\.\d*)?)(?:e[+\-]?\d+)?))/i],[PR.PR_PLAIN,/^(?:[a-z_]\w*[!?#]?|``[^\r\n\t`]*(?:``|$))/i],[PR.PR_PUNCTUATION,/^[^\t\n\r \xA0\"\'\w]+/]]),['fs','ml'])
\ No newline at end of file
+PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\r \xa0]+/,null,"\t\n\r \u00a0"],["com",/^#(?:if[\t\n\r \xa0]+(?:[$_a-z][\w']*|``[^\t\n\r`]*(?:``|$))|else|endif|light)/i,null,"#"],["str",/^(?:"(?:[^"\\]|\\[\S\s])*(?:"|$)|'(?:[^'\\]|\\[\S\s])(?:'|$))/,null,"\"'"]],[["com",/^(?:\/\/[^\n\r]*|\(\*[\S\s]*?\*\))/],["kwd",/^(?:abstract|and|as|assert|begin|class|default|delegate|do|done|downcast|downto|elif|else|end|exception|extern|false|finally|for|fun|function|if|in|inherit|inline|interface|internal|lazy|let|match|member|module|mutable|namespace|new|null|of|open|or|override|private|public|rec|return|static|struct|then|to|true|try|type|upcast|use|val|void|when|while|with|yield|asr|land|lor|lsl|lsr|lxor|mod|sig|atomic|break|checked|component|const|constraint|constructor|continue|eager|event|external|fixed|functor|global|include|method|mixin|object|parallel|process|protected|pure|sealed|trait|virtual|volatile)\b/],
+["lit",/^[+-]?(?:0x[\da-f]+|(?:\.\d+|\d+(?:\.\d*)?)(?:e[+-]?\d+)?)/i],["pln",/^(?:[_a-z][\w']*[!#?]?|``[^\t\n\r`]*(?:``|$))/i],["pun",/^[^\w\t\n\r "'\xa0]+/]]),["fs","ml"]);
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-n.js b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-n.js
new file mode 100644
index 0000000..27812a5
--- /dev/null
+++ b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-n.js
@@ -0,0 +1,4 @@
+var a=null;
+PR.registerLangHandler(PR.createSimpleLexer([["str",/^(?:'(?:[^\n\r'\\]|\\.)*'|"(?:[^\n\r"\\]|\\.)*(?:"|$))/,a,'"'],["com",/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\n\r]*)/,a,"#"],["pln",/^\s+/,a," \r\n\t\u00a0"]],[["str",/^@"(?:[^"]|"")*(?:"|$)/,a],["str",/^<#[^#>]*(?:#>|$)/,a],["str",/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,a],["com",/^\/\/[^\n\r]*/,a],["com",/^\/\*[\S\s]*?(?:\*\/|$)/,
+a],["kwd",/^(?:abstract|and|as|base|catch|class|def|delegate|enum|event|extern|false|finally|fun|implements|interface|internal|is|macro|match|matches|module|mutable|namespace|new|null|out|override|params|partial|private|protected|public|ref|sealed|static|struct|syntax|this|throw|true|try|type|typeof|using|variant|virtual|volatile|when|where|with|assert|assert2|async|break|checked|continue|do|else|ensures|for|foreach|if|late|lock|new|nolate|otherwise|regexp|repeat|requires|return|surroundwith|unchecked|unless|using|while|yield)\b/,
+a],["typ",/^(?:array|bool|byte|char|decimal|double|float|int|list|long|object|sbyte|short|string|ulong|uint|ufloat|ulong|ushort|void)\b/,a],["lit",/^@[$_a-z][\w$@]*/i,a],["typ",/^@[A-Z]+[a-z][\w$@]*/,a],["pln",/^'?[$_a-z][\w$@]*/i,a],["lit",/^(?:0x[\da-f]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+-]?\d+)?)[a-z]*/i,a,"0123456789"],["pun",/^.[^\s\w"-$'./@`]*/,a]]),["n","nemerle"]);
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-proto.js b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-proto.js
index e67967f..f006ad8 100644
--- a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-proto.js
+++ b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-proto.js
@@ -1 +1 @@
-PR.registerLangHandler(PR.sourceDecorator({keywords:'bool bytes default double enum extend extensions false fixed32 fixed64 float group import int32 int64 max message option optional package repeated required returns rpc service sfixed32 sfixed64 sint32 sint64 string syntax to true uint32 uint64',cStyleComments:true}),['proto'])
\ No newline at end of file
+PR.registerLangHandler(PR.sourceDecorator({keywords:"bytes,default,double,enum,extend,extensions,false,group,import,max,message,option,optional,package,repeated,required,returns,rpc,service,syntax,to,true",types:/^(bool|(double|s?fixed|[su]?int)(32|64)|float|string)\b/,cStyleComments:!0}),["proto"]);
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-scala.js b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-scala.js
new file mode 100644
index 0000000..3f97dba
--- /dev/null
+++ b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-scala.js
@@ -0,0 +1,2 @@
+PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\r \xa0]+/,null,"\t\n\r \u00a0"],["str",/^"(?:""(?:""?(?!")|[^"\\]|\\.)*"{0,3}|(?:[^\n\r"\\]|\\.)*"?)/,null,'"'],["lit",/^`(?:[^\n\r\\`]|\\.)*`?/,null,"`"],["pun",/^[!#%&(--:-@[-^{-~]+/,null,"!#%&()*+,-:;<=>?@[\\]^{|}~"]],[["str",/^'(?:[^\n\r'\\]|\\(?:'|[^\n\r']+))'/],["lit",/^'[$A-Z_a-z][\w$]*(?![\w$'])/],["kwd",/^(?:abstract|case|catch|class|def|do|else|extends|final|finally|for|forSome|if|implicit|import|lazy|match|new|object|override|package|private|protected|requires|return|sealed|super|throw|trait|try|type|val|var|while|with|yield)\b/],
+["lit",/^(?:true|false|null|this)\b/],["lit",/^(?:0(?:[0-7]+|x[\da-f]+)l?|(?:0|[1-9]\d*)(?:(?:\.\d+)?(?:e[+-]?\d+)?f?|l?)|\\.\d+(?:e[+-]?\d+)?f?)/i],["typ",/^[$_]*[A-Z][\d$A-Z_]*[a-z][\w$]*/],["pln",/^[$A-Z_a-z][\w$]*/],["com",/^\/(?:\/.*|\*(?:\/|\**[^*/])*(?:\*+\/?)?)/],["pun",/^(?:\.+|\/)/]]),["scala"]);
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-sql.js b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-sql.js
index ff381cd4..2fddd3e 100644
--- a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-sql.js
+++ b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-sql.js
@@ -1 +1,2 @@
-PR.registerLangHandler(PR.createSimpleLexer([[PR.PR_PLAIN,/^[\t\n\r \xA0]+/,null,'	\n\r \xa0'],[PR.PR_STRING,/^(?:"(?:[^\"\\]|\\.)*"|'(?:[^\'\\]|\\.)*')/,null,'\"\'']],[[PR.PR_COMMENT,/^(?:--[^\r\n]*|\/\*[\s\S]*?(?:\*\/|$))/],[PR.PR_KEYWORD,/^(?:ADD|ALL|ALTER|AND|ANY|AS|ASC|AUTHORIZATION|BACKUP|BEGIN|BETWEEN|BREAK|BROWSE|BULK|BY|CASCADE|CASE|CHECK|CHECKPOINT|CLOSE|CLUSTERED|COALESCE|COLLATE|COLUMN|COMMIT|COMPUTE|CONSTRAINT|CONTAINS|CONTAINSTABLE|CONTINUE|CONVERT|CREATE|CROSS|CURRENT|CURRENT_DATE|CURRENT_TIME|CURRENT_TIMESTAMP|CURRENT_USER|CURSOR|DATABASE|DBCC|DEALLOCATE|DECLARE|DEFAULT|DELETE|DENY|DESC|DISK|DISTINCT|DISTRIBUTED|DOUBLE|DROP|DUMMY|DUMP|ELSE|END|ERRLVL|ESCAPE|EXCEPT|EXEC|EXECUTE|EXISTS|EXIT|FETCH|FILE|FILLFACTOR|FOR|FOREIGN|FREETEXT|FREETEXTTABLE|FROM|FULL|FUNCTION|GOTO|GRANT|GROUP|HAVING|HOLDLOCK|IDENTITY|IDENTITYCOL|IDENTITY_INSERT|IF|IN|INDEX|INNER|INSERT|INTERSECT|INTO|IS|JOIN|KEY|KILL|LEFT|LIKE|LINENO|LOAD|NATIONAL|NOCHECK|NONCLUSTERED|NOT|NULL|NULLIF|OF|OFF|OFFSETS|ON|OPEN|OPENDATASOURCE|OPENQUERY|OPENROWSET|OPENXML|OPTION|OR|ORDER|OUTER|OVER|PERCENT|PLAN|PRECISION|PRIMARY|PRINT|PROC|PROCEDURE|PUBLIC|RAISERROR|READ|READTEXT|RECONFIGURE|REFERENCES|REPLICATION|RESTORE|RESTRICT|RETURN|REVOKE|RIGHT|ROLLBACK|ROWCOUNT|ROWGUIDCOL|RULE|SAVE|SCHEMA|SELECT|SESSION_USER|SET|SETUSER|SHUTDOWN|SOME|STATISTICS|SYSTEM_USER|TABLE|TEXTSIZE|THEN|TO|TOP|TRAN|TRANSACTION|TRIGGER|TRUNCATE|TSEQUAL|UNION|UNIQUE|UPDATE|UPDATETEXT|USE|USER|VALUES|VARYING|VIEW|WAITFOR|WHEN|WHERE|WHILE|WITH|WRITETEXT)(?=[^\w-]|$)/i,null],[PR.PR_LITERAL,/^[+-]?(?:0x[\da-f]+|(?:(?:\.\d+|\d+(?:\.\d*)?)(?:e[+\-]?\d+)?))/i],[PR.PR_PLAIN,/^[a-z_][\w-]*/i],[PR.PR_PUNCTUATION,/^[^\w\t\n\r \xA0\"\'][^\w\t\n\r \xA0+\-\"\']*/]]),['sql'])
\ No newline at end of file
+PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\r \xa0]+/,null,"\t\n\r \u00a0"],["str",/^(?:"(?:[^"\\]|\\.)*"|'(?:[^'\\]|\\.)*')/,null,"\"'"]],[["com",/^(?:--[^\n\r]*|\/\*[\S\s]*?(?:\*\/|$))/],["kwd",/^(?:add|all|alter|and|any|apply|as|asc|authorization|backup|begin|between|break|browse|bulk|by|cascade|case|check|checkpoint|close|clustered|coalesce|collate|column|commit|compute|constraint|contains|containstable|continue|convert|create|cross|current|current_date|current_time|current_timestamp|current_user|cursor|database|dbcc|deallocate|declare|default|delete|deny|desc|disk|distinct|distributed|double|drop|dummy|dump|else|end|errlvl|escape|except|exec|execute|exists|exit|fetch|file|fillfactor|following|for|foreign|freetext|freetexttable|from|full|function|goto|grant|group|having|holdlock|identity|identitycol|identity_insert|if|in|index|inner|insert|intersect|into|is|join|key|kill|left|like|lineno|load|match|merge|national|nocheck|nonclustered|not|null|nullif|of|off|offsets|on|open|opendatasource|openquery|openrowset|openxml|option|or|order|outer|over|percent|plan|preceding|precision|primary|print|proc|procedure|public|raiserror|read|readtext|reconfigure|references|replication|restore|restrict|return|revoke|right|rollback|rowcount|rowguidcol|rows?|rule|save|schema|select|session_user|set|setuser|shutdown|some|statistics|system_user|table|textsize|then|to|top|tran|transaction|trigger|truncate|tsequal|unbounded|union|unique|update|updatetext|use|user|using|values|varying|view|waitfor|when|where|while|with|writetext)(?=[^\w-]|$)/i,
+null],["lit",/^[+-]?(?:0x[\da-f]+|(?:\.\d+|\d+(?:\.\d*)?)(?:e[+-]?\d+)?)/i],["pln",/^[_a-z][\w-]*/i],["pun",/^[^\w\t\n\r "'\xa0][^\w\t\n\r "'+\xa0-]*/]]),["sql"]);
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-tex.js b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-tex.js
new file mode 100644
index 0000000..dcfdadd
--- /dev/null
+++ b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-tex.js
@@ -0,0 +1 @@
+PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\r \xa0]+/,null,"\t\n\r \u00a0"],["com",/^%[^\n\r]*/,null,"%"]],[["kwd",/^\\[@-Za-z]+/],["kwd",/^\\./],["typ",/^[$&]/],["lit",/[+-]?(?:\.\d+|\d+(?:\.\d*)?)(cm|em|ex|in|pc|pt|bp|mm)/i],["pun",/^[()=[\]{}]+/]]),["latex","tex"]);
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-vb.js b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-vb.js
index cabce85..b151b7c 100644
--- a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-vb.js
+++ b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-vb.js
@@ -1 +1,2 @@
-PR.registerLangHandler(PR.createSimpleLexer([[PR.PR_PLAIN,/^[\t\n\r \xA0\u2028\u2029]+/,null,'	\n\r \xa0\u2028\u2029'],[PR.PR_STRING,/^(?:[\"\u201C\u201D](?:[^\"\u201C\u201D]|[\"\u201C\u201D]{2})(?:[\"\u201C\u201D]c|$)|[\"\u201C\u201D](?:[^\"\u201C\u201D]|[\"\u201C\u201D]{2})*(?:[\"\u201C\u201D]|$))/i,null,'\"\u201c\u201d'],[PR.PR_COMMENT,/^[\'\u2018\u2019][^\r\n\u2028\u2029]*/,null,'\'\u2018\u2019']],[[PR.PR_KEYWORD,/^(?:AddHandler|AddressOf|Alias|And|AndAlso|Ansi|As|Assembly|Auto|Boolean|ByRef|Byte|ByVal|Call|Case|Catch|CBool|CByte|CChar|CDate|CDbl|CDec|Char|CInt|Class|CLng|CObj|Const|CShort|CSng|CStr|CType|Date|Decimal|Declare|Default|Delegate|Dim|DirectCast|Do|Double|Each|Else|ElseIf|End|EndIf|Enum|Erase|Error|Event|Exit|Finally|For|Friend|Function|Get|GetType|GoSub|GoTo|Handles|If|Implements|Imports|In|Inherits|Integer|Interface|Is|Let|Lib|Like|Long|Loop|Me|Mod|Module|MustInherit|MustOverride|MyBase|MyClass|Namespace|New|Next|Not|NotInheritable|NotOverridable|Object|On|Option|Optional|Or|OrElse|Overloads|Overridable|Overrides|ParamArray|Preserve|Private|Property|Protected|Public|RaiseEvent|ReadOnly|ReDim|RemoveHandler|Resume|Return|Select|Set|Shadows|Shared|Short|Single|Static|Step|Stop|String|Structure|Sub|SyncLock|Then|Throw|To|Try|TypeOf|Unicode|Until|Variant|Wend|When|While|With|WithEvents|WriteOnly|Xor|EndIf|GoSub|Let|Variant|Wend)\b/i,null],[PR.PR_COMMENT,/^REM[^\r\n\u2028\u2029]*/i],[PR.PR_LITERAL,/^(?:True\b|False\b|Nothing\b|\d+(?:E[+\-]?\d+[FRD]?|[FRDSIL])?|(?:&H[0-9A-F]+|&O[0-7]+)[SIL]?|\d*\.\d+(?:E[+\-]?\d+)?[FRD]?|#\s+(?:\d+[\-\/]\d+[\-\/]\d+(?:\s+\d+:\d+(?::\d+)?(\s*(?:AM|PM))?)?|\d+:\d+(?::\d+)?(\s*(?:AM|PM))?)\s+#)/i],[PR.PR_PLAIN,/^(?:(?:[a-z]|_\w)\w*|\[(?:[a-z]|_\w)\w*\])/i],[PR.PR_PUNCTUATION,/^[^\w\t\n\r \"\'\[\]\xA0\u2018\u2019\u201C\u201D\u2028\u2029]+/],[PR.PR_PUNCTUATION,/^(?:\[|\])/]]),['vb','vbs'])
\ No newline at end of file
+PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\r \xa0\u2028\u2029]+/,null,"\t\n\r \u00a0\u2028\u2029"],["str",/^(?:["\u201c\u201d](?:[^"\u201c\u201d]|["\u201c\u201d]{2})(?:["\u201c\u201d]c|$)|["\u201c\u201d](?:[^"\u201c\u201d]|["\u201c\u201d]{2})*(?:["\u201c\u201d]|$))/i,null,'"\u201c\u201d'],["com",/^['\u2018\u2019](?:_(?:\r\n?|[^\r]?)|[^\n\r_\u2028\u2029])*/,null,"'\u2018\u2019"]],[["kwd",/^(?:addhandler|addressof|alias|and|andalso|ansi|as|assembly|auto|boolean|byref|byte|byval|call|case|catch|cbool|cbyte|cchar|cdate|cdbl|cdec|char|cint|class|clng|cobj|const|cshort|csng|cstr|ctype|date|decimal|declare|default|delegate|dim|directcast|do|double|each|else|elseif|end|endif|enum|erase|error|event|exit|finally|for|friend|function|get|gettype|gosub|goto|handles|if|implements|imports|in|inherits|integer|interface|is|let|lib|like|long|loop|me|mod|module|mustinherit|mustoverride|mybase|myclass|namespace|new|next|not|notinheritable|notoverridable|object|on|option|optional|or|orelse|overloads|overridable|overrides|paramarray|preserve|private|property|protected|public|raiseevent|readonly|redim|removehandler|resume|return|select|set|shadows|shared|short|single|static|step|stop|string|structure|sub|synclock|then|throw|to|try|typeof|unicode|until|variant|wend|when|while|with|withevents|writeonly|xor|endif|gosub|let|variant|wend)\b/i,
+null],["com",/^rem.*/i],["lit",/^(?:true\b|false\b|nothing\b|\d+(?:e[+-]?\d+[dfr]?|[dfilrs])?|(?:&h[\da-f]+|&o[0-7]+)[ils]?|\d*\.\d+(?:e[+-]?\d+)?[dfr]?|#\s+(?:\d+[/-]\d+[/-]\d+(?:\s+\d+:\d+(?::\d+)?(\s*(?:am|pm))?)?|\d+:\d+(?::\d+)?(\s*(?:am|pm))?)\s+#)/i],["pln",/^(?:(?:[a-z]|_\w)\w*|\[(?:[a-z]|_\w)\w*])/i],["pun",/^[^\w\t\n\r "'[\]\xa0\u2018\u2019\u201c\u201d\u2028\u2029]+/],["pun",/^(?:\[|])/]]),["vb","vbs"]);
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-vhdl.js b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-vhdl.js
new file mode 100644
index 0000000..51f3017
--- /dev/null
+++ b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-vhdl.js
@@ -0,0 +1,3 @@
+PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\r \xa0]+/,null,"\t\n\r \u00a0"]],[["str",/^(?:[box]?"(?:[^"]|"")*"|'.')/i],["com",/^--[^\n\r]*/],["kwd",/^(?:abs|access|after|alias|all|and|architecture|array|assert|attribute|begin|block|body|buffer|bus|case|component|configuration|constant|disconnect|downto|else|elsif|end|entity|exit|file|for|function|generate|generic|group|guarded|if|impure|in|inertial|inout|is|label|library|linkage|literal|loop|map|mod|nand|new|next|nor|not|null|of|on|open|or|others|out|package|port|postponed|procedure|process|pure|range|record|register|reject|rem|report|return|rol|ror|select|severity|shared|signal|sla|sll|sra|srl|subtype|then|to|transport|type|unaffected|units|until|use|variable|wait|when|while|with|xnor|xor)(?=[^\w-]|$)/i,
+null],["typ",/^(?:bit|bit_vector|character|boolean|integer|real|time|string|severity_level|positive|natural|signed|unsigned|line|text|std_u?logic(?:_vector)?)(?=[^\w-]|$)/i,null],["typ",/^'(?:active|ascending|base|delayed|driving|driving_value|event|high|image|instance_name|last_active|last_event|last_value|left|leftof|length|low|path_name|pos|pred|quiet|range|reverse_range|right|rightof|simple_name|stable|succ|transaction|val|value)(?=[^\w-]|$)/i,null],["lit",/^\d+(?:_\d+)*(?:#[\w.\\]+#(?:[+-]?\d+(?:_\d+)*)?|(?:\.\d+(?:_\d+)*)?(?:e[+-]?\d+(?:_\d+)*)?)/i],
+["pln",/^(?:[a-z]\w*|\\[^\\]*\\)/i],["pun",/^[^\w\t\n\r "'\xa0][^\w\t\n\r "'\xa0-]*/]]),["vhdl","vhd"]);
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-wiki.js b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-wiki.js
index 00a1b6b..96c1e34 100644
--- a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-wiki.js
+++ b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-wiki.js
@@ -1 +1,2 @@
-PR.registerLangHandler(PR.createSimpleLexer([[PR.PR_PLAIN,/^[\t \xA0a-gi-z0-9]+/,null,'	 \xa0abcdefgijklmnopqrstuvwxyz0123456789'],[PR.PR_PUNCTUATION,/^[=*~\^\[\]]+/,null,'=*~^[]']],[['lang-wiki.meta',/(?:^^|\r\n?|\n)(#[a-z]+)\b/],[PR.PR_LITERAL,/^(?:[A-Z][a-z][a-z0-9]+[A-Z][a-z][a-zA-Z0-9]+)\b/],['lang-',/^\{\{\{([\s\S]+?)\}\}\}/],['lang-',/^`([^\r\n`]+)`/],[PR.PR_STRING,/^https?:\/\/[^\/?#\s]*(?:\/[^?#\s]*)?(?:\?[^#\s]*)?(?:#\S*)?/i],[PR.PR_PLAIN,/^(?:\r\n|[\s\S])[^#=*~^A-Zh\{`\[\r\n]*/]]),['wiki']),PR.registerLangHandler(PR.createSimpleLexer([[PR.PR_KEYWORD,/^#[a-z]+/i,null,'#']],[]),['wiki.meta'])
\ No newline at end of file
+PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\d\t a-gi-z\xa0]+/,null,"\t \u00a0abcdefgijklmnopqrstuvwxyz0123456789"],["pun",/^[*=[\]^~]+/,null,"=*~^[]"]],[["lang-wiki.meta",/(?:^^|\r\n?|\n)(#[a-z]+)\b/],["lit",/^[A-Z][a-z][\da-z]+[A-Z][a-z][^\W_]+\b/],["lang-",/^{{{([\S\s]+?)}}}/],["lang-",/^`([^\n\r`]+)`/],["str",/^https?:\/\/[^\s#/?]*(?:\/[^\s#?]*)?(?:\?[^\s#]*)?(?:#\S*)?/i],["pln",/^(?:\r\n|[\S\s])[^\n\r#*=A-[^`h{~]*/]]),["wiki"]);
+PR.registerLangHandler(PR.createSimpleLexer([["kwd",/^#[a-z]+/i,null,"#"]],[]),["wiki.meta"]);
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-xq.js b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-xq.js
new file mode 100644
index 0000000..e323ae3
--- /dev/null
+++ b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-xq.js
@@ -0,0 +1,3 @@
+PR.registerLangHandler(PR.createSimpleLexer([["var pln",/^\$[\w-]+/,null,"$"]],[["pln",/^[\s=][<>][\s=]/],["lit",/^@[\w-]+/],["tag",/^<\/?[a-z](?:[\w-.:]*\w)?|\/?>$/i],["com",/^\(:[\S\s]*?:\)/],["pln",/^[(),/;[\]{}]$/],["str",/^(?:"(?:[^"\\{]|\\[\S\s])*(?:"|$)|'(?:[^'\\{]|\\[\S\s])*(?:'|$))/,null,"\"'"],["kwd",/^(?:xquery|where|version|variable|union|typeswitch|treat|to|then|text|stable|sortby|some|self|schema|satisfies|returns|return|ref|processing-instruction|preceding-sibling|preceding|precedes|parent|only|of|node|namespace|module|let|item|intersect|instance|in|import|if|function|for|follows|following-sibling|following|external|except|every|else|element|descending|descendant-or-self|descendant|define|default|declare|comment|child|cast|case|before|attribute|assert|ascending|as|ancestor-or-self|ancestor|after|eq|order|by|or|and|schema-element|document-node|node|at)\b/],
+["typ",/^(?:xs:yearMonthDuration|xs:unsignedLong|xs:time|xs:string|xs:short|xs:QName|xs:Name|xs:long|xs:integer|xs:int|xs:gYearMonth|xs:gYear|xs:gMonthDay|xs:gDay|xs:float|xs:duration|xs:double|xs:decimal|xs:dayTimeDuration|xs:dateTime|xs:date|xs:byte|xs:boolean|xs:anyURI|xf:yearMonthDuration)\b/,null],["fun pln",/^(?:xp:dereference|xinc:node-expand|xinc:link-references|xinc:link-expand|xhtml:restructure|xhtml:clean|xhtml:add-lists|xdmp:zip-manifest|xdmp:zip-get|xdmp:zip-create|xdmp:xquery-version|xdmp:word-convert|xdmp:with-namespaces|xdmp:version|xdmp:value|xdmp:user-roles|xdmp:user-last-login|xdmp:user|xdmp:url-encode|xdmp:url-decode|xdmp:uri-is-file|xdmp:uri-format|xdmp:uri-content-type|xdmp:unquote|xdmp:unpath|xdmp:triggers-database|xdmp:trace|xdmp:to-json|xdmp:tidy|xdmp:subbinary|xdmp:strftime|xdmp:spawn-in|xdmp:spawn|xdmp:sleep|xdmp:shutdown|xdmp:set-session-field|xdmp:set-response-encoding|xdmp:set-response-content-type|xdmp:set-response-code|xdmp:set-request-time-limit|xdmp:set|xdmp:servers|xdmp:server-status|xdmp:server-name|xdmp:server|xdmp:security-database|xdmp:security-assert|xdmp:schema-database|xdmp:save|xdmp:role-roles|xdmp:role|xdmp:rethrow|xdmp:restart|xdmp:request-timestamp|xdmp:request-status|xdmp:request-cancel|xdmp:request|xdmp:redirect-response|xdmp:random|xdmp:quote|xdmp:query-trace|xdmp:query-meters|xdmp:product-edition|xdmp:privilege-roles|xdmp:privilege|xdmp:pretty-print|xdmp:powerpoint-convert|xdmp:platform|xdmp:permission|xdmp:pdf-convert|xdmp:path|xdmp:octal-to-integer|xdmp:node-uri|xdmp:node-replace|xdmp:node-kind|xdmp:node-insert-child|xdmp:node-insert-before|xdmp:node-insert-after|xdmp:node-delete|xdmp:node-database|xdmp:mul64|xdmp:modules-root|xdmp:modules-database|xdmp:merging|xdmp:merge-cancel|xdmp:merge|xdmp:md5|xdmp:logout|xdmp:login|xdmp:log-level|xdmp:log|xdmp:lock-release|xdmp:lock-acquire|xdmp:load|xdmp:invoke-in|xdmp:invoke|xdmp:integer-to-octal|xdmp:integer-to-hex|xdmp:http-put|xdmp:http-post|xdmp:http-options|xdmp:http-head|xdmp:http-get|xdmp:http-delete|xdmp:hosts|xdmp:host-status|xdmp:host-name|xdmp:host|xdmp:hex-to-integer|xdmp:hash64|xdmp:hash32|xdmp:has-privilege|xdmp:groups|xdmp:group-serves|xdmp:group-servers|xdmp:group-name|xdmp:group-hosts|xdmp:group|xdmp:get-session-field-names|xdmp:get-session-field|xdmp:get-response-encoding|xdmp:get-response-code|xdmp:get-request-username|xdmp:get-request-user|xdmp:get-request-url|xdmp:get-request-protocol|xdmp:get-request-path|xdmp:get-request-method|xdmp:get-request-header-names|xdmp:get-request-header|xdmp:get-request-field-names|xdmp:get-request-field-filename|xdmp:get-request-field-content-type|xdmp:get-request-field|xdmp:get-request-client-certificate|xdmp:get-request-client-address|xdmp:get-request-body|xdmp:get-current-user|xdmp:get-current-roles|xdmp:get|xdmp:function-name|xdmp:function-module|xdmp:function|xdmp:from-json|xdmp:forests|xdmp:forest-status|xdmp:forest-restore|xdmp:forest-restart|xdmp:forest-name|xdmp:forest-delete|xdmp:forest-databases|xdmp:forest-counts|xdmp:forest-clear|xdmp:forest-backup|xdmp:forest|xdmp:filesystem-file|xdmp:filesystem-directory|xdmp:exists|xdmp:excel-convert|xdmp:eval-in|xdmp:eval|xdmp:estimate|xdmp:email|xdmp:element-content-type|xdmp:elapsed-time|xdmp:document-set-quality|xdmp:document-set-property|xdmp:document-set-properties|xdmp:document-set-permissions|xdmp:document-set-collections|xdmp:document-remove-properties|xdmp:document-remove-permissions|xdmp:document-remove-collections|xdmp:document-properties|xdmp:document-locks|xdmp:document-load|xdmp:document-insert|xdmp:document-get-quality|xdmp:document-get-properties|xdmp:document-get-permissions|xdmp:document-get-collections|xdmp:document-get|xdmp:document-forest|xdmp:document-delete|xdmp:document-add-properties|xdmp:document-add-permissions|xdmp:document-add-collections|xdmp:directory-properties|xdmp:directory-locks|xdmp:directory-delete|xdmp:directory-create|xdmp:directory|xdmp:diacritic-less|xdmp:describe|xdmp:default-permissions|xdmp:default-collections|xdmp:databases|xdmp:database-restore-validate|xdmp:database-restore-status|xdmp:database-restore-cancel|xdmp:database-restore|xdmp:database-name|xdmp:database-forests|xdmp:database-backup-validate|xdmp:database-backup-status|xdmp:database-backup-purge|xdmp:database-backup-cancel|xdmp:database-backup|xdmp:database|xdmp:collection-properties|xdmp:collection-locks|xdmp:collection-delete|xdmp:collation-canonical-uri|xdmp:castable-as|xdmp:can-grant-roles|xdmp:base64-encode|xdmp:base64-decode|xdmp:architecture|xdmp:apply|xdmp:amp-roles|xdmp:amp|xdmp:add64|xdmp:add-response-header|xdmp:access|trgr:trigger-set-recursive|trgr:trigger-set-permissions|trgr:trigger-set-name|trgr:trigger-set-module|trgr:trigger-set-event|trgr:trigger-set-description|trgr:trigger-remove-permissions|trgr:trigger-module|trgr:trigger-get-permissions|trgr:trigger-enable|trgr:trigger-disable|trgr:trigger-database-online-event|trgr:trigger-data-event|trgr:trigger-add-permissions|trgr:remove-trigger|trgr:property-content|trgr:pre-commit|trgr:post-commit|trgr:get-trigger-by-id|trgr:get-trigger|trgr:document-scope|trgr:document-content|trgr:directory-scope|trgr:create-trigger|trgr:collection-scope|trgr:any-property-content|thsr:set-entry|thsr:remove-term|thsr:remove-synonym|thsr:remove-entry|thsr:query-lookup|thsr:lookup|thsr:load|thsr:insert|thsr:expand|thsr:add-synonym|spell:suggest-detailed|spell:suggest|spell:remove-word|spell:make-dictionary|spell:load|spell:levenshtein-distance|spell:is-correct|spell:insert|spell:double-metaphone|spell:add-word|sec:users-collection|sec:user-set-roles|sec:user-set-password|sec:user-set-name|sec:user-set-description|sec:user-set-default-permissions|sec:user-set-default-collections|sec:user-remove-roles|sec:user-privileges|sec:user-get-roles|sec:user-get-description|sec:user-get-default-permissions|sec:user-get-default-collections|sec:user-doc-permissions|sec:user-doc-collections|sec:user-add-roles|sec:unprotect-collection|sec:uid-for-name|sec:set-realm|sec:security-version|sec:security-namespace|sec:security-installed|sec:security-collection|sec:roles-collection|sec:role-set-roles|sec:role-set-name|sec:role-set-description|sec:role-set-default-permissions|sec:role-set-default-collections|sec:role-remove-roles|sec:role-privileges|sec:role-get-roles|sec:role-get-description|sec:role-get-default-permissions|sec:role-get-default-collections|sec:role-doc-permissions|sec:role-doc-collections|sec:role-add-roles|sec:remove-user|sec:remove-role-from-users|sec:remove-role-from-role|sec:remove-role-from-privileges|sec:remove-role-from-amps|sec:remove-role|sec:remove-privilege|sec:remove-amp|sec:protect-collection|sec:privileges-collection|sec:privilege-set-roles|sec:privilege-set-name|sec:privilege-remove-roles|sec:privilege-get-roles|sec:privilege-add-roles|sec:priv-doc-permissions|sec:priv-doc-collections|sec:get-user-names|sec:get-unique-elem-id|sec:get-role-names|sec:get-role-ids|sec:get-privilege|sec:get-distinct-permissions|sec:get-collection|sec:get-amp|sec:create-user-with-role|sec:create-user|sec:create-role|sec:create-privilege|sec:create-amp|sec:collections-collection|sec:collection-set-permissions|sec:collection-remove-permissions|sec:collection-get-permissions|sec:collection-add-permissions|sec:check-admin|sec:amps-collection|sec:amp-set-roles|sec:amp-remove-roles|sec:amp-get-roles|sec:amp-doc-permissions|sec:amp-doc-collections|sec:amp-add-roles|search:unparse|search:suggest|search:snippet|search:search|search:resolve-nodes|search:resolve|search:remove-constraint|search:parse|search:get-default-options|search:estimate|search:check-options|prof:value|prof:reset|prof:report|prof:invoke|prof:eval|prof:enable|prof:disable|prof:allowed|ppt:clean|pki:template-set-request|pki:template-set-name|pki:template-set-key-type|pki:template-set-key-options|pki:template-set-description|pki:template-in-use|pki:template-get-version|pki:template-get-request|pki:template-get-name|pki:template-get-key-type|pki:template-get-key-options|pki:template-get-id|pki:template-get-description|pki:need-certificate|pki:is-temporary|pki:insert-trusted-certificates|pki:insert-template|pki:insert-signed-certificates|pki:insert-certificate-revocation-list|pki:get-trusted-certificate-ids|pki:get-template-ids|pki:get-template-certificate-authority|pki:get-template-by-name|pki:get-template|pki:get-pending-certificate-requests-xml|pki:get-pending-certificate-requests-pem|pki:get-pending-certificate-request|pki:get-certificates-for-template-xml|pki:get-certificates-for-template|pki:get-certificates|pki:get-certificate-xml|pki:get-certificate-pem|pki:get-certificate|pki:generate-temporary-certificate-if-necessary|pki:generate-temporary-certificate|pki:generate-template-certificate-authority|pki:generate-certificate-request|pki:delete-template|pki:delete-certificate|pki:create-template|pdf:make-toc|pdf:insert-toc-headers|pdf:get-toc|pdf:clean|p:status-transition|p:state-transition|p:remove|p:pipelines|p:insert|p:get-by-id|p:get|p:execute|p:create|p:condition|p:collection|p:action|ooxml:runs-merge|ooxml:package-uris|ooxml:package-parts-insert|ooxml:package-parts|msword:clean|mcgm:polygon|mcgm:point|mcgm:geospatial-query-from-elements|mcgm:geospatial-query|mcgm:circle|math:tanh|math:tan|math:sqrt|math:sinh|math:sin|math:pow|math:modf|math:log10|math:log|math:ldexp|math:frexp|math:fmod|math:floor|math:fabs|math:exp|math:cosh|math:cos|math:ceil|math:atan2|math:atan|math:asin|math:acos|map:put|map:map|map:keys|map:get|map:delete|map:count|map:clear|lnk:to|lnk:remove|lnk:insert|lnk:get|lnk:from|lnk:create|kml:polygon|kml:point|kml:interior-polygon|kml:geospatial-query-from-elements|kml:geospatial-query|kml:circle|kml:box|gml:polygon|gml:point|gml:interior-polygon|gml:geospatial-query-from-elements|gml:geospatial-query|gml:circle|gml:box|georss:point|georss:geospatial-query|georss:circle|geo:polygon|geo:point|geo:interior-polygon|geo:geospatial-query-from-elements|geo:geospatial-query|geo:circle|geo:box|fn:zero-or-one|fn:years-from-duration|fn:year-from-dateTime|fn:year-from-date|fn:upper-case|fn:unordered|fn:true|fn:translate|fn:trace|fn:tokenize|fn:timezone-from-time|fn:timezone-from-dateTime|fn:timezone-from-date|fn:sum|fn:subtract-dateTimes-yielding-yearMonthDuration|fn:subtract-dateTimes-yielding-dayTimeDuration|fn:substring-before|fn:substring-after|fn:substring|fn:subsequence|fn:string-to-codepoints|fn:string-pad|fn:string-length|fn:string-join|fn:string|fn:static-base-uri|fn:starts-with|fn:seconds-from-time|fn:seconds-from-duration|fn:seconds-from-dateTime|fn:round-half-to-even|fn:round|fn:root|fn:reverse|fn:resolve-uri|fn:resolve-QName|fn:replace|fn:remove|fn:QName|fn:prefix-from-QName|fn:position|fn:one-or-more|fn:number|fn:not|fn:normalize-unicode|fn:normalize-space|fn:node-name|fn:node-kind|fn:nilled|fn:namespace-uri-from-QName|fn:namespace-uri-for-prefix|fn:namespace-uri|fn:name|fn:months-from-duration|fn:month-from-dateTime|fn:month-from-date|fn:minutes-from-time|fn:minutes-from-duration|fn:minutes-from-dateTime|fn:min|fn:max|fn:matches|fn:lower-case|fn:local-name-from-QName|fn:local-name|fn:last|fn:lang|fn:iri-to-uri|fn:insert-before|fn:index-of|fn:in-scope-prefixes|fn:implicit-timezone|fn:idref|fn:id|fn:hours-from-time|fn:hours-from-duration|fn:hours-from-dateTime|fn:floor|fn:false|fn:expanded-QName|fn:exists|fn:exactly-one|fn:escape-uri|fn:escape-html-uri|fn:error|fn:ends-with|fn:encode-for-uri|fn:empty|fn:document-uri|fn:doc-available|fn:doc|fn:distinct-values|fn:distinct-nodes|fn:default-collation|fn:deep-equal|fn:days-from-duration|fn:day-from-dateTime|fn:day-from-date|fn:data|fn:current-time|fn:current-dateTime|fn:current-date|fn:count|fn:contains|fn:concat|fn:compare|fn:collection|fn:codepoints-to-string|fn:codepoint-equal|fn:ceiling|fn:boolean|fn:base-uri|fn:avg|fn:adjust-time-to-timezone|fn:adjust-dateTime-to-timezone|fn:adjust-date-to-timezone|fn:abs|feed:unsubscribe|feed:subscription|feed:subscribe|feed:request|feed:item|feed:description|excel:clean|entity:enrich|dom:set-pipelines|dom:set-permissions|dom:set-name|dom:set-evaluation-context|dom:set-domain-scope|dom:set-description|dom:remove-pipeline|dom:remove-permissions|dom:remove|dom:get|dom:evaluation-context|dom:domains|dom:domain-scope|dom:create|dom:configuration-set-restart-user|dom:configuration-set-permissions|dom:configuration-set-evaluation-context|dom:configuration-set-default-domain|dom:configuration-get|dom:configuration-create|dom:collection|dom:add-pipeline|dom:add-permissions|dls:retention-rules|dls:retention-rule-remove|dls:retention-rule-insert|dls:retention-rule|dls:purge|dls:node-expand|dls:link-references|dls:link-expand|dls:documents-query|dls:document-versions-query|dls:document-version-uri|dls:document-version-query|dls:document-version-delete|dls:document-version-as-of|dls:document-version|dls:document-update|dls:document-unmanage|dls:document-set-quality|dls:document-set-property|dls:document-set-properties|dls:document-set-permissions|dls:document-set-collections|dls:document-retention-rules|dls:document-remove-properties|dls:document-remove-permissions|dls:document-remove-collections|dls:document-purge|dls:document-manage|dls:document-is-managed|dls:document-insert-and-manage|dls:document-include-query|dls:document-history|dls:document-get-permissions|dls:document-extract-part|dls:document-delete|dls:document-checkout-status|dls:document-checkout|dls:document-checkin|dls:document-add-properties|dls:document-add-permissions|dls:document-add-collections|dls:break-checkout|dls:author-query|dls:as-of-query|dbk:convert|dbg:wait|dbg:value|dbg:stopped|dbg:stop|dbg:step|dbg:status|dbg:stack|dbg:out|dbg:next|dbg:line|dbg:invoke|dbg:function|dbg:finish|dbg:expr|dbg:eval|dbg:disconnect|dbg:detach|dbg:continue|dbg:connect|dbg:clear|dbg:breakpoints|dbg:break|dbg:attached|dbg:attach|cvt:save-converted-documents|cvt:part-uri|cvt:destination-uri|cvt:basepath|cvt:basename|cts:words|cts:word-query-weight|cts:word-query-text|cts:word-query-options|cts:word-query|cts:word-match|cts:walk|cts:uris|cts:uri-match|cts:train|cts:tokenize|cts:thresholds|cts:stem|cts:similar-query-weight|cts:similar-query-nodes|cts:similar-query|cts:shortest-distance|cts:search|cts:score|cts:reverse-query-weight|cts:reverse-query-nodes|cts:reverse-query|cts:remainder|cts:registered-query-weight|cts:registered-query-options|cts:registered-query-ids|cts:registered-query|cts:register|cts:query|cts:quality|cts:properties-query-query|cts:properties-query|cts:polygon-vertices|cts:polygon|cts:point-longitude|cts:point-latitude|cts:point|cts:or-query-queries|cts:or-query|cts:not-query-weight|cts:not-query-query|cts:not-query|cts:near-query-weight|cts:near-query-queries|cts:near-query-options|cts:near-query-distance|cts:near-query|cts:highlight|cts:geospatial-co-occurrences|cts:frequency|cts:fitness|cts:field-words|cts:field-word-query-weight|cts:field-word-query-text|cts:field-word-query-options|cts:field-word-query-field-name|cts:field-word-query|cts:field-word-match|cts:entity-highlight|cts:element-words|cts:element-word-query-weight|cts:element-word-query-text|cts:element-word-query-options|cts:element-word-query-element-name|cts:element-word-query|cts:element-word-match|cts:element-values|cts:element-value-ranges|cts:element-value-query-weight|cts:element-value-query-text|cts:element-value-query-options|cts:element-value-query-element-name|cts:element-value-query|cts:element-value-match|cts:element-value-geospatial-co-occurrences|cts:element-value-co-occurrences|cts:element-range-query-weight|cts:element-range-query-value|cts:element-range-query-options|cts:element-range-query-operator|cts:element-range-query-element-name|cts:element-range-query|cts:element-query-query|cts:element-query-element-name|cts:element-query|cts:element-pair-geospatial-values|cts:element-pair-geospatial-value-match|cts:element-pair-geospatial-query-weight|cts:element-pair-geospatial-query-region|cts:element-pair-geospatial-query-options|cts:element-pair-geospatial-query-longitude-name|cts:element-pair-geospatial-query-latitude-name|cts:element-pair-geospatial-query-element-name|cts:element-pair-geospatial-query|cts:element-pair-geospatial-boxes|cts:element-geospatial-values|cts:element-geospatial-value-match|cts:element-geospatial-query-weight|cts:element-geospatial-query-region|cts:element-geospatial-query-options|cts:element-geospatial-query-element-name|cts:element-geospatial-query|cts:element-geospatial-boxes|cts:element-child-geospatial-values|cts:element-child-geospatial-value-match|cts:element-child-geospatial-query-weight|cts:element-child-geospatial-query-region|cts:element-child-geospatial-query-options|cts:element-child-geospatial-query-element-name|cts:element-child-geospatial-query-child-name|cts:element-child-geospatial-query|cts:element-child-geospatial-boxes|cts:element-attribute-words|cts:element-attribute-word-query-weight|cts:element-attribute-word-query-text|cts:element-attribute-word-query-options|cts:element-attribute-word-query-element-name|cts:element-attribute-word-query-attribute-name|cts:element-attribute-word-query|cts:element-attribute-word-match|cts:element-attribute-values|cts:element-attribute-value-ranges|cts:element-attribute-value-query-weight|cts:element-attribute-value-query-text|cts:element-attribute-value-query-options|cts:element-attribute-value-query-element-name|cts:element-attribute-value-query-attribute-name|cts:element-attribute-value-query|cts:element-attribute-value-match|cts:element-attribute-value-geospatial-co-occurrences|cts:element-attribute-value-co-occurrences|cts:element-attribute-range-query-weight|cts:element-attribute-range-query-value|cts:element-attribute-range-query-options|cts:element-attribute-range-query-operator|cts:element-attribute-range-query-element-name|cts:element-attribute-range-query-attribute-name|cts:element-attribute-range-query|cts:element-attribute-pair-geospatial-values|cts:element-attribute-pair-geospatial-value-match|cts:element-attribute-pair-geospatial-query-weight|cts:element-attribute-pair-geospatial-query-region|cts:element-attribute-pair-geospatial-query-options|cts:element-attribute-pair-geospatial-query-longitude-name|cts:element-attribute-pair-geospatial-query-latitude-name|cts:element-attribute-pair-geospatial-query-element-name|cts:element-attribute-pair-geospatial-query|cts:element-attribute-pair-geospatial-boxes|cts:document-query-uris|cts:document-query|cts:distance|cts:directory-query-uris|cts:directory-query-depth|cts:directory-query|cts:destination|cts:deregister|cts:contains|cts:confidence|cts:collections|cts:collection-query-uris|cts:collection-query|cts:collection-match|cts:classify|cts:circle-radius|cts:circle-center|cts:circle|cts:box-west|cts:box-south|cts:box-north|cts:box-east|cts:box|cts:bearing|cts:arc-intersection|cts:and-query-queries|cts:and-query-options|cts:and-query|cts:and-not-query-positive-query|cts:and-not-query-negative-query|cts:and-not-query|css:get|css:convert|cpf:success|cpf:failure|cpf:document-set-state|cpf:document-set-processing-status|cpf:document-set-last-updated|cpf:document-set-error|cpf:document-get-state|cpf:document-get-processing-status|cpf:document-get-last-updated|cpf:document-get-error|cpf:check-transition|alert:spawn-matching-actions|alert:rule-user-id-query|alert:rule-set-user-id|alert:rule-set-query|alert:rule-set-options|alert:rule-set-name|alert:rule-set-description|alert:rule-set-action|alert:rule-remove|alert:rule-name-query|alert:rule-insert|alert:rule-id-query|alert:rule-get-user-id|alert:rule-get-query|alert:rule-get-options|alert:rule-get-name|alert:rule-get-id|alert:rule-get-description|alert:rule-get-action|alert:rule-action-query|alert:remove-triggers|alert:make-rule|alert:make-log-action|alert:make-config|alert:make-action|alert:invoke-matching-actions|alert:get-my-rules|alert:get-all-rules|alert:get-actions|alert:find-matching-rules|alert:create-triggers|alert:config-set-uri|alert:config-set-trigger-ids|alert:config-set-options|alert:config-set-name|alert:config-set-description|alert:config-set-cpf-domain-names|alert:config-set-cpf-domain-ids|alert:config-insert|alert:config-get-uri|alert:config-get-trigger-ids|alert:config-get-options|alert:config-get-name|alert:config-get-id|alert:config-get-description|alert:config-get-cpf-domain-names|alert:config-get-cpf-domain-ids|alert:config-get|alert:config-delete|alert:action-set-options|alert:action-set-name|alert:action-set-module-root|alert:action-set-module-db|alert:action-set-module|alert:action-set-description|alert:action-remove|alert:action-insert|alert:action-get-options|alert:action-get-name|alert:action-get-module-root|alert:action-get-module-db|alert:action-get-module|alert:action-get-description|zero-or-one|years-from-duration|year-from-dateTime|year-from-date|upper-case|unordered|true|translate|trace|tokenize|timezone-from-time|timezone-from-dateTime|timezone-from-date|sum|subtract-dateTimes-yielding-yearMonthDuration|subtract-dateTimes-yielding-dayTimeDuration|substring-before|substring-after|substring|subsequence|string-to-codepoints|string-pad|string-length|string-join|string|static-base-uri|starts-with|seconds-from-time|seconds-from-duration|seconds-from-dateTime|round-half-to-even|round|root|reverse|resolve-uri|resolve-QName|replace|remove|QName|prefix-from-QName|position|one-or-more|number|not|normalize-unicode|normalize-space|node-name|node-kind|nilled|namespace-uri-from-QName|namespace-uri-for-prefix|namespace-uri|name|months-from-duration|month-from-dateTime|month-from-date|minutes-from-time|minutes-from-duration|minutes-from-dateTime|min|max|matches|lower-case|local-name-from-QName|local-name|last|lang|iri-to-uri|insert-before|index-of|in-scope-prefixes|implicit-timezone|idref|id|hours-from-time|hours-from-duration|hours-from-dateTime|floor|false|expanded-QName|exists|exactly-one|escape-uri|escape-html-uri|error|ends-with|encode-for-uri|empty|document-uri|doc-available|doc|distinct-values|distinct-nodes|default-collation|deep-equal|days-from-duration|day-from-dateTime|day-from-date|data|current-time|current-dateTime|current-date|count|contains|concat|compare|collection|codepoints-to-string|codepoint-equal|ceiling|boolean|base-uri|avg|adjust-time-to-timezone|adjust-dateTime-to-timezone|adjust-date-to-timezone|abs)\b/],
+["pln",/^[\w:-]+/],["pln",/^[\t\n\r \xa0]+/]]),["xq","xquery"]);
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-yaml.js b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-yaml.js
new file mode 100644
index 0000000..c38729b
--- /dev/null
+++ b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/lang-yaml.js
@@ -0,0 +1,2 @@
+var a=null;
+PR.registerLangHandler(PR.createSimpleLexer([["pun",/^[:>?|]+/,a,":|>?"],["dec",/^%(?:YAML|TAG)[^\n\r#]+/,a,"%"],["typ",/^&\S+/,a,"&"],["typ",/^!\S*/,a,"!"],["str",/^"(?:[^"\\]|\\.)*(?:"|$)/,a,'"'],["str",/^'(?:[^']|'')*(?:'|$)/,a,"'"],["com",/^#[^\n\r]*/,a,"#"],["pln",/^\s+/,a," \t\r\n"]],[["dec",/^(?:---|\.\.\.)(?:[\n\r]|$)/],["pun",/^-/],["kwd",/^\w+:[\n\r ]/],["pln",/^\w+/]]),["yaml","yml"]);
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/prettify.css b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/prettify.css
index 26e3e26..4bfda8e 100644
--- a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/prettify.css
+++ b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/prettify.css
@@ -1,40 +1,53 @@
 /* Pretty printing styles. Used with prettify.js. */
+@external .*;
 
-@external .str;
-@external .kwd;
-@external .com;
-@external .typ;
-@external .lit;
-@external .pun;
-@external .pln;
-@external .tag;
-@external .atn;
-@external .atv;
-@external .dec;
-@external .prettyprint;
+/* SPAN elements with the classes below are added by prettyprint. */
+.pln { color: #000 }  /* plain text */
 
-.str { color: #080; }
-.kwd { color: #008; }
-.com { color: #800; }
-.typ { color: #606; }
-.lit { color: #066; }
-.pun { color: #660; }
-.pln { color: #000; }
-.tag { color: #008; }
-.atn { color: #606; }
-.atv { color: #080; }
-.dec { color: #606; }
-pre.prettyprint { padding: 2px; border: 1px solid #888; }
-
-@media print {
-  .str { color: #060; }
-  .kwd { color: #006; font-weight: bold; }
-  .com { color: #600; font-style: italic; }
-  .typ { color: #404; font-weight: bold; }
-  .lit { color: #044; }
-  .pun { color: #440; }
-  .pln { color: #000; }
-  .tag { color: #006; font-weight: bold; }
-  .atn { color: #404; }
-  .atv { color: #060; }
+@media screen {
+  .str { color: #080 }  /* string content */
+  .kwd { color: #008 }  /* a keyword */
+  .com { color: #800 }  /* a comment */
+  .typ { color: #606 }  /* a type name */
+  .lit { color: #066 }  /* a literal value */
+  /* punctuation, lisp open bracket, lisp close bracket */
+  .pun, .opn, .clo { color: #660 }
+  .tag { color: #008 }  /* a markup tag name */
+  .atn { color: #606 }  /* a markup attribute name */
+  .atv { color: #080 }  /* a markup attribute value */
+  .dec, .var { color: #606 }  /* a declaration; a variable name */
+  .fun { color: red }  /* a function name */
 }
+
+/* Use higher contrast and text-weight for printable form. */
+@media print, projection {
+  .str { color: #060 }
+  .kwd { color: #006; font-weight: bold }
+  .com { color: #600; font-style: italic }
+  .typ { color: #404; font-weight: bold }
+  .lit { color: #044 }
+  .pun, .opn, .clo { color: #440 }
+  .tag { color: #006; font-weight: bold }
+  .atn { color: #404 }
+  .atv { color: #060 }
+}
+
+/* Put a border around prettyprinted code snippets. */
+pre.prettyprint { padding: 2px; border: 1px solid #888 }
+
+/* Specify class=linenums on a pre to get line numbering */
+ol.linenums { margin-top: 0; margin-bottom: 0 } /* IE indents via margin-left */
+li.L0,
+li.L1,
+li.L2,
+li.L3,
+li.L5,
+li.L6,
+li.L7,
+li.L8 { list-style-type: none }
+/* Alternate shading for lines */
+li.L1,
+li.L3,
+li.L5,
+li.L7,
+li.L9 { background: #eee }
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/prettify.js b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/prettify.js
index 29b5e73..4827bc3 100644
--- a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/prettify.js
+++ b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/prettify.js
@@ -1,46 +1,28 @@
-window.PR_SHOULD_USE_CONTINUATION=true,window.PR_TAB_WIDTH=8,window.PR_normalizedHtml=window.PR=window.prettyPrintOne=window.prettyPrint=void
-0,window._pr_isIE6=function(){var a=navigator&&navigator.userAgent&&navigator.userAgent.match(/\bMSIE ([678])\./);return a=a?+a[1]:false,window._pr_isIE6=function(){return a},a},(function(){var
-a=true,b=null,c='break continue do else for if return while auto case char const default double enum extern float goto int long register short signed sizeof static struct switch typedef union unsigned void volatile catch class delete false import new operator private protected public this throw true try typeof ',d=c+'alignof align_union asm axiom bool '+'concept concept_map const_cast constexpr decltype '+'dynamic_cast explicit export friend inline late_check '+'mutable namespace nullptr reinterpret_cast static_assert static_cast '+'template typeid typename using virtual wchar_t where ',e=c+'abstract boolean byte extends final finally implements import '+'instanceof null native package strictfp super synchronized throws '+'transient ',f=e+'as base by checked decimal delegate descending event '+'fixed foreach from group implicit in interface internal into is lock '+'object out override orderby params partial readonly ref sbyte sealed '+'stackalloc string select uint ulong unchecked unsafe ushort var ',g=c+'debugger eval export function get null set undefined var with '+'Infinity NaN ',h='caller delete die do dump elsif eval exit foreach for goto if import last local my next no our print package redo require sub undef unless until use wantarray while BEGIN END ',i='break continue do else for if return while and as assert class def del elif except exec finally from global import in is lambda nonlocal not or pass print raise try with yield False True None ',j='break continue do else for if return while alias and begin case class def defined elsif end ensure false in module next nil not or redo rescue retry self super then true undef unless until when yield BEGIN END ',k='break continue do else for if return while case done elif esac eval fi function in local set then until ',l=d+f+g+h+i+j+k,m=(function(){var
-a=['!','!=','!==','#','%','%=','&','&&','&&=','&=','(','*','*=','+=',',','-=','->','/','/=',':','::',';','<','<<','<<=','<=','=','==','===','>','>=','>>','>>=','>>>','>>>=','?','@','[','^','^=','^^','^^=','{','|','|=','||','||=','~','break','case','continue','delete','do','else','finally','instanceof','return','throw','try','typeof'],b='(?:^^|[+-]',c;for(c=0;c<a.length;++c)b+='|'+a[c].replace(/([^=<>:&a-z])/g,'\\$1');return b+=')\\s*',b})(),n=/&/g,o=/</g,p=/>/g,q=/\"/g,r,s,t,u,v,w,x,y,z,A,B,C,D,E,F;function
-G(a){return a.replace(n,'&amp;').replace(o,'&lt;').replace(p,'&gt;').replace(q,'&quot;')}function
-H(a){return a.replace(n,'&amp;').replace(o,'&lt;').replace(p,'&gt;')}C=/&lt;/g,B=/&gt;/g,w=/&apos;/g,E=/&quot;/g,v=/&amp;/g,D=/&nbsp;/g;function
-I(a){var b=a.indexOf('&'),c,d,e,f;if(b<0)return a;for(--b;(b=a.indexOf('&#',b+1))>=0;)d=a.indexOf(';',b),d>=0&&(e=a.substring(b+3,d),f=10,e&&e.charAt(0)==='x'&&(e=e.substring(1),f=16),c=parseInt(e,f),isNaN(c)||(a=a.substring(0,b)+String.fromCharCode(c)+a.substring(d+1)));return a.replace(C,'<').replace(B,'>').replace(w,'\'').replace(E,'\"').replace(D,' ').replace(v,'&')}function
-J(a){return'XMP'===a.tagName}u=/[\r\n]/g;function K(c,d){var e;return'PRE'===c.tagName?a:u.test(d)?(e='',c.currentStyle?(e=c.currentStyle.whiteSpace):window.getComputedStyle&&(e=window.getComputedStyle(c,b).whiteSpace),!e||e==='pre'):a}function
-L(a,b){var c,d,e,f;switch(a.nodeType){case 1:f=a.tagName.toLowerCase(),b.push('<',f);for(e=0;e<a.attributes.length;++e){c=a.attributes[e];if(!c.specified)continue;b.push(' '),L(c,b)}b.push('>');for(d=a.firstChild;d;d=d.nextSibling)L(d,b);(a.firstChild||!/^(?:br|link|img)$/.test(f))&&b.push('</',f,'>');break;case
-2:b.push(a.name.toLowerCase(),'=\"',G(a.value),'\"');break;case 3:case 4:b.push(H(a.nodeValue))}}function
-M(b){var c=0,d=false,e=false,f,g,h,i;for(f=0,g=b.length;f<g;++f){h=b[f];if(h.ignoreCase)e=a;else
-if(/[a-z]/i.test(h.source.replace(/\\u[0-9a-f]{4}|\\x[0-9a-f]{2}|\\[^ux]/gi,''))){d=a,e=false;break}}function
-j(a){if(a.charAt(0)!=='\\')return a.charCodeAt(0);switch(a.charAt(1)){case'b':return 8;case't':return 9;case'n':return 10;case'v':return 11;case'f':return 12;case'r':return 13;case'u':case'x':return parseInt(a.substring(2),16)||a.charCodeAt(1);case'0':case'1':case'2':case'3':case'4':case'5':case'6':case'7':return parseInt(a.substring(1),8);default:return a.charCodeAt(1)}}function
-k(a){var b;return a<32?(a<16?'\\x0':'\\x')+a.toString(16):(b=String.fromCharCode(a),(b==='\\'||b==='-'||b==='['||b===']')&&(b='\\'+b),b)}function
-l(a){var b=a.substring(1,a.length-1).match(new RegExp('\\\\u[0-9A-Fa-f]{4}|\\\\x[0-9A-Fa-f]{2}|\\\\[0-3][0-7]{0,2}|\\\\[0-7]{1,2}|\\\\[\\s\\S]|-|[^-\\\\]','g')),c=[],d=[],e=b[0]==='^',f,g,h,i,m,n,o,p,q;for(h=e?1:0,m=b.length;h<m;++h){o=b[h];switch(o){case'\\B':case'\\b':case'\\D':case'\\d':case'\\S':case'\\s':case'\\W':case'\\w':c.push(o);continue}q=j(o),h+2<m&&'-'===b[h+1]?(g=j(b[h+2]),h+=2):(g=q),d.push([q,g]),g<65||q>122||(g<65||q>90||d.push([Math.max(65,q)|32,Math.min(g,90)|32]),g<97||q>122||d.push([Math.max(97,q)&-33,Math.min(g,122)&-33]))}d.sort(function(a,b){return a[0]-b[0]||b[1]-a[1]}),f=[],i=[NaN,NaN];for(h=0;h<d.length;++h)p=d[h],p[0]<=i[1]+1?(i[1]=Math.max(i[1],p[1])):f.push(i=p);n=['['],e&&n.push('^'),n.push.apply(n,c);for(h=0;h<f.length;++h)p=f[h],n.push(k(p[0])),p[1]
->p[0]&&(p[1]+1>p[0]&&n.push('-'),n.push(k(p[1])));return n.push(']'),n.join('')}function
-m(a){var b=a.source.match(new RegExp('(?:\\[(?:[^\\x5C\\x5D]|\\\\[\\s\\S])*\\]|\\\\u[A-Fa-f0-9]{4}|\\\\x[A-Fa-f0-9]{2}|\\\\[0-9]+|\\\\[^ux0-9]|\\(\\?[:!=]|[\\(\\)\\^]|[^\\x5B\\x5C\\(\\)\\^]+)','g')),e=b.length,f=[],g,h,i,j,k;for(j=0,i=0;j<e;++j)k=b[j],k==='('?++i:'\\'===k.charAt(0)&&(h=+k.substring(1),h&&h<=i&&(f[h]=-1));for(j=1;j<f.length;++j)-1===f[j]&&(f[j]=++c);for(j=0,i=0;j<e;++j)k=b[j],k==='('?(++i,f[i]===void
-0&&(b[j]='(?:')):'\\'===k.charAt(0)&&(h=+k.substring(1),h&&h<=i&&(b[j]='\\'+f[i]));for(j=0,i=0;j<e;++j)'^'===b[j]&&'^'!==b[j+1]&&(b[j]='');if(a.ignoreCase&&d)for(j=0;j<e;++j)k=b[j],g=k.charAt(0),k.length>=2&&g==='['?(b[j]=l(k)):g!=='\\'&&(b[j]=k.replace(/[a-zA-Z]/g,function(a){var
-b=a.charCodeAt(0);return'['+String.fromCharCode(b&-33,b|32)+']'}));return b.join('')}i=[];for(f=0,g=b.length;f<g;++f){h=b[f];if(h.global||h.multiline)throw new
-Error(''+h);i.push('(?:'+m(h)+')')}return new RegExp(i.join('|'),e?'gi':'g')}r=b;function
-N(a){var c,d,e,f;b===r&&(f=document.createElement('PRE'),f.appendChild(document.createTextNode('<!DOCTYPE foo PUBLIC \"foo bar\">\n<foo />')),r=!/</.test(f.innerHTML));if(r)return d=a.innerHTML,J(a)?(d=H(d)):K(a,d)||(d=d.replace(/(<br\s*\/?>)[\r\n]+/g,'$1').replace(/(?:[\r\n]+[ \t]*)+/g,' ')),d;e=[];for(c=a.firstChild;c;c=c.nextSibling)L(c,e);return e.join('')}function
-O(a){var c=0;return function(d){var e=b,f=0,g,h,i,j;for(h=0,i=d.length;h<i;++h){g=d.charAt(h);switch(g){case'	':e||(e=[]),e.push(d.substring(f,h)),j=a-c%a,c+=j;for(;j>=0;j-='                '.length)e.push('                '.substring(0,j));f=h+1;break;case'\n':c=0;break;default:++c}}return e?(e.push(d.substring(f)),e.join('')):d}}z=new
-RegExp('[^<]+|<!--[\\s\\S]*?-->|<!\\[CDATA\\[[\\s\\S]*?\\]\\]>|</?[a-zA-Z](?:[^>\"\']|\'[^\']*\'|\"[^\"]*\")*>|<','g'),A=/^<\!--/,y=/^<!\[CDATA\[/,x=/^<br\b/i,F=/^<(\/?)([a-zA-Z][a-zA-Z0-9]*)/;function
-P(a){var b=a.match(z),c=[],d=0,e=[],f,g,h,i,j,k,l,m;if(b)for(g=0,k=b.length;g<k;++g){j=b[g];if(j.length>1&&j.charAt(0)==='<'){if(A.test(j))continue;if(y.test(j))c.push(j.substring(9,j.length-3)),d+=j.length-12;else
-if(x.test(j))c.push('\n'),++d;else if(j.indexOf('nocode')>=0&&Q(j)){l=(j.match(F))[2],f=1;for(h=g+1;h<k;++h){m=b[h].match(F);if(m&&m[2]===l)if(m[1]==='/'){if(--f===0)break}else++f}h<k?(e.push(d,b.slice(g,h+1).join('')),g=h):e.push(d,j)}else
-e.push(d,j)}else i=I(j),c.push(i),d+=i.length}return{source:c.join(''),tags:e}}function
-Q(a){return!!a.replace(/\s(\w+)\s*=\s*(?:\"([^\"]*)\"|'([^\']*)'|(\S+))/g,' $1=\"$2$3$4\"').match(/[cC][lL][aA][sS][sS]=\"[^\"]*\bnocode\b/)}function
-R(a,b,c,d){var e;if(!b)return;e={source:b,basePos:a},c(e),d.push.apply(d,e.decorations)}function
-S(a,c){var d={},e,f,g,h;return(function(){var e=a.concat(c),f=[],g={},i,j,k,l,m,n,o;for(j=0,l=e.length;j<l;++j){m=e[j],o=m[3];if(o)for(i=o.length;--i>=0;)d[o.charAt(i)]=m;n=m[1],k=''+n,g.hasOwnProperty(k)||(f.push(n),g[k]=b)}f.push(/[\0-\uffff]/),h=M(f)})(),f=c.length,g=/\S/,e=function(a){var
-b=a.source,g=a.basePos,i,j,k,l,m,n,o,p,q,r,s,t,u,v,w,x,y;i=[g,'pln'],s=0,y=b.match(h)||[],u={};for(v=0,q=y.length;v<q;++v){w=y[v],t=u[w],p=void
-0;if(typeof t==='string')n=false;else{r=d[w.charAt(0)];if(r)p=w.match(r[1]),t=r[0];else{for(m=0;m<f;++m){r=c[m],p=w.match(r[1]);if(p){t=r[0];break}}p||(t='pln')}n=t.length>=5&&'lang-'===t.substring(0,5),n&&!(p&&typeof
-p[1]==='string')&&(n=false,t='src'),n||(u[w]=t)}x=s,s+=w.length,n?(j=p[1],l=w.indexOf(j),k=l+j.length,p[2]&&(k=w.length-p[2].length,l=k-j.length),o=t.substring(5),R(g+x,w.substring(0,l),e,i),R(g+x+l,j,W(o,j),i),R(g+x+k,w.substring(k),e,i)):i.push(g+x,t)}a.decorations=i},e}function
-T(a){var c=[],d=[],e,f;return a.tripleQuotedStrings?c.push(['str',/^(?:\'\'\'(?:[^\'\\]|\\[\s\S]|\'{1,2}(?=[^\']))*(?:\'\'\'|$)|\"\"\"(?:[^\"\\]|\\[\s\S]|\"{1,2}(?=[^\"]))*(?:\"\"\"|$)|\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$))/,b,'\'\"']):a.multiLineStrings?c.push(['str',/^(?:\'(?:[^\\\']|\\[\s\S])*(?:\'|$)|\"(?:[^\\\"]|\\[\s\S])*(?:\"|$)|\`(?:[^\\\`]|\\[\s\S])*(?:\`|$))/,b,'\'\"`']):c.push(['str',/^(?:\'(?:[^\\\'\r\n]|\\.)*(?:\'|$)|\"(?:[^\\\"\r\n]|\\.)*(?:\"|$))/,b,'\"\'']),a.verbatimStrings&&d.push(['str',/^@\"(?:[^\"]|\"\")*(?:\"|$)/,b]),a.hashComments&&(a.cStyleComments?(c.push(['com',/^#(?:(?:define|elif|else|endif|error|ifdef|include|ifndef|line|pragma|undef|warning)\b|[^\r\n]*)/,b,'#']),d.push(['str',/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h|[a-z]\w*)>/,b])):c.push(['com',/^#[^\r\n]*/,b,'#'])),a.cStyleComments&&(d.push(['com',/^\/\/[^\r\n]*/,b]),d.push(['com',/^\/\*[\s\S]*?(?:\*\/|$)/,b])),a.regexLiterals&&(e='/(?=[^/*])(?:[^/\\x5B\\x5C]|\\x5C[\\s\\S]|\\x5B(?:[^\\x5C\\x5D]|\\x5C[\\s\\S])*(?:\\x5D|$))+/',d.push(['lang-regex',new
-RegExp('^'+m+'('+e+')')])),f=a.keywords.replace(/^\s+|\s+$/g,''),f.length&&d.push(['kwd',new
-RegExp('^(?:'+f.replace(/\s+/g,'|')+')\\b'),b]),c.push(['pln',/^\s+/,b,' \r\n	\xa0']),d.push(['lit',/^@[a-z_$][a-z_$@0-9]*/i,b],['typ',/^@?[A-Z]+[a-z][A-Za-z_$@0-9]*/,b],['pln',/^[a-z_$][a-z_$@0-9]*/i,b],['lit',new
-RegExp('^(?:0x[a-f0-9]+|(?:\\d(?:_\\d+)*\\d*(?:\\.\\d*)?|\\.\\d\\+)(?:e[+\\-]?\\d+)?)[a-z]*','i'),b,'0123456789'],['pun',/^.[^\s\w\.$@\'\"\`\/\#]*/,b]),S(c,d)}s=T({keywords:l,hashComments:a,cStyleComments:a,multiLineStrings:a,regexLiterals:a});function
-U(c){var d=c.source,e=c.extractedTags,f=c.decorations,g=[],h=0,i=b,j=b,k=0,l=0,m=O(window.PR_TAB_WIDTH),n=/([\r\n ]) /g,o=/(^| ) /gm,p=/\r\n?|\n/g,q=/[ \r\n]$/,r=a,s;function
-t(a){var c,e;a>h&&(i&&i!==j&&(g.push('</span>'),i=b),!i&&j&&(i=j,g.push('<span class=\"',i,'\">')),c=H(m(d.substring(h,a))).replace(r?o:n,'$1&nbsp;'),r=q.test(c),e=window._pr_isIE6()?'&nbsp;<br />':'<br />',g.push(c.replace(p,e)),h=a)}while(a){k<e.length?l<f.length?(s=e[k]<=f[l]):(s=a):(s=false);if(s)t(e[k]),i&&(g.push('</span>'),i=b),g.push(e[k+1]),k+=2;else
-if(l<f.length)t(f[l]),j=f[l+1],l+=2;else break}t(d.length),i&&g.push('</span>'),c.prettyPrintedHtml=g.join('')}t={};function
-V(a,b){var c,d;for(d=b.length;--d>=0;)c=b[d],t.hasOwnProperty(c)?'console'in window&&console.warn('cannot override language handler %s',c):(t[c]=a)}function
-W(a,b){return a&&t.hasOwnProperty(a)||(a=/^\s*</.test(b)?'default-markup':'default-code'),t[a]}V(s,['default-code']),V(S([],[['pln',/^[^<?]+/],['dec',/^<!\w[^>]*(?:>|$)/],['com',/^<\!--[\s\S]*?(?:-\->|$)/],['lang-',/^<\?([\s\S]+?)(?:\?>|$)/],['lang-',/^<%([\s\S]+?)(?:%>|$)/],['pun',/^(?:<[%?]|[%?]>)/],['lang-',/^<xmp\b[^>]*>([\s\S]+?)<\/xmp\b[^>]*>/i],['lang-js',/^<script\b[^>]*>([\s\S]*?)(<\/script\b[^>]*>)/i],['lang-css',/^<style\b[^>]*>([\s\S]*?)(<\/style\b[^>]*>)/i],['lang-in.tag',/^(<\/?[a-z][^<>]*>)/i]]),['default-markup','htm','html','mxml','xhtml','xml','xsl']),V(S([['pln',/^[\s]+/,b,' 	\r\n'],['atv',/^(?:\"[^\"]*\"?|\'[^\']*\'?)/,b,'\"\'']],[['tag',/^^<\/?[a-z](?:[\w.:-]*\w)?|\/?>$/i],['atn',/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],['lang-uq.val',/^=\s*([^>\'\"\s]*(?:[^>\'\"\s\/]|\/(?=\s)))/],['pun',/^[=<>\/]+/],['lang-js',/^on\w+\s*=\s*\"([^\"]+)\"/i],['lang-js',/^on\w+\s*=\s*\'([^\']+)\'/i],['lang-js',/^on\w+\s*=\s*([^\"\'>\s]+)/i],['lang-css',/^style\s*=\s*\"([^\"]+)\"/i],['lang-css',/^style\s*=\s*\'([^\']+)\'/i],['lang-css',/^style\s*=\s*([^\"\'>\s]+)/i]]),['in.tag']),V(S([],[['atv',/^[\s\S]+/]]),['uq.val']),V(T({keywords:d,hashComments:a,cStyleComments:a}),['c','cc','cpp','cxx','cyc','m']),V(T({keywords:'null true false'}),['json']),V(T({keywords:f,hashComments:a,cStyleComments:a,verbatimStrings:a}),['cs']),V(T({keywords:e,cStyleComments:a}),['java']),V(T({keywords:k,hashComments:a,multiLineStrings:a}),['bsh','csh','sh']),V(T({keywords:i,hashComments:a,multiLineStrings:a,tripleQuotedStrings:a}),['cv','py']),V(T({keywords:h,hashComments:a,multiLineStrings:a,regexLiterals:a}),['perl','pl','pm']),V(T({keywords:j,hashComments:a,multiLineStrings:a,regexLiterals:a}),['rb']),V(T({keywords:g,cStyleComments:a,regexLiterals:a}),['js']),V(S([],[['str',/^[\s\S]+/]]),['regex']);function
-X(a){var b=a.sourceCodeHtml,c=a.langExtension,d,e;a.prettyPrintedHtml=b;try{e=P(b),d=e.source,a.source=d,a.basePos=0,a.extractedTags=e.tags,W(c,d)(a),U(a)}catch(f){'console'in
-window&&(console.log(f),console.trace())}}function Y(a,b){var c={sourceCodeHtml:a,langExtension:b};return X(c),c.prettyPrintedHtml}function
-Z(c){var d=window._pr_isIE6(),e=d===6?'\r\n':'\r',f=[document.getElementsByTagName('pre'),document.getElementsByTagName('code'),document.getElementsByTagName('xmp')],g=[],h,i,j,k,l,m;for(i=0;i<f.length;++i)for(j=0,l=f[i].length;j<l;++j)g.push(f[i][j]);f=b,h=Date,h.now||(h={now:function(){return(new
-Date).getTime()}}),k=0;function n(){var b=window.PR_SHOULD_USE_CONTINUATION?h.now()+250:Infinity,d,e,f,i,j;for(;k<g.length&&h.now()<b;++k){e=g[k];if(e.className&&e.className.indexOf('prettyprint')>=0){f=e.className.match(/\blang-(\w+)\b/),f&&(f=f[1]),i=false;for(j=e.parentNode;j;j=j.parentNode)if((j.tagName==='pre'||j.tagName==='code'||j.tagName==='xmp')&&j.className&&j.className.indexOf('prettyprint')>=0){i=a;break}i||(d=N(e),d=d.replace(/(?:\r\n?|\n)$/,''),m={sourceCodeHtml:d,langExtension:f,sourceNode:e},X(m),o())}}k<g.length?setTimeout(n,250):c&&c()}function
-o(){var a=m.prettyPrintedHtml,b,c,f,g,h,i,j,k;if(!a)return;f=m.sourceNode;if(!J(f))f.innerHTML=a;else{k=document.createElement('PRE');for(g=0;g<f.attributes.length;++g)b=f.attributes[g],b.specified&&(c=b.name.toLowerCase(),c==='class'?(k.className=b.value):k.setAttribute(b.name,b.value));k.innerHTML=a,f.parentNode.replaceChild(k,f),f=k}if(d&&f.tagName==='PRE'){j=f.getElementsByTagName('br');for(h=j.length;--h>=0;)i=j[h],i.parentNode.replaceChild(document.createTextNode(e),i)}}n()}window.PR_normalizedHtml=L,window.prettyPrintOne=Y,window.prettyPrint=Z,window.PR={combinePrefixPatterns:M,createSimpleLexer:S,registerLangHandler:V,sourceDecorator:T,PR_ATTRIB_NAME:'atn',PR_ATTRIB_VALUE:'atv',PR_COMMENT:'com',PR_DECLARATION:'dec',PR_KEYWORD:'kwd',PR_LITERAL:'lit',PR_NOCODE:'nocode',PR_PLAIN:'pln',PR_PUNCTUATION:'pun',PR_SOURCE:'src',PR_STRING:'str',PR_TAG:'tag',PR_TYPE:'typ'}})()
\ No newline at end of file
+var r=null;window.PR_SHOULD_USE_CONTINUATION=!0;
+(function(){function O(a){function i(d){var a=d.charCodeAt(0);if(a!==92)return a;var f=d.charAt(1);return(a=s[f])?a:"0"<=f&&f<="7"?parseInt(d.substring(1),8):f==="u"||f==="x"?parseInt(d.substring(2),16):d.charCodeAt(1)}function g(d){if(d<32)return(d<16?"\\x0":"\\x")+d.toString(16);d=String.fromCharCode(d);return d==="\\"||d==="-"||d==="]"||d==="^"?"\\"+d:d}function j(d){var a=d.substring(1,d.length-1).match(/\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\[0-3][0-7]{0,2}|\\[0-7]{1,2}|\\[\S\s]|[^\\]/g),d=[],f=
+a[0]==="^",b=["["];f&&b.push("^");for(var f=f?1:0,c=a.length;f<c;++f){var h=a[f];if(/\\[bdsw]/i.test(h))b.push(h);else{var h=i(h),e;f+2<c&&"-"===a[f+1]?(e=i(a[f+2]),f+=2):e=h;d.push([h,e]);e<65||h>122||(e<65||h>90||d.push([Math.max(65,h)|32,Math.min(e,90)|32]),e<97||h>122||d.push([Math.max(97,h)&-33,Math.min(e,122)&-33]))}}d.sort(function(d,a){return d[0]-a[0]||a[1]-d[1]});a=[];c=[];for(f=0;f<d.length;++f)h=d[f],h[0]<=c[1]+1?c[1]=Math.max(c[1],h[1]):a.push(c=h);for(f=0;f<a.length;++f)h=a[f],b.push(g(h[0])),
+h[1]>h[0]&&(h[1]+1>h[0]&&b.push("-"),b.push(g(h[1])));b.push("]");return b.join("")}function t(d){for(var a=d.source.match(/\[(?:[^\\\]]|\\[\S\s])*]|\\u[\dA-Fa-f]{4}|\\x[\dA-Fa-f]{2}|\\\d+|\\[^\dux]|\(\?[!:=]|[()^]|[^()[\\^]+/g),b=a.length,i=[],c=0,h=0;c<b;++c){var e=a[c];e==="("?++h:"\\"===e.charAt(0)&&(e=+e.substring(1))&&(e<=h?i[e]=-1:a[c]=g(e))}for(c=1;c<i.length;++c)-1===i[c]&&(i[c]=++z);for(h=c=0;c<b;++c)e=a[c],e==="("?(++h,i[h]||(a[c]="(?:")):"\\"===e.charAt(0)&&(e=+e.substring(1))&&e<=h&&
+(a[c]="\\"+i[e]);for(c=0;c<b;++c)"^"===a[c]&&"^"!==a[c+1]&&(a[c]="");if(d.ignoreCase&&w)for(c=0;c<b;++c)e=a[c],d=e.charAt(0),e.length>=2&&d==="["?a[c]=j(e):d!=="\\"&&(a[c]=e.replace(/[A-Za-z]/g,function(d){d=d.charCodeAt(0);return"["+String.fromCharCode(d&-33,d|32)+"]"}));return a.join("")}for(var z=0,w=!1,k=!1,m=0,b=a.length;m<b;++m){var o=a[m];if(o.ignoreCase)k=!0;else if(/[a-z]/i.test(o.source.replace(/\\u[\da-f]{4}|\\x[\da-f]{2}|\\[^UXux]/gi,""))){w=!0;k=!1;break}}for(var s={b:8,t:9,n:10,v:11,
+f:12,r:13},q=[],m=0,b=a.length;m<b;++m){o=a[m];if(o.global||o.multiline)throw Error(""+o);q.push("(?:"+t(o)+")")}return RegExp(q.join("|"),k?"gi":"g")}function P(a,i){function g(a){switch(a.nodeType){case 1:if(j.test(a.className))break;for(var b=a.firstChild;b;b=b.nextSibling)g(b);b=a.nodeName.toLowerCase();if("br"===b||"li"===b)t[k]="\n",w[k<<1]=z++,w[k++<<1|1]=a;break;case 3:case 4:b=a.nodeValue,b.length&&(b=i?b.replace(/\r\n?/g,"\n"):b.replace(/[\t\n\r ]+/g," "),t[k]=b,w[k<<1]=z,z+=b.length,w[k++<<
+1|1]=a)}}var j=/(?:^|\s)nocode(?:\s|$)/,t=[],z=0,w=[],k=0;g(a);return{a:t.join("").replace(/\n$/,""),d:w}}function E(a,i,g,j){i&&(a={a:i,e:a},g(a),j.push.apply(j,a.g))}function x(a,i){function g(a){for(var k=a.e,m=[k,"pln"],b=0,o=a.a.match(t)||[],s={},q=0,d=o.length;q<d;++q){var v=o[q],f=s[v],u=void 0,c;if(typeof f==="string")c=!1;else{var h=j[v.charAt(0)];if(h)u=v.match(h[1]),f=h[0];else{for(c=0;c<z;++c)if(h=i[c],u=v.match(h[1])){f=h[0];break}u||(f="pln")}if((c=f.length>=5&&"lang-"===f.substring(0,
+5))&&!(u&&typeof u[1]==="string"))c=!1,f="src";c||(s[v]=f)}h=b;b+=v.length;if(c){c=u[1];var e=v.indexOf(c),p=e+c.length;u[2]&&(p=v.length-u[2].length,e=p-c.length);f=f.substring(5);E(k+h,v.substring(0,e),g,m);E(k+h+e,c,F(f,c),m);E(k+h+p,v.substring(p),g,m)}else m.push(k+h,f)}a.g=m}var j={},t;(function(){for(var g=a.concat(i),k=[],m={},b=0,o=g.length;b<o;++b){var s=g[b],q=s[3];if(q)for(var d=q.length;--d>=0;)j[q.charAt(d)]=s;s=s[1];q=""+s;m.hasOwnProperty(q)||(k.push(s),m[q]=r)}k.push(/[\S\s]/);t=
+O(k)})();var z=i.length;return g}function l(a){var i=[],g=[];a.tripleQuotedStrings?i.push(["str",/^(?:'''(?:[^'\\]|\\[\S\s]|''?(?=[^']))*(?:'''|$)|"""(?:[^"\\]|\\[\S\s]|""?(?=[^"]))*(?:"""|$)|'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$))/,r,"'\""]):a.multiLineStrings?i.push(["str",/^(?:'(?:[^'\\]|\\[\S\s])*(?:'|$)|"(?:[^"\\]|\\[\S\s])*(?:"|$)|`(?:[^\\`]|\\[\S\s])*(?:`|$))/,r,"'\"`"]):i.push(["str",/^(?:'(?:[^\n\r'\\]|\\.)*(?:'|$)|"(?:[^\n\r"\\]|\\.)*(?:"|$))/,r,"\"'"]);a.verbatimStrings&&
+g.push(["str",/^@"(?:[^"]|"")*(?:"|$)/,r]);var j=a.hashComments;j&&(a.cStyleComments?(j>1?i.push(["com",/^#(?:##(?:[^#]|#(?!##))*(?:###|$)|.*)/,r,"#"]):i.push(["com",/^#(?:(?:define|e(?:l|nd)if|else|error|ifn?def|include|line|pragma|undef|warning)\b|[^\n\r]*)/,r,"#"]),g.push(["str",/^<(?:(?:(?:\.\.\/)*|\/?)(?:[\w-]+(?:\/[\w-]+)+)?[\w-]+\.h(?:h|pp|\+\+)?|[a-z]\w*)>/,r])):i.push(["com",/^#[^\n\r]*/,r,"#"]));a.cStyleComments&&(g.push(["com",/^\/\/[^\n\r]*/,r]),g.push(["com",/^\/\*[\S\s]*?(?:\*\/|$)/,
+r]));a.regexLiterals&&g.push(["lang-regex",/^(?:^^\.?|[+-]|[!=]={0,2}|#|%=?|&&?=?|\(|\*=?|[+-]=|->|\/=?|::?|<<?=?|>{1,3}=?|[,;?@[{~]|\^\^?=?|\|\|?=?|break|case|continue|delete|do|else|finally|instanceof|return|throw|try|typeof)\s*(\/(?=[^*/])(?:[^/[\\]|\\[\S\s]|\[(?:[^\\\]]|\\[\S\s])*(?:]|$))+\/)/]);(j=a.types)&&g.push(["typ",j]);a=(""+a.keywords).replace(/^ | $/g,"");a.length&&g.push(["kwd",RegExp("^(?:"+a.replace(/[\s,]+/g,"|")+")\\b"),r]);i.push(["pln",/^\s+/,r," \r\n\t\u00a0"]);g.push(["lit",
+/^@[$_a-z][\w$@]*/i,r],["typ",/^(?:[@_]?[A-Z]+[a-z][\w$@]*|\w+_t\b)/,r],["pln",/^[$_a-z][\w$@]*/i,r],["lit",/^(?:0x[\da-f]+|(?:\d(?:_\d+)*\d*(?:\.\d*)?|\.\d\+)(?:e[+-]?\d+)?)[a-z]*/i,r,"0123456789"],["pln",/^\\[\S\s]?/,r],["pun",/^.[^\s\w"$'./@\\`]*/,r]);return x(i,g)}function G(a,i,g){function j(a){switch(a.nodeType){case 1:if(z.test(a.className))break;if("br"===a.nodeName)t(a),a.parentNode&&a.parentNode.removeChild(a);else for(a=a.firstChild;a;a=a.nextSibling)j(a);break;case 3:case 4:if(g){var b=
+a.nodeValue,f=b.match(n);if(f){var i=b.substring(0,f.index);a.nodeValue=i;(b=b.substring(f.index+f[0].length))&&a.parentNode.insertBefore(k.createTextNode(b),a.nextSibling);t(a);i||a.parentNode.removeChild(a)}}}}function t(a){function i(a,b){var d=b?a.cloneNode(!1):a,e=a.parentNode;if(e){var e=i(e,1),f=a.nextSibling;e.appendChild(d);for(var g=f;g;g=f)f=g.nextSibling,e.appendChild(g)}return d}for(;!a.nextSibling;)if(a=a.parentNode,!a)return;for(var a=i(a.nextSibling,0),f;(f=a.parentNode)&&f.nodeType===
+1;)a=f;b.push(a)}for(var z=/(?:^|\s)nocode(?:\s|$)/,n=/\r\n?|\n/,k=a.ownerDocument,m=k.createElement("li");a.firstChild;)m.appendChild(a.firstChild);for(var b=[m],o=0;o<b.length;++o)j(b[o]);i===(i|0)&&b[0].setAttribute("value",i);var s=k.createElement("ol");s.className="linenums";for(var i=Math.max(0,i-1|0)||0,o=0,q=b.length;o<q;++o)m=b[o],m.className="L"+(o+i)%10,m.firstChild||m.appendChild(k.createTextNode("\u00a0")),s.appendChild(m);a.appendChild(s)}function n(a,i){for(var g=i.length;--g>=0;){var j=
+i[g];A.hasOwnProperty(j)?C.console&&console.warn("cannot override language handler %s",j):A[j]=a}}function F(a,i){if(!a||!A.hasOwnProperty(a))a=/^\s*</.test(i)?"default-markup":"default-code";return A[a]}function H(a){var i=a.h;try{var g=P(a.c,a.i),j=g.a;a.a=j;a.d=g.d;a.e=0;F(i,j)(a);var t=/\bMSIE\s(\d+)/.exec(navigator.userAgent),t=t&&+t[1]<=8,i=/\n/g,n=a.a,w=n.length,g=0,k=a.d,m=k.length,j=0,b=a.g,o=b.length,s=0;b[o]=w;var q,d;for(d=q=0;d<o;)b[d]!==b[d+2]?(b[q++]=b[d++],b[q++]=b[d++]):d+=2;o=q;
+for(d=q=0;d<o;){for(var v=b[d],f=b[d+1],u=d+2;u+2<=o&&b[u+1]===f;)u+=2;b[q++]=v;b[q++]=f;d=u}b.length=q;var c=a.c,h;if(c)h=c.style.display,c.style.display="none";try{for(;j<m;){var e=k[j+2]||w,p=b[s+2]||w,u=Math.min(e,p),l=k[j+1],D;if(l.nodeType!==1&&(D=n.substring(g,u))){t&&(D=D.replace(i,"\r"));l.nodeValue=D;var y=l.ownerDocument,x=y.createElement("span");x.className=b[s+1];var B=l.parentNode;B.replaceChild(x,l);x.appendChild(l);g<e&&(k[j+1]=l=y.createTextNode(n.substring(u,e)),B.insertBefore(l,
+x.nextSibling))}g=u;g>=e&&(j+=2);g>=p&&(s+=2)}}finally{if(c)c.style.display=h}}catch(A){C.console&&console.log(A&&A.stack?A.stack:A)}}var C=window,y=["break,continue,do,else,for,if,return,while"],B=[[y,"auto,case,char,const,default,double,enum,extern,float,goto,int,long,register,short,signed,sizeof,static,struct,switch,typedef,union,unsigned,void,volatile"],"catch,class,delete,false,import,new,operator,private,protected,public,this,throw,true,try,typeof"],I=[B,"alignof,align_union,asm,axiom,bool,concept,concept_map,const_cast,constexpr,decltype,dynamic_cast,explicit,export,friend,inline,late_check,mutable,namespace,nullptr,reinterpret_cast,static_assert,static_cast,template,typeid,typename,using,virtual,where"],
+J=[B,"abstract,boolean,byte,extends,final,finally,implements,import,instanceof,null,native,package,strictfp,super,synchronized,throws,transient"],K=[J,"as,base,by,checked,decimal,delegate,descending,dynamic,event,fixed,foreach,from,group,implicit,in,interface,internal,into,is,let,lock,object,out,override,orderby,params,partial,readonly,ref,sbyte,sealed,stackalloc,string,select,uint,ulong,unchecked,unsafe,ushort,var,virtual,where"],B=[B,"debugger,eval,export,function,get,null,set,undefined,var,with,Infinity,NaN"],
+L=[y,"and,as,assert,class,def,del,elif,except,exec,finally,from,global,import,in,is,lambda,nonlocal,not,or,pass,print,raise,try,with,yield,False,True,None"],M=[y,"alias,and,begin,case,class,def,defined,elsif,end,ensure,false,in,module,next,nil,not,or,redo,rescue,retry,self,super,then,true,undef,unless,until,when,yield,BEGIN,END"],y=[y,"case,done,elif,esac,eval,fi,function,in,local,set,then,until"],N=/^(DIR|FILE|vector|(de|priority_)?queue|list|stack|(const_)?iterator|(multi)?(set|map)|bitset|u?(int|float)\d*)\b/,
+Q=/\S/,R=l({keywords:[I,K,B,"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END"+L,M,y],hashComments:!0,cStyleComments:!0,multiLineStrings:!0,regexLiterals:!0}),A={};n(R,["default-code"]);n(x([],[["pln",/^[^<?]+/],["dec",/^<!\w[^>]*(?:>|$)/],["com",/^<\!--[\S\s]*?(?:--\>|$)/],["lang-",/^<\?([\S\s]+?)(?:\?>|$)/],["lang-",/^<%([\S\s]+?)(?:%>|$)/],["pun",/^(?:<[%?]|[%?]>)/],["lang-",
+/^<xmp\b[^>]*>([\S\s]+?)<\/xmp\b[^>]*>/i],["lang-js",/^<script\b[^>]*>([\S\s]*?)(<\/script\b[^>]*>)/i],["lang-css",/^<style\b[^>]*>([\S\s]*?)(<\/style\b[^>]*>)/i],["lang-in.tag",/^(<\/?[a-z][^<>]*>)/i]]),["default-markup","htm","html","mxml","xhtml","xml","xsl"]);n(x([["pln",/^\s+/,r," \t\r\n"],["atv",/^(?:"[^"]*"?|'[^']*'?)/,r,"\"'"]],[["tag",/^^<\/?[a-z](?:[\w-.:]*\w)?|\/?>$/i],["atn",/^(?!style[\s=]|on)[a-z](?:[\w:-]*\w)?/i],["lang-uq.val",/^=\s*([^\s"'>]*(?:[^\s"'/>]|\/(?=\s)))/],["pun",/^[/<->]+/],
+["lang-js",/^on\w+\s*=\s*"([^"]+)"/i],["lang-js",/^on\w+\s*=\s*'([^']+)'/i],["lang-js",/^on\w+\s*=\s*([^\s"'>]+)/i],["lang-css",/^style\s*=\s*"([^"]+)"/i],["lang-css",/^style\s*=\s*'([^']+)'/i],["lang-css",/^style\s*=\s*([^\s"'>]+)/i]]),["in.tag"]);n(x([],[["atv",/^[\S\s]+/]]),["uq.val"]);n(l({keywords:I,hashComments:!0,cStyleComments:!0,types:N}),["c","cc","cpp","cxx","cyc","m"]);n(l({keywords:"null,true,false"}),["json"]);n(l({keywords:K,hashComments:!0,cStyleComments:!0,verbatimStrings:!0,types:N}),
+["cs"]);n(l({keywords:J,cStyleComments:!0}),["java"]);n(l({keywords:y,hashComments:!0,multiLineStrings:!0}),["bsh","csh","sh"]);n(l({keywords:L,hashComments:!0,multiLineStrings:!0,tripleQuotedStrings:!0}),["cv","py"]);n(l({keywords:"caller,delete,die,do,dump,elsif,eval,exit,foreach,for,goto,if,import,last,local,my,next,no,our,print,package,redo,require,sub,undef,unless,until,use,wantarray,while,BEGIN,END",hashComments:!0,multiLineStrings:!0,regexLiterals:!0}),["perl","pl","pm"]);n(l({keywords:M,hashComments:!0,
+multiLineStrings:!0,regexLiterals:!0}),["rb"]);n(l({keywords:B,cStyleComments:!0,regexLiterals:!0}),["js"]);n(l({keywords:"all,and,by,catch,class,else,extends,false,finally,for,if,in,is,isnt,loop,new,no,not,null,of,off,on,or,return,super,then,throw,true,try,unless,until,when,while,yes",hashComments:3,cStyleComments:!0,multilineStrings:!0,tripleQuotedStrings:!0,regexLiterals:!0}),["coffee"]);n(x([],[["str",/^[\S\s]+/]]),["regex"]);var S=C.PR={createSimpleLexer:x,registerLangHandler:n,sourceDecorator:l,
+PR_ATTRIB_NAME:"atn",PR_ATTRIB_VALUE:"atv",PR_COMMENT:"com",PR_DECLARATION:"dec",PR_KEYWORD:"kwd",PR_LITERAL:"lit",PR_NOCODE:"nocode",PR_PLAIN:"pln",PR_PUNCTUATION:"pun",PR_SOURCE:"src",PR_STRING:"str",PR_TAG:"tag",PR_TYPE:"typ",prettyPrintOne:C.prettyPrintOne=function(a,i,g){var j=document.createElement("pre");j.innerHTML=a;g&&G(j,g,!0);H({h:i,j:g,c:j,i:1});return j.innerHTML},prettyPrint:C.prettyPrint=function(a){function i(){var u;for(var g=C.PR_SHOULD_USE_CONTINUATION?k.now()+250:Infinity;m<j.length&&
+k.now()<g;m++){var c=j[m],h=c.className;if(s.test(h)&&!q.test(h)){for(var e=!1,p=c.parentNode;p;p=p.parentNode)if(f.test(p.tagName)&&p.className&&s.test(p.className)){e=!0;break}if(!e){c.className+=" prettyprinted";var h=h.match(o),n;if(e=!h){for(var e=c,p=void 0,l=e.firstChild;l;l=l.nextSibling)var t=l.nodeType,p=t===1?p?e:l:t===3?Q.test(l.nodeValue)?e:p:p;e=(n=p===e?void 0:p)&&v.test(n.tagName)}e&&(h=n.className.match(o));h&&(h=h[1]);u=d.test(c.tagName)?1:(e=(e=c.currentStyle)?e.whiteSpace:document.defaultView&&
+document.defaultView.getComputedStyle?document.defaultView.getComputedStyle(c,r).getPropertyValue("white-space"):0)&&"pre"===e.substring(0,3),e=u;(p=(p=c.className.match(/\blinenums\b(?::(\d+))?/))?p[1]&&p[1].length?+p[1]:!0:!1)&&G(c,p,e);b={h:h,c:c,j:p,i:e};H(b)}}}m<j.length?setTimeout(i,250):a&&a()}for(var g=[document.getElementsByTagName("pre"),document.getElementsByTagName("code"),document.getElementsByTagName("xmp")],j=[],n=0;n<g.length;++n)for(var l=0,w=g[n].length;l<w;++l)j.push(g[n][l]);var g=
+r,k=Date;k.now||(k={now:function(){return+new Date}});var m=0,b,o=/\blang(?:uage)?-([\w.]+)(?!\S)/,s=/\bprettyprint\b/,q=/\bprettyprinted\b/,d=/pre|xmp/i,v=/^code$/i,f=/^(?:pre|code|xmp)$/i;i()}};typeof define==="function"&&define.amd&&define("google-code-prettify",[],function(){return S})})();
diff --git a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/server-env.js b/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/server-env.js
deleted file mode 100644
index 28d49c0..0000000
--- a/gerrit-prettify/src/main/resources/com/google/gerrit/prettify/client/server-env.js
+++ /dev/null
@@ -1,22 +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.
-
-// Mozilla Rhino is not IE 6.
-//
-window._pr_isIE6 = function () { return false; };
-
-// Expose the function at the top level to simplify calls.
-//
-prettyPrintOne = window.prettyPrintOne;
-PR = window.PR;
diff --git a/gerrit-reviewdb/pom.xml b/gerrit-reviewdb/pom.xml
index f9fb49e..96599b3 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.5-SNAPSHOT</version>
+    <version>2.6-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-reviewdb</artifactId>
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Account.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Account.java
index b23aa86..94b37e1 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Account.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Account.java
@@ -169,7 +169,11 @@
 
   /** Set the full name of the user ("Given-name Surname" style). */
   public void setFullName(final String name) {
-    fullName = name;
+    if (name != null && !name.trim().isEmpty()) {
+      fullName = name.trim();
+    } else {
+      fullName = null;
+    }
   }
 
   /** Email address the user prefers to be contacted through. */
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroup.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroup.java
index 061ef3e..ea9c52d 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroup.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroup.java
@@ -85,6 +85,11 @@
         || uuid.get().matches("^[0-9a-f]{40}$");
   }
 
+  /** @return true if the UUID is for a system group managed within Gerrit. */
+  public static boolean isSystemGroup(AccountGroup.UUID uuid) {
+    return uuid.get().startsWith("global:");
+  }
+
   /** Synthetic key to link to within the database */
   public static class Id extends IntKey<com.google.gwtorm.client.Key<?>> {
     private static final long serialVersionUID = 1L;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupInclude.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupInclude.java
deleted file mode 100644
index 4b58689..0000000
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupInclude.java
+++ /dev/null
@@ -1,81 +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.reviewdb.client;
-
-import com.google.gwtorm.client.Column;
-import com.google.gwtorm.client.CompoundKey;
-
-/** Membership of an {@link AccountGroup} in an {@link AccountGroup}. */
-public final class AccountGroupInclude {
-  public static class Key extends CompoundKey<AccountGroup.Id> {
-    private static final long serialVersionUID = 1L;
-
-    @Column(id = 1)
-    protected AccountGroup.Id groupId;
-
-    @Column(id = 2)
-    protected AccountGroup.Id includeId;
-
-    protected Key() {
-      groupId = new AccountGroup.Id();
-      includeId = new AccountGroup.Id();
-    }
-
-    public Key(final AccountGroup.Id g, final AccountGroup.Id i) {
-      groupId = g;
-      includeId = i;
-    }
-
-    @Override
-    public AccountGroup.Id getParentKey() {
-      return groupId;
-    }
-
-    public AccountGroup.Id getGroupId() {
-      return groupId;
-    }
-
-    public AccountGroup.Id getIncludeId() {
-      return includeId;
-    }
-
-    @Override
-    public com.google.gwtorm.client.Key<?>[] members() {
-      return new com.google.gwtorm.client.Key<?>[] {includeId};
-    }
-  }
-
-  @Column(id = 1, name = Column.NONE)
-  protected Key key;
-
-  protected AccountGroupInclude() {
-  }
-
-  public AccountGroupInclude(final AccountGroupInclude.Key k) {
-    key = k;
-  }
-
-  public AccountGroupInclude.Key getKey() {
-    return key;
-  }
-
-  public AccountGroup.Id getGroupId() {
-    return key.groupId;
-  }
-
-  public AccountGroup.Id getIncludeId() {
-    return key.includeId;
-  }
-}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupIncludeAudit.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupIncludeAudit.java
deleted file mode 100644
index 275c3c3..0000000
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupIncludeAudit.java
+++ /dev/null
@@ -1,105 +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.reviewdb.client;
-
-import com.google.gwtorm.client.Column;
-import com.google.gwtorm.client.CompoundKey;
-
-import java.sql.Timestamp;
-
-/** Inclusion of an {@link AccountGroup} in another {@link AccountGroup}. */
-public final class AccountGroupIncludeAudit {
-  public static class Key extends CompoundKey<AccountGroup.Id> {
-    private static final long serialVersionUID = 1L;
-
-    @Column(id = 1)
-    protected AccountGroup.Id groupId;
-
-    @Column(id = 2)
-    protected AccountGroup.Id includeId;
-
-    @Column(id = 3)
-    protected Timestamp addedOn;
-
-    protected Key() {
-      groupId = new AccountGroup.Id();
-      includeId = new AccountGroup.Id();
-    }
-
-    public Key(final AccountGroup.Id g, final AccountGroup.Id i, final Timestamp t) {
-      groupId = g;
-      includeId = i;
-      addedOn = t;
-    }
-
-    @Override
-    public AccountGroup.Id getParentKey() {
-      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};
-    }
-  }
-
-  @Column(id = 1, name = Column.NONE)
-  protected Key key;
-
-  @Column(id = 2)
-  protected Account.Id addedBy;
-
-  @Column(id = 3, notNull = false)
-  protected Account.Id removedBy;
-
-  @Column(id = 4, notNull = false)
-  protected Timestamp removedOn;
-
-  protected AccountGroupIncludeAudit() {
-  }
-
-  public AccountGroupIncludeAudit(final AccountGroupInclude m,
-      final Account.Id adder) {
-    final AccountGroup.Id group = m.getGroupId();
-    final AccountGroup.Id include = m.getIncludeId();
-    key = new AccountGroupIncludeAudit.Key(group, include, now());
-    addedBy = adder;
-  }
-
-  public AccountGroupIncludeAudit.Key getKey() {
-    return key;
-  }
-
-  public boolean isActive() {
-    return removedOn == null;
-  }
-
-  public void removed(final Account.Id deleter) {
-    removedBy = deleter;
-    removedOn = now();
-  }
-
-  private static Timestamp now() {
-    return new Timestamp(System.currentTimeMillis());
-  }
-}
\ No newline at end of file
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupIncludeByUuid.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupIncludeByUuid.java
new file mode 100644
index 0000000..a5b35ed
--- /dev/null
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupIncludeByUuid.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.reviewdb.client;
+
+import com.google.gwtorm.client.Column;
+import com.google.gwtorm.client.CompoundKey;
+
+/** Membership of an {@link AccountGroup} in an {@link AccountGroup}. */
+public final class AccountGroupIncludeByUuid {
+  public static class Key extends CompoundKey<AccountGroup.Id> {
+    private static final long serialVersionUID = 1L;
+
+    @Column(id = 1)
+    protected AccountGroup.Id groupId;
+
+    @Column(id = 2)
+    protected AccountGroup.UUID includeUUID;
+
+    protected Key() {
+      groupId = new AccountGroup.Id();
+      includeUUID = new AccountGroup.UUID();
+    }
+
+    public Key(final AccountGroup.Id g, final AccountGroup.UUID u) {
+      groupId = g;
+      includeUUID = u;
+    }
+
+    @Override
+    public AccountGroup.Id getParentKey() {
+      return groupId;
+    }
+
+    public AccountGroup.Id getGroupId() {
+      return groupId;
+    }
+
+    public AccountGroup.UUID getIncludeUUID() {
+      return includeUUID;
+    }
+
+    @Override
+    public com.google.gwtorm.client.Key<?>[] members() {
+      return new com.google.gwtorm.client.Key<?>[] {includeUUID};
+    }
+  }
+
+  @Column(id = 1, name = Column.NONE)
+  protected Key key;
+
+  protected AccountGroupIncludeByUuid() {
+  }
+
+  public AccountGroupIncludeByUuid(final AccountGroupIncludeByUuid.Key k) {
+    key = k;
+  }
+
+  public AccountGroupIncludeByUuid.Key getKey() {
+    return key;
+  }
+
+  public AccountGroup.Id getGroupId() {
+    return key.groupId;
+  }
+
+  public AccountGroup.UUID getIncludeUUID() {
+    return key.includeUUID;
+  }
+}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupIncludeByUuidAudit.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupIncludeByUuidAudit.java
new file mode 100644
index 0000000..6625197
--- /dev/null
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountGroupIncludeByUuidAudit.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.reviewdb.client;
+
+import com.google.gwtorm.client.Column;
+import com.google.gwtorm.client.CompoundKey;
+
+import java.sql.Timestamp;
+
+/** Inclusion of an {@link AccountGroup} in another {@link AccountGroup}. */
+public final class AccountGroupIncludeByUuidAudit {
+  public static class Key extends CompoundKey<AccountGroup.Id> {
+    private static final long serialVersionUID = 1L;
+
+    @Column(id = 1)
+    protected AccountGroup.Id groupId;
+
+    @Column(id = 2)
+    protected AccountGroup.UUID includeUUID;
+
+    @Column(id = 3)
+    protected Timestamp addedOn;
+
+    protected Key() {
+      groupId = new AccountGroup.Id();
+      includeUUID = new AccountGroup.UUID();
+    }
+
+    public Key(final AccountGroup.Id g, final AccountGroup.UUID u, final Timestamp t) {
+      groupId = g;
+      includeUUID = u;
+      addedOn = t;
+    }
+
+    @Override
+    public AccountGroup.Id getParentKey() {
+      return groupId;
+    }
+
+    public AccountGroup.UUID getIncludeUUID() {
+      return includeUUID;
+    }
+
+    public Timestamp getAddedOn() {
+      return addedOn;
+    }
+
+    @Override
+    public com.google.gwtorm.client.Key<?>[] members() {
+      return new com.google.gwtorm.client.Key<?>[] {includeUUID};
+    }
+  }
+
+  @Column(id = 1, name = Column.NONE)
+  protected Key key;
+
+  @Column(id = 2)
+  protected Account.Id addedBy;
+
+  @Column(id = 3, notNull = false)
+  protected Account.Id removedBy;
+
+  @Column(id = 4, notNull = false)
+  protected Timestamp removedOn;
+
+  protected AccountGroupIncludeByUuidAudit() {
+  }
+
+  public AccountGroupIncludeByUuidAudit(final AccountGroupIncludeByUuid m,
+      final Account.Id adder, final Timestamp when) {
+    final AccountGroup.Id group = m.getGroupId();
+    final AccountGroup.UUID include = m.getIncludeUUID();
+    key = new AccountGroupIncludeByUuidAudit.Key(group, include, when);
+    addedBy = adder;
+  }
+
+  public AccountGroupIncludeByUuidAudit(final AccountGroupIncludeByUuid m,
+      final Account.Id adder) {
+    this(m, adder, now());
+  }
+
+  public AccountGroupIncludeByUuidAudit.Key getKey() {
+    return key;
+  }
+
+  public boolean isActive() {
+    return removedOn == null;
+  }
+
+  public void removed(final Account.Id deleter) {
+    removedBy = deleter;
+    removedOn = now();
+  }
+
+  public void removed(final Account.Id deleter, final Timestamp when) {
+    removedBy = deleter;
+    removedOn = when;
+  }
+
+  private static Timestamp now() {
+    return new Timestamp(System.currentTimeMillis());
+  }
+}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountProjectWatch.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountProjectWatch.java
index e592101..c7f52b0 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountProjectWatch.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/AccountProjectWatch.java
@@ -22,7 +22,8 @@
 public final class AccountProjectWatch {
 
   public enum NotifyType {
-    NEW_CHANGES, ALL_COMMENTS, SUBMITTED_CHANGES, ALL
+    NEW_CHANGES, NEW_PATCHSETS, ALL_COMMENTS, SUBMITTED_CHANGES,
+    ABANDONED_CHANGES, ALL
   }
 
   public static final String FILTER_ALL = "*";
@@ -109,6 +110,12 @@
   @Column(id = 4)
   protected boolean notifySubmittedChanges;
 
+  @Column(id = 5)
+  protected boolean notifyNewPatchSets;
+
+  @Column(id = 6)
+  protected boolean notifyAbandonedChanges;
+
   protected AccountProjectWatch() {
   }
 
@@ -137,11 +144,20 @@
       case NEW_CHANGES:
         return notifyNewChanges;
 
+      case NEW_PATCHSETS:
+        return notifyNewPatchSets;
+
       case ALL_COMMENTS:
         return notifyAllComments;
 
       case SUBMITTED_CHANGES:
         return notifySubmittedChanges;
+
+      case ABANDONED_CHANGES:
+        return notifyAbandonedChanges;
+
+      case ALL:
+        break;
     }
     return false;
   }
@@ -152,6 +168,10 @@
         notifyNewChanges = v;
         break;
 
+      case NEW_PATCHSETS:
+        notifyNewPatchSets = v;
+        break;
+
       case ALL_COMMENTS:
         notifyAllComments = v;
         break;
@@ -160,10 +180,16 @@
         notifySubmittedChanges = v;
         break;
 
+      case ABANDONED_CHANGES:
+        notifyAbandonedChanges = v;
+        break;
+
       case ALL:
         notifyNewChanges = v;
+        notifyNewPatchSets = v;
         notifyAllComments = v;
         notifySubmittedChanges = v;
+        notifyAbandonedChanges = v;
         break;
     }
   }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/ApprovalCategory.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/ApprovalCategory.java
deleted file mode 100644
index 7d61975..0000000
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/ApprovalCategory.java
+++ /dev/null
@@ -1,151 +0,0 @@
-// Copyright (C) 2008 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.reviewdb.client;
-
-import com.google.gwtorm.client.Column;
-import com.google.gwtorm.client.Key;
-import com.google.gwtorm.client.StringKey;
-
-/** Types of approvals that can be associated with a {@link Change}. */
-public final class ApprovalCategory {
-  /** Id of the special "Submit" action (and category). */
-  public static final ApprovalCategory.Id SUBMIT =
-      new ApprovalCategory.Id("SUBM");
-
-  public static class Id extends StringKey<Key<?>> {
-    private static final long serialVersionUID = 1L;
-
-    @Column(id = 1, length = 4)
-    protected String id;
-
-    protected Id() {
-    }
-
-    public Id(final String a) {
-      id = a;
-    }
-
-    @Override
-    public String get() {
-      return id;
-    }
-
-    @Override
-    protected void set(String newValue) {
-      id = newValue;
-    }
-  }
-
-  /** Internal short unique identifier for this category. */
-  @Column(id = 1)
-  protected Id categoryId;
-
-  /** Unique name for this category, shown in the web interface to users. */
-  @Column(id = 2, length = 20)
-  protected String name;
-
-  /** Abbreviated form of {@link #name} for display in very wide tables. */
-  @Column(id = 3, length = 4, notNull = false)
-  protected String abbreviatedName;
-
-  /** Order of this category within the Approvals table when presented. */
-  @Column(id = 4)
-  protected short position;
-
-  /** Identity of the function used to aggregate the category's value. */
-  @Column(id = 5)
-  protected String functionName;
-
-  /** If set, the minimum score is copied during patch set replacement. */
-  @Column(id = 6)
-  protected boolean copyMinScore;
-
-  /** Computed name derived from {@link #name}. */
-  protected String labelName;
-
-  protected ApprovalCategory() {
-  }
-
-  public ApprovalCategory(final ApprovalCategory.Id id, final String name) {
-    this.categoryId = id;
-    this.name = name;
-    this.functionName = "MaxWithBlock";
-  }
-
-  public ApprovalCategory.Id getId() {
-    return categoryId;
-  }
-
-  public String getName() {
-    return name;
-  }
-
-  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') //
-            || (c == '-')) {
-          r.append(c);
-        } else if (c == ' ') {
-          r.append('-');
-        }
-      }
-      labelName = r.toString();
-    }
-    return labelName;
-  }
-
-  public String getAbbreviatedName() {
-    return abbreviatedName;
-  }
-
-  public void setAbbreviatedName(final String n) {
-    abbreviatedName = n;
-  }
-
-  public short getPosition() {
-    return position;
-  }
-
-  public void setPosition(final short p) {
-    position = p;
-  }
-
-  public String getFunctionName() {
-    return functionName;
-  }
-
-  public void setFunctionName(final String name) {
-    functionName = name;
-  }
-
-  public boolean isCopyMinScore() {
-    return copyMinScore;
-  }
-
-  public void setCopyMinScore(final boolean copy) {
-    copyMinScore = copy;
-  }
-}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/ApprovalCategoryValue.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/ApprovalCategoryValue.java
deleted file mode 100644
index b7761c5..0000000
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/ApprovalCategoryValue.java
+++ /dev/null
@@ -1,108 +0,0 @@
-// Copyright (C) 2008 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.reviewdb.client;
-
-import com.google.gwtorm.client.Column;
-import com.google.gwtorm.client.ShortKey;
-
-/** Valid value for a {@link ApprovalCategory}. */
-public final class ApprovalCategoryValue {
-  public static class Id extends ShortKey<ApprovalCategory.Id> {
-    private static final long serialVersionUID = 1L;
-
-    @Column(id = 1)
-    protected ApprovalCategory.Id categoryId;
-
-    @Column(id = 2)
-    protected short value;
-
-    protected Id() {
-      categoryId = new ApprovalCategory.Id();
-    }
-
-    public Id(final ApprovalCategory.Id cat, final short v) {
-      categoryId = cat;
-      value = v;
-    }
-
-    @Override
-    public ApprovalCategory.Id getParentKey() {
-      return categoryId;
-    }
-
-    @Override
-    public short get() {
-      return value;
-    }
-
-    @Override
-    protected void set(short newValue) {
-      value = newValue;
-    }
-  }
-
-  @Column(id = 1, name = Column.NONE)
-  protected Id key;
-
-  @Column(id = 2, length = 50)
-  protected String name;
-
-  protected ApprovalCategoryValue() {
-  }
-
-  public ApprovalCategoryValue(final ApprovalCategoryValue.Id id,
-      final String name) {
-    this.key = id;
-    this.name = name;
-  }
-
-  public ApprovalCategoryValue.Id getId() {
-    return key;
-  }
-
-  public ApprovalCategory.Id getCategoryId() {
-    return key.categoryId;
-  }
-
-  public short getValue() {
-    return key.value;
-  }
-
-  public String getName() {
-    return name;
-  }
-
-  public void setName(final String n) {
-    name = n;
-  }
-
-  public String formatValue() {
-    if (getValue() < 0) {
-      return Short.toString(getValue());
-    } else if (getValue() == 0) {
-      return " 0";
-    } else {
-      return "+" + Short.toString(getValue());
-    }
-  }
-
-  public String format() {
-    final StringBuilder m = new StringBuilder();
-    m.append(formatValue());
-    m.append(' ');
-    m.append(getName());
-    return m.toString();
-  }
-}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
index bfbacc0..2a51872 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Change.java
@@ -193,6 +193,9 @@
   /** Database constant for {@link Status#MERGED}. */
   public static final char STATUS_MERGED = 'M';
 
+  /** ID number of the first patch set in a change. */
+  public static final int INITIAL_PATCH_SET_ID = 1;
+
   /**
    * Current state within the basic workflow of the change.
    *
@@ -359,10 +362,6 @@
   @Column(id = 10)
   protected char status;
 
-  /** The total number of {@link PatchSet} children in this Change. */
-  @Column(id = 11)
-  protected int nbrPatchSets;
-
   /** The current patch set. */
   @Column(id = 12)
   protected int currentPatchSetId;
@@ -428,12 +427,12 @@
     return lastUpdatedOn;
   }
 
-  public void resetLastUpdatedOn() {
-    lastUpdatedOn = new Timestamp(System.currentTimeMillis());
+  public void setLastUpdatedOn(Timestamp now) {
+    lastUpdatedOn = now;
   }
 
-  public int getNumberOfPatchSets() {
-    return nbrPatchSets;
+  public void resetLastUpdatedOn() {
+    lastUpdatedOn = new Timestamp(System.currentTimeMillis());
   }
 
   public String getSortKey() {
@@ -473,32 +472,6 @@
     subject = ps.getSubject();
   }
 
-  /**
-   * Allocate a new PatchSet id within this change.
-   * <p>
-   * <b>Note: This makes the change dirty. Call update() after.</b>
-   */
-  public void nextPatchSetId() {
-    ++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 void updateNumberOfPatchSets(int max) {
-    nbrPatchSets = Math.max(nbrPatchSets, max);
-  }
-
-  public PatchSet.Id currPatchSetId() {
-    return new PatchSet.Id(changeId, nbrPatchSets);
-  }
-
   public Status getStatus() {
     return Status.forCode(status);
   }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/InheritedBoolean.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/InheritedBoolean.java
new file mode 100644
index 0000000..954b494
--- /dev/null
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/InheritedBoolean.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.reviewdb.client;
+
+import com.google.gerrit.reviewdb.client.Project.InheritableBoolean;
+
+public class InheritedBoolean {
+
+  public InheritableBoolean value;
+  public boolean inheritedValue;
+
+  public InheritedBoolean() {
+  }
+
+  public void setValue(final InheritableBoolean value) {
+    this.value = value;
+  }
+
+  public void setInheritedValue(final boolean inheritedValue) {
+    this.inheritedValue = inheritedValue;
+  }
+}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Patch.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Patch.java
index 00e282b..6ddd6d2 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Patch.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Patch.java
@@ -84,7 +84,7 @@
     /** Path was copied from {@link Patch#getSourceFileName()}. */
     COPIED('C'),
 
-    /** Sufficient amount of content changed to claim the file was written. */
+    /** Sufficient amount of content changed to claim the file was rewritten. */
     REWRITE('W');
 
     private final char code;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchLineComment.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchLineComment.java
index c8a970d..af35e52f 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchLineComment.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchLineComment.java
@@ -138,6 +138,10 @@
     return lineNbr;
   }
 
+  public void setLine(int line) {
+    lineNbr = line;
+  }
+
   public Account.Id getAuthor() {
     return author;
   }
@@ -174,7 +178,15 @@
     writtenOn = new Timestamp(System.currentTimeMillis());
   }
 
+  public void setWrittenOn(Timestamp ts) {
+    writtenOn = ts;
+  }
+
   public String getParentUuid() {
     return parentUuid;
   }
+
+  public void setParentUuid(String inReplyTo) {
+    parentUuid = inReplyTo;
+  }
 }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSet.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSet.java
index 83ce828..54c556d 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSet.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSet.java
@@ -25,7 +25,27 @@
 
   /** Is the reference name a change reference? */
   public static boolean isRef(final String name) {
-    return name.matches("^refs/changes/.*/[1-9][0-9]*/[1-9][0-9]*$");
+    if (name == null || !name.startsWith(REFS_CHANGES)) {
+      return false;
+    }
+    boolean accepted = false;
+    int numsFound = 0;
+    for (int i = name.length() - 1; i >= REFS_CHANGES.length() - 1; i--) {
+      char c = name.charAt(i);
+      if (c >= '0' && c <= '9') {
+        accepted = (c != '0');
+      } else if (c == '/') {
+        if (accepted) {
+          if (++numsFound == 2) {
+            return true;
+          }
+          accepted = false;
+        }
+      } else {
+        return false;
+      }
+    }
+    return false;
   }
 
   public static class Id extends IntKey<Change.Id> {
@@ -61,6 +81,22 @@
       patchSetId = newValue;
     }
 
+    public String toRefName() {
+      StringBuilder r = new StringBuilder();
+      r.append(REFS_CHANGES);
+      int change = changeId.get();
+      int m = change % 100;
+      if (m < 10) {
+        r.append('0');
+      }
+      r.append(m);
+      r.append('/');
+      r.append(change);
+      r.append('/');
+      r.append(patchSetId);
+      return r.toString();
+    }
+
     /** Parse a PatchSet.Id out of a string representation. */
     public static Id parse(final String str) {
       final Id r = new Id();
@@ -148,19 +184,7 @@
   }
 
   public String getRefName() {
-    final StringBuilder r = new StringBuilder();
-    r.append(REFS_CHANGES);
-    final int changeId = id.getParentKey().get();
-    final int m = changeId % 100;
-    if (m < 10) {
-      r.append('0');
-    }
-    r.append(m);
-    r.append('/');
-    r.append(changeId);
-    r.append('/');
-    r.append(id.get());
-    return r.toString();
+    return id.toRefName();
   }
 
   @Override
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSetApproval.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSetApproval.java
index 04b2b6f..8d79d66 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSetApproval.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/PatchSetApproval.java
@@ -16,11 +16,52 @@
 
 import com.google.gwtorm.client.Column;
 import com.google.gwtorm.client.CompoundKey;
+import com.google.gwtorm.client.StringKey;
 
 import java.sql.Timestamp;
 
 /** An approval (or negative approval) on a patch set. */
 public final class PatchSetApproval {
+  public static class LabelId extends
+      StringKey<com.google.gwtorm.client.Key<?>> {
+    private static final long serialVersionUID = 1L;
+
+    public static final LabelId SUBMIT = new LabelId("SUBM");
+
+    @Column(id = 1)
+    protected String id;
+
+    protected LabelId() {
+    }
+
+    public LabelId(final String n) {
+      id = n;
+    }
+
+    @Override
+    public String get() {
+      return id;
+    }
+
+    @Override
+    protected void set(String newValue) {
+      id = newValue;
+    }
+
+    @Override
+    public int hashCode() {
+      return get().hashCode();
+    }
+
+    @Override
+    public boolean equals(Object b) {
+      if (b instanceof LabelId) {
+        return get().equals(((LabelId) b).get());
+      }
+      return false;
+    }
+  }
+
   public static class Key extends CompoundKey<PatchSet.Id> {
     private static final long serialVersionUID = 1L;
 
@@ -31,16 +72,16 @@
     protected Account.Id accountId;
 
     @Column(id = 3)
-    protected ApprovalCategory.Id categoryId;
+    protected LabelId categoryId;
 
     protected Key() {
       patchSetId = new PatchSet.Id();
       accountId = new Account.Id();
-      categoryId = new ApprovalCategory.Id();
+      categoryId = new LabelId();
     }
 
     public Key(final PatchSet.Id ps, final Account.Id a,
-        final ApprovalCategory.Id c) {
+        final LabelId c) {
       this.patchSetId = ps;
       this.accountId = a;
       this.categoryId = c;
@@ -55,7 +96,7 @@
       return accountId;
     }
 
-    public ApprovalCategory.Id getCategoryId() {
+    public LabelId getLabelId() {
       return categoryId;
     }
 
@@ -108,7 +149,7 @@
 
   public PatchSetApproval(final PatchSet.Id psId, final PatchSetApproval src) {
     key =
-        new PatchSetApproval.Key(psId, src.getAccountId(), src.getCategoryId());
+        new PatchSetApproval.Key(psId, src.getAccountId(), src.getLabelId());
     changeOpen = true;
     value = src.getValue();
     granted = src.granted;
@@ -126,7 +167,7 @@
     return key.accountId;
   }
 
-  public ApprovalCategory.Id getCategoryId() {
+  public LabelId getLabelId() {
     return key.categoryId;
   }
 
@@ -146,8 +187,26 @@
     granted = new Timestamp(System.currentTimeMillis());
   }
 
+  public void setGranted(Timestamp ts) {
+    granted = ts;
+  }
+
   public void cache(final Change c) {
     changeOpen = c.open;
     changeSortKey = c.sortKey;
   }
+
+  public String getLabel() {
+    return getLabelId().get();
+  }
+
+  public boolean isSubmit() {
+    return LabelId.SUBMIT.get().equals(getLabel());
+  }
+
+  @Override
+  public String toString() {
+    return new StringBuilder().append('[').append(key).append(": ")
+        .append(value).append(']').toString();
+  }
 }
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Project.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Project.java
index a2fc46f..c070e3e 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Project.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/client/Project.java
@@ -70,6 +70,8 @@
 
     MERGE_IF_NECESSARY,
 
+    REBASE_IF_NECESSARY,
+
     MERGE_ALWAYS,
 
     CHERRY_PICK;
@@ -83,13 +85,19 @@
     HIDDEN;
   }
 
+  public static enum InheritableBoolean {
+    TRUE,
+    FALSE,
+    INHERIT;
+  }
+
   protected NameKey name;
 
   protected String description;
 
-  protected boolean useContributorAgreements;
+  protected InheritableBoolean useContributorAgreements;
 
-  protected boolean useSignedOffBy;
+  protected InheritableBoolean useSignedOffBy;
 
   protected SubmitType submitType;
 
@@ -97,9 +105,13 @@
 
   protected NameKey parent;
 
-  protected boolean requireChangeID;
+  protected InheritableBoolean requireChangeID;
 
-  protected boolean useContentMerge;
+  protected InheritableBoolean useContentMerge;
+
+  protected String defaultDashboardId;
+
+  protected String localDefaultDashboardId;
 
   protected Project() {
   }
@@ -108,6 +120,10 @@
     name = nameKey;
     submitType = SubmitType.MERGE_IF_NECESSARY;
     state = State.ACTIVE;
+    useContributorAgreements = InheritableBoolean.INHERIT;
+    useSignedOffBy = InheritableBoolean.INHERIT;
+    requireChangeID = InheritableBoolean.INHERIT;
+    useContentMerge = InheritableBoolean.INHERIT;
   }
 
   public Project.NameKey getNameKey() {
@@ -126,35 +142,35 @@
     description = d;
   }
 
-  public boolean isUseContributorAgreements() {
+  public InheritableBoolean getUseContributorAgreements() {
     return useContributorAgreements;
   }
 
-  public void setUseContributorAgreements(final boolean u) {
-    useContributorAgreements = u;
-  }
-
-  public boolean isUseSignedOffBy() {
+  public InheritableBoolean getUseSignedOffBy() {
     return useSignedOffBy;
   }
 
-  public boolean isUseContentMerge() {
+  public InheritableBoolean getUseContentMerge() {
     return useContentMerge;
   }
 
-  public boolean isRequireChangeID() {
+  public InheritableBoolean getRequireChangeID() {
     return requireChangeID;
   }
 
-  public void setUseSignedOffBy(final boolean sbo) {
+  public void setUseContributorAgreements(final InheritableBoolean u) {
+    useContributorAgreements = u;
+  }
+
+  public void setUseSignedOffBy(final InheritableBoolean sbo) {
     useSignedOffBy = sbo;
   }
 
-  public void setUseContentMerge(final boolean cm) {
+  public void setUseContentMerge(final InheritableBoolean cm) {
     useContentMerge = cm;
   }
 
-  public void setRequireChangeID(final boolean cid) {
+  public void setRequireChangeID(final InheritableBoolean cid) {
     requireChangeID = cid;
   }
 
@@ -174,6 +190,22 @@
     state = newState;
   }
 
+  public String getDefaultDashboard() {
+    return defaultDashboardId;
+  }
+
+  public void setDefaultDashboard(final String defaultDashboardId) {
+    this.defaultDashboardId = defaultDashboardId;
+  }
+
+  public String getLocalDefaultDashboard() {
+    return localDefaultDashboardId;
+  }
+
+  public void setLocalDefaultDashboard(final String localDefaultDashboardId) {
+    this.localDefaultDashboardId = localDefaultDashboardId;
+  }
+
   public void copySettingsFrom(final Project update) {
     description = update.description;
     useContributorAgreements = update.useContributorAgreements;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeAccess.java
deleted file mode 100644
index 3ee4ba0..0000000
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeAccess.java
+++ /dev/null
@@ -1,35 +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.reviewdb.server;
-
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupInclude;
-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> {
-  @PrimaryKey("key")
-  AccountGroupInclude get(AccountGroupInclude.Key key) throws OrmException;
-
-  @Query("WHERE key.includeId = ?")
-  ResultSet<AccountGroupInclude> byInclude(AccountGroup.Id id) throws OrmException;
-
-  @Query("WHERE key.groupId = ?")
-  ResultSet<AccountGroupInclude> byGroup(AccountGroup.Id id) throws OrmException;
-}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeAuditAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeAuditAccess.java
deleted file mode 100644
index b3f4f88..0000000
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeAuditAccess.java
+++ /dev/null
@@ -1,34 +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.reviewdb.server;
-
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupIncludeAudit;
-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> {
-  @PrimaryKey("key")
-  AccountGroupIncludeAudit get(AccountGroupIncludeAudit.Key key)
-      throws OrmException;
-
-  @Query("WHERE key.groupId = ? AND key.includeId = ?")
-  ResultSet<AccountGroupIncludeAudit> byGroupInclude(AccountGroup.Id groupId,
-      AccountGroup.Id incGroupId) throws OrmException;
-}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeByUuidAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeByUuidAccess.java
new file mode 100644
index 0000000..50e23eb
--- /dev/null
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeByUuidAccess.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.reviewdb.server;
+
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuid;
+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 AccountGroupIncludeByUuidAccess extends
+    Access<AccountGroupIncludeByUuid, AccountGroupIncludeByUuid.Key> {
+  @PrimaryKey("key")
+  AccountGroupIncludeByUuid get(AccountGroupIncludeByUuid.Key key) throws OrmException;
+
+  @Query("WHERE key.includeUUID = ?")
+  ResultSet<AccountGroupIncludeByUuid> byIncludeUUID(AccountGroup.UUID uuid) throws OrmException;
+
+  @Query("WHERE key.groupId = ?")
+  ResultSet<AccountGroupIncludeByUuid> byGroup(AccountGroup.Id id) throws OrmException;
+
+  @Query("")
+  ResultSet<AccountGroupIncludeByUuid> all() throws OrmException;
+}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeByUuidAuditAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeByUuidAuditAccess.java
new file mode 100644
index 0000000..1c95f75
--- /dev/null
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/AccountGroupIncludeByUuidAuditAccess.java
@@ -0,0 +1,34 @@
+// 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.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuidAudit;
+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 AccountGroupIncludeByUuidAuditAccess extends
+    Access<AccountGroupIncludeByUuidAudit, AccountGroupIncludeByUuidAudit.Key> {
+  @PrimaryKey("key")
+  AccountGroupIncludeByUuidAudit get(AccountGroupIncludeByUuidAudit.Key key)
+      throws OrmException;
+
+  @Query("WHERE key.groupId = ? AND key.includeUUID = ?")
+  ResultSet<AccountGroupIncludeByUuidAudit> byGroupInclude(AccountGroup.Id groupId,
+      AccountGroup.UUID incGroupUUID) throws OrmException;
+}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ApprovalCategoryAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ApprovalCategoryAccess.java
deleted file mode 100644
index db9886e..0000000
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ApprovalCategoryAccess.java
+++ /dev/null
@@ -1,31 +0,0 @@
-// Copyright (C) 2008 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.reviewdb.server;
-
-import com.google.gerrit.reviewdb.client.ApprovalCategory;
-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> {
-  @PrimaryKey("categoryId")
-  ApprovalCategory get(ApprovalCategory.Id id) throws OrmException;
-
-  @Query("ORDER BY position, name")
-  ResultSet<ApprovalCategory> all() throws OrmException;
-}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ApprovalCategoryValueAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ApprovalCategoryValueAccess.java
deleted file mode 100644
index 0bc9981..0000000
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ApprovalCategoryValueAccess.java
+++ /dev/null
@@ -1,33 +0,0 @@
-// Copyright (C) 2008 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.reviewdb.server;
-
-import com.google.gerrit.reviewdb.client.ApprovalCategory;
-import com.google.gerrit.reviewdb.client.ApprovalCategoryValue;
-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> {
-  @PrimaryKey("key")
-  ApprovalCategoryValue get(ApprovalCategoryValue.Id key) throws OrmException;
-
-  @Query("WHERE key.categoryId = ? ORDER BY key.value")
-  ResultSet<ApprovalCategoryValue> byCategory(ApprovalCategory.Id id)
-      throws OrmException;
-}
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDb.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDb.java
index 149626b..f1ab752 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDb.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/ReviewDb.java
@@ -43,11 +43,9 @@
   @Relation(id = 2)
   SystemConfigAccess systemConfig();
 
-  @Relation(id = 3)
-  ApprovalCategoryAccess approvalCategories();
+  // Deleted @Relation(id = 3)
 
-  @Relation(id = 4)
-  ApprovalCategoryValueAccess approvalCategoryValues();
+  // Deleted @Relation(id = 4)
 
   @Relation(id = 6)
   AccountAccess accounts();
@@ -70,12 +68,6 @@
   @Relation(id = 13)
   AccountGroupMemberAuditAccess accountGroupMembersAudit();
 
-  @Relation(id = 14)
-  AccountGroupIncludeAccess accountGroupIncludes();
-
-  @Relation(id = 15)
-  AccountGroupIncludeAuditAccess accountGroupIncludesAudit();
-
   @Relation(id = 17)
   AccountDiffPreferenceAccess accountDiffPreferences();
 
@@ -112,6 +104,12 @@
   @Relation(id = 28)
   SubmoduleSubscriptionAccess submoduleSubscriptions();
 
+  @Relation(id = 29)
+  AccountGroupIncludeByUuidAccess accountGroupIncludesByUuid();
+
+  @Relation(id = 30)
+  AccountGroupIncludeByUuidAuditAccess accountGroupIncludesByUuidAudit();
+
   /** Create the next unique id for an {@link Account}. */
   @Sequence(startWith = 1000000)
   int nextAccountId() throws OrmException;
diff --git a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SubmoduleSubscriptionAccess.java b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SubmoduleSubscriptionAccess.java
index 0090df8..c0e1eb6 100644
--- a/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SubmoduleSubscriptionAccess.java
+++ b/gerrit-reviewdb/src/main/java/com/google/gerrit/reviewdb/server/SubmoduleSubscriptionAccess.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.reviewdb.server;
 
 import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.SubmoduleSubscription;
 import com.google.gwtorm.server.Access;
 import com.google.gwtorm.server.OrmException;
@@ -31,7 +32,39 @@
   ResultSet<SubmoduleSubscription> bySuperProject(Branch.NameKey superProject)
       throws OrmException;
 
+  /**
+   * Fetches all <code>SubmoduleSubscription</code>s in which some branch of
+   * <code>superProject</code> subscribes a branch.
+   *
+   * Use {@link #bySuperproject(Branch.NameKey)} to fetch for a branch instead
+   * of a project.
+   *
+   * @param superProject the project to fetch subscriptions for
+   * @return <code>SubmoduleSubscription</code>s that are subscribed by some
+   * branch of <code>superProject</code>.
+   * @throws OrmException
+   */
+  @Query("WHERE key.superProject.projectName = ?")
+  ResultSet<SubmoduleSubscription> bySuperProjectProject(Project.NameKey superProject)
+      throws OrmException;
+
   @Query("WHERE submodule = ?")
   ResultSet<SubmoduleSubscription> bySubmodule(Branch.NameKey submodule)
       throws OrmException;
+
+  /**
+   * Fetches all <code>SubmoduleSubscription</code>s in which some branch of
+   * <code>submodule</code> is subscribed.
+   *
+   * Use {@link #bySubmodule(Branch.NameKey)} to fetch for a branch instead of
+   * a project.
+   *
+   * @param submodule the project to fetch subscriptions for.
+   * @return <code>SubmoduleSubscription</code>s that subscribe some branch of
+   * <code>submodule</code>.
+   * @throws OrmException
+   */
+  @Query("WHERE submodule.projectName = ?")
+  ResultSet<SubmoduleSubscription> bySubmoduleProject(Project.NameKey submodule)
+      throws OrmException;
 }
diff --git a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql
index 9d453fc..d6609e7 100644
--- a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql
+++ b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_generic.sql
@@ -34,10 +34,10 @@
 
 
 -- *********************************************************************
--- AccountGroupIncludeAccess
+-- AccountGroupIncludeByUuidAccess
 --    @PrimaryKey covers: byGroup
-CREATE INDEX account_group_includes_byInclude
-ON account_group_includes (include_id);
+CREATE INDEX account_group_includes_by_uuid_byInclude
+ON account_group_includes_by_uuid (include_uuid);
 
 
 -- *********************************************************************
diff --git a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
index 2c91db4..3b62e84 100644
--- a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
+++ b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/index_postgres.sql
@@ -47,30 +47,6 @@
 
 DROP FUNCTION make_plpgsql();
 
--- Define our schema upgrade support function.
---
-
-delimiter //
-
-CREATE OR REPLACE FUNCTION
-check_schema_version (exp INT)
-RETURNS VARCHAR(255)
-AS $$
-DECLARE
-  l_act INT;
-BEGIN
-  SELECT version_nbr INTO l_act
-  FROM schema_version;
-
-  IF l_act <> exp
-  THEN
-    RAISE EXCEPTION 'expected schema %, found %', exp, l_act;
-  END IF;
-  RETURN 'OK';
-END;
-$$ LANGUAGE plpgsql;
-//
-
 delimiter ;
 
 -- Indexes to support @Query
@@ -106,10 +82,10 @@
 
 
 -- *********************************************************************
--- AccountGroupIncludeAccess
+-- AccountGroupIncludeByUuidAccess
 --    @PrimaryKey covers: byGroup
-CREATE INDEX account_group_includes_byInclude
-ON account_group_includes (include_id);
+CREATE INDEX account_group_includes_by_uuid_byInclude
+ON account_group_includes_by_uuid (include_uuid);
 
 
 -- *********************************************************************
diff --git a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/mysql_nextval.sql b/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/mysql_nextval.sql
deleted file mode 100644
index 2479010..0000000
--- a/gerrit-reviewdb/src/main/resources/com/google/gerrit/reviewdb/server/mysql_nextval.sql
+++ /dev/null
@@ -1,14 +0,0 @@
--- Gerrit 2 : MySQL
---
-delimiter //
-
-CREATE FUNCTION nextval_account_id ()
-  RETURNS BIGINT
-  LANGUAGE SQL
-  NOT DETERMINISTIC
-  MODIFIES SQL DATA
-BEGIN
-  INSERT INTO account_id (s) VALUES (NULL);
-  RETURN LAST_INSERT_ID();
-END;
-//
diff --git a/gerrit-server/pom.xml b/gerrit-server/pom.xml
index af18173..0b13fbd 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.5-SNAPSHOT</version>
+    <version>2.6-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-server</artifactId>
@@ -80,7 +80,6 @@
     <dependency>
       <groupId>bouncycastle</groupId>
       <artifactId>bcpg-jdk15</artifactId>
-      <version>140</version>
       <scope>provided</scope>
     </dependency>
 
@@ -162,7 +161,7 @@
     </dependency>
 
     <dependency>
-      <groupId>com.google.gerrit</groupId>
+      <groupId>com.googlecode.juniversalchardet</groupId>
       <artifactId>juniversalchardet</artifactId>
     </dependency>
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/audit/AuditEvent.java b/gerrit-server/src/main/java/com/google/gerrit/audit/AuditEvent.java
index 364df80..173ced6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/audit/AuditEvent.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/audit/AuditEvent.java
@@ -14,22 +14,22 @@
 
 package com.google.gerrit.audit;
 
+import com.google.common.base.Objects;
 import com.google.common.base.Preconditions;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.Multimap;
 import com.google.gerrit.server.CurrentUser;
 
-import java.util.Collections;
-import java.util.List;
-
 public class AuditEvent {
 
   public static final String UNKNOWN_SESSION_ID = "000000000000000000000000000";
-  private static final Object UNKNOWN_RESULT = "N/A";
+  protected static final Multimap<String, ?> EMPTY_PARAMS = HashMultimap.create();
 
   public final String sessionId;
   public final CurrentUser who;
   public final long when;
   public final String what;
-  public final List<?> params;
+  public final Multimap<String, ?> params;
   public final Object result;
   public final long timeAtStart;
   public final long elapsed;
@@ -73,19 +73,6 @@
   }
 
   /**
-   * Creates a new audit event.
-   *
-   * @param sessionId session id the event belongs to
-   * @param who principal that has generated the event
-   * @param what object of the event
-   * @param params parameters of the event
-   */
-  public AuditEvent(String sessionId, CurrentUser who, String what, List<?> params) {
-    this(sessionId, who, what, System.currentTimeMillis(), params,
-        UNKNOWN_RESULT);
-  }
-
-  /**
    * Creates a new audit event with results
    *
    * @param sessionId session id the event belongs to
@@ -96,28 +83,20 @@
    * @param result result of the event
    */
   public AuditEvent(String sessionId, CurrentUser who, String what, long when,
-      List<?> params, Object result) {
+      Multimap<String, ?> params, Object result) {
     Preconditions.checkNotNull(what, "what is a mandatory not null param !");
 
-    this.sessionId = getValueWithDefault(sessionId, UNKNOWN_SESSION_ID);
+    this.sessionId = Objects.firstNonNull(sessionId, UNKNOWN_SESSION_ID);
     this.who = who;
     this.what = what;
     this.when = when;
     this.timeAtStart = this.when;
-    this.params = getValueWithDefault(params, Collections.emptyList());
+    this.params = Objects.firstNonNull(params, EMPTY_PARAMS);
     this.uuid = new UUID();
     this.result = result;
     this.elapsed = System.currentTimeMillis() - timeAtStart;
   }
 
-  private <T> T getValueWithDefault(T value, T defaultValueIfNull) {
-    if (value == null) {
-      return defaultValueIfNull;
-    } else {
-      return value;
-    }
-  }
-
   @Override
   public int hashCode() {
     return uuid.hashCode();
@@ -135,38 +114,7 @@
 
   @Override
   public String toString() {
-    StringBuilder sb = new StringBuilder();
-    sb.append(uuid.toString());
-    sb.append("|");
-    sb.append(sessionId);
-    sb.append('|');
-    sb.append(who);
-    sb.append('|');
-    sb.append(when);
-    sb.append('|');
-    sb.append(what);
-    sb.append('|');
-    sb.append(elapsed);
-    sb.append('|');
-    if (params != null) {
-      sb.append('[');
-      for (int i = 0; i < params.size(); i++) {
-        if (i > 0) sb.append(',');
-
-        Object param = params.get(i);
-        if (param == null) {
-          sb.append("null");
-        } else {
-          sb.append(param);
-        }
-      }
-      sb.append(']');
-    }
-    sb.append('|');
-    if (result != null) {
-      sb.append(result);
-    }
-
-    return sb.toString();
+    return String.format("AuditEvent UUID:%s, SID:%s, TS:%d, who:%s, what:%s",
+        uuid.get(), sessionId, when, who, what);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/audit/HttpAuditEvent.java b/gerrit-server/src/main/java/com/google/gerrit/audit/HttpAuditEvent.java
new file mode 100644
index 0000000..4bf9723
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/audit/HttpAuditEvent.java
@@ -0,0 +1,41 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.gerrit.audit;
+
+import com.google.common.collect.Multimap;
+import com.google.gerrit.server.CurrentUser;
+
+public class HttpAuditEvent extends AuditEvent {
+  public final String httpMethod;
+  public final int httpStatus;
+  public final Object input;
+
+  /**
+   * Creates a new audit event with results
+   *
+   * @param sessionId session id the event belongs to
+   * @param who principal that has generated the event
+   * @param what object of the event
+   * @param when time-stamp of when the event started
+   * @param params parameters of the event
+   * @param result result of the event
+   */
+  public HttpAuditEvent(String sessionId, CurrentUser who, String what, long when,
+      Multimap<String, ?> params, String httpMethod, Object input, int status, Object result) {
+    super(sessionId, who, what, when, params, result);
+    this.httpMethod = httpMethod;
+    this.input = input;
+    this.httpStatus = status;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/audit/RpcAuditEvent.java b/gerrit-server/src/main/java/com/google/gerrit/audit/RpcAuditEvent.java
new file mode 100644
index 0000000..c41ab3a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/audit/RpcAuditEvent.java
@@ -0,0 +1,36 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.gerrit.audit;
+
+import com.google.common.collect.Multimap;
+import com.google.gerrit.server.CurrentUser;
+
+public class RpcAuditEvent extends HttpAuditEvent {
+
+  /**
+   * Creates a new audit event with results
+   *
+   * @param sessionId session id the event belongs to
+   * @param who principal that has generated the event
+   * @param what object of the event
+   * @param when time-stamp of when the event started
+   * @param params parameters of the event
+   * @param result result of the event
+   */
+  public RpcAuditEvent(String sessionId, CurrentUser who, String what,
+      long when, Multimap<String, ?> params, String httpMethod, Object input,
+      int status, Object result) {
+    super(sessionId, who, what, when, params, httpMethod, input, status, result);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/audit/SshAuditEvent.java b/gerrit-server/src/main/java/com/google/gerrit/audit/SshAuditEvent.java
new file mode 100644
index 0000000..58864c8
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/audit/SshAuditEvent.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.gerrit.audit;
+
+import com.google.common.collect.Multimap;
+import com.google.gerrit.server.CurrentUser;
+
+public class SshAuditEvent extends AuditEvent {
+
+  public SshAuditEvent(String sessionId, CurrentUser who, String what,
+      long when, Multimap<String, ?> params, Object result) {
+    super(sessionId, who, what, when, params, result);
+  }
+}
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 37be293..4116633 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
@@ -14,12 +14,13 @@
 
 package com.google.gerrit.common;
 
-import com.google.gerrit.common.data.ApprovalType;
-import com.google.gerrit.common.data.ApprovalTypes;
 import com.google.gerrit.common.data.ContributorAgreement;
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.common.data.LabelTypes;
+import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.lifecycle.LifecycleModule;
 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.PatchSet;
@@ -39,15 +40,16 @@
 import com.google.gerrit.server.events.CommentAddedEvent;
 import com.google.gerrit.server.events.DraftPublishedEvent;
 import com.google.gerrit.server.events.EventFactory;
+import com.google.gerrit.server.events.MergeFailedEvent;
 import com.google.gerrit.server.events.PatchSetCreatedEvent;
 import com.google.gerrit.server.events.RefUpdatedEvent;
+import com.google.gerrit.server.events.ReviewerAddedEvent;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.WorkQueue;
 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;
 
@@ -61,24 +63,34 @@
 import java.io.BufferedReader;
 import java.io.File;
 import java.io.IOException;
+import java.io.InputStream;
 import java.io.InputStreamReader;
+import java.io.StringReader;
+import java.io.StringWriter;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
 import java.util.Map.Entry;
+import java.util.concurrent.Callable;
 import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.FutureTask;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.TimeoutException;
 
 /** Spawns local executables when a hook action occurs. */
 @Singleton
-public class ChangeHookRunner implements ChangeHooks {
+public class ChangeHookRunner implements ChangeHooks, LifecycleListener {
     /** A logger for this class. */
     private static final Logger log = LoggerFactory.getLogger(ChangeHookRunner.class);
 
-    public static class Module extends AbstractModule {
+    public static class Module extends LifecycleModule {
       @Override
       protected void configure() {
         bind(ChangeHookRunner.class);
         bind(ChangeHooks.class).to(ChangeHookRunner.class);
+        listener().to(ChangeHookRunner.class);
       }
     }
 
@@ -92,10 +104,61 @@
         }
     }
 
-    /** Listeners to receive changes as they happen. */
+    /** Container class used to hold the return code and output of script hook execution */
+    public static class HookResult {
+      private int exitValue = -1;
+      private String output;
+      private String executionError;
+
+      private HookResult(int exitValue, String output) {
+        this.exitValue = exitValue;
+        this.output = output;
+      }
+
+      private HookResult(String output, String executionError) {
+        this.output = output;
+        this.executionError = executionError;
+      }
+
+      public int getExitValue() {
+        return exitValue;
+      }
+
+      public void setExitValue(int exitValue) {
+        this.exitValue = exitValue;
+      }
+
+      public String getOutput() {
+        return output;
+      }
+
+      public String toString() {
+        StringBuilder sb = new StringBuilder();
+
+        if (output != null && output.length() != 0) {
+          sb.append(output);
+
+          if (executionError != null) {
+            sb.append(" - ");
+          }
+        }
+
+        if (executionError != null ) {
+          sb.append(executionError);
+        }
+
+        return sb.toString();
+      }
+    }
+
+    /** Listeners to receive changes as they happen (limited by visibility
+     *  of holder's user). */
     private final Map<ChangeListener, ChangeListenerHolder> listeners =
       new ConcurrentHashMap<ChangeListener, ChangeListenerHolder>();
 
+    /** Listeners to receive all changes as they happen. */
+    private final DynamicSet<ChangeListener> unrestrictedListeners;
+
     /** Filename of the new patchset hook. */
     private final File patchsetCreatedHook;
 
@@ -108,6 +171,9 @@
     /** Filename of the change merged hook. */
     private final File changeMergedHook;
 
+    /** Filename of the merge failed hook. */
+    private final File mergeFailedHook;
+
     /** Filename of the change abandoned hook. */
     private final File changeAbandonedHook;
 
@@ -117,9 +183,15 @@
     /** Filename of the ref updated hook. */
     private final File refUpdatedHook;
 
+    /** Filename of the reviewer added hook. */
+    private final File reviewerAddedHook;
+
     /** Filename of the cla signed hook. */
     private final File claSignedHook;
 
+    /** Filename of the update hook. */
+    private final File refUpdateHook;
+
     private final String anonymousCowardName;
 
     /** Repository Manager. */
@@ -132,12 +204,16 @@
 
     private final AccountCache accountCache;
 
-    private final ApprovalTypes approvalTypes;
-
     private final EventFactory eventFactory;
 
     private final SitePaths sitePaths;
 
+    /** Thread pool used to monitor sync hooks */
+    private final ExecutorService syncHookThreadPool = Executors.newCachedThreadPool();
+
+    /** Timeout value for synchronous hooks */
+    private final int syncHookTimeout;
+
     /**
      * Create a new ChangeHookRunner.
      *
@@ -152,17 +228,20 @@
       final GitRepositoryManager repoManager,
       final @GerritServerConfig Config config,
       final @AnonymousCowardName String anonymousCowardName,
-      final SitePaths sitePath, final ProjectCache projectCache,
-      final AccountCache accountCache, final ApprovalTypes approvalTypes,
-      final EventFactory eventFactory, final SitePaths sitePaths) {
+      final SitePaths sitePath,
+      final ProjectCache projectCache,
+      final AccountCache accountCache,
+      final EventFactory eventFactory,
+      final SitePaths sitePaths,
+      final DynamicSet<ChangeListener> unrestrictedListeners) {
         this.anonymousCowardName = anonymousCowardName;
         this.repoManager = repoManager;
         this.hookQueue = queue.createQueue(1, "hook");
         this.projectCache = projectCache;
         this.accountCache = accountCache;
-        this.approvalTypes = approvalTypes;
         this.eventFactory = eventFactory;
         this.sitePaths = sitePath;
+        this.unrestrictedListeners = unrestrictedListeners;
 
         final File hooksPath = sitePath.resolve(getValue(config, "hooks", "path", sitePath.hooks_dir.getAbsolutePath()));
 
@@ -170,10 +249,14 @@
         draftPublishedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "draftPublishedHook", "draft-published")).getPath());
         commentAddedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "commentAddedHook", "comment-added")).getPath());
         changeMergedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "changeMergedHook", "change-merged")).getPath());
+        mergeFailedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "mergeFailed", "merge-failed")).getPath());
         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());
+        reviewerAddedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "reviewerAddedHook", "reviewer-added")).getPath());
         claSignedHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "claSignedHook", "cla-signed")).getPath());
+        refUpdateHook = sitePath.resolve(new File(hooksPath, getValue(config, "hooks", "refUpdateHook", "ref-update")).getPath());
+        syncHookTimeout = config.getInt("hooks", "syncHookTimeout", 30);
     }
 
     public void addChangeListener(ChangeListener listener, IdentifiedUser user) {
@@ -220,6 +303,38 @@
         }
     }
 
+    /**
+     * Fire the update hook
+     *
+     */
+    public HookResult doRefUpdateHook(final Project project, final String refname,
+        final Account uploader, final ObjectId oldId, final ObjectId newId) {
+
+      final List<String> args = new ArrayList<String>();
+      addArg(args, "--project", project.getName());
+      addArg(args, "--refname", refname);
+      addArg(args, "--uploader", getDisplayName(uploader));
+      addArg(args, "--oldrev", oldId.getName());
+      addArg(args, "--newrev", newId.getName());
+
+      HookResult hookResult;
+
+      try {
+        hookResult = runSyncHook(project.getNameKey(), refUpdateHook, args);
+      } catch (TimeoutException e) {
+        hookResult = new HookResult(-1, "Synchronous hook timed out");
+      }
+
+      return hookResult;
+    }
+
+    /**
+     * Fire the Patchset Created Hook.
+     *
+     * @param change The change itself.
+     * @param patchSet The Patchset that was created.
+     * @throws OrmException
+     */
     public void doPatchsetCreatedHook(final Change change, final PatchSet patchSet,
           final ReviewDb db) throws OrmException {
         final PatchSetCreatedEvent event = new PatchSetCreatedEvent();
@@ -268,8 +383,8 @@
     }
 
     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 PatchSet patchSet, final String comment, final Map<String, Short> approvals,
+          final ReviewDb db) throws OrmException {
         final CommentAddedEvent event = new CommentAddedEvent();
 
         event.change = eventFactory.asChangeAttribute(change);
@@ -277,11 +392,12 @@
         event.patchSet = eventFactory.asPatchSetAttribute(patchSet);
         event.comment = comment;
 
+        LabelTypes labelTypes = projectCache.get(change.getProject()).getLabelTypes();
         if (approvals.size() > 0) {
             event.approvals = new ApprovalAttribute[approvals.size()];
             int i = 0;
-            for (Map.Entry<ApprovalCategory.Id, ApprovalCategoryValue.Id> approval : approvals.entrySet()) {
-                event.approvals[i++] = getApprovalAttribute(approval);
+            for (Map.Entry<String, Short> approval : approvals.entrySet()) {
+                event.approvals[i++] = getApprovalAttribute(labelTypes, approval);
             }
         }
 
@@ -289,6 +405,7 @@
 
         final List<String> args = new ArrayList<String>();
         addArg(args, "--change", event.change.id);
+        addArg(args, "--is-draft", patchSet.isDraft() ? "true" : "false");
         addArg(args, "--change-url", event.change.url);
         addArg(args, "--project", event.change.project);
         addArg(args, "--branch", event.change.branch);
@@ -296,8 +413,11 @@
         addArg(args, "--author", getDisplayName(account));
         addArg(args, "--commit", event.patchSet.revision);
         addArg(args, "--comment", comment == null ? "" : comment);
-        for (Map.Entry<ApprovalCategory.Id, ApprovalCategoryValue.Id> approval : approvals.entrySet()) {
-            addArg(args, "--" + approval.getKey().get(), Short.toString(approval.getValue().get()));
+        for (Map.Entry<String, Short> approval : approvals.entrySet()) {
+          LabelType lt = labelTypes.byLabel(approval.getKey());
+          if (lt != null) {
+            addArg(args, "--" + lt.getName(), Short.toString(approval.getValue()));
+          }
         }
 
         runHook(change.getProject(), commentAddedHook, args);
@@ -324,6 +444,30 @@
         runHook(change.getProject(), changeMergedHook, args);
     }
 
+    public void doMergeFailedHook(final Change change, final Account account,
+          final PatchSet patchSet, final String reason,
+          final ReviewDb db) throws OrmException {
+        final MergeFailedEvent event = new MergeFailedEvent();
+
+        event.change = eventFactory.asChangeAttribute(change);
+        event.submitter = eventFactory.asAccountAttribute(account);
+        event.patchSet = eventFactory.asPatchSetAttribute(patchSet);
+        event.reason = reason;
+        fireEvent(change, event, db);
+
+        final List<String> args = new ArrayList<String>();
+        addArg(args, "--change", event.change.id);
+        addArg(args, "--change-url", event.change.url);
+        addArg(args, "--project", event.change.project);
+        addArg(args, "--branch", event.change.branch);
+        addArg(args, "--topic", event.change.topic);
+        addArg(args, "--submitter", getDisplayName(account));
+        addArg(args, "--commit", event.patchSet.revision);
+        addArg(args, "--reason",  reason == null ? "" : reason);
+
+        runHook(change.getProject(), mergeFailedHook, args);
+    }
+
     public void doChangeAbandonedHook(final Change change, final Account account,
           final String reason, final ReviewDb db) throws OrmException {
         final ChangeAbandonedEvent event = new ChangeAbandonedEvent();
@@ -391,6 +535,25 @@
       runHook(refName.getParentKey(), refUpdatedHook, args);
     }
 
+    public void doReviewerAddedHook(final Change change, final Account account,
+        final PatchSet patchSet, final ReviewDb db) throws OrmException {
+      final ReviewerAddedEvent event = new ReviewerAddedEvent();
+
+      event.change = eventFactory.asChangeAttribute(change);
+      event.patchSet = eventFactory.asPatchSetAttribute(patchSet);
+      event.reviewer = eventFactory.asAccountAttribute(account);
+      fireEvent(change, event, db);
+
+      final List<String> args = new ArrayList<String>();
+      addArg(args, "--change", event.change.id);
+      addArg(args, "--change-url", event.change.url);
+      addArg(args, "--project", event.change.project);
+      addArg(args, "--branch", event.change.branch);
+      addArg(args, "--reviewer", getDisplayName(account));
+
+      runHook(change.getProject(), reviewerAddedHook, args);
+    }
+
     public void doClaSignupHook(Account account, ContributorAgreement cla) {
       if (account != null) {
         final List<String> args = new ArrayList<String>();
@@ -402,12 +565,20 @@
       }
     }
 
+    private void fireEventForUnrestrictedListeners(final ChangeEvent event) {
+      for (ChangeListener listener : unrestrictedListeners) {
+          listener.onChangeEvent(event);
+      }
+    }
+
     private void fireEvent(final Change change, final ChangeEvent event, final ReviewDb db) throws OrmException {
       for (ChangeListenerHolder holder : listeners.values()) {
           if (isVisibleTo(change, holder.user, db)) {
               holder.listener.onChangeEvent(event);
           }
       }
+
+      fireEventForUnrestrictedListeners( event );
     }
 
     private void fireEvent(Branch.NameKey branchName, final ChangeEvent event) {
@@ -416,6 +587,8 @@
               holder.listener.onChangeEvent(event);
           }
       }
+
+      fireEventForUnrestrictedListeners( event );
     }
 
     private boolean isVisibleTo(Change change, IdentifiedUser user, ReviewDb db) throws OrmException {
@@ -441,15 +614,15 @@
      * @param approval
      * @return object suitable for serialization to JSON
      */
-    private ApprovalAttribute getApprovalAttribute(
-            Entry<ApprovalCategory.Id, ApprovalCategoryValue.Id> approval) {
+    private ApprovalAttribute getApprovalAttribute(LabelTypes labelTypes,
+            Entry<String, Short> approval) {
         ApprovalAttribute a = new ApprovalAttribute();
-        a.type = approval.getKey().get();
-        ApprovalType at = approvalTypes.byId(approval.getKey());
-        if (at != null) {
-          a.description = at.getCategory().getName();
+        a.type = approval.getKey();
+        LabelType lt = labelTypes.byLabel(approval.getKey());
+        if (lt != null) {
+          a.description = lt.getName();
         }
-        a.value = Short.toString(approval.getValue().get());
+        a.value = Short.toString(approval.getValue());
         return a;
     }
 
@@ -481,31 +654,84 @@
   private synchronized void runHook(Project.NameKey project, File hook,
       List<String> args) {
     if (project != null && hook.exists()) {
-      hookQueue.execute(new HookTask(project, hook, args));
+      hookQueue.execute(new AsyncHookTask(project, hook, args));
     }
   }
 
   private synchronized void runHook(File hook, List<String> args) {
     if (hook.exists()) {
-      hookQueue.execute(new HookTask(null, hook, args));
+      hookQueue.execute(new AsyncHookTask(null, hook, args));
     }
   }
 
-  private final class HookTask implements Runnable {
+  private HookResult runSyncHook(Project.NameKey project,
+      File hook, List<String> args) throws TimeoutException {
+
+    if (!hook.exists()) {
+      return null;
+    }
+
+    SyncHookTask syncHook = new SyncHookTask(project, hook, args);
+    FutureTask<HookResult> task = new FutureTask<HookResult>(syncHook);
+
+    syncHookThreadPool.execute(task);
+
+    String message;
+
+    try {
+      return task.get(syncHookTimeout, TimeUnit.SECONDS);
+    } catch (TimeoutException e) {
+      message = "Synchronous hook timed out "  + hook.getAbsolutePath();
+      log.error(message);
+    } catch (Exception e) {
+      message = "Error running hook " + hook.getAbsolutePath();
+      log.error(message, e);
+    }
+
+    task.cancel(true);
+    syncHook.cancel();
+    return  new HookResult(syncHook.getOutput(), message);
+  }
+
+  @Override
+  public void start() {
+  }
+
+  @Override
+  public void stop() {
+    syncHookThreadPool.shutdown();
+    boolean isTerminated;
+    do {
+      try {
+        isTerminated = syncHookThreadPool.awaitTermination(10, TimeUnit.SECONDS);
+      } catch (InterruptedException ie) {
+        isTerminated = false;
+      }
+    } while (!isTerminated);
+  }
+
+  private class HookTask {
     private final Project.NameKey project;
     private final File hook;
     private final List<String> args;
+    private StringWriter output;
+    private Process ps;
 
-    private HookTask(Project.NameKey project, File hook, List<String> args) {
+    protected HookTask(Project.NameKey project, File hook, List<String> args) {
       this.project = project;
       this.hook = hook;
       this.args = args;
     }
 
-    @Override
-    public void run() {
+    public String getOutput() {
+      return output != null ? output.toString() : null;
+    }
+
+    protected HookResult runHook() {
       Repository repo = null;
+      HookResult result = null;
       try {
+
         final List<String> argv = new ArrayList<String>(1 + args.size());
         argv.add(hook.getAbsolutePath());
         argv.addAll(args);
@@ -526,23 +752,22 @@
           env.put("GIT_DIR", repo.getDirectory().getAbsolutePath());
         }
 
-        Process ps = pb.start();
+        ps = pb.start();
         ps.getOutputStream().close();
-
-        BufferedReader br =
-            new BufferedReader(new InputStreamReader(ps.getInputStream()));
+        InputStream is = ps.getInputStream();
+        String output = null;
         try {
-          String line;
-          while ((line = br.readLine()) != null) {
-            log.info("hook[" + hook.getName() + "] output: " + line);
-          }
+          output = readOutput(is);
         } finally {
           try {
-            br.close();
+            is.close();
           } catch (IOException closeErr) {
           }
           ps.waitFor();
+          result = new HookResult(ps.exitValue(), output);
         }
+      } catch (InterruptedException iex) {
+        // InterruptedExeception - timeout or cancel
       } catch (Throwable err) {
         log.error("Error running hook " + hook.getAbsolutePath(), err);
       } finally {
@@ -550,11 +775,79 @@
           repo.close();
         }
       }
+
+      final int exitValue = result.getExitValue();
+      if (exitValue == 0) {
+        log.debug("hook[" + getName() + "] exitValue:" + exitValue);
+      } else {
+        log.info("hook[" + getName() + "] exitValue:" + exitValue);
+      }
+
+      BufferedReader br =
+          new BufferedReader(new StringReader(result.getOutput()));
+      try {
+        String line;
+        while ((line = br.readLine()) != null) {
+          log.info("hook[" + getName() + "] output: " + line);
+        }
+      }
+      catch(IOException  iox) {
+        log.error("Error writing hook output", iox);
+      }
+
+      return result;
+    }
+
+    private String readOutput(InputStream is) throws IOException {
+      output = new StringWriter();
+      InputStreamReader input = new InputStreamReader(is);
+      char[] buffer = new char[4096];
+      int n = 0;
+      while ((n = input.read(buffer)) != -1) {
+        output.write(buffer, 0, n);
+      }
+
+      return output.toString();
+    }
+
+    protected String getName() {
+      return hook.getName();
     }
 
     @Override
     public String toString() {
       return "hook " + hook.getName();
     }
+
+    public void cancel() {
+      ps.destroy();
+    }
+  }
+
+  /** Callable type used to run synchronous hooks */
+  private final class SyncHookTask extends HookTask
+      implements Callable<HookResult> {
+
+    private SyncHookTask(Project.NameKey project, File hook, List<String> args) {
+      super(project, hook, args);
+    }
+
+    @Override
+    public HookResult call() throws Exception {
+      return super.runHook();
+    }
+  }
+
+  /** Runable type used to run async hooks */
+  private final class AsyncHookTask extends HookTask implements Runnable {
+
+    private AsyncHookTask(Project.NameKey project, File hook, List<String> args) {
+      super(project, hook, args);
+    }
+
+    @Override
+    public void run() {
+      super.runHook();
+    }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHooks.java b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHooks.java
index 134057d..48a52a0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHooks.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/ChangeHooks.java
@@ -14,13 +14,13 @@
 
 package com.google.gerrit.common;
 
+import com.google.gerrit.common.ChangeHookRunner.HookResult;
 import com.google.gerrit.common.data.ContributorAgreement;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.ApprovalCategory;
-import com.google.gerrit.reviewdb.client.ApprovalCategoryValue;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gwtorm.server.OrmException;
@@ -50,7 +50,7 @@
    * Fire the Draft Published Hook.
    *
    * @param change The change itself.
-   * @param patchSet The Patchset that was created.
+   * @param patchSet The Patchset that was published.
    * @throws OrmException
    */
   public void doDraftPublishedHook(Change change, PatchSet patchSet,
@@ -63,12 +63,12 @@
    * @param patchSet The patchset this comment is related to.
    * @param account The gerrit user who added the comment.
    * @param comment The comment given.
-   * @param approvals Map of Approval Categories and Scores
+   * @param approvals Map of label IDs to scores
    * @throws OrmException
    */
   public void doCommentAddedHook(Change change, Account account,
       PatchSet patchSet, String comment,
-      Map<ApprovalCategory.Id, ApprovalCategoryValue.Id> approvals, ReviewDb db)
+      Map<String, Short> approvals, ReviewDb db)
       throws OrmException;
 
   /**
@@ -83,6 +83,18 @@
       PatchSet patchSet, ReviewDb db) throws OrmException;
 
   /**
+   * Fire the Merge Failed Hook.
+   *
+   * @param change The change itself.
+   * @param account The gerrit user who attempted to submit the change.
+   * @param patchSet The patchset that failed to merge.
+   * @param reason The reason that the change failed to merge.
+   * @throws OrmException
+   */
+  public void doMergeFailedHook(Change change, Account account,
+      PatchSet patchSet, String reason, ReviewDb db) throws OrmException;
+
+  /**
    * Fire the Change Abandoned Hook.
    *
    * @param change The change itself.
@@ -125,5 +137,28 @@
   public void doRefUpdatedHook(Branch.NameKey refName, ObjectId oldId,
       ObjectId newId, Account account);
 
+  /**
+   * Fire the Reviewer Added Hook
+   *
+   * @param change The change itself.
+   * @param patchSet The patchset that the reviewer was added on.
+   * @param account The gerrit user who was added as reviewer.
+   */
+  public void doReviewerAddedHook(Change change, Account account,
+      PatchSet patchSet, ReviewDb db) throws OrmException;
+
   public void doClaSignupHook(Account account, ContributorAgreement cla);
+
+  /**
+   * Fire the Ref update Hook
+   *
+   * @param project The target project
+   * @param refName The Branch.NameKey of the ref provided by client
+   * @param uploader The gerrit user running the command
+   * @param oldId The ref's old id
+   * @param newId The ref's new id
+   * @param account The gerrit user who moved the ref
+   */
+  public HookResult doRefUpdateHook(Project project,  String refName,
+       Account uploader, ObjectId oldId, ObjectId newId);
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java b/gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java
index f30f5ea..6011ab0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/common/DisabledChangeHooks.java
@@ -14,13 +14,13 @@
 
 package com.google.gerrit.common;
 
+import com.google.gerrit.common.ChangeHookRunner.HookResult;
 import com.google.gerrit.common.data.ContributorAgreement;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.ApprovalCategory;
-import com.google.gerrit.reviewdb.client.ApprovalCategoryValue;
+import com.google.gerrit.reviewdb.client.Branch.NameKey;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Branch.NameKey;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 
@@ -46,6 +46,11 @@
   }
 
   @Override
+  public void doMergeFailedHook(Change change, Account account,
+      PatchSet patchSet, String reason, ReviewDb db) {
+  }
+
+  @Override
   public void doChangeRestoredHook(Change change, Account account,
       String reason, ReviewDb db) {
   }
@@ -57,7 +62,7 @@
   @Override
   public void doCommentAddedHook(Change change, Account account,
       PatchSet patchSet, String comment,
-      Map<ApprovalCategory.Id, ApprovalCategoryValue.Id> approvals, ReviewDb db) {
+      Map<String, Short> approvals, ReviewDb db) {
   }
 
   @Override
@@ -81,6 +86,17 @@
   }
 
   @Override
+  public void doReviewerAddedHook(Change change, Account account, PatchSet patchSet,
+      ReviewDb db) {
+  }
+
+  @Override
   public void removeChangeListener(ChangeListener listener) {
   }
+
+  @Override
+  public HookResult doRefUpdateHook(Project project, String refName,
+      Account uploader, ObjectId oldId, ObjectId newId) {
+    return null;
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/rules/PredicateClassLoader.java b/gerrit-server/src/main/java/com/google/gerrit/rules/PredicateClassLoader.java
new file mode 100644
index 0000000..eb2d264
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/rules/PredicateClassLoader.java
@@ -0,0 +1,65 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.gerrit.rules;
+
+import com.google.common.collect.LinkedHashMultimap;
+import com.google.common.collect.Multimap;
+import com.google.gerrit.extensions.registration.DynamicSet;
+
+import java.util.Collection;
+
+/**
+ * Loads the classes for Prolog predicates.
+ */
+public class PredicateClassLoader extends ClassLoader {
+
+  private final Multimap<String, ClassLoader> packageClassLoaderMap =
+      LinkedHashMultimap.create();
+
+  public PredicateClassLoader(
+      final DynamicSet<PredicateProvider> predicateProviders,
+      final ClassLoader parent) {
+    super(parent);
+
+    for (PredicateProvider predicateProvider : predicateProviders) {
+      for (String pkg : predicateProvider.getPackages()) {
+        packageClassLoaderMap.put(pkg, predicateProvider.getClass()
+            .getClassLoader());
+      }
+    }
+  }
+
+  @Override
+  protected Class<?> findClass(final String className)
+      throws ClassNotFoundException {
+    final Collection<ClassLoader> classLoaders =
+        packageClassLoaderMap.get(getPackageName(className));
+    for (final ClassLoader cl : classLoaders) {
+      try {
+        return Class.forName(className, true, cl);
+      } catch (ClassNotFoundException e) {
+        // ignore
+      }
+    }
+    throw new ClassNotFoundException(className);
+  }
+
+  private static String getPackageName(String className) {
+    final int pos = className.lastIndexOf('.');
+    if (pos < 0) {
+      return "";
+    }
+    return className.substring(0, pos);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/rules/PredicateProvider.java b/gerrit-server/src/main/java/com/google/gerrit/rules/PredicateProvider.java
new file mode 100644
index 0000000..4bd9423
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/rules/PredicateProvider.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+package com.google.gerrit.rules;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+
+import com.googlecode.prolog_cafe.lang.Predicate;
+
+/**
+ * Provides additional packages that contain Prolog predicates that should be
+ * made available in the Prolog environment. The predicates can e.g. be used in
+ * the project submit rules.
+ *
+ * Each Java class defining a Prolog predicate must be in one of the provided
+ * packages and its name must apply to the 'PRED_[functor]_[arity]' format. In
+ * addition it must extend {@link Predicate}.
+ */
+@ExtensionPoint
+public interface PredicateProvider {
+  /** Return set of packages that contain Prolog predicates */
+  public ImmutableSet<String> getPackages();
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/rules/PrologModule.java b/gerrit-server/src/main/java/com/google/gerrit/rules/PrologModule.java
index d361790..92b8b1b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/rules/PrologModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/rules/PrologModule.java
@@ -14,11 +14,13 @@
 
 package com.google.gerrit.rules;
 
+import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.server.config.FactoryModule;
 
 public class PrologModule extends FactoryModule {
   @Override
   protected void configure() {
+    DynamicSet.setOf(binder(), PredicateProvider.class);
     factory(PrologEnvironment.Factory.class);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/rules/RulesCache.java b/gerrit-server/src/main/java/com/google/gerrit/rules/RulesCache.java
index fbee145..dad53b9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/rules/RulesCache.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/rules/RulesCache.java
@@ -16,6 +16,9 @@
 
 import static com.googlecode.prolog_cafe.lang.PrologMachineCopy.save;
 
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
@@ -42,7 +45,10 @@
 
 import java.io.File;
 import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
 import java.io.PushbackReader;
+import java.io.Reader;
 import java.io.StringReader;
 import java.lang.ref.Reference;
 import java.lang.ref.ReferenceQueue;
@@ -52,6 +58,7 @@
 import java.net.URLClassLoader;
 import java.util.EnumSet;
 import java.util.HashMap;
+import java.util.List;
 import java.util.Map;
 
 /**
@@ -69,10 +76,8 @@
   /** 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 static final List<String> PACKAGE_LIST = ImmutableList.of(
+      Prolog.BUILTIN, "gerrit");
 
   private final Map<ObjectId, MachineRef> machineCache =
       new HashMap<ObjectId, MachineRef>();
@@ -94,21 +99,27 @@
   private final File cacheDir;
   private final File rulesDir;
   private final GitRepositoryManager gitMgr;
+  private final DynamicSet<PredicateProvider> predicateProviders;
   private final ClassLoader systemLoader;
   private final PrologMachineCopy defaultMachine;
 
   @Inject
   protected RulesCache(@GerritServerConfig Config config, SitePaths site,
-      GitRepositoryManager gm) {
+      GitRepositoryManager gm, DynamicSet<PredicateProvider> predicateProviders) {
     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;
+    this.predicateProviders = predicateProviders;
 
     systemLoader = getClass().getClassLoader();
     defaultMachine = save(newEmptyMachine(systemLoader));
   }
 
+  public boolean isProjectRulesEnabled() {
+    return enableProjectRules;
+  }
+
   /**
    * Locate a cached Prolog machine state, or create one if not available.
    *
@@ -142,6 +153,15 @@
     return pcm;
   }
 
+  public PrologMachineCopy loadMachine(String name, InputStream in)
+      throws CompileException {
+    PrologMachineCopy pmc = consultRules(name, new InputStreamReader(in));
+    if (pmc == null) {
+      throw new CompileException("Cannot consult rules from the stream " + name);
+    }
+    return pmc;
+  }
+
   private void gc() {
     Reference<?> ref;
     while ((ref = dead.poll()) != null) {
@@ -168,16 +188,21 @@
     // 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);
+    PrologMachineCopy pmc = consultRules("rules.pl", new StringReader(rules));
+    if (pmc == null) {
+      throw new CompileException("Cannot consult rules of " + project);
+    }
+    return pmc;
+  }
 
+  private PrologMachineCopy consultRules(String name, Reader rules) {
+    BufferingPrologControl ctl = newEmptyMachine(systemLoader);
+    PushbackReader in = new PushbackReader(rules, Prolog.PUSHBACK_SIZE);
     if (!ctl.execute(
         Prolog.BUILTIN, "consult_stream",
-        SymbolTerm.intern("rules.pl"),
+        SymbolTerm.intern(name),
         new JavaObjectTerm(in))) {
-      throw new CompileException("Cannot consult rules of " + project);
+      return null;
     }
     return save(ctl);
   }
@@ -207,15 +232,22 @@
     }
   }
 
-  private static BufferingPrologControl newEmptyMachine(ClassLoader cl) {
+  private BufferingPrologControl newEmptyMachine(ClassLoader cl) {
     BufferingPrologControl ctl = new BufferingPrologControl();
     ctl.setMaxArity(PrologEnvironment.MAX_ARITY);
     ctl.setMaxDatabaseSize(DB_MAX);
-    ctl.setPrologClassLoader(new PrologClassLoader(cl));
+    ctl.setPrologClassLoader(new PrologClassLoader(new PredicateClassLoader(
+        predicateProviders, cl)));
     ctl.setEnabled(EnumSet.allOf(Prolog.Feature.class), false);
 
+    List<String> packages = Lists.newArrayList();
+    packages.addAll(PACKAGE_LIST);
+    for (PredicateProvider predicateProvider : predicateProviders) {
+      packages.addAll(predicateProvider.getPackages());
+    }
+
     // Bootstrap the interpreter and ensure there is clean state.
-    ctl.initialize(PACKAGE_LIST);
+    ctl.initialize(packages.toArray(new String[packages.size()]));
     return ctl;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/AccessPath.java b/gerrit-server/src/main/java/com/google/gerrit/server/AccessPath.java
index ae76a6e..31df875 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/AccessPath.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/AccessPath.java
@@ -19,8 +19,14 @@
   /** An unknown access path, probably should not be special. */
   UNKNOWN,
 
-  /** Access through the web UI. */
-  WEB_UI,
+  /** Access through the REST API. */
+  REST_API,
+
+  /** Access through the old JSON-RPC interface. */
+  JSON_RPC,
+
+  /** Access by a web cookie. This path is not protected like REST_API. */
+  WEB_BROWSER,
 
   /** Access through an SSH command that is not invoked by Git. */
   SSH_COMMAND,
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 5d36b33..f1fe72d 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
@@ -30,7 +30,7 @@
 public class AnonymousUser extends CurrentUser {
   @Inject
   AnonymousUser(CapabilityControl.Factory capabilityControlFactory) {
-    super(capabilityControlFactory, AccessPath.UNKNOWN);
+    super(capabilityControlFactory);
   }
 
   @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
index a295c49..0ce6892 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ApprovalsUtil.java
@@ -14,22 +14,22 @@
 
 package com.google.gerrit.server;
 
+import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Sets;
-import com.google.gerrit.common.data.ApprovalType;
-import com.google.gerrit.common.data.ApprovalTypes;
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Account.Id;
-import com.google.gerrit.reviewdb.client.ApprovalCategory;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.PatchSetApproval.LabelId;
 import com.google.gerrit.reviewdb.client.PatchSetInfo;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 
-import java.io.IOException;
 import java.util.Collections;
 import java.util.List;
 import java.util.Set;
@@ -46,20 +46,17 @@
  */
 public class ApprovalsUtil {
   private final ReviewDb db;
-  private final ApprovalTypes approvalTypes;
 
   @Inject
-  ApprovalsUtil(ReviewDb db, ApprovalTypes approvalTypes) {
+  public ApprovalsUtil(ReviewDb db) {
     this.db = db;
-    this.approvalTypes = approvalTypes;
   }
 
   /**
    * Resync the changeOpen status which is cached in the approvals table for
    * performance reasons
    */
-  public void syncChangeStatus(final Change change)
-      throws OrmException {
+  public void syncChangeStatus(final Change change) throws OrmException {
     final List<PatchSetApproval> approvals =
         db.patchSetApprovals().byChange(change.getId()).toList();
     for (PatchSetApproval a : approvals) {
@@ -69,53 +66,41 @@
   }
 
   /**
-   * Moves the PatchSetApprovals to the last PatchSet on the change while
-   * keeping the vetos.
+   * Moves the PatchSetApprovals to the specified PatchSet on the change from
+   * the prior PatchSet, while keeping the vetos.
    *
-   * @param change Change to update
+   * @param db database connection to use for updates.
+   * @param dest PatchSet to copy to
    * @throws OrmException
-   * @throws IOException
    * @return List<PatchSetApproval> The previous approvals
    */
-  public List<PatchSetApproval> copyVetosToLatestPatchSet(Change change)
-      throws OrmException, IOException {
+  public List<PatchSetApproval> copyVetosToPatchSet(ReviewDb db,
+      LabelTypes labelTypes, PatchSet.Id dest) throws OrmException {
     PatchSet.Id source;
-    if (change.getNumberOfPatchSets() > 1) {
-      source = new PatchSet.Id(change.getId(), change.getNumberOfPatchSets() - 1);
+    if (dest.get() > 1) {
+      source = new PatchSet.Id(dest.getParentKey(), dest.get() - 1);
     } else {
-      throw new IOException("Previous patch set could not be found");
+      throw new OrmException("Previous patch set could not be found");
     }
 
-    PatchSet.Id dest = change.currPatchSetId();
-    List<PatchSetApproval> patchSetApprovals = db.patchSetApprovals().byChange(change.getId()).toList();
+    List<PatchSetApproval> patchSetApprovals =
+        db.patchSetApprovals().byChange(dest.getParentKey()).toList();
     for (PatchSetApproval a : patchSetApprovals) {
-      // ApprovalCategory.SUBMIT is still in db but not relevant in git-store
-      if (!ApprovalCategory.SUBMIT.equals(a.getCategoryId())) {
-        final ApprovalType type = approvalTypes.byId(a.getCategoryId());
-        if (a.getPatchSetId().equals(source) &&
-            type.getCategory().isCopyMinScore() &&
-            type.isMaxNegative(a)) {
-          db.patchSetApprovals().insert(
-              Collections.singleton(new PatchSetApproval(dest, a)));
-        }
+      LabelType type = labelTypes.byLabel(a.getLabelId());
+      if (type != null && a.getPatchSetId().equals(source) &&
+          type.isCopyMinScore() &&
+          type.isMaxNegative(a)) {
+        db.patchSetApprovals().insert(
+            Collections.singleton(new PatchSetApproval(dest, a)));
       }
     }
     return patchSetApprovals;
   }
 
-
-  /** Attach reviewers to a change. */
-  public void addReviewers(Change change, PatchSet ps, PatchSetInfo info,
-      Set<Account.Id> wantReviewers) throws OrmException {
-    Set<Id> existing = Sets.<Account.Id> newHashSet();
-    addReviewers(change, ps, info, wantReviewers, existing);
-  }
-
-  /** Attach reviewers to a change. */
-  public void addReviewers(Change change, PatchSet ps, PatchSetInfo info,
-      Set<Account.Id> wantReviewers, Set<Account.Id> existingReviewers)
-      throws OrmException {
-    List<ApprovalType> allTypes = approvalTypes.getApprovalTypes();
+  public void addReviewers(ReviewDb db, LabelTypes labelTypes, Change change,
+      PatchSet ps, PatchSetInfo info, Set<Id> wantReviewers,
+      Set<Account.Id> existingReviewers) throws OrmException {
+    List<LabelType> allTypes = labelTypes.getLabelTypes();
     if (allTypes.isEmpty()) {
       return;
     }
@@ -138,10 +123,10 @@
     need.removeAll(existingReviewers);
 
     List<PatchSetApproval> cells = Lists.newArrayListWithCapacity(need.size());
-    ApprovalCategory.Id catId = allTypes.get(allTypes.size() - 1).getCategory().getId();
+    LabelId labelId = Iterables.getLast(allTypes).getLabelId();
     for (Account.Id account : need) {
       PatchSetApproval psa = new PatchSetApproval(
-          new PatchSetApproval.Key(ps.getId(), account, catId),
+          new PatchSetApproval.Key(ps.getId(), account, labelId),
           (short) 0);
       psa.cache(change);
       cells.add(psa);
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 535ad8e..41a97d2 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,41 +14,41 @@
 
 package com.google.gerrit.server;
 
-import com.google.common.collect.Sets;
-import com.google.gerrit.common.ChangeHookRunner;
+import com.google.common.base.CharMatcher;
 import com.google.gerrit.common.ChangeHooks;
-import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.common.errors.EmailException;
 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.change.ChangeMessages;
 import com.google.gerrit.server.config.TrackingFooter;
 import com.google.gerrit.server.config.TrackingFooters;
+import com.google.gerrit.server.events.CommitReceivedEvent;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.MergeOp;
-import com.google.gerrit.server.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.git.validators.CommitValidationException;
+import com.google.gerrit.server.git.validators.CommitValidators;
+import com.google.gerrit.server.mail.CommitMessageEditedSender;
 import com.google.gerrit.server.mail.RevertedSender;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
-import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
 import com.google.gerrit.server.project.InvalidChangeOperationException;
 import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.project.RefControl;
+import com.google.gerrit.server.util.IdGenerator;
 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;
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectInserter;
@@ -56,31 +56,27 @@
 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;
+import org.eclipse.jgit.transport.ReceiveCommand;
 import org.eclipse.jgit.util.Base64;
 import org.eclipse.jgit.util.ChangeIdUtil;
 import org.eclipse.jgit.util.NB;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
 import java.sql.Timestamp;
+import java.text.MessageFormat;
 import java.util.ArrayList;
-import java.util.Arrays;
 import java.util.Collections;
+import java.util.Date;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 import java.util.regex.Matcher;
 
 public class ChangeUtil {
-
-  private static final Logger log = LoggerFactory.getLogger(ChangeUtil.class);
-
   private static int uuidPrefix;
   private static int uuidSeq;
 
@@ -95,7 +91,12 @@
   public static String messageUUID(final ReviewDb db) throws OrmException {
     final byte[] raw = new byte[8];
     fill(raw, db);
-    return Base64.encodeBytes(raw);
+
+    // Make the resulting base64 string more URL friendly.
+    return CharMatcher.is('A').trimLeadingFrom(
+           CharMatcher.is('=').trimTrailingFrom(Base64.encodeBytes(raw)))
+        .replace('+', '.')
+        .replace('/', '-');
   }
 
   private static synchronized void fill(byte[] raw, ReviewDb db)
@@ -105,7 +106,7 @@
       uuidSeq = Integer.MAX_VALUE;
     }
     NB.encodeInt32(raw, 0, uuidPrefix);
-    NB.encodeInt32(raw, 4, uuidSeq--);
+    NB.encodeInt32(raw, 4, IdGenerator.mix(uuidPrefix, uuidSeq--));
   }
 
   public static void touch(final Change change, ReviewDb db)
@@ -176,7 +177,8 @@
     db.trackingIds().delete(toDelete);
   }
 
-  public static void testMerge(MergeOp.Factory opFactory, Change change) {
+  public static void testMerge(MergeOp.Factory opFactory, Change change)
+      throws NoSuchProjectException {
     opFactory.create(change.getDest()).verifyMergeability(change);
   }
 
@@ -193,290 +195,21 @@
     db.patchSetAncestors().insert(toInsert);
   }
 
-  /**
-   * Rebases a commit
-   *
-   * @param git Repository to find commits in
-   * @param inserter inserter to handle new trees and blobs.
-   * @param original The commit to rebase
-   * @param base Base to rebase against
-   * @return CommitBuilder the newly rebased commit
-   * @throws IOException Merged failed
-   */
-  public static CommitBuilder rebaseCommit(Repository git,
-      final ObjectInserter inserter, 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.setObjectInserter(new ObjectInserter.Filter() {
-      @Override
-      protected ObjectInserter delegate() {
-        return inserter;
-      }
-
-      @Override
-      public void flush() {
-      }
-
-      @Override
-      public void release() {
-      }
-    });
-    merger.setBase(parentCommit);
-    merger.merge(original, base);
-
-    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 GitReferenceUpdated replication, PersonIdent myIdent,
-      final ChangeControl.Factory changeControlFactory,
-      final ApprovalsUtil approvalsUtil) throws NoSuchChangeException,
-      EmailException, OrmException, MissingObjectException,
-      IncorrectObjectTypeException, IOException,
-      InvalidChangeOperationException {
-
-    final Change.Id changeId = patchSetId.getParentKey();
-    final ChangeControl changeControl =
-        changeControlFactory.validateFor(changeId);
-
-    if (!changeControl.canRebase()) {
-      throw new InvalidChangeOperationException(
-          "Cannot rebase: New patch sets are not allowed to be added to change: "
-              + changeId.toString());
-    }
-
-    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());
-        }
-        if (patchSetAncestors.size() == 1) {
-          List<PatchSet> depPatchSetList = db.patchSets()
-                  .byRevision(patchSetAncestors.get(0).getAncestorRevision())
-                  .toList();
-          if (!depPatchSetList.isEmpty()) {
-            PatchSet depPatchSet = depPatchSetList.get(0);
-
-            Change.Id depChangeId = depPatchSet.getId().getParentKey();
-            Change depChange = db.changes().get(depChangeId);
-
-            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.");
-              }
-            }
-          }
-        }
-
-        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());
-        }
-
-        final RevCommit rebasedCommit;
-        final ObjectInserter oi = git.newObjectInserter();
-        try {
-          ObjectId oldId = ObjectId.fromString(originalPatchSet.getRevision().get());
-          ObjectId newId = oi.insert(rebaseCommit(
-              git, oi, revWalk.parseCommit(oldId), branchTipCommit, myIdent));
-          oi.flush();
-          rebasedCommit = revWalk.parseCommit(newId);
-        } finally {
-          oi.release();
-        }
-
-        change.nextPatchSetId();
-        final PatchSet newPatchSet = new PatchSet(change.currPatchSetId());
-        newPatchSet.setCreatedOn(new Timestamp(System.currentTimeMillis()));
-        newPatchSet.setUploader(user.getAccountId());
-        newPatchSet.setRevision(new RevId(rebasedCommit.name()));
-        newPatchSet.setDraft(originalPatchSet.isDraft());
-
-        final PatchSetInfo info =
-            patchSetInfoFactory.get(rebasedCommit, newPatchSet.getId());
-
-        RefUpdate ru = git.updateRef(newPatchSet.getRefName());
-        ru.setExpectedOldObjectId(ObjectId.zeroId());
-        ru.setNewObjectId(rebasedCommit);
-        ru.disableRefLog();
-        if (ru.update(revWalk) != RefUpdate.Result.NEW) {
-          throw new IOException(String.format(
-              "Failed to create ref %s in %s: %s", newPatchSet.getRefName(),
-              change.getDest().getParentKey().get(), ru.getResult()));
-        }
-        replication.fire(change.getProject(), ru.getName());
-
-        final Set<Account.Id> oldReviewers = Sets.newHashSet();
-        final Set<Account.Id> oldCC = Sets.newHashSet();
-        db.changes().beginTransaction(change.getId());
-        try {
-          Change updatedChange;
-
-          updatedChange = db.changes().atomicUpdate(changeId,
-              new AtomicUpdate<Change>() {
-                @Override
-                public Change update(Change change) {
-                  if (change.getStatus().isOpen()) {
-                    change.updateNumberOfPatchSets(newPatchSet.getPatchSetId());
-                    return change;
-                  } else {
-                    return null;
-                  }
-                }
-              });
-          if (updatedChange != null) {
-            change = updatedChange;
-          } else {
-            throw new InvalidChangeOperationException(
-                String.format("Change %s is closed", change.getId()));
-          }
-
-          insertAncestors(db, newPatchSet.getId(), rebasedCommit);
-          db.patchSets().insert(Collections.singleton(newPatchSet));
-          updatedChange = db.changes().atomicUpdate(changeId,
-              new AtomicUpdate<Change>() {
-                @Override
-                public Change update(Change change) {
-                  if (change.getStatus().isClosed()) {
-                    return null;
-                  }
-                  if (!change.currentPatchSetId().equals(patchSetId)) {
-                    return null;
-                  }
-                  if (change.getStatus() != Change.Status.DRAFT) {
-                    change.setStatus(Change.Status.NEW);
-                  }
-                  change.setLastSha1MergeTested(null);
-                  change.setCurrentPatchSet(info);
-                  ChangeUtil.updated(change);
-                  return change;
-                }
-              });
-          if (updatedChange != null) {
-            change = updatedChange;
-          } else {
-            throw new InvalidChangeOperationException(
-                String.format("Change %s was modified", change.getId()));
-          }
-
-          for (PatchSetApproval a : approvalsUtil.copyVetosToLatestPatchSet(change)) {
-            if (a.getValue() != 0) {
-              oldReviewers.add(a.getAccountId());
-            } else {
-              oldCC.add(a.getAccountId());
-            }
-          }
-
-          final ChangeMessage cmsg =
-              new ChangeMessage(new ChangeMessage.Key(changeId,
-                  ChangeUtil.messageUUID(db)), user.getAccountId(), patchSetId);
-          cmsg.setMessage("Patch Set " + patchSetId.get() + ": Rebased");
-          db.changeMessages().insert(Collections.singleton(cmsg));
-          db.commit();
-        } finally {
-          db.rollback();
-        }
-
-        final ReplacePatchSetSender cm =
-            rebasedPatchSetSenderFactory.create(change);
-        cm.setFrom(user.getAccountId());
-        cm.setPatchSet(newPatchSet);
-        cm.addReviewers(oldReviewers);
-        cm.addExtraCC(oldCC);
-        cm.send();
-
-        hooks.doPatchsetCreatedHook(change, newPatchSet, db);
-      } finally {
-        revWalk.release();
-      }
-    } finally {
-      git.close();
-    }
-  }
-
-  public static Change.Id revert(final PatchSet.Id patchSetId,
-      final IdentifiedUser user, final String message, final ReviewDb db,
-      final RevertedSender.Factory revertedSenderFactory,
-      final ChangeHooks hooks, GitRepositoryManager gitManager,
-      final PatchSetInfoFactory patchSetInfoFactory,
-      final GitReferenceUpdated replication, PersonIdent myIdent)
-      throws NoSuchChangeException, EmailException, OrmException,
-      MissingObjectException, IncorrectObjectTypeException, IOException {
+  public static Change.Id revert(RefControl refControl, PatchSet.Id patchSetId,
+      IdentifiedUser user, CommitValidators commitValidators, String message,
+      ReviewDb db, RevertedSender.Factory revertedSenderFactory,
+      ChangeHooks hooks, Repository git,
+      PatchSetInfoFactory patchSetInfoFactory,
+      GitReferenceUpdated gitRefUpdated, PersonIdent myIdent,
+      String canonicalWebUrl) throws NoSuchChangeException, EmailException,
+      OrmException, MissingObjectException, IncorrectObjectTypeException,
+      IOException, InvalidChangeOperationException {
     final Change.Id changeId = patchSetId.getParentKey();
     final PatchSet patch = db.patchSets().get(patchSetId);
     if (patch == null) {
       throw new NoSuchChangeException(changeId);
     }
-
-    final Repository git;
-    try {
-      git = gitManager.openRepository(db.changes().get(changeId).getProject());
-    } catch (RepositoryNotFoundException e) {
-      throw new NoSuchChangeException(changeId, e);
-    }
+    final Change changeToRevert = db.changes().get(changeId);
 
     final RevWalk revWalk = new RevWalk(git);
     try {
@@ -495,6 +228,12 @@
       revertCommitBuilder.setAuthor(authorIdent);
       revertCommitBuilder.setCommitter(myIdent);
 
+      if (message == null) {
+        message = MessageFormat.format(
+            ChangeMessages.get().revertChangeDefaultMessage,
+            changeToRevert.getSubject(), patch.getRevision().get());
+      }
+
       final ObjectId computedChangeId =
           ChangeIdUtil.computeChangeId(parentToCommitToRevert.getTree(),
               commitToRevert, authorIdent, myIdent, message);
@@ -514,14 +253,28 @@
           new Change.Key("I" + computedChangeId.name()),
           new Change.Id(db.nextChangeId()),
           user.getAccountId(),
-          db.changes().get(changeId).getDest());
-      change.nextPatchSetId();
+          changeToRevert.getDest());
+      change.setTopic(changeToRevert.getTopic());
 
-      final PatchSet ps = new PatchSet(change.currPatchSetId());
+      PatchSet.Id id =
+          new PatchSet.Id(change.getId(), Change.INITIAL_PATCH_SET_ID);
+      final PatchSet ps = new PatchSet(id);
       ps.setCreatedOn(change.getCreatedOn());
       ps.setUploader(change.getOwner());
       ps.setRevision(new RevId(revertCommit.name()));
 
+      CommitReceivedEvent commitReceivedEvent =
+          new CommitReceivedEvent(new ReceiveCommand(ObjectId.zeroId(),
+              revertCommit.getId(), ps.getRefName()), refControl
+              .getProjectControl().getProject(), refControl.getRefName(),
+              revertCommit, user);
+
+      try {
+        commitValidators.validateForGerritCommits(commitReceivedEvent);
+      } catch (CommitValidationException e) {
+        throw new InvalidChangeOperationException(e.getMessage());
+      }
+
       change.setCurrentPatchSet(patchSetInfoFactory.get(revertCommit, ps.getId()));
       ChangeUtil.updated(change);
 
@@ -534,7 +287,7 @@
             "Failed to create ref %s in %s: %s", ps.getRefName(),
             change.getDest().getParentKey().get(), ru.getResult()));
       }
-      replication.fire(change.getProject(), ru.getName());
+      gitRefUpdated.fire(change.getProject(), ru);
 
       db.changes().beginTransaction(change.getId());
       try {
@@ -567,13 +320,165 @@
       return change.getId();
     } finally {
       revWalk.release();
-      git.close();
+    }
+  }
+
+  public static Change.Id editCommitMessage(final PatchSet.Id patchSetId,
+      final RefControl refControl, CommitValidators commitValidators,
+      final IdentifiedUser user, final String message, final ReviewDb db,
+      final CommitMessageEditedSender.Factory commitMessageEditedSenderFactory,
+      final ChangeHooks hooks, Repository git,
+      final PatchSetInfoFactory patchSetInfoFactory,
+      final GitReferenceUpdated gitRefUpdated, PersonIdent myIdent,
+      final ApprovalsUtil approvalsUtil, final TrackingFooters trackingFooters)
+      throws NoSuchChangeException, EmailException, OrmException,
+      MissingObjectException, IncorrectObjectTypeException, IOException,
+      InvalidChangeOperationException, PatchSetInfoNotAvailableException {
+    final Change.Id changeId = patchSetId.getParentKey();
+    final PatchSet originalPS = db.patchSets().get(patchSetId);
+    if (originalPS == null) {
+      throw new NoSuchChangeException(changeId);
+    }
+
+    if (message == null || message.length() == 0) {
+      throw new InvalidChangeOperationException("The commit message cannot be empty");
+    }
+
+    final RevWalk revWalk = new RevWalk(git);
+    try {
+      RevCommit commit =
+          revWalk.parseCommit(ObjectId.fromString(originalPS.getRevision().get()));
+      if (commit.getFullMessage().equals(message)) {
+        throw new InvalidChangeOperationException("New commit message cannot be same as existing commit message");
+      }
+
+      Date now = myIdent.getWhen();
+      Change change = db.changes().get(changeId);
+      PersonIdent authorIdent =
+          user.newCommitterIdent(now, myIdent.getTimeZone());
+
+      CommitBuilder commitBuilder = new CommitBuilder();
+      commitBuilder.setTreeId(commit.getTree());
+      commitBuilder.setParentIds(commit.getParents());
+      commitBuilder.setAuthor(commit.getAuthorIdent());
+      commitBuilder.setCommitter(authorIdent);
+      commitBuilder.setMessage(message);
+
+      RevCommit newCommit;
+      final ObjectInserter oi = git.newObjectInserter();
+      try {
+        ObjectId id = oi.insert(commitBuilder);
+        oi.flush();
+        newCommit = revWalk.parseCommit(id);
+      } finally {
+        oi.release();
+      }
+
+      PatchSet.Id id = nextPatchSetId(git, change.currentPatchSetId());
+      final PatchSet newPatchSet = new PatchSet(id);
+      newPatchSet.setCreatedOn(new Timestamp(now.getTime()));
+      newPatchSet.setUploader(user.getAccountId());
+      newPatchSet.setRevision(new RevId(newCommit.name()));
+      newPatchSet.setDraft(originalPS.isDraft());
+
+      final PatchSetInfo info =
+          patchSetInfoFactory.get(newCommit, newPatchSet.getId());
+
+      CommitReceivedEvent commitReceivedEvent =
+          new CommitReceivedEvent(new ReceiveCommand(ObjectId.zeroId(),
+              newCommit.getId(), newPatchSet.getRefName()), refControl
+              .getProjectControl().getProject(), refControl.getRefName(),
+              newCommit, user);
+
+      try {
+        commitValidators.validateForReceiveCommits(commitReceivedEvent);
+      } catch (CommitValidationException e) {
+        throw new InvalidChangeOperationException(e.getMessage());
+      }
+
+      final RefUpdate ru = git.updateRef(newPatchSet.getRefName());
+      ru.setExpectedOldObjectId(ObjectId.zeroId());
+      ru.setNewObjectId(newCommit);
+      ru.disableRefLog();
+      if (ru.update(revWalk) != RefUpdate.Result.NEW) {
+        throw new IOException(String.format(
+            "Failed to create ref %s in %s: %s", newPatchSet.getRefName(),
+            change.getDest().getParentKey().get(), ru.getResult()));
+      }
+      gitRefUpdated.fire(change.getProject(), ru);
+
+      db.changes().beginTransaction(change.getId());
+      try {
+        Change updatedChange = db.changes().get(change.getId());
+        if (updatedChange != null && updatedChange.getStatus().isOpen()) {
+          change = updatedChange;
+        } else {
+          throw new InvalidChangeOperationException(String.format(
+              "Change %s is closed", change.getId()));
+        }
+
+        ChangeUtil.insertAncestors(db, newPatchSet.getId(), commit);
+        db.patchSets().insert(Collections.singleton(newPatchSet));
+        updatedChange =
+            db.changes().atomicUpdate(change.getId(), new AtomicUpdate<Change>() {
+              @Override
+              public Change update(Change change) {
+                if (change.getStatus().isClosed()) {
+                  return null;
+                }
+                if (!change.currentPatchSetId().equals(patchSetId)) {
+                  return null;
+                }
+                if (change.getStatus() != Change.Status.DRAFT) {
+                  change.setStatus(Change.Status.NEW);
+                }
+                change.setLastSha1MergeTested(null);
+                change.setCurrentPatchSet(info);
+                ChangeUtil.updated(change);
+                return change;
+              }
+            });
+        if (updatedChange != null) {
+          change = updatedChange;
+        } else {
+          throw new InvalidChangeOperationException(String.format(
+              "Change %s was modified", change.getId()));
+        }
+
+        approvalsUtil.copyVetosToPatchSet(db,
+            refControl.getProjectControl().getLabelTypes(),
+            change.currentPatchSetId());
+
+        final List<FooterLine> footerLines = newCommit.getFooterLines();
+        updateTrackingIds(db, change, trackingFooters, footerLines);
+
+        final ChangeMessage cmsg =
+            new ChangeMessage(new ChangeMessage.Key(changeId,
+                ChangeUtil.messageUUID(db)), user.getAccountId(), patchSetId);
+        final String msg = "Patch Set " + newPatchSet.getPatchSetId() + ": Commit message was updated";
+        cmsg.setMessage(msg);
+        db.changeMessages().insert(Collections.singleton(cmsg));
+        db.commit();
+
+        final CommitMessageEditedSender cm = commitMessageEditedSenderFactory.create(change);
+        cm.setFrom(user.getAccountId());
+        cm.setChangeMessage(cmsg);
+        cm.send();
+      } finally {
+        db.rollback();
+      }
+
+      hooks.doPatchsetCreatedHook(change, newPatchSet, db);
+
+      return change.getId();
+    } finally {
+      revWalk.release();
     }
   }
 
   public static void deleteDraftChange(final PatchSet.Id patchSetId,
       GitRepositoryManager gitManager,
-      final GitReferenceUpdated replication, final ReviewDb db)
+      final GitReferenceUpdated gitRefUpdated, final ReviewDb db)
       throws NoSuchChangeException, OrmException, IOException {
     final Change.Id changeId = patchSetId.getParentKey();
     final Change change = db.changes().get(changeId);
@@ -583,7 +488,7 @@
 
     for (PatchSet ps : db.patchSets().byChange(changeId)) {
       // These should all be draft patch sets.
-      deleteOnlyDraftPatchSet(ps, change, gitManager, replication, db);
+      deleteOnlyDraftPatchSet(ps, change, gitManager, gitRefUpdated, db);
     }
 
     db.changeMessages().delete(db.changeMessages().byChange(changeId));
@@ -594,7 +499,7 @@
 
   public static void deleteOnlyDraftPatchSet(final PatchSet patch,
       final Change change, GitRepositoryManager gitManager,
-      final GitReferenceUpdated replication, final ReviewDb db)
+      final GitReferenceUpdated gitRefUpdated, final ReviewDb db)
       throws NoSuchChangeException, OrmException, IOException {
     final PatchSet.Id patchSetId = patch.getId();
     if (patch == null || !patch.isDraft()) {
@@ -617,7 +522,7 @@
           throw new IOException("Failed to delete ref " + patch.getRefName() +
               " in " + repo.getDirectory() + ": " + update.getResult());
       }
-      replication.fire(change.getProject(), update.getName());
+      gitRefUpdated.fire(change.getProject(), update);
     } finally {
       repo.close();
     }
@@ -631,25 +536,6 @@
     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)
-      throws OrmException {
-    db.changeMessages().insert(Collections.singleton(cmsg));
-
-    new ApprovalsUtil(db, null).syncChangeStatus(change);
-
-    // Email the reviewers
-    try {
-      final ReplyToChangeSender cm = senderFactory.create(change);
-      cm.setFrom(user.getAccountId());
-      cm.setChangeMessage(cmsg);
-      cm.send();
-    } catch (Exception e) {
-      log.error("Cannot email update for change " + change.getChangeId(), e);
-    }
-  }
-
   public static String sortKey(long lastUpdated, int id){
     // The encoding uses minutes since Wed Oct 1 00:00:00 2008 UTC.
     // We overrun approximately 4,085 years later, so ~6093.
@@ -668,6 +554,23 @@
     c.setSortKey(sortKey(lastUpdated, id));
   }
 
+  public static PatchSet.Id nextPatchSetId(Map<String, Ref> allRefs,
+      PatchSet.Id id) {
+    PatchSet.Id next = nextPatchSetId(id);
+    while (allRefs.containsKey(next.toRefName())) {
+      next = nextPatchSetId(next);
+    }
+    return next;
+  }
+
+  public static PatchSet.Id nextPatchSetId(Repository git, PatchSet.Id id) {
+    return nextPatchSetId(git.getAllRefs(), id);
+  }
+
+  private static PatchSet.Id nextPatchSetId(PatchSet.Id id) {
+    return new PatchSet.Id(id.getParentKey(), id.get() + 1);
+  }
+
   private static final char[] hexchar =
       {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', //
           'a', 'b', 'c', 'd', 'e', 'f'};
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 f057c3a..86a6ef8 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
@@ -33,15 +33,12 @@
  */
 public abstract class CurrentUser {
   private final CapabilityControl.Factory capabilityControlFactory;
-  private final AccessPath accessPath;
+  private AccessPath accessPath = AccessPath.UNKNOWN;
 
   private CapabilityControl capabilities;
 
-  protected CurrentUser(
-      CapabilityControl.Factory capabilityControlFactory,
-      AccessPath accessPath) {
+  protected CurrentUser(CapabilityControl.Factory capabilityControlFactory) {
     this.capabilityControlFactory = capabilityControlFactory;
-    this.accessPath = accessPath;
   }
 
   /** How this user is accessing the Gerrit Code Review application. */
@@ -49,6 +46,10 @@
     return accessPath;
   }
 
+  public void setAccessPath(AccessPath path) {
+    accessPath = path;
+  }
+
   /**
    * Get the set of groups the user is currently a member of.
    * <p>
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 89cbac1..3826293 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
@@ -38,6 +38,7 @@
 import com.google.inject.OutOfScopeException;
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
+import com.google.inject.util.Providers;
 
 import org.eclipse.jgit.lib.PersonIdent;
 import org.eclipse.jgit.util.SystemReader;
@@ -90,20 +91,19 @@
     }
 
     public IdentifiedUser create(final Account.Id id) {
-      return create(AccessPath.UNKNOWN, null, id);
+      return create((SocketAddress) null, id);
     }
 
     public IdentifiedUser create(Provider<ReviewDb> db, Account.Id id) {
-      return new IdentifiedUser(capabilityControlFactory, AccessPath.UNKNOWN,
+      return new IdentifiedUser(capabilityControlFactory,
           authConfig, anonymousCowardName, canonicalUrl, realm, accountCache,
           groupBackend, null, db, id);
     }
 
-    public IdentifiedUser create(AccessPath accessPath,
-        Provider<SocketAddress> remotePeerProvider, Account.Id id) {
-      return new IdentifiedUser(capabilityControlFactory, accessPath,
+    public IdentifiedUser create(SocketAddress remotePeer, Account.Id id) {
+      return new IdentifiedUser(capabilityControlFactory,
           authConfig, anonymousCowardName, canonicalUrl, realm, accountCache,
-          groupBackend, remotePeerProvider, null, id);
+          groupBackend, Providers.of(remotePeer), null, id);
     }
   }
 
@@ -149,9 +149,8 @@
       this.dbProvider = dbProvider;
     }
 
-    public IdentifiedUser create(final AccessPath accessPath,
-        final Account.Id id) {
-      return new IdentifiedUser(capabilityControlFactory, accessPath,
+    public IdentifiedUser create(Account.Id id) {
+      return new IdentifiedUser(capabilityControlFactory,
           authConfig, anonymousCowardName, canonicalUrl, realm, accountCache,
           groupBackend, remotePeerProvider, dbProvider, id);
     }
@@ -187,7 +186,6 @@
 
   private IdentifiedUser(
       CapabilityControl.Factory capabilityControlFactory,
-      final AccessPath accessPath,
       final AuthConfig authConfig,
       final String anonymousCowardName,
       final Provider<String> canonicalUrl,
@@ -195,7 +193,7 @@
       final GroupBackend groupBackend,
       @Nullable final Provider<SocketAddress> remotePeerProvider,
       @Nullable final Provider<ReviewDb> dbProvider, final Account.Id id) {
-    super(capabilityControlFactory, accessPath);
+    super(capabilityControlFactory);
     this.canonicalUrl = canonicalUrl;
     this.accountCache = accountCache;
     this.groupBackend = groupBackend;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/InternalUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/InternalUser.java
index eba59c8..6f5618b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/InternalUser.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/InternalUser.java
@@ -31,6 +31,8 @@
  * anything it wants, anytime it wants, given the JVM's own direct access to
  * data. Plugins may use this when they need to have a CurrentUser with read
  * permission on anything.
+ *
+ * @see PluginUser
  */
 public class InternalUser extends CurrentUser {
   public interface Factory {
@@ -39,7 +41,7 @@
 
   @Inject
   protected InternalUser(CapabilityControl.Factory capabilityControlFactory) {
-    super(capabilityControlFactory, AccessPath.UNKNOWN);
+    super(capabilityControlFactory);
   }
 
   @Override
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 75ba0d7..4d26f02 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
@@ -40,7 +40,7 @@
   @Inject
   protected PeerDaemonUser(CapabilityControl.Factory capabilityControlFactory,
       @Assisted SocketAddress peer) {
-    super(capabilityControlFactory, AccessPath.SSH_COMMAND);
+    super(capabilityControlFactory);
     this.peer = peer;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/PluginUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/PluginUser.java
new file mode 100644
index 0000000..490ab07
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/PluginUser.java
@@ -0,0 +1,46 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.server.account.CapabilityControl;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+/** User identity for plugin code that needs an identity. */
+public class PluginUser extends InternalUser {
+  public interface Factory {
+    PluginUser create(String pluginName);
+  }
+
+  private final String pluginName;
+
+  @Inject
+  protected PluginUser(
+      CapabilityControl.Factory capabilityControlFactory,
+      @Assisted String pluginName) {
+    super(capabilityControlFactory);
+    this.pluginName = pluginName;
+  }
+
+  @Override
+  public String getUserName() {
+    return "plugin " + pluginName;
+  }
+
+  @Override
+  public String toString() {
+    return "PluginUser[" + pluginName + "]";
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/UrlEncoded.java b/gerrit-server/src/main/java/com/google/gerrit/server/UrlEncoded.java
index f1f6dcd..3ac6c98 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/UrlEncoded.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/UrlEncoded.java
@@ -15,8 +15,8 @@
 
 package com.google.gerrit.server;
 
-import java.io.UnsupportedEncodingException;
-import java.net.URLEncoder;
+import com.google.gerrit.extensions.restapi.Url;
+
 import java.util.LinkedHashMap;
 import java.util.Map;
 
@@ -46,21 +46,13 @@
       if (separator != 0) {
         buffer.append(separator);
       }
-      buffer.append(encode(key));
+      buffer.append(Url.encode(key));
       buffer.append('=');
       if (val != null) {
-        buffer.append(encode(val));
+        buffer.append(Url.encode(val));
       }
       separator = '&';
     }
     return buffer.toString();
   }
-
-  private static String encode(final String str) {
-    try {
-      return URLEncoder.encode(str, "UTF-8");
-    } catch (UnsupportedEncodingException e) {
-      throw new RuntimeException(e);
-    }
-  }
 }
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 056babd..86308a9 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
@@ -20,6 +20,8 @@
 public interface AccountCache {
   public AccountState get(Account.Id accountId);
 
+  public AccountState getIfPresent(Account.Id accountId);
+
   public AccountState getByUsername(String username);
 
   public void evict(Account.Id accountId);
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 4217f9f..b74daa5 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
@@ -87,6 +87,10 @@
     }
   }
 
+  public AccountState getIfPresent(Account.Id accountId) {
+    return byId.getIfPresent(accountId);
+  }
+
   @Override
   public AccountState getByUsername(String username) {
     try {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountControl.java
index 32b4e2c..f148b31 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountControl.java
@@ -139,7 +139,7 @@
       default:
         throw new IllegalStateException("Bad AccountVisibility " + accountVisibility);
     }
-    return false;
+    return currentUser.getCapabilities().canAdministrateServer();
   }
 
   private Set<AccountGroup.UUID> groupsOf(Account.Id account) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountInfo.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountInfo.java
new file mode 100644
index 0000000..a296716
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountInfo.java
@@ -0,0 +1,123 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.ArrayListMultimap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
+import com.google.gerrit.reviewdb.client.Account;
+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.assistedinject.Assisted;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+public class AccountInfo {
+  public static class Loader {
+    public interface Factory {
+      Loader create(boolean detailed);
+    }
+
+    private final Provider<ReviewDb> db;
+    private final AccountCache accountCache;
+    private final boolean detailed;
+    private final Map<Account.Id, AccountInfo> created;
+    private final List<AccountInfo> provided;
+
+    @Inject
+    Loader(Provider<ReviewDb> db,
+        AccountCache accountCache,
+        @Assisted boolean detailed) {
+      this.db = db;
+      this.accountCache = accountCache;
+      this.detailed = detailed;
+      created = Maps.newHashMap();
+      provided = Lists.newArrayList();
+    }
+
+    public AccountInfo get(Account.Id id) {
+      if (id == null) {
+        return null;
+      }
+      AccountInfo info = created.get(id);
+      if (info == null) {
+        info = new AccountInfo(id);
+        created.put(id, info);
+      }
+      return info;
+    }
+
+    public void put(AccountInfo info) {
+      provided.add(info);
+    }
+
+    public void fill() throws OrmException {
+      Multimap<Account.Id, AccountInfo> missing = ArrayListMultimap.create();
+      for (AccountInfo info : Iterables.concat(created.values(), provided)) {
+        AccountState state = accountCache.getIfPresent(info._id);
+        if (state != null) {
+          info.fill(state.getAccount(), detailed);
+        } else {
+          missing.put(info._id, info);
+        }
+      }
+      if (!missing.isEmpty()) {
+        for (Account account : db.get().accounts().get(missing.keySet())) {
+          for (AccountInfo info : missing.get(account.getId())) {
+            info.fill(account, detailed);
+          }
+        }
+      }
+    }
+
+    public void fill(Collection<? extends AccountInfo> infos)
+        throws OrmException {
+      for (AccountInfo info : infos) {
+        put(info);
+      }
+      fill();
+    }
+  }
+
+  public static AccountInfo parse(Account a, boolean detailed) {
+    AccountInfo ai = new AccountInfo(a.getId());
+    ai.fill(a, detailed);
+    return ai;
+  }
+
+  public transient Account.Id _id;
+
+  public AccountInfo(Account.Id id) {
+    _id = id;
+  }
+
+  public Integer _account_id;
+  public String name;
+  public String email;
+
+  private void fill(Account account, boolean detailed) {
+    name = account.getFullName();
+    if (detailed) {
+      _account_id = account.getId().get();
+      email = account.getPreferredEmail();
+    }
+  }
+}
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 abdf29e..67d87b3 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
@@ -60,7 +60,7 @@
   }
 
   /**
-   * Locate exactly one account matching the name or name/email string.
+   * Find all accounts matching the name or name/email string.
    *
    * @param nameOrEmail a string of the format
    *        "Full Name &lt;email@example&gt;", just the email address
@@ -71,11 +71,21 @@
   public Set<Account.Id> findAll(String nameOrEmail) throws OrmException {
     Matcher m = Pattern.compile("^.* \\(([1-9][0-9]*)\\)$").matcher(nameOrEmail);
     if (m.matches()) {
-      return Collections.singleton(Account.Id.parse(m.group(1)));
+      Account.Id id = Account.Id.parse(m.group(1));
+      if (exists(id)) {
+        return Collections.singleton(id);
+      } else {
+        return Collections.emptySet();
+      }
     }
 
     if (nameOrEmail.matches("^[1-9][0-9]*$")) {
-      return Collections.singleton(Account.Id.parse(nameOrEmail));
+      Account.Id id = Account.Id.parse(nameOrEmail);
+      if (exists(id)) {
+        return Collections.singleton(id);
+      } else {
+        return Collections.emptySet();
+      }
     }
 
     if (nameOrEmail.matches(Account.USER_NAME_PATTERN)) {
@@ -88,6 +98,10 @@
     return findAllByNameOrEmail(nameOrEmail);
   }
 
+  private boolean exists(Account.Id id) throws OrmException {
+    return schema.get().accounts().get(id) != null;
+  }
+
   /**
    * Locate exactly one account matching the name or name/email string.
    *
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResource.java
new file mode 100644
index 0000000..9dc423a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountResource.java
@@ -0,0 +1,60 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import com.google.gerrit.extensions.restapi.RestResource;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.inject.TypeLiteral;
+
+public class AccountResource implements RestResource {
+  public static final TypeLiteral<RestView<AccountResource>> ACCOUNT_KIND =
+      new TypeLiteral<RestView<AccountResource>>() {};
+
+  public static final TypeLiteral<RestView<Capability>> CAPABILITY_KIND =
+      new TypeLiteral<RestView<Capability>>() {};
+
+  private final IdentifiedUser user;
+
+  public AccountResource(IdentifiedUser user) {
+    this.user = user;
+  }
+
+  public IdentifiedUser getUser() {
+    return user;
+  }
+
+  static class Capability implements RestResource {
+    private final IdentifiedUser user;
+    private final String capability;
+
+    Capability(IdentifiedUser user, String capability) {
+      this.user = user;
+      this.capability = capability;
+    }
+
+    public IdentifiedUser getUser() {
+      return user;
+    }
+
+    public String getCapability() {
+      return capability;
+    }
+
+    public boolean has() {
+      return user.getCapabilities().canPerform(getCapability());
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.java
new file mode 100644
index 0000000..674046c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/AccountsCollection.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.server.account;
+
+import com.google.common.collect.Iterables;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestCollection;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.AnonymousUser;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.util.Set;
+
+public class AccountsCollection implements
+    RestCollection<TopLevelResource, AccountResource> {
+  private final Provider<CurrentUser> self;
+  private final AccountResolver resolver;
+  private final AccountControl.Factory accountControlFactory;
+  private final IdentifiedUser.GenericFactory userFactory;
+  private final DynamicMap<RestView<AccountResource>> views;
+
+  @Inject
+  AccountsCollection(Provider<CurrentUser> self,
+      AccountResolver resolver,
+      AccountControl.Factory accountControlFactory,
+      IdentifiedUser.GenericFactory userFactory,
+      DynamicMap<RestView<AccountResource>> views) {
+    this.self = self;
+    this.resolver = resolver;
+    this.accountControlFactory = accountControlFactory;
+    this.userFactory = userFactory;
+    this.views = views;
+  }
+
+  @Override
+  public AccountResource parse(TopLevelResource root, IdString id)
+      throws ResourceNotFoundException, AuthException, OrmException {
+    IdentifiedUser user = _parse(id.get());
+    if (user == null) {
+      throw new ResourceNotFoundException(id);
+    } else if (!accountControlFactory.get().canSee(user.getAccount())) {
+      throw new ResourceNotFoundException(id);
+    }
+    return new AccountResource(user);
+  }
+
+  /**
+   * Parses a account ID from a request body and returns the user.
+   *
+   * @param id ID of the account, can be a string of the format
+   *        "Full Name <email@example.com>", just the email address, a full name
+   *        if it is unique, an account ID, a user name or 'self' for the
+   *        calling user
+   * @return the project
+   * @throws UnprocessableEntityException thrown if the account ID cannot be
+   *         resolved or if the account is not visible to the calling user
+   */
+  public IdentifiedUser parse(String id) throws AuthException,
+      UnprocessableEntityException, OrmException {
+    IdentifiedUser user = _parse(id);
+    if (user == null) {
+      throw new UnprocessableEntityException(String.format(
+          "Account Not Found: %s", id));
+    }
+    return user;
+  }
+
+  private IdentifiedUser _parse(String id) throws AuthException, OrmException {
+    CurrentUser user = self.get();
+
+    if (id.equals("self")) {
+      if (user instanceof IdentifiedUser) {
+        return (IdentifiedUser) user;
+      } else if (user instanceof AnonymousUser) {
+        throw new AuthException("Authentication required");
+      } else {
+        return null;
+      }
+    }
+
+    Set<Account.Id> matches = resolver.findAll(id);
+    if (matches.size() != 1) {
+      return null;
+    }
+    return userFactory.create(Iterables.getOnlyElement(matches));
+  }
+
+  @Override
+  public RestView<TopLevelResource> list() throws ResourceNotFoundException {
+    throw new ResourceNotFoundException();
+  }
+
+  @Override
+  public DynamicMap<RestView<AccountResource>> views() {
+    return views;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthMethod.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthMethod.java
deleted file mode 100644
index fdaabd2..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/AuthMethod.java
+++ /dev/null
@@ -1,30 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.account;
-
-/** Method by which a user has authenticated for a given request. */
-public enum AuthMethod {
-  /** The user is not authenticated */
-  NONE,
-
-  /** The user is authenticated via a cookie. */
-  COOKIE,
-
-  /** The user authenticated with a password for this request. */
-  PASSWORD,
-
-  /** The user has used a credentialess development feature to login. */
-  BACKDOOR;
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/Capabilities.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/Capabilities.java
new file mode 100644
index 0000000..38e5013
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/Capabilities.java
@@ -0,0 +1,71 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ChildCollection;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.account.AccountResource.Capability;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+class Capabilities implements
+    ChildCollection<AccountResource, AccountResource.Capability> {
+  private final Provider<CurrentUser> self;
+  private final DynamicMap<RestView<AccountResource.Capability>> views;
+  private final Provider<GetCapabilities> get;
+
+  @Inject
+  Capabilities(
+      Provider<CurrentUser> self,
+      DynamicMap<RestView<AccountResource.Capability>> views,
+      Provider<GetCapabilities> get) {
+    this.self = self;
+    this.views = views;
+    this.get = get;
+  }
+
+  @Override
+  public GetCapabilities list() throws ResourceNotFoundException {
+    return get.get();
+  }
+
+  @Override
+  public Capability parse(AccountResource parent, IdString id)
+      throws ResourceNotFoundException, AuthException {
+    if (self.get() != parent.getUser()
+        && !self.get().getCapabilities().canAdministrateServer()) {
+      throw new AuthException("restricted to administrator");
+    }
+
+    String name = id.get();
+    CapabilityControl cap = parent.getUser().getCapabilities();
+    if (cap.canPerform(name)
+        || (cap.canAdministrateServer() && GlobalCapability.isCapability(name))) {
+      return new AccountResource.Capability(parent.getUser(), name);
+    }
+    throw new ResourceNotFoundException(id);
+  }
+
+  @Override
+  public DynamicMap<RestView<Capability>> views() {
+    return views;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
index 1524185..942b0d7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/CapabilityControl.java
@@ -130,12 +130,24 @@
       || canAdministrateServer();
   }
 
+
+  /** @return true if the user can access the database (with gsql). */
+  public boolean canAccessDatabase() {
+    return canPerform(GlobalCapability.ACCESS_DATABASE);
+  }
+
   /** @return true if the user can force replication to any configured destination. */
   public boolean canStartReplication() {
     return canPerform(GlobalCapability.START_REPLICATION)
         || canAdministrateServer();
   }
 
+  /** @return true if the user can run the Git garbage collection. */
+  public boolean canRunGC() {
+    return canPerform(GlobalCapability.RUN_GC)
+        || 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)
@@ -159,6 +171,11 @@
           case BATCH:
             batch = true;
             break;
+
+          case ALLOW:
+          case BLOCK:
+          case DENY:
+            break;
         }
       }
     }
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 d6f45f5..1b73c54 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
@@ -123,7 +123,7 @@
 
         // Otherwise, someone else has this identity.
         //
-        throw new NameAlreadyUsedException();
+        throw new NameAlreadyUsedException(newUsername);
       }
     }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetAccount.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetAccount.java
new file mode 100644
index 0000000..b022420
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetAccount.java
@@ -0,0 +1,24 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.extensions.restapi.RestReadView;
+
+public class GetAccount implements RestReadView<AccountResource> {
+  @Override
+  public AccountInfo apply(AccountResource rsrc) {
+    return AccountInfo.parse(rsrc.getUser().getAccount(), true);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetAvatar.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetAvatar.java
new file mode 100644
index 0000000..1c66555
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetAvatar.java
@@ -0,0 +1,54 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.Strings;
+import com.google.gerrit.extensions.registration.DynamicItem;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.avatar.AvatarProvider;
+import com.google.inject.Inject;
+
+import org.kohsuke.args4j.Option;
+
+class GetAvatar implements RestReadView<AccountResource> {
+  private final DynamicItem<AvatarProvider> avatarProvider;
+
+  @Option(name = "--size", aliases = {"-s"},
+      usage = "recommended size in pixels, height and width")
+  private int size;
+
+  @Inject
+  GetAvatar(DynamicItem<AvatarProvider> avatarProvider) {
+    this.avatarProvider = avatarProvider;
+  }
+
+  @Override
+  public Response.Redirect apply(AccountResource rsrc)
+      throws ResourceNotFoundException {
+    AvatarProvider impl = avatarProvider.get();
+    if (impl == null) {
+      throw new ResourceNotFoundException();
+    }
+
+    String url = impl.getUrl(rsrc.getUser(), size);
+    if (Strings.isNullOrEmpty(url)) {
+      throw new ResourceNotFoundException();
+    } else {
+      return Response.redirect(url);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java
new file mode 100644
index 0000000..81221aa
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetCapabilities.java
@@ -0,0 +1,180 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import static com.google.gerrit.common.data.GlobalCapability.ACCESS_DATABASE;
+import static com.google.gerrit.common.data.GlobalCapability.CREATE_ACCOUNT;
+import static com.google.gerrit.common.data.GlobalCapability.CREATE_GROUP;
+import static com.google.gerrit.common.data.GlobalCapability.CREATE_PROJECT;
+import static com.google.gerrit.common.data.GlobalCapability.EMAIL_REVIEWERS;
+import static com.google.gerrit.common.data.GlobalCapability.FLUSH_CACHES;
+import static com.google.gerrit.common.data.GlobalCapability.KILL_TASK;
+import static com.google.gerrit.common.data.GlobalCapability.PRIORITY;
+import static com.google.gerrit.common.data.GlobalCapability.RUN_GC;
+import static com.google.gerrit.common.data.GlobalCapability.START_REPLICATION;
+import static com.google.gerrit.common.data.GlobalCapability.VIEW_CACHES;
+import static com.google.gerrit.common.data.GlobalCapability.VIEW_CONNECTIONS;
+import static com.google.gerrit.common.data.GlobalCapability.VIEW_QUEUE;
+
+import com.google.common.base.Function;
+import com.google.common.base.Splitter;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.common.data.PermissionRange;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.BinaryResult;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.OutputFormat;
+import com.google.gerrit.server.account.AccountResource.Capability;
+import com.google.gerrit.server.git.QueueProvider;
+import com.google.gson.reflect.TypeToken;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.kohsuke.args4j.Option;
+
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Set;
+
+class GetCapabilities implements RestReadView<AccountResource> {
+  @Deprecated
+  @Option(name = "--format", usage = "(deprecated) output format")
+  private OutputFormat format;
+
+  @Option(name = "-q", metaVar = "CAP", multiValued = true, usage = "Capability to inspect")
+  void addQuery(String name) {
+    if (query == null) {
+      query = Sets.newHashSet();
+    }
+    Iterables.addAll(query, Iterables.transform(
+        Splitter.onPattern("[, ]").omitEmptyStrings().trimResults().split(name),
+        new Function<String, String>() {
+          @Override
+          public String apply(String input) {
+            return input.toLowerCase();
+          }
+        }));
+  }
+  private Set<String> query;
+
+  private final Provider<CurrentUser> self;
+
+  @Inject
+  GetCapabilities(Provider<CurrentUser> self) {
+    this.self = self;
+  }
+
+  @Override
+  public Object apply(AccountResource resource)
+      throws BadRequestException, Exception {
+    if (self.get() != resource.getUser()
+        && !self.get().getCapabilities().canAdministrateServer()) {
+      throw new AuthException("restricted to administrator");
+    }
+
+    CapabilityControl cc = resource.getUser().getCapabilities();
+    Map<String, Object> have = Maps.newLinkedHashMap();
+    for (String name : GlobalCapability.getAllNames()) {
+      if (!name.equals(PRIORITY) && want(name) && cc.canPerform(name)) {
+        if (GlobalCapability.hasRange(name)) {
+          have.put(name, new Range(cc.getRange(name)));
+        } else {
+          have.put(name, true);
+        }
+      }
+    }
+
+    have.put(CREATE_ACCOUNT, cc.canCreateAccount());
+    have.put(CREATE_GROUP, cc.canCreateGroup());
+    have.put(CREATE_PROJECT, cc.canCreateProject());
+    have.put(EMAIL_REVIEWERS, cc.canEmailReviewers());
+    have.put(KILL_TASK, cc.canKillTask());
+    have.put(VIEW_CACHES, cc.canViewCaches());
+    have.put(FLUSH_CACHES, cc.canFlushCaches());
+    have.put(VIEW_CONNECTIONS, cc.canViewConnections());
+    have.put(VIEW_QUEUE, cc.canViewQueue());
+    have.put(RUN_GC, cc.canRunGC());
+    have.put(START_REPLICATION, cc.canStartReplication());
+    have.put(ACCESS_DATABASE, cc.canAccessDatabase());
+
+    QueueProvider.QueueType queue = cc.getQueueType();
+    if (queue != QueueProvider.QueueType.INTERACTIVE
+        || (query != null && query.contains(PRIORITY))) {
+      have.put(PRIORITY, queue);
+    }
+
+    Iterator<Map.Entry<String, Object>> itr = have.entrySet().iterator();
+    while (itr.hasNext()) {
+      Map.Entry<String, Object> e = itr.next();
+      if (!want(e.getKey())) {
+        itr.remove();
+      } else if (e.getValue() instanceof Boolean && !((Boolean) e.getValue())) {
+        itr.remove();
+      }
+    }
+
+    if (format == OutputFormat.TEXT) {
+      StringBuilder sb = new StringBuilder();
+      for (Map.Entry<String, Object> e : have.entrySet()) {
+        sb.append(e.getKey());
+        if (!(e.getValue() instanceof Boolean)) {
+          sb.append(": ");
+          sb.append(e.getValue().toString());
+        }
+        sb.append('\n');
+      }
+      return BinaryResult.create(sb.toString());
+    } else {
+      return OutputFormat.JSON.newGson().toJsonTree(
+        have,
+        new TypeToken<Map<String, Object>>() {}.getType());
+    }
+  }
+
+  private boolean want(String name) {
+    return query == null || query.contains(name.toLowerCase());
+  }
+
+  private static class Range {
+    private transient PermissionRange range;
+    @SuppressWarnings("unused")
+    private int min;
+    @SuppressWarnings("unused")
+    private int max;
+
+    Range(PermissionRange r) {
+      range = r;
+      min = r.getMin();
+      max = r.getMax();
+    }
+
+    @Override
+    public String toString() {
+      return range.toString();
+    }
+  }
+
+  static class CheckOne implements RestReadView<AccountResource.Capability> {
+    @Override
+    public Object apply(Capability resource) {
+      return BinaryResult.create("ok\n");
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GetGroups.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetGroups.java
new file mode 100644
index 0000000..97e4e70
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GetGroups.java
@@ -0,0 +1,58 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+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.group.GroupJson;
+import com.google.gerrit.server.group.GroupJson.GroupInfo;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+
+import java.util.List;
+
+public class GetGroups implements RestReadView<AccountResource> {
+  private final GroupControl.Factory groupControlFactory;
+  private final GroupJson json;
+
+  @Inject
+  GetGroups(GroupControl.Factory groupControlFactory, GroupJson json) {
+    this.groupControlFactory = groupControlFactory;
+    this.json = json;
+  }
+
+  @Override
+  public List<GroupInfo> apply(AccountResource resource) throws OrmException {
+    IdentifiedUser user = resource.getUser();
+    Account.Id userId = user.getAccountId();
+    List<GroupInfo> groups = Lists.newArrayList();
+    for (AccountGroup.UUID uuid : user.getEffectiveGroups().getKnownGroups()) {
+      GroupControl ctl;
+      try {
+        ctl = groupControlFactory.controlFor(uuid);
+      } catch (NoSuchGroupException e) {
+        continue;
+      }
+      if (ctl.isVisible() && ctl.canSeeMember(userId)) {
+        groups.add(json.format(ctl.getGroup()));
+      }
+    }
+    return groups;
+  }
+}
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 d9b12ac..2a8e7c9 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
@@ -21,11 +21,34 @@
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.InternalUser;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
+import com.google.inject.Singleton;
 
 /** Access control management for a group of accounts managed in Gerrit. */
 public class GroupControl {
+
+  @Singleton
+  public static class GenericFactory {
+    private final GroupBackend groupBackend;
+
+    @Inject
+    GenericFactory(final GroupBackend gb) {
+      groupBackend = gb;
+    }
+
+    public GroupControl controlFor(final CurrentUser who,
+        final AccountGroup.UUID groupId)
+        throws NoSuchGroupException {
+      final GroupDescription.Basic group = groupBackend.get(groupId);
+      if (group == null) {
+        throw new NoSuchGroupException(groupId);
+      }
+      return new GroupControl(who, group);
+    }
+  }
+
   public static class Factory {
     private final GroupCache groupCache;
     private final Provider<CurrentUser> user;
@@ -45,7 +68,7 @@
       if (group == null) {
         throw new NoSuchGroupException(groupId);
       }
-      return new GroupControl(user.get(), group);
+      return controlFor(GroupDescriptions.forAccountGroup(group));
     }
 
     public GroupControl controlFor(final AccountGroup.UUID groupId)
@@ -54,10 +77,14 @@
       if (group == null) {
         throw new NoSuchGroupException(groupId);
       }
-      return new GroupControl(user.get(), group);
+      return controlFor(group);
     }
 
-    public GroupControl controlFor(final AccountGroup group) {
+    public GroupControl controlFor(AccountGroup group) {
+      return controlFor(GroupDescriptions.forAccountGroup(group));
+    }
+
+    public GroupControl controlFor(GroupDescription.Basic group) {
       return new GroupControl(user.get(), group);
     }
 
@@ -69,6 +96,15 @@
       }
       return c;
     }
+
+    public GroupControl validateFor(final AccountGroup.UUID groupUUID)
+        throws NoSuchGroupException {
+      final GroupControl c = controlFor(groupUUID);
+      if (!c.isVisible()) {
+        throw new NoSuchGroupException(groupUUID);
+      }
+      return c;
+    }
   }
 
   private final CurrentUser user;
@@ -80,8 +116,8 @@
     group =  gd;
   }
 
-  GroupControl(CurrentUser who, AccountGroup ag) {
-    this(who, GroupDescriptions.forAccountGroup(ag));
+  public GroupDescription.Basic getGroup() {
+    return group;
   }
 
   public CurrentUser getCurrentUser() {
@@ -90,7 +126,9 @@
 
   /** Can this user see this group exists? */
   public boolean isVisible() {
-    return group.isVisibleToAll()
+    AccountGroup accountGroup = GroupDescriptions.toAccountGroup(group);
+    return (accountGroup != null && accountGroup.isVisibleToAll())
+      || user instanceof InternalUser
       || user.getEffectiveGroups().contains(group.getGroupUUID())
       || isOwner();
   }
@@ -123,19 +161,21 @@
     return canSeeMembers();
   }
 
-  public boolean canAddGroup(AccountGroup.Id id) {
+  public boolean canAddGroup(AccountGroup.UUID uuid) {
     return isOwner();
   }
 
-  public boolean canRemoveGroup(AccountGroup.Id id) {
+  public boolean canRemoveGroup(AccountGroup.UUID uuid) {
     return isOwner();
   }
 
-  public boolean canSeeGroup(AccountGroup.Id id) {
+  public boolean canSeeGroup(AccountGroup.UUID uuid) {
     return canSeeMembers();
   }
 
   private boolean canSeeMembers() {
-    return group.isVisibleToAll() || isOwner();
+    AccountGroup accountGroup = GroupDescriptions.toAccountGroup(group);
+    return (accountGroup != null && accountGroup.isVisibleToAll())
+        || isOwner();
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupDetailFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupDetailFactory.java
index 2e500bd..cade9c7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupDetailFactory.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupDetailFactory.java
@@ -20,7 +20,7 @@
 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.AccountGroupIncludeByUuid;
 import com.google.gerrit.reviewdb.client.AccountGroupMember;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gwtorm.server.OrmException;
@@ -80,10 +80,11 @@
         detail.setMembers(loadMembers());
         detail.setIncludes(loadIncludes());
         break;
+      case SYSTEM:
+        break;
     }
     detail.setAccounts(aic.create());
     detail.setCanModify(control.isOwner());
-    detail.setGroups(gic.create());
     return detail;
   }
 
@@ -121,36 +122,34 @@
     return members;
   }
 
-  private List<AccountGroupInclude> loadIncludes() throws OrmException {
-    List<AccountGroupInclude> groups = new ArrayList<AccountGroupInclude>();
+  private List<AccountGroupIncludeByUuid> loadIncludes() throws OrmException {
+    List<AccountGroupIncludeByUuid> groups = new ArrayList<AccountGroupIncludeByUuid>();
 
-    for (final AccountGroupInclude m : db.accountGroupIncludes().byGroup(groupId)) {
-      if (control.canSeeGroup(m.getIncludeId())) {
-        gic.want(m.getIncludeId());
+    for (final AccountGroupIncludeByUuid m : db.accountGroupIncludesByUuid().byGroup(groupId)) {
+      if (control.canSeeGroup(m.getIncludeUUID())) {
+        gic.want(m.getIncludeUUID());
         groups.add(m);
       }
     }
 
-    Collections.sort(groups, new Comparator<AccountGroupInclude>() {
-      public int compare(final AccountGroupInclude o1,
-          final AccountGroupInclude o2) {
-        final AccountGroup a = gic.get(o1.getIncludeId());
-        final AccountGroup b = gic.get(o2.getIncludeId());
+    Collections.sort(groups, new Comparator<AccountGroupIncludeByUuid>() {
+      public int compare(final AccountGroupIncludeByUuid o1,
+          final AccountGroupIncludeByUuid o2) {
+        GroupDescription.Basic a = gic.get(o1.getIncludeUUID());
+        GroupDescription.Basic b = gic.get(o2.getIncludeUUID());
         return n(a).compareTo(n(b));
       }
 
-      private String n(final AccountGroup a) {
+      private String n (GroupDescription.Basic a) {
+        if (a == null) {
+          return "";
+        }
+
         String n = a.getName();
         if (n != null && n.length() > 0) {
           return n;
         }
-
-        n = a.getDescription();
-        if (n != null && n.length() > 0) {
-          return n;
-        }
-
-        return a.getId().toString();
+        return a.getGroupUUID().get();
       }
     });
 
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 432a8b8..d130243 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
@@ -16,12 +16,19 @@
 
 import com.google.gerrit.reviewdb.client.AccountGroup;
 
-import java.util.Collection;
+import java.util.Set;
 
 /** Tracks group inclusions in memory for efficient access. */
 public interface GroupIncludeCache {
-  public Collection<AccountGroup.UUID> getByInclude(AccountGroup.UUID groupId);
+  /** @return groups directly a member of the passed group. */
+  public Set<AccountGroup.UUID> membersOf(AccountGroup.UUID group);
 
-  public void evictInclude(AccountGroup.UUID groupId);
+  /** @return any groups the passed group belongs to. */
+  public Set<AccountGroup.UUID> memberIn(AccountGroup.UUID groupId);
+
+  /** @return set of any UUIDs that are not internal groups. */
+  public Set<AccountGroup.UUID> allExternalMembers();
+
+  public void evictMembersOf(AccountGroup.UUID groupId);
+  public void evictMemberIn(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 7fbba45..bbcdfaf 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
@@ -19,7 +19,7 @@
 import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Sets;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupInclude;
+import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuid;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gwtorm.server.SchemaFactory;
@@ -32,7 +32,6 @@
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
-import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
 import java.util.Set;
@@ -44,6 +43,8 @@
   private static final Logger log = LoggerFactory
       .getLogger(GroupIncludeCacheImpl.class);
   private static final String BYINCLUDE_NAME = "groups_byinclude";
+  private static final String MEMBERS_NAME = "groups_members";
+  private static final String EXTERNAL_NAME = "groups_external";
 
   public static Module module() {
     return new CacheModule() {
@@ -52,7 +53,17 @@
         cache(BYINCLUDE_NAME,
             AccountGroup.UUID.class,
             new TypeLiteral<Set<AccountGroup.UUID>>() {})
-          .loader(ByIncludeLoader.class);
+          .loader(MemberInLoader.class);
+
+        cache(MEMBERS_NAME,
+            AccountGroup.UUID.class,
+            new TypeLiteral<Set<AccountGroup.UUID>>() {})
+          .loader(MembersOfLoader.class);
+
+        cache(EXTERNAL_NAME,
+            String.class,
+            new TypeLiteral<Set<AccountGroup.UUID>>() {})
+          .loader(AllExternalLoader.class);
 
         bind(GroupIncludeCacheImpl.class);
         bind(GroupIncludeCache.class).to(GroupIncludeCacheImpl.class);
@@ -60,35 +71,74 @@
     };
   }
 
-  private final LoadingCache<AccountGroup.UUID, Set<AccountGroup.UUID>> byInclude;
+  private final LoadingCache<AccountGroup.UUID, Set<AccountGroup.UUID>> membersOf;
+  private final LoadingCache<AccountGroup.UUID, Set<AccountGroup.UUID>> memberIn;
+  private final LoadingCache<String, Set<AccountGroup.UUID>> external;
 
   @Inject
   GroupIncludeCacheImpl(
-      @Named(BYINCLUDE_NAME) LoadingCache<AccountGroup.UUID, Set<AccountGroup.UUID>> byInclude) {
-    this.byInclude = byInclude;
+      @Named(MEMBERS_NAME) LoadingCache<AccountGroup.UUID, Set<AccountGroup.UUID>> membersOf,
+      @Named(BYINCLUDE_NAME) LoadingCache<AccountGroup.UUID, Set<AccountGroup.UUID>> memberIn,
+      @Named(EXTERNAL_NAME) LoadingCache<String, Set<AccountGroup.UUID>> external) {
+    this.membersOf = membersOf;
+    this.memberIn = memberIn;
+    this.external = external;
   }
 
-  public Collection<AccountGroup.UUID> getByInclude(AccountGroup.UUID groupId) {
+  @Override
+  public Set<AccountGroup.UUID> membersOf(AccountGroup.UUID groupId) {
     try {
-      return byInclude.get(groupId);
+      return membersOf.get(groupId);
+    } catch (ExecutionException e) {
+      log.warn("Cannot load members of group", e);
+      return Collections.emptySet();
+    }
+  }
+
+  @Override
+  public Set<AccountGroup.UUID> memberIn(AccountGroup.UUID groupId) {
+    try {
+      return memberIn.get(groupId);
     } catch (ExecutionException e) {
       log.warn("Cannot load included groups", e);
       return Collections.emptySet();
     }
   }
 
-  public void evictInclude(AccountGroup.UUID groupId) {
+  @Override
+  public void evictMembersOf(AccountGroup.UUID groupId) {
     if (groupId != null) {
-      byInclude.invalidate(groupId);
+      membersOf.invalidate(groupId);
     }
   }
 
-  static class ByIncludeLoader extends
+  @Override
+  public void evictMemberIn(AccountGroup.UUID groupId) {
+    if (groupId != null) {
+      memberIn.invalidate(groupId);
+
+      if (!AccountGroup.isInternalGroup(groupId)) {
+        external.invalidate(EXTERNAL_NAME);
+      }
+    }
+  }
+
+  @Override
+  public Set<AccountGroup.UUID> allExternalMembers() {
+    try {
+      return external.get(EXTERNAL_NAME);
+    } catch (ExecutionException e) {
+      log.warn("Cannot load set of non-internal groups", e);
+      return Collections.emptySet();
+    }
+  }
+
+  static class MembersOfLoader extends
       CacheLoader<AccountGroup.UUID, Set<AccountGroup.UUID>> {
     private final SchemaFactory<ReviewDb> schema;
 
     @Inject
-    ByIncludeLoader(final SchemaFactory<ReviewDb> sf) {
+    MembersOfLoader(final SchemaFactory<ReviewDb> sf) {
       schema = sf;
     }
 
@@ -101,9 +151,34 @@
           return Collections.emptySet();
         }
 
+        Set<AccountGroup.UUID> ids = Sets.newHashSet();
+        for (AccountGroupIncludeByUuid agi : db.accountGroupIncludesByUuid()
+            .byGroup(group.get(0).getId())) {
+          ids.add(agi.getIncludeUUID());
+        }
+        return ImmutableSet.copyOf(ids);
+      } finally {
+        db.close();
+      }
+    }
+  }
+
+  static class MemberInLoader extends
+      CacheLoader<AccountGroup.UUID, Set<AccountGroup.UUID>> {
+    private final SchemaFactory<ReviewDb> schema;
+
+    @Inject
+    MemberInLoader(final SchemaFactory<ReviewDb> sf) {
+      schema = sf;
+    }
+
+    @Override
+    public Set<AccountGroup.UUID> load(AccountGroup.UUID key) throws Exception {
+      final ReviewDb db = schema.open();
+      try {
         Set<AccountGroup.Id> ids = Sets.newHashSet();
-        for (AccountGroupInclude agi : db.accountGroupIncludes()
-            .byInclude(group.get(0).getId())) {
+        for (AccountGroupIncludeByUuid agi : db.accountGroupIncludesByUuid()
+            .byIncludeUUID(key)) {
           ids.add(agi.getGroupId());
         }
 
@@ -117,4 +192,30 @@
       }
     }
   }
+
+  static class AllExternalLoader extends
+      CacheLoader<String, Set<AccountGroup.UUID>> {
+    private final SchemaFactory<ReviewDb> schema;
+
+    @Inject
+    AllExternalLoader(final SchemaFactory<ReviewDb> sf) {
+      schema = sf;
+    }
+
+    @Override
+    public Set<AccountGroup.UUID> load(String key) throws Exception {
+      final ReviewDb db = schema.open();
+      try {
+        Set<AccountGroup.UUID> ids = Sets.newHashSet();
+        for (AccountGroupIncludeByUuid agi : db.accountGroupIncludesByUuid().all()) {
+          if (!AccountGroup.isInternalGroup(agi.getIncludeUUID())) {
+            ids.add(agi.getIncludeUUID());
+          }
+        }
+        return ImmutableSet.copyOf(ids);
+      } finally {
+        db.close();
+      }
+    }
+  }
 }
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 19d953d..e346801 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
@@ -14,14 +14,12 @@
 
 package com.google.gerrit.server.account;
 
-import com.google.gerrit.common.data.GroupInfo;
+import com.google.common.collect.Maps;
+import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.common.data.GroupInfoCache;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.inject.Inject;
 
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
 import java.util.Map;
 
 /** Efficiently builds a {@link GroupInfoCache}. */
@@ -30,46 +28,35 @@
     GroupInfoCacheFactory create();
   }
 
-  private final GroupCache groupCache;
-  private final Map<AccountGroup.Id, AccountGroup> out;
+  private final GroupBackend groupBackend;
+  private final Map<AccountGroup.UUID, GroupDescription.Basic> out;
 
   @Inject
-  GroupInfoCacheFactory(final GroupCache groupCache) {
-    this.groupCache = groupCache;
-    this.out = new HashMap<AccountGroup.Id, AccountGroup>();
+  GroupInfoCacheFactory(GroupBackend groupBackend) {
+    this.groupBackend = groupBackend;
+    this.out = Maps.newHashMap();
   }
 
   /**
    * Indicate a group will be needed later on.
    *
-   * @param id identity that will be needed in the future; may be null.
+   * @param uuid identity that will be needed in the future; may be null.
    */
-  public void want(final AccountGroup.Id id) {
-    if (id != null && !out.containsKey(id)) {
-      out.put(id, groupCache.get(id));
+  public void want(final AccountGroup.UUID uuid) {
+    if (uuid != null && !out.containsKey(uuid)) {
+      out.put(uuid, groupBackend.get(uuid));
     }
   }
 
   /** Indicate one or more groups will be needed later on. */
-  public void want(final Iterable<AccountGroup.Id> ids) {
-    for (final AccountGroup.Id id : ids) {
-      want(id);
+  public void want(final Iterable<AccountGroup.UUID> uuids) {
+    for (final AccountGroup.UUID uuid : uuids) {
+      want(uuid);
     }
   }
 
-  public AccountGroup get(final AccountGroup.Id id) {
-    want(id);
-    return out.get(id);
-  }
-
-  /**
-   * Create an GroupInfoCache with the currently loaded AccountGroup entities.
-   * */
-  public GroupInfoCache create() {
-    final List<GroupInfo> r = new ArrayList<GroupInfo>(out.size());
-    for (final AccountGroup a : out.values()) {
-      r.add(new GroupInfo(a));
-    }
-    return new GroupInfoCache(r);
+  public GroupDescription.Basic get(final AccountGroup.UUID uuid) {
+    want(uuid);
+    return out.get(uuid);
   }
 }
\ No newline at end of file
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
index 84282b5..08cf1a7 100644
--- 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
@@ -18,14 +18,15 @@
 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.AccountGroupIncludeByUuid;
 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.CurrentUser;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
 
 import java.util.Collections;
 import java.util.HashSet;
@@ -33,21 +34,21 @@
 
 public class GroupMembers {
   public interface Factory {
-    GroupMembers create();
+    GroupMembers create(CurrentUser currentUser);
   }
 
   private final GroupCache groupCache;
   private final GroupDetailFactory.Factory groupDetailFactory;
   private final AccountCache accountCache;
   private final ProjectControl.GenericFactory projectControl;
-  private final IdentifiedUser currentUser;
+  private final CurrentUser currentUser;
 
   @Inject
   GroupMembers(final GroupCache groupCache,
       final GroupDetailFactory.Factory groupDetailFactory,
       final AccountCache accountCache,
       final ProjectControl.GenericFactory projectControl,
-      final IdentifiedUser currentUser) {
+      @Assisted final CurrentUser currentUser) {
     this.groupCache = groupCache;
     this.groupDetailFactory = groupDetailFactory;
     this.accountCache = accountCache;
@@ -111,10 +112,10 @@
       }
     }
     if (groupDetail.includes != null) {
-      for (final AccountGroupInclude groupInclude : groupDetail.includes) {
+      for (final AccountGroupIncludeByUuid groupInclude : groupDetail.includes) {
         final AccountGroup includedGroup =
-            groupCache.get(groupInclude.getIncludeId());
-        if (!seen.contains(includedGroup.getGroupUUID())) {
+            groupCache.get(groupInclude.getIncludeUUID());
+        if (includedGroup != null && !seen.contains(includedGroup.getGroupUUID())) {
           members.addAll(listAccounts(includedGroup.getGroupUUID(), project, seen));
         }
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembership.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembership.java
index d536c09..d7a97fb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembership.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/GroupMembership.java
@@ -40,6 +40,20 @@
   boolean containsAnyOf(Iterable<AccountGroup.UUID> groupIds);
 
   /**
+   * Returns a set containing an input member of {@code contains(id)} is true.
+   * <p>
+   * This is batch form of contains that returns specific group information.
+   * Implementors may implement the method as:
+   *
+   * <pre>
+   * Set&lt;AccountGroup.UUID&gt; r = Sets.newHashSet();
+   * for (AccountGroup.UUID id : groupIds)
+   *   if (contains(id)) r.add(id);
+   * </pre>
+   */
+  Set<AccountGroup.UUID> intersection(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
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/IncludingGroupMembership.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/IncludingGroupMembership.java
index d448fff..b020e22 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/IncludingGroupMembership.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/IncludingGroupMembership.java
@@ -14,36 +14,49 @@
 
 package com.google.gerrit.server.account;
 
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
 import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.IdentifiedUser;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
-import java.util.Collections;
-import java.util.Queue;
+import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 /**
- * Creates a GroupMembership checker for the internal group system, which
- * starts with the seed groups and includes all child groups.
+ * Group membership checker for the internal group system.
+ * <p>
+ * Groups the user is directly a member of are pulled from the in-memory
+ * AccountCache by way of the IdentifiedUser. Transitive group memberhips are
+ * resolved on demand starting from the requested group and looking for a path
+ * to a group the user is a member of. Other group backends are supported by
+ * recursively invoking the universal GroupMembership.
  */
 public class IncludingGroupMembership implements GroupMembership {
   public interface Factory {
-    IncludingGroupMembership create(Iterable<AccountGroup.UUID> groupIds);
+    IncludingGroupMembership create(IdentifiedUser user);
   }
 
-  private final GroupIncludeCache groupIncludeCache;
-  private final Set<AccountGroup.UUID> includes;
-  private final Queue<AccountGroup.UUID> groupQueue;
+  private final GroupIncludeCache includeCache;
+  private final IdentifiedUser user;
+  private final Map<AccountGroup.UUID, Boolean> memberOf;
+  private Set<AccountGroup.UUID> knownGroups;
 
   @Inject
-  IncludingGroupMembership(
-      GroupIncludeCache groupIncludeCache,
-      @Assisted Iterable<AccountGroup.UUID> seedGroups) {
-    this.groupIncludeCache = groupIncludeCache;
-    this.includes = Sets.newHashSet(seedGroups);
-    this.groupQueue = Lists.newLinkedList(seedGroups);
+  IncludingGroupMembership(GroupIncludeCache includeCache,
+      @Assisted IdentifiedUser user) {
+    this.includeCache = includeCache;
+    this.user = user;
+
+    Set<AccountGroup.UUID> groups = user.state().getInternalGroups();
+    memberOf = Maps.newHashMapWithExpectedSize(groups.size());
+    for (AccountGroup.UUID g : groups) {
+      memberOf.put(g, true);
+    }
   }
 
   @Override
@@ -51,44 +64,87 @@
     if (id == null) {
       return false;
     }
-    if (includes.contains(id)) {
-      return true;
-    }
-    return findIncludedGroup(Collections.singleton(id));
+
+    Boolean b = memberOf.get(id);
+    return b != null ? b : containsAnyOf(ImmutableSet.of(id));
   }
 
   @Override
-  public boolean containsAnyOf(Iterable<AccountGroup.UUID> ids) {
-    Set<AccountGroup.UUID> query = Sets.newHashSet();
-    for (AccountGroup.UUID groupId : ids) {
-      if (includes.contains(groupId)) {
+  public boolean containsAnyOf(Iterable<AccountGroup.UUID> queryIds) {
+    // Prefer lookup of a cached result over expanding includes.
+    boolean tryExpanding = false;
+    for (AccountGroup.UUID id : queryIds) {
+      Boolean b = memberOf.get(id);
+      if (b == null) {
+        tryExpanding = true;
+      } else if (b) {
         return true;
       }
-      query.add(groupId);
     }
 
-    return findIncludedGroup(query);
-  }
+    if (tryExpanding) {
+      for (AccountGroup.UUID id : queryIds) {
+        if (memberOf.containsKey(id)) {
+          // Membership was earlier proven to be false.
+          continue;
+        }
 
-  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);
+        memberOf.put(id, false);
+        if (search(includeCache.membersOf(id))) {
+          memberOf.put(id, true);
+          return true;
         }
       }
     }
 
-    return found;
+    return false;
+  }
+
+  @Override
+  public Set<AccountGroup.UUID> intersection(Iterable<AccountGroup.UUID> groupIds) {
+    Set<AccountGroup.UUID> r = Sets.newHashSet();
+    for (AccountGroup.UUID id : groupIds) {
+      if (contains(id)) {
+        r.add(id);
+      }
+    }
+    return r;
+  }
+
+  private boolean search(Set<AccountGroup.UUID> ids) {
+    return user.getEffectiveGroups().containsAnyOf(ids);
+  }
+
+  private ImmutableSet<AccountGroup.UUID> computeKnownGroups() {
+    GroupMembership membership = user.getEffectiveGroups();
+    Set<AccountGroup.UUID> direct = user.state().getInternalGroups();
+    Set<AccountGroup.UUID> r = Sets.newHashSet(direct);
+    List<AccountGroup.UUID> q = Lists.newArrayList(r);
+
+    for (AccountGroup.UUID g : membership.intersection(
+        includeCache.allExternalMembers())) {
+      if (r.add(g)) {
+        q.add(g);
+      }
+    }
+
+    while (!q.isEmpty()) {
+      AccountGroup.UUID id = q.remove(q.size() - 1);
+      for (AccountGroup.UUID g : includeCache.memberIn(id)) {
+        if (r.add(g)) {
+          q.add(g);
+          memberOf.put(g, true);
+        }
+      }
+    }
+    return ImmutableSet.copyOf(r);
   }
 
   @Override
   public Set<AccountGroup.UUID> getKnownGroups() {
-    findIncludedGroup(Collections.<AccountGroup.UUID>emptySet()); // find all
-    return Sets.newHashSet(includes);
+    if (knownGroups == null) {
+      knownGroups = computeKnownGroups();
+    }
+    return knownGroups;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalGroupBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalGroupBackend.java
index ad65499..d06db4d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalGroupBackend.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/InternalGroupBackend.java
@@ -28,9 +28,7 @@
 
 import java.util.Collection;
 
-/**
- * Implementation of GroupBackend for the internal group system.
- */
+/** Implementation of GroupBackend for the internal group system. */
 @Singleton
 public class InternalGroupBackend implements GroupBackend {
   private static final Function<AccountGroup, GroupReference> ACT_GROUP_TO_GROUP_REF =
@@ -45,7 +43,6 @@
   private final GroupCache groupCache;
   private final IncludingGroupMembership.Factory groupMembershipFactory;
 
-
   @Inject
   InternalGroupBackend(GroupControl.Factory groupControlFactory,
       GroupCache groupCache,
@@ -89,6 +86,6 @@
 
   @Override
   public GroupMembership membershipsOf(IdentifiedUser user) {
-    return groupMembershipFactory.create(user.state().getInternalGroups());
+    return groupMembershipFactory.create(user);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/ListGroupMembership.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/ListGroupMembership.java
index 346f406..118940f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/ListGroupMembership.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/ListGroupMembership.java
@@ -24,7 +24,6 @@
  * GroupMembership over an explicit list.
  */
 public class ListGroupMembership implements GroupMembership {
-
   private final Set<AccountGroup.UUID> groups;
 
   public ListGroupMembership(Iterable<AccountGroup.UUID> groupIds) {
@@ -47,6 +46,11 @@
   }
 
   @Override
+  public Set<AccountGroup.UUID> intersection(Iterable<AccountGroup.UUID> groupIds) {
+    return Sets.intersection(ImmutableSet.copyOf(groupIds), groups);
+  }
+
+  @Override
   public Set<AccountGroup.UUID> getKnownGroups() {
     return Sets.newHashSet(groups);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java
new file mode 100644
index 0000000..ac045f75e9
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/Module.java
@@ -0,0 +1,38 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.account;
+
+import static com.google.gerrit.server.account.AccountResource.ACCOUNT_KIND;
+import static com.google.gerrit.server.account.AccountResource.CAPABILITY_KIND;
+
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.RestApiModule;
+
+public class Module extends RestApiModule {
+  @Override
+  protected void configure() {
+    bind(AccountsCollection.class);
+    bind(Capabilities.class);
+
+    DynamicMap.mapOf(binder(), ACCOUNT_KIND);
+    DynamicMap.mapOf(binder(), CAPABILITY_KIND);
+
+    get(ACCOUNT_KIND).to(GetAccount.class);
+    get(ACCOUNT_KIND, "avatar").to(GetAvatar.class);
+    child(ACCOUNT_KIND, "capabilities").to(Capabilities.class);
+    get(ACCOUNT_KIND, "groups").to(GetGroups.class);
+    get(CAPABILITY_KIND).to(GetCapabilities.CheckOne.class);
+  }
+}
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 1ff1a3e..8b966cb 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
@@ -18,8 +18,8 @@
 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.AccountGroupIncludeByUuid;
+import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuidAudit;
 import com.google.gerrit.reviewdb.client.AccountGroupMember;
 import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
 import com.google.gerrit.reviewdb.client.AccountGroupName;
@@ -35,7 +35,6 @@
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.HashSet;
 import java.util.List;
 
 public class PerformCreateGroup {
@@ -85,11 +84,11 @@
    *         name already exists
    * @throws PermissionDeniedException user cannot create a group.
    */
-  public AccountGroup.Id createGroup(final String groupName,
+  public AccountGroup 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)
+      final Collection<? extends AccountGroup.UUID> initialGroups)
       throws OrmException, NameAlreadyUsedException, PermissionDeniedException {
     if (!currentUser.getCapabilities().canCreateGroup()) {
       throw new PermissionDeniedException(String.format(
@@ -121,7 +120,7 @@
     try {
       db.accountGroupNames().insert(Collections.singleton(gn));
     } catch (OrmDuplicateKeyException e) {
-      throw new NameAlreadyUsedException();
+      throw new NameAlreadyUsedException(groupName);
     }
     db.accountGroups().insert(Collections.singleton(group));
 
@@ -129,11 +128,12 @@
 
     if (initialGroups != null) {
       addGroups(groupId, initialGroups);
+      groupIncludeCache.evictMembersOf(uuid);
     }
 
     groupCache.onCreateGroup(nameKey);
 
-    return groupId;
+    return group;
   }
 
   private void addMembers(final AccountGroup.Id groupId,
@@ -160,26 +160,25 @@
   }
 
   private void addGroups(final AccountGroup.Id groupId,
-      final Collection<? extends AccountGroup.Id> groups) throws OrmException {
-    final List<AccountGroupInclude> includeList =
-      new ArrayList<AccountGroupInclude>();
-    final List<AccountGroupIncludeAudit> includesAudit =
-      new ArrayList<AccountGroupIncludeAudit>();
-    for (AccountGroup.Id includeId : groups) {
-      final AccountGroupInclude groupInclude =
-        new AccountGroupInclude(new AccountGroupInclude.Key(groupId, includeId));
+      final Collection<? extends AccountGroup.UUID> groups) throws OrmException {
+    final List<AccountGroupIncludeByUuid> includeList =
+      new ArrayList<AccountGroupIncludeByUuid>();
+    final List<AccountGroupIncludeByUuidAudit> includesAudit =
+      new ArrayList<AccountGroupIncludeByUuidAudit>();
+    for (AccountGroup.UUID includeUUID : groups) {
+      final AccountGroupIncludeByUuid groupInclude =
+        new AccountGroupIncludeByUuid(new AccountGroupIncludeByUuid.Key(groupId, includeUUID));
       includeList.add(groupInclude);
 
-      final AccountGroupIncludeAudit audit =
-        new AccountGroupIncludeAudit(groupInclude, currentUser.getAccountId());
+      final AccountGroupIncludeByUuidAudit audit =
+        new AccountGroupIncludeByUuidAudit(groupInclude, currentUser.getAccountId());
       includesAudit.add(audit);
     }
-    db.accountGroupIncludes().insert(includeList);
-    db.accountGroupIncludesAudit().insert(includesAudit);
+    db.accountGroupIncludesByUuid().insert(includeList);
+    db.accountGroupIncludesByUuidAudit().insert(includesAudit);
 
-    for (AccountGroup group : db.accountGroups().get(
-        new HashSet<AccountGroup.Id>(groups))) {
-      groupIncludeCache.evictInclude(group.getGroupUUID());
+    for (AccountGroup.UUID uuid : groups) {
+      groupIncludeCache.evictMemberIn(uuid);
     }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/PerformRenameGroup.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/PerformRenameGroup.java
index 4c72d49..3623cc3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/PerformRenameGroup.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/PerformRenameGroup.java
@@ -23,7 +23,6 @@
 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;
 
@@ -89,17 +88,21 @@
     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.
-      //
+    } catch (OrmException e) {
       AccountGroupName other = db.accountGroupNames().get(key);
-      if (other != null && other.getId().equals(groupId)) {
-        return groupDetailFactory.create(groupId).call();
-      }
+      if (other != null) {
+        // If we are using this identity, don't report the exception.
+        //
+        if (other.getId().equals(groupId)) {
+          return groupDetailFactory.create(groupId).call();
+        }
 
-      // Otherwise, someone else has this identity.
-      //
-      throw new NameAlreadyUsedException();
+        // Otherwise, someone else has this identity.
+        //
+        throw new NameAlreadyUsedException(newName);
+      } else {
+        throw e;
+      }
     }
 
     group.setNameKey(key);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/UniversalGroupBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/UniversalGroupBackend.java
index 1974961..d9c9257 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/UniversalGroupBackend.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/account/UniversalGroupBackend.java
@@ -18,6 +18,7 @@
 
 import com.google.common.collect.ArrayListMultimap;
 import com.google.common.collect.ImmutableMap;
+import com.google.common.collect.Iterables;
 import com.google.common.collect.Multimap;
 import com.google.common.collect.Sets;
 import com.google.gerrit.common.data.GroupDescription;
@@ -128,34 +129,60 @@
      return m.contains(uuid);
    }
 
-   @Override
-   public boolean containsAnyOf(Iterable<AccountGroup.UUID> uuids) {
-     Multimap<GroupMembership, AccountGroup.UUID> lookups =
-         ArrayListMultimap.create();
-     for (AccountGroup.UUID uuid : uuids) {
-       GroupMembership m = membership(uuid);
-       if (m == null) {
-         log.warn("Unknown GroupMembership for UUID: " + uuid);
-         continue;
-       }
-       lookups.put(m, uuid);
-     }
-     for (Map.Entry<GroupMembership, Collection<AccountGroup.UUID>> entry :
-          lookups.asMap().entrySet()) {
-       if (entry.getKey().containsAnyOf(entry.getValue())) {
-         return true;
-       }
-     }
-     return false;
-   }
+    @Override
+    public boolean containsAnyOf(Iterable<AccountGroup.UUID> uuids) {
+      Multimap<GroupMembership, AccountGroup.UUID> lookups =
+          ArrayListMultimap.create();
+      for (AccountGroup.UUID uuid : uuids) {
+        GroupMembership m = membership(uuid);
+        if (m == null) {
+          log.warn("Unknown GroupMembership for UUID: " + uuid);
+          continue;
+        }
+        lookups.put(m, uuid);
+      }
+      for (Map.Entry<GroupMembership, Collection<AccountGroup.UUID>> entry
+          : lookups .asMap().entrySet()) {
+        GroupMembership m = entry.getKey();
+        Collection<AccountGroup.UUID> ids = entry.getValue();
+        if (ids.size() == 1) {
+          if (m.contains(Iterables.getOnlyElement(ids))) {
+            return true;
+          }
+        } else if (m.containsAnyOf(ids)) {
+          return true;
+        }
+      }
+      return false;
+    }
 
-   @Override
-   public Set<AccountGroup.UUID> getKnownGroups() {
-     Set<AccountGroup.UUID> groups = Sets.newHashSet();
-     for (GroupMembership m : memberships.values()) {
-       groups.addAll(m.getKnownGroups());
-     }
-     return groups;
-   }
+    @Override
+    public Set<AccountGroup.UUID> intersection(Iterable<AccountGroup.UUID> uuids) {
+      Multimap<GroupMembership, AccountGroup.UUID> lookups =
+          ArrayListMultimap.create();
+      for (AccountGroup.UUID uuid : uuids) {
+        GroupMembership m = membership(uuid);
+        if (m == null) {
+          log.warn("Unknown GroupMembership for UUID: " + uuid);
+          continue;
+        }
+        lookups.put(m, uuid);
+      }
+      Set<AccountGroup.UUID> groups = Sets.newHashSet();
+      for (Map.Entry<GroupMembership, Collection<AccountGroup.UUID>> entry
+          : lookups.asMap().entrySet()) {
+        groups.addAll(entry.getKey().intersection(entry.getValue()));
+      }
+      return groups;
+    }
+
+    @Override
+    public Set<AccountGroup.UUID> getKnownGroups() {
+      Set<AccountGroup.UUID> groups = Sets.newHashSet();
+      for (GroupMembership m : memberships.values()) {
+        groups.addAll(m.getKnownGroups());
+      }
+      return groups;
+    }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/account/VisibleGroups.java b/gerrit-server/src/main/java/com/google/gerrit/server/account/VisibleGroups.java
deleted file mode 100644
index d3b2c83..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/account/VisibleGroups.java
+++ /dev/null
@@ -1,133 +0,0 @@
-// Copyright (C) 2011 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License
-
-package com.google.gerrit.server.account;
-
-import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
-import com.google.gerrit.common.data.GroupList;
-import com.google.gerrit.common.data.GroupReference;
-import com.google.gerrit.common.errors.NoSuchGroupException;
-import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.project.ProjectControl;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-
-import java.util.Collection;
-import java.util.Collections;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-
-public class VisibleGroups {
-
-  public interface Factory {
-    VisibleGroups create();
-  }
-
-  private final Provider<IdentifiedUser> identifiedUser;
-  private final GroupCache groupCache;
-  private final GroupControl.Factory groupControlFactory;
-
-  private boolean onlyVisibleToAll;
-  private AccountGroup.Type groupType;
-
-  @Inject
-  VisibleGroups(final Provider<IdentifiedUser> currentUser,
-      final GroupCache groupCache,
-      final GroupControl.Factory groupControlFactory) {
-    this.identifiedUser = currentUser;
-    this.groupCache = groupCache;
-    this.groupControlFactory = groupControlFactory;
-  }
-
-  public void setOnlyVisibleToAll(final boolean onlyVisibleToAll) {
-    this.onlyVisibleToAll = onlyVisibleToAll;
-  }
-
-  public void setGroupType(final AccountGroup.Type groupType) {
-    this.groupType = groupType;
-  }
-
-  public GroupList get() {
-    return createGroupList(filterGroups(groupCache.all()));
-  }
-
-  public GroupList get(final Collection<ProjectControl> projects)
-      throws NoSuchGroupException {
-    Map<AccountGroup.UUID, AccountGroup> groups = Maps.newHashMap();
-    for (final ProjectControl projectControl : projects) {
-      final Set<GroupReference> groupsRefs = projectControl.getAllGroups();
-      for (final GroupReference groupRef : groupsRefs) {
-        final AccountGroup group = groupCache.get(groupRef.getUUID());
-        if (group == null) {
-          throw new NoSuchGroupException(groupRef.getUUID());
-        }
-        groups.put(group.getGroupUUID(), group);
-      }
-    }
-    return createGroupList(filterGroups(groups.values()));
-  }
-
-  /**
-   * 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 NoSuchGroupException {
-    if (identifiedUser.get().getAccountId().equals(user.getAccountId())
-        || identifiedUser.get().getCapabilities().canAdministrateServer()) {
-      Set<AccountGroup.UUID> mine = user.getEffectiveGroups().getKnownGroups();
-      Map<AccountGroup.UUID, AccountGroup> groups = Maps.newHashMap();
-      for (final AccountGroup.UUID groupId : mine) {
-        AccountGroup group = groupCache.get(groupId);
-        if (group != null) {
-          groups.put(groupId, group);
-        }
-      }
-      return createGroupList(filterGroups(groups.values()));
-    } else {
-      throw new NoSuchGroupException("Groups of user '" + user.getAccountId()
-          + "' are not visible.");
-    }
-  }
-
-  private List<AccountGroup> filterGroups(final Iterable<AccountGroup> groups) {
-    final List<AccountGroup> filteredGroups = Lists.newArrayList();
-    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);
-    }
-    Collections.sort(filteredGroups, new GroupComparator());
-    return filteredGroups;
-  }
-
-  private GroupList createGroupList(final List<AccountGroup> groups) {
-    return new GroupList(groups, identifiedUser.get()
-        .getCapabilities().canCreateGroup());
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/args4j/ProjectControlHandler.java b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/ProjectControlHandler.java
index da033e7..7b44c02 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/args4j/ProjectControlHandler.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/args4j/ProjectControlHandler.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.args4j;
 
+import com.google.gerrit.common.ProjectUtil;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.project.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectControl;
@@ -42,23 +43,12 @@
   @Override
   public final int parseArguments(final Parameters params)
       throws CmdLineException {
-    final String token = params.getParameter(0);
-    String projectName = token;
+    String projectName = params.getParameter(0);
 
     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);
-      }
-    }
-
     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
@@ -68,12 +58,14 @@
       projectName = projectName.substring(1);
     }
 
+    String nameWithoutSuffix = ProjectUtil.stripGitSuffix(projectName);
+
     final ProjectControl control;
     try {
-      Project.NameKey nameKey = new Project.NameKey(projectName);
+      Project.NameKey nameKey = new Project.NameKey(nameWithoutSuffix);
       control = projectControlFactory.validateFor(nameKey, ProjectControl.OWNER | ProjectControl.VISIBLE);
     } catch (NoSuchProjectException e) {
-      throw new CmdLineException(owner, "'" + token + "': not a Gerrit project");
+      throw new CmdLineException(owner, e.getMessage());
     }
 
     setter.addValue(control);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/AuthBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/AuthBackend.java
new file mode 100644
index 0000000..1050926
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/AuthBackend.java
@@ -0,0 +1,49 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.auth;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+
+/**
+ * Implementations of AuthBackend authenticate users for incoming request.
+ */
+@ExtensionPoint
+public interface AuthBackend {
+
+  /** @return an identifier that uniquely describes the backend. */
+  String getDomain();
+
+  /**
+   * Authenticate inspects the AuthRequest and returns authenticated user. If
+   * the request is unable to be authenticated, an exception will be thrown. The
+   * {@link MissingCredentialsException} must be thrown when there are no
+   * credentials for the request. It is expected that at most one AuthBackend
+   * will either return an AuthUser or throw a non-MissingCredentialsException.
+   *
+   * @param req the object describing the request.
+   * @return the successfully authenticated user.
+   * @throws MissingCredentialsException when there are no credentials.
+   * @throws InvalidCredentialsException when the credentials are present and
+   *         invalid.
+   * @throws UnknownUserException when the credentials are valid but there is
+   *         no matching user.
+   * @throws UserNotAllowedException when the credentials are valid but the user
+   *         is not allowed.
+   * @throws AuthException when any other error occurs.
+   */
+  AuthUser authenticate(AuthRequest req) throws MissingCredentialsException,
+      InvalidCredentialsException, UnknownUserException,
+      UserNotAllowedException, AuthException;
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/AuthException.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/AuthException.java
new file mode 100644
index 0000000..8cf4a0a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/AuthException.java
@@ -0,0 +1,37 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.auth;
+
+/**
+ * Base type for authentication exceptions.
+ */
+public class AuthException extends Exception {
+  private static final long serialVersionUID = -8946302676525580372L;
+
+  public AuthException() {
+  }
+
+  public AuthException(String msg) {
+    super(msg);
+  }
+
+  public AuthException(Throwable ex) {
+    super(ex);
+  }
+
+  public AuthException(String msg, Throwable ex) {
+    super(msg, ex);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/AuthRequest.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/AuthRequest.java
new file mode 100644
index 0000000..8179d4f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/AuthRequest.java
@@ -0,0 +1,58 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.auth;
+
+import com.google.common.base.Objects;
+
+import javax.annotation.Nullable;
+
+/**
+ * Defines an abstract request for user authentication to Gerrit.
+ */
+public abstract class AuthRequest {
+  private final String username;
+  private final String password;
+
+  protected AuthRequest(String username, String password) {
+    this.username = username;
+    this.password = password;
+  }
+
+  /**
+   * Returns the username to be authenticated.
+   *
+   * @return username for authentication or null for anonymous access.
+   */
+  @Nullable
+  public final String getUsername() {
+    return username;
+  }
+
+  /**
+   * Returns the user's credentials
+   *
+   * @return user's credentials or null
+   */
+  @Nullable
+  public final String getPassword() {
+    return password;
+  }
+
+  public void checkPassword(String pwd) throws AuthException {
+    if (!Objects.equal(getPassword(), pwd)) {
+      throw new InvalidCredentialsException();
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/AuthUser.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/AuthUser.java
new file mode 100644
index 0000000..7d08173
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/AuthUser.java
@@ -0,0 +1,113 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.auth;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import javax.annotation.Nullable;
+
+/**
+ * An authenticated user as specified by the AuthBackend.
+ */
+public class AuthUser {
+
+  /**
+   * Globally unique identifier for the user.
+   */
+  public final static class UUID {
+    private final String uuid;
+
+    /**
+     * A new unique identifier.
+     *
+     * @param uuid the unique identifier.
+     */
+    public UUID(String uuid) {
+      this.uuid = checkNotNull(uuid);
+    }
+
+    /** @return the globally unique identifier. */
+    public String get() {
+      return uuid;
+    }
+
+    @Override
+    public boolean equals(Object obj) {
+      if (obj instanceof UUID) {
+        return get().equals(((UUID) obj).get());
+      }
+      return false;
+    }
+
+    @Override
+    public int hashCode() {
+      return get().hashCode();
+    }
+
+    @Override
+    public String toString() {
+      return String.format("AuthUser.UUID[%s]", get());
+    }
+  }
+
+  private final UUID uuid;
+  private final String username;
+
+  /**
+   * An authenticated user.
+   *
+   * @param uuid the globally unique ID.
+   * @param username the name of the authenticated user.
+   */
+  public AuthUser(UUID uuid, @Nullable String username) {
+    this.uuid = checkNotNull(uuid);
+    this.username = username;
+  }
+
+  /** @return the globally unique identifier. */
+  public final UUID getUUID() {
+    return uuid;
+  }
+
+  /** @return the backend specific user name, or null if one does not exist. */
+  @Nullable
+  public final String getUsername() {
+    return username;
+  }
+
+  /** @return {@code true} if {@link #getUsername()} is not null. */
+  public final boolean hasUsername() {
+    return getUsername() != null;
+  }
+
+  @Override
+  public boolean equals(Object obj) {
+    if (obj instanceof AuthUser) {
+      return getUUID().equals(((AuthUser) obj).getUUID());
+    }
+    return false;
+  }
+
+  @Override
+  public int hashCode() {
+    return getUUID().hashCode();
+  }
+
+  @Override
+  public String toString() {
+    return String.format("AuthUser[uuid=%s, username=%s]", getUUID(),
+        getUsername());
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/InternalAuthBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/InternalAuthBackend.java
new file mode 100644
index 0000000..6e9e71b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/InternalAuthBackend.java
@@ -0,0 +1,67 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.auth;
+
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.AccountState;
+import com.google.gerrit.server.config.AuthConfig;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import java.util.Locale;
+
+@Singleton
+public class InternalAuthBackend implements AuthBackend {
+  private final AccountCache accountCache;
+  private final AuthConfig authConfig;
+
+  @Inject
+  InternalAuthBackend(AccountCache accountCache, AuthConfig authConfig) {
+    this.accountCache = accountCache;
+    this.authConfig = authConfig;
+  }
+
+  @Override
+  public String getDomain() {
+    return "gerrit";
+  }
+
+  @Override
+  public AuthUser authenticate(AuthRequest req)
+      throws MissingCredentialsException, InvalidCredentialsException,
+      UnknownUserException, UserNotAllowedException, AuthException {
+    if (req.getUsername() == null || req.getPassword() == null) {
+      throw new MissingCredentialsException();
+    }
+
+    String username;
+    if (authConfig.isUserNameToLowerCase()) {
+      username = req.getUsername().toLowerCase(Locale.US);
+    } else {
+      username = req.getUsername();
+    }
+
+    final AccountState who = accountCache.getByUsername(username);
+    if (who == null) {
+      throw new UnknownUserException();
+    } else if (!who.getAccount().isActive()) {
+      throw new UserNotAllowedException("Authentication failed for " + username
+          + ": account inactive or not provisioned in Gerrit");
+    }
+
+    req.checkPassword(who.getPassword(username));
+    return new AuthUser(new AuthUser.UUID(username), username);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/InvalidCredentialsException.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/InvalidCredentialsException.java
new file mode 100644
index 0000000..bca8586
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/InvalidCredentialsException.java
@@ -0,0 +1,38 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.auth;
+
+/**
+ * An authentication exception that is thrown when the credentials are present
+ * and are unable to be verified.
+ */
+public class InvalidCredentialsException extends AuthException {
+  private static final long serialVersionUID = 3709201042080444276L;
+
+  public InvalidCredentialsException() {
+  }
+
+  public InvalidCredentialsException(String msg) {
+    super(msg);
+  }
+
+  public InvalidCredentialsException(Throwable ex) {
+    super(ex);
+  }
+
+  public InvalidCredentialsException(String msg, Throwable ex) {
+    super(msg, ex);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/MissingCredentialsException.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/MissingCredentialsException.java
new file mode 100644
index 0000000..062728a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/MissingCredentialsException.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.auth;
+
+/**
+ * An authentication exception that is thrown when the credentials are not
+ * present. This indicates that the AuthBackend has none of the needed
+ * information in the request to perform authentication. If parts of the
+ * authentication information is available to the backend, then a different
+ * AuthException should be used.
+ */
+public class MissingCredentialsException extends AuthException {
+  private static final long serialVersionUID = -6499866977513508051L;
+
+  public MissingCredentialsException() {
+  }
+
+  public MissingCredentialsException(String msg) {
+    super(msg);
+  }
+
+  public MissingCredentialsException(Throwable ex) {
+    super(ex);
+  }
+
+  public MissingCredentialsException(String msg, Throwable ex) {
+    super(msg, ex);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/UniversalAuthBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/UniversalAuthBackend.java
new file mode 100644
index 0000000..689c94c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/UniversalAuthBackend.java
@@ -0,0 +1,73 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.auth;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.collect.Lists;
+import com.google.gerrit.extensions.registration.DynamicSet;
+
+import java.util.List;
+
+import javax.inject.Inject;
+import javax.inject.Singleton;
+
+/**
+ * Universal implementation of the AuthBackend that works with the injected
+ * set of AuthBackends.
+ */
+@Singleton
+public final class UniversalAuthBackend implements AuthBackend {
+  private final DynamicSet<AuthBackend> authBackends;
+
+  @Inject
+  UniversalAuthBackend(DynamicSet<AuthBackend> authBackends) {
+    this.authBackends = authBackends;
+  }
+
+  @Override
+  public AuthUser authenticate(final AuthRequest request) throws AuthException {
+    List<AuthUser> authUsers = Lists.newArrayList();
+    List<AuthException> authExs = Lists.newArrayList();
+    for (AuthBackend backend : authBackends) {
+      try {
+        authUsers.add(checkNotNull(backend.authenticate(request)));
+      } catch (MissingCredentialsException ex) {
+        // Not handled by this backend.
+      } catch (AuthException ex) {
+        authExs.add(ex);
+      }
+    }
+
+    // Handle the valid responses
+    if (authUsers.size() == 1) {
+      return authUsers.get(0);
+    } else if (authUsers.isEmpty() && authExs.size() == 1) {
+      throw authExs.get(0);
+    } else if (authExs.isEmpty() && authUsers.isEmpty()) {
+      throw new MissingCredentialsException();
+    }
+
+    String msg = String.format("Multiple AuthBackends attempted to handle request:"
+        + " authUsers=%s authExs=%s", authUsers, authExs);
+    throw new AuthException(msg);
+  }
+
+  @Override
+  public String getDomain() {
+    throw new UnsupportedOperationException(
+        "UniversalAuthBackend doesn't support domain.");
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/UnknownUserException.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/UnknownUserException.java
new file mode 100644
index 0000000..124912a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/UnknownUserException.java
@@ -0,0 +1,38 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.auth;
+
+/**
+ * An authentication exception that is thrown when credentials are presented for
+ * an unknown user.
+ */
+public class UnknownUserException extends AuthException {
+  private static final long serialVersionUID = 1626186166924670754L;
+
+  public UnknownUserException() {
+  }
+
+  public UnknownUserException(String msg) {
+    super(msg);
+  }
+
+  public UnknownUserException(Throwable ex) {
+    super(ex);
+  }
+
+  public UnknownUserException(String msg, Throwable ex) {
+    super(msg, ex);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/UserNotAllowedException.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/UserNotAllowedException.java
new file mode 100644
index 0000000..2420330
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/UserNotAllowedException.java
@@ -0,0 +1,39 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.auth;
+
+/**
+ * An authentication exception that is thrown when the user credentials are
+ * valid, but not allowed to authenticate for other reasons i.e. account
+ * disabled.
+ */
+public class UserNotAllowedException extends AuthException {
+  private static final long serialVersionUID = -1531411999932922558L;
+
+  public UserNotAllowedException() {
+  }
+
+  public UserNotAllowedException(String msg) {
+    super(msg);
+  }
+
+  public UserNotAllowedException(Throwable ex) {
+    super(ex);
+  }
+
+  public UserNotAllowedException(String msg, Throwable ex) {
+    super(msg, ex);
+  }
+}
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 644f5df..0151dde 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,6 +14,7 @@
 
 package com.google.gerrit.server.auth.ldap;
 
+import com.google.common.base.Throwables;
 import com.google.common.cache.Cache;
 import com.google.common.collect.ImmutableSet;
 import com.google.gerrit.common.data.ParameterizedString;
@@ -28,6 +29,8 @@
 
 import org.eclipse.jgit.lib.Config;
 
+import java.security.PrivilegedActionException;
+import java.security.PrivilegedExceptionAction;
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.HashMap;
@@ -47,6 +50,9 @@
 import javax.naming.directory.DirContext;
 import javax.naming.directory.InitialDirContext;
 import javax.net.ssl.SSLSocketFactory;
+import javax.security.auth.Subject;
+import javax.security.auth.login.LoginContext;
+import javax.security.auth.login.LoginException;
 
 @Singleton class Helper {
   static final String LDAP_UUID = "ldap:";
@@ -58,6 +64,7 @@
   private final String password;
   private final String referral;
   private final boolean sslVerify;
+  private final String authentication;
   private volatile LdapSchema ldapSchema;
   private final String readTimeOutMillis;
 
@@ -66,11 +73,12 @@
       @Named(LdapModule.GROUPS_BYINCLUDE_CACHE)
       Cache<String, ImmutableSet<String>> groupsByInclude) {
     this.config = config;
-    this.server = LdapRealm.required(config, "server");
+    this.server = LdapRealm.optional(config, "server");
     this.username = LdapRealm.optional(config, "username");
     this.password = LdapRealm.optional(config, "password");
     this.referral = LdapRealm.optional(config, "referral");
     this.sslVerify = config.getBoolean("ldap", "sslverify", true);
+    this.authentication = LdapRealm.optional(config, "authentication");
     String timeout = LdapRealm.optional(config, "readTimeout");
     if (timeout != null) {
       readTimeOutMillis =
@@ -96,15 +104,41 @@
     return env;
   }
 
-  DirContext open() throws NamingException {
+  DirContext open() throws NamingException, LoginException {
     final Properties env = createContextProperties();
-    if (username != null) {
-      env.put(Context.SECURITY_AUTHENTICATION, "simple");
-      env.put(Context.SECURITY_PRINCIPAL, username);
-      env.put(Context.SECURITY_CREDENTIALS, password != null ? password : "");
-      env.put(Context.REFERRAL, referral != null ? referral : "ignore");
+    env.put(Context.SECURITY_AUTHENTICATION, authentication != null ? authentication : "simple");
+    env.put(Context.REFERRAL, referral != null ? referral : "ignore");
+    if ("GSSAPI".equals(authentication)) {
+      return kerberosOpen(env);
+    } else {
+      if (username != null) {
+        env.put(Context.SECURITY_PRINCIPAL, username);
+        env.put(Context.SECURITY_CREDENTIALS, password != null ? password : "");
+      }
+      return new InitialDirContext(env);
     }
-    return new InitialDirContext(env);
+  }
+
+  private DirContext kerberosOpen(final Properties env) throws LoginException,
+      NamingException {
+    LoginContext ctx = new LoginContext("KerberosLogin");
+    ctx.login();
+    Subject subject = ctx.getSubject();
+    try {
+      return Subject.doAs(subject, new PrivilegedExceptionAction<DirContext>() {
+          @Override
+          public DirContext run() throws NamingException {
+            return new InitialDirContext(env);
+          }
+        });
+    } catch (PrivilegedActionException e) {
+      Throwables.propagateIfPossible(e.getException(), NamingException.class);
+      Throwables.propagateIfPossible(e.getException(), RuntimeException.class);
+      LdapRealm.log.warn("Internal error", e.getException());
+      return null;
+    } finally {
+      ctx.logout();
+    }
   }
 
   DirContext authenticate(String dn, String password) throws AccountException {
@@ -207,7 +241,7 @@
     if (actual.isEmpty()) {
       return Collections.emptySet();
     } else {
-      return Collections.unmodifiableSet(actual);
+      return ImmutableSet.copyOf(actual);
     }
   }
 
@@ -256,6 +290,7 @@
     final List<String> groupBases;
     final SearchScope groupScope;
     final ParameterizedString groupPattern;
+    final ParameterizedString groupName;
     final List<LdapQuery> groupMemberQueryList;
 
     LdapSchema(final DirContext ctx) {
@@ -271,6 +306,7 @@
       groupBases = LdapRealm.optionalList(config, "groupBase");
       groupScope = LdapRealm.scope(config, "groupScope");
       groupPattern = LdapRealm.paramString(config, "groupPattern", type.groupPattern());
+      groupName = LdapRealm.paramString(config, "groupName", type.groupName());
       final String groupMemberPattern =
           LdapRealm.optdef(config, "groupMemberPattern", type.groupMemberPattern());
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapAuthBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapAuthBackend.java
new file mode 100644
index 0000000..cf68a8b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapAuthBackend.java
@@ -0,0 +1,113 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.auth.ldap;
+
+import com.google.gerrit.reviewdb.client.AuthType;
+import com.google.gerrit.server.account.AccountException;
+import com.google.gerrit.server.auth.AuthBackend;
+import com.google.gerrit.server.auth.AuthException;
+import com.google.gerrit.server.auth.AuthRequest;
+import com.google.gerrit.server.auth.AuthUser;
+import com.google.gerrit.server.auth.InvalidCredentialsException;
+import com.google.gerrit.server.auth.MissingCredentialsException;
+import com.google.gerrit.server.auth.UnknownUserException;
+import com.google.gerrit.server.auth.UserNotAllowedException;
+import com.google.gerrit.server.config.AuthConfig;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.lib.Config;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Locale;
+
+import javax.naming.NamingException;
+import javax.naming.directory.DirContext;
+import javax.security.auth.login.LoginException;
+
+/**
+ * Implementation of AuthBackend for the LDAP authentication system.
+ */
+public class LdapAuthBackend implements AuthBackend {
+  private static final Logger log = LoggerFactory.getLogger(LdapAuthBackend.class);
+
+  private final Helper helper;
+  private final AuthConfig authConfig;
+  private final boolean lowerCaseUsername;
+
+  @Inject
+  public LdapAuthBackend(Helper helper,
+      AuthConfig authConfig,
+      @GerritServerConfig Config config) {
+    this.helper = helper;
+    this.authConfig = authConfig;
+    this.lowerCaseUsername =
+        config.getBoolean("ldap", "localUsernameToLowerCase", false);
+  }
+
+  @Override
+  public String getDomain() {
+    return "ldap";
+  }
+
+  @Override
+  public AuthUser authenticate(AuthRequest req)
+      throws MissingCredentialsException, InvalidCredentialsException,
+      UnknownUserException, UserNotAllowedException, AuthException {
+    if (req.getUsername() == null) {
+      throw new MissingCredentialsException();
+    }
+
+    final String username = lowerCaseUsername
+        ? req.getUsername().toLowerCase(Locale.US)
+        : req.getUsername();
+    try {
+      final DirContext ctx;
+      if (authConfig.getAuthType() == AuthType.LDAP_BIND) {
+        ctx = helper.authenticate(username, req.getPassword());
+      } else {
+        ctx = helper.open();
+      }
+      try {
+        final Helper.LdapSchema schema = helper.getSchema(ctx);
+        final LdapQuery.Result m = helper.findAccount(schema, ctx, username);
+
+        if (authConfig.getAuthType() == AuthType.LDAP) {
+          // We found the user account, but we need to verify
+          // the password matches it before we can continue.
+          //
+          helper.authenticate(m.getDN(), req.getPassword());
+        }
+        return new AuthUser(new AuthUser.UUID(username), username);
+      } finally {
+        try {
+          ctx.close();
+        } catch (NamingException e) {
+          log.warn("Cannot close LDAP query handle", e);
+        }
+      }
+    } catch (AccountException e) {
+      log.error("Cannot query LDAP to authenticate user", e);
+      throw new InvalidCredentialsException("Cannot query LDAP for account", e);
+    } catch (NamingException e) {
+      log.error("Cannot query LDAP to authenticate user", e);
+      throw new AuthException("Cannot query LDAP for account", e);
+    } catch (LoginException e) {
+      log.error("Cannot authenticate server via JAAS", e);
+      throw new AuthException("Cannot query LDAP for account", e);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapGroupBackend.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapGroupBackend.java
index 5c30e5c..2cf372b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapGroupBackend.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapGroupBackend.java
@@ -32,6 +32,7 @@
 import com.google.gerrit.server.account.GroupMembership;
 import com.google.gerrit.server.account.ListGroupMembership;
 import com.google.gerrit.server.auth.ldap.Helper.LdapSchema;
+import com.google.gerrit.server.project.ProjectCache;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.name.Named;
@@ -41,15 +42,18 @@
 
 import java.util.Collection;
 import java.util.Collections;
+import java.util.HashSet;
 import java.util.Map;
 import java.util.Set;
 import java.util.concurrent.ExecutionException;
 
+import javax.annotation.Nullable;
 import javax.naming.InvalidNameException;
 import javax.naming.NamingException;
 import javax.naming.directory.DirContext;
 import javax.naming.ldap.LdapName;
 import javax.naming.ldap.Rdn;
+import javax.security.auth.login.LoginException;
 
 /**
  * Implementation of GroupBackend for the LDAP group system.
@@ -63,6 +67,7 @@
   private final Helper helper;
   private final LoadingCache<String, Set<AccountGroup.UUID>> membershipCache;
   private final LoadingCache<String, Boolean> existsCache;
+  private final ProjectCache projectCache;
   private final Provider<CurrentUser> userProvider;
 
   @Inject
@@ -70,22 +75,24 @@
       Helper helper,
       @Named(GROUP_CACHE) LoadingCache<String, Set<AccountGroup.UUID>> membershipCache,
       @Named(GROUP_EXIST_CACHE) LoadingCache<String, Boolean> existsCache,
+      ProjectCache projectCache,
       Provider<CurrentUser> userProvider) {
     this.helper = helper;
     this.membershipCache = membershipCache;
+    this.projectCache = projectCache;
     this.existsCache = existsCache;
     this.userProvider = userProvider;
   }
 
-  private static boolean isLdapUUID(AccountGroup.UUID uuid) {
+  private boolean isLdapUUID(AccountGroup.UUID uuid) {
     return uuid.get().startsWith(LDAP_UUID);
   }
 
-  private static GroupReference groupReference(LdapQuery.Result res)
-      throws NamingException {
+  private static GroupReference groupReference(ParameterizedString p,
+      LdapQuery.Result res) throws NamingException {
     return new GroupReference(
         new AccountGroup.UUID(LDAP_UUID + res.getDN()),
-        LDAP_NAME + cnFor(res.getDN()));
+        LDAP_NAME + LdapRealm.apply(p, res));
   }
 
   private static String cnFor(String dn) {
@@ -143,8 +150,15 @@
       }
 
       @Override
-      public boolean isVisibleToAll() {
-        return false;
+      @Nullable
+      public String getEmailAddress() {
+        return null;
+      }
+
+      @Override
+      @Nullable
+      public String getUrl() {
+        return null;
       }
     };
   }
@@ -172,7 +186,15 @@
     }
 
     try {
-      return new ListGroupMembership(membershipCache.get(id));
+      final Set<AccountGroup.UUID> groups = membershipCache.get(id);
+      return new ListGroupMembership(groups) {
+        @Override
+        public Set<AccountGroup.UUID> getKnownGroups() {
+          Set<AccountGroup.UUID> g = Sets.newHashSet(groups);
+          g.retainAll(projectCache.guessRelevantGroupUUIDs());
+          return g;
+        }
+      };
     } catch (ExecutionException e) {
       log.warn(String.format("Cannot lookup membershipsOf %s in LDAP", id), e);
       return GroupMembership.EMPTY;
@@ -203,13 +225,14 @@
         LdapSchema schema = helper.getSchema(ctx);
         ParameterizedString filter = ParameterizedString.asis(
             schema.groupPattern.replace(GROUPNAME, name).toString());
-        Set<String> returnAttrs = Collections.<String>emptySet();
+        Set<String> returnAttrs =
+            new HashSet<String>(schema.groupName.getParameterNames());
         Map<String, String> params = Collections.emptyMap();
         for (String groupBase : schema.groupBases) {
           LdapQuery query = new LdapQuery(
               groupBase, schema.groupScope, filter, returnAttrs);
           for (LdapQuery.Result res : query.query(ctx, params)) {
-            out.add(groupReference(res));
+            out.add(groupReference(schema.groupName, res));
           }
         }
       } finally {
@@ -221,6 +244,8 @@
       }
     } catch (NamingException e) {
       log.warn("Cannot query LDAP for groups matching requested name", e);
+    } catch (LoginException e) {
+      log.warn("Cannot query LDAP for groups matching requested name", e);
     }
     return out;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapModule.java
index f1b15f9..88cf45b 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
@@ -60,8 +60,8 @@
         new TypeLiteral<ImmutableSet<String>>() {})
       .expireAfterWrite(1, HOURS);
 
-    bind(Realm.class).to(LdapRealm.class).in(Scopes.SINGLETON);
     bind(Helper.class);
+    bind(Realm.class).to(LdapRealm.class).in(Scopes.SINGLETON);
 
     DynamicSet.bind(binder(), GroupBackend.class).to(LdapGroupBackend.class);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapRealm.java
index 72eb7ec..fc1102e 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
@@ -56,6 +56,7 @@
 import javax.naming.Name;
 import javax.naming.NamingException;
 import javax.naming.directory.DirContext;
+import javax.security.auth.login.LoginException;
 
 @Singleton
 class LdapRealm implements Realm {
@@ -167,7 +168,7 @@
     return !readOnlyAccountFields.contains(field);
   }
 
-  private static String apply(ParameterizedString p, LdapQuery.Result m)
+  static String apply(ParameterizedString p, LdapQuery.Result m)
       throws NamingException {
     if (p == null) {
       return null;
@@ -240,6 +241,9 @@
     } catch (NamingException e) {
       log.error("Cannot query LDAP to autenticate user", e);
       throw new AuthenticationUnavailableException("Cannot query LDAP for account", e);
+    } catch (LoginException e) {
+      log.error("Cannot authenticate server via JAAS", e);
+      throw new AuthenticationUnavailableException("Cannot query LDAP for account", e);
     }
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapType.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapType.java
index 3c4a54b..db5baeb 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapType.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/ldap/LdapType.java
@@ -25,8 +25,9 @@
   static LdapType guessType(final DirContext ctx) throws NamingException {
     final Attributes rootAtts = ctx.getAttributes("");
     Attribute supported = rootAtts.get("supportedCapabilities");
-    if (supported != null && supported.contains("1.2.840.113556.1.4.800")) {
-      return new ActiveDirectory(rootAtts);
+    if (supported != null && (supported.contains("1.2.840.113556.1.4.800")
+          || supported.contains("1.2.840.113556.1.4.1851"))) {
+      return new ActiveDirectory();
     }
 
     return RFC_2307;
@@ -36,6 +37,8 @@
 
   abstract String groupMemberPattern();
 
+  abstract String groupName();
+
   abstract String accountFullName();
 
   abstract String accountEmailAddress();
@@ -58,6 +61,11 @@
     }
 
     @Override
+    String groupName() {
+      return "cn";
+    }
+
+    @Override
     String accountFullName() {
       return "displayName";
     }
@@ -84,23 +92,17 @@
   }
 
   private static class ActiveDirectory extends LdapType {
-    ActiveDirectory(final Attributes atts) throws NamingException {
-      // Convert "defaultNamingContext: DC=foo,DC=example,DC=com" into
-      // the a standard DNS name as we would expect to find in the suffix
-      // part of the userPrincipalName.
-      //
-      Attribute defaultNamingContext = atts.get("defaultNamingContext");
-      if (defaultNamingContext == null || defaultNamingContext.size() < 1) {
-        throw new NamingException("rootDSE has no defaultNamingContext");
-      }
-    }
-
     @Override
     String groupPattern() {
       return "(&(objectClass=group)(cn=${groupname}))";
     }
 
     @Override
+    String groupName() {
+      return "cn";
+    }
+
+    @Override
     String groupMemberPattern() {
       return null; // Active Directory uses memberOf in the account
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/auth/openid/OpenIdProviderPattern.java b/gerrit-server/src/main/java/com/google/gerrit/server/auth/openid/OpenIdProviderPattern.java
new file mode 100644
index 0000000..7e15f52
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/auth/openid/OpenIdProviderPattern.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.auth.openid;
+
+import com.google.gerrit.reviewdb.client.AccountExternalId;
+
+public class OpenIdProviderPattern {
+  public static OpenIdProviderPattern create(String pattern) {
+    OpenIdProviderPattern r = new OpenIdProviderPattern();
+    r.regex = pattern.startsWith("^") && pattern.endsWith("$");
+    r.pattern = pattern;
+    return r;
+  }
+
+  protected boolean regex;
+  protected String pattern;
+
+  protected OpenIdProviderPattern() {
+  }
+
+  public boolean matches(String id) {
+    return regex ? id.matches(pattern) : id.startsWith(pattern);
+  }
+
+  public boolean matches(AccountExternalId id) {
+    return matches(id.getExternalId());
+  }
+
+  @Override
+  public String toString() {
+    return pattern;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/avatar/AvatarProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/avatar/AvatarProvider.java
new file mode 100644
index 0000000..c0140d0
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/avatar/AvatarProvider.java
@@ -0,0 +1,50 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.avatar;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.server.IdentifiedUser;
+
+/**
+ * Provide avatar URLs for specified user.
+ *
+ * Invoked by Gerrit when Avatar image requests are made.
+ */
+@ExtensionPoint
+public interface AvatarProvider {
+  /**
+   * Get avatar URL.
+   *
+   * @param forUser The user for which to load an avatar image
+   * @param imageSize A requested image size, in pixels. An imageSize of 0
+   *        indicates to use whatever default size the provider determines.
+   *        AvatarProviders may ignore the requested image size. The web
+   *        interface will resize any image to match imageSize, so ideally the
+   *        provider should return an image sized correctly.
+   * @return a URL of an avatar image for the specified user. A return value of
+   *         {@code null} is acceptable, and results in the server responding
+   *         with a 404. This will hide the avatar image in the web UI.
+   */
+  public String getUrl(IdentifiedUser forUser, int imageSize);
+
+  /**
+   * Gets a URL for a user to modify their avatar image.
+   *
+   * @param user The user wishing to change their avatar image
+   * @return a URL the user should visit to modify their avatar, or null if
+   *         modification is not possible.
+   */
+  public String getChangeAvatarUrl(IdentifiedUser forUser);
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java
new file mode 100644
index 0000000..f766297
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Abandon.java
@@ -0,0 +1,148 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.DefaultInput;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ApprovalsUtil;
+import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.change.Abandon.Input;
+import com.google.gerrit.server.mail.AbandonedSender;
+import com.google.gerrit.server.mail.ReplyToChangeSender;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gwtorm.server.AtomicUpdate;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collections;
+
+public class Abandon implements RestModifyView<ChangeResource, Input> {
+  private static final Logger log = LoggerFactory.getLogger(Abandon.class);
+
+  private final ChangeHooks hooks;
+  private final AbandonedSender.Factory abandonedSenderFactory;
+  private final Provider<ReviewDb> dbProvider;
+  private final ChangeJson json;
+
+  public static class Input {
+    @DefaultInput
+    public String message;
+  }
+
+  @Inject
+  Abandon(ChangeHooks hooks,
+      AbandonedSender.Factory abandonedSenderFactory,
+      Provider<ReviewDb> dbProvider,
+      ChangeJson json) {
+    this.hooks = hooks;
+    this.abandonedSenderFactory = abandonedSenderFactory;
+    this.dbProvider = dbProvider;
+    this.json = json;
+  }
+
+  @Override
+  public Object apply(ChangeResource req, Input input)
+      throws BadRequestException, AuthException,
+      ResourceConflictException, Exception {
+    ChangeControl control = req.getControl();
+    IdentifiedUser caller = (IdentifiedUser) control.getCurrentUser();
+    Change change = req.getChange();
+    if (!control.canAbandon()) {
+      throw new AuthException("abandon not permitted");
+    } else if (!change.getStatus().isOpen()) {
+      throw new ResourceConflictException("change is " + status(change));
+    }
+
+    ChangeMessage message;
+    ReviewDb db = dbProvider.get();
+    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.setStatus(Change.Status.ABANDONED);
+              ChangeUtil.updated(change);
+              return change;
+            }
+            return null;
+          }
+        });
+      if (change == null) {
+        throw new ResourceConflictException("change is "
+            + status(db.changes().get(req.getChange().getId())));
+      }
+      message = newMessage(input, caller, change);
+      db.changeMessages().insert(Collections.singleton(message));
+      new ApprovalsUtil(db).syncChangeStatus(change);
+      db.commit();
+    } finally {
+      db.rollback();
+    }
+
+    try {
+      ReplyToChangeSender cm = abandonedSenderFactory.create(change);
+      cm.setFrom(caller.getAccountId());
+      cm.setChangeMessage(message);
+      cm.send();
+    } catch (Exception e) {
+      log.error("Cannot email update for change " + change.getChangeId(), e);
+    }
+    hooks.doChangeAbandonedHook(change,
+        caller.getAccount(),
+        Strings.emptyToNull(input.message),
+        db);
+    return json.format(change);
+  }
+
+  private ChangeMessage newMessage(Input input, IdentifiedUser caller,
+      Change change) throws OrmException {
+    StringBuilder msg = new StringBuilder();
+    msg.append("Abandoned");
+    if (!Strings.nullToEmpty(input.message).trim().isEmpty()) {
+      msg.append("\n\n");
+      msg.append(input.message.trim());
+    }
+
+    ChangeMessage message = new ChangeMessage(
+        new ChangeMessage.Key(
+            change.getId(),
+            ChangeUtil.messageUUID(dbProvider.get())),
+        caller.getAccountId(),
+        change.getLastUpdatedOn(),
+        change.currentPatchSetId());
+    message.setMessage(msg.toString());
+    return message;
+  }
+
+  private static String status(Change change) {
+    return change != null ? change.getStatus().name().toLowerCase() : "deleted";
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
new file mode 100644
index 0000000..e0c78ea
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeInserter.java
@@ -0,0 +1,76 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.common.data.LabelTypes;
+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.server.ReviewDb;
+import com.google.gerrit.server.ApprovalsUtil;
+import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.config.TrackingFooters;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.revwalk.FooterLine;
+import org.eclipse.jgit.revwalk.RevCommit;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+public class ChangeInserter {
+  private final GitReferenceUpdated gitRefUpdated;
+  private final ChangeHooks hooks;
+  private final ApprovalsUtil approvalsUtil;
+  private final TrackingFooters trackingFooters;
+
+  @Inject
+  public ChangeInserter(final GitReferenceUpdated gitRefUpdated,
+      ChangeHooks hooks, ApprovalsUtil approvalsUtil,
+      TrackingFooters trackingFooters) {
+    this.gitRefUpdated = gitRefUpdated;
+    this.hooks = hooks;
+    this.approvalsUtil = approvalsUtil;
+    this.trackingFooters = trackingFooters;
+  }
+
+  public void insertChange(ReviewDb db, Change change, PatchSet ps,
+      RevCommit commit, LabelTypes labelTypes, List<FooterLine> footerLines,
+      PatchSetInfo info, Set<Account.Id> reviewers) throws OrmException {
+
+    db.changes().beginTransaction(change.getId());
+    try {
+      ChangeUtil.insertAncestors(db, ps.getId(), commit);
+      db.patchSets().insert(Collections.singleton(ps));
+      db.changes().insert(Collections.singleton(change));
+      ChangeUtil.updateTrackingIds(db, change, trackingFooters, footerLines);
+      approvalsUtil.addReviewers(db, labelTypes, change, ps, info, reviewers,
+          Collections.<Account.Id> emptySet());
+      db.commit();
+    } finally {
+      db.rollback();
+    }
+
+    gitRefUpdated.fire(change.getProject(), ps.getRefName(), ObjectId.zeroId(),
+        commit);
+    hooks.doPatchsetCreatedHook(change, ps, db);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
new file mode 100644
index 0000000..3592bbf
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeJson.java
@@ -0,0 +1,934 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import static com.google.gerrit.common.changes.ListChangesOption.ALL_COMMITS;
+import static com.google.gerrit.common.changes.ListChangesOption.ALL_FILES;
+import static com.google.gerrit.common.changes.ListChangesOption.ALL_REVISIONS;
+import static com.google.gerrit.common.changes.ListChangesOption.CURRENT_COMMIT;
+import static com.google.gerrit.common.changes.ListChangesOption.CURRENT_FILES;
+import static com.google.gerrit.common.changes.ListChangesOption.CURRENT_REVISION;
+import static com.google.gerrit.common.changes.ListChangesOption.DETAILED_ACCOUNTS;
+import static com.google.gerrit.common.changes.ListChangesOption.DETAILED_LABELS;
+import static com.google.gerrit.common.changes.ListChangesOption.LABELS;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Objects;
+import com.google.common.base.Strings;
+import com.google.common.collect.HashBasedTable;
+import com.google.common.collect.HashMultimap;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.LinkedListMultimap;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
+import com.google.common.collect.Sets;
+import com.google.common.collect.Table;
+import com.google.gerrit.common.changes.ListChangesOption;
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.common.data.LabelTypes;
+import com.google.gerrit.common.data.LabelValue;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.common.data.PermissionRange;
+import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.extensions.restapi.Url;
+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.Patch;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.PatchSetInfo;
+import com.google.gerrit.reviewdb.client.PatchSetInfo.ParentInfo;
+import com.google.gerrit.reviewdb.client.UserIdentity;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.AnonymousUser;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountInfo;
+import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.git.LabelNormalizer;
+import com.google.gerrit.server.patch.PatchList;
+import com.google.gerrit.server.patch.PatchListCache;
+import com.google.gerrit.server.patch.PatchListEntry;
+import com.google.gerrit.server.patch.PatchListNotAvailableException;
+import com.google.gerrit.server.patch.PatchSetInfoFactory;
+import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.ssh.SshInfo;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.Singleton;
+
+import com.jcraft.jsch.HostKey;
+
+import org.eclipse.jgit.lib.Config;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.sql.Timestamp;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+import java.util.TreeMap;
+
+public class ChangeJson {
+  private static final Logger log = LoggerFactory.getLogger(ChangeJson.class);
+
+  @Singleton
+  static class Urls {
+    final String git;
+    final String http;
+
+    @Inject
+    Urls(@GerritServerConfig Config cfg) {
+      this.git = ensureSlash(cfg.getString("gerrit", null, "canonicalGitUrl"));
+      this.http = ensureSlash(cfg.getString("gerrit", null, "gitHttpUrl"));
+    }
+
+    private static String ensureSlash(String in) {
+      if (in != null && !in.endsWith("/")) {
+        return in + "/";
+      }
+      return in;
+    }
+  }
+
+  private final Provider<ReviewDb> db;
+  private final LabelNormalizer labelNormalizer;
+  private final CurrentUser user;
+  private final AnonymousUser anonymous;
+  private final IdentifiedUser.GenericFactory userFactory;
+  private final ChangeControl.GenericFactory changeControlGenericFactory;
+  private final PatchSetInfoFactory patchSetInfoFactory;
+  private final PatchListCache patchListCache;
+  private final AccountInfo.Loader.Factory accountLoaderFactory;
+  private final Provider<String> urlProvider;
+  private final Urls urls;
+  private ChangeControl.Factory changeControlUserFactory;
+  private SshInfo sshInfo;
+  private EnumSet<ListChangesOption> options;
+  private AccountInfo.Loader accountLoader;
+  private ChangeControl lastControl;
+
+  @Inject
+  ChangeJson(
+      Provider<ReviewDb> db,
+      LabelNormalizer ln,
+      CurrentUser u,
+      AnonymousUser au,
+      IdentifiedUser.GenericFactory uf,
+      ChangeControl.GenericFactory ccf,
+      PatchSetInfoFactory psi,
+      PatchListCache plc,
+      AccountInfo.Loader.Factory ailf,
+      @CanonicalWebUrl Provider<String> curl,
+      Urls urls) {
+    this.db = db;
+    this.labelNormalizer = ln;
+    this.user = u;
+    this.anonymous = au;
+    this.userFactory = uf;
+    this.changeControlGenericFactory = ccf;
+    this.patchSetInfoFactory = psi;
+    this.patchListCache = plc;
+    this.accountLoaderFactory = ailf;
+    this.urlProvider = curl;
+    this.urls = urls;
+
+    options = EnumSet.noneOf(ListChangesOption.class);
+  }
+
+  public ChangeJson addOption(ListChangesOption o) {
+    options.add(o);
+    return this;
+  }
+
+  public ChangeJson addOptions(Collection<ListChangesOption> o) {
+    options.addAll(o);
+    return this;
+  }
+
+  public ChangeJson setSshInfo(SshInfo info) {
+    sshInfo = info;
+    return this;
+  }
+
+  public ChangeJson setChangeControlFactory(ChangeControl.Factory cf) {
+    changeControlUserFactory = cf;
+    return this;
+  }
+
+  public ChangeInfo format(ChangeResource rsrc) throws OrmException {
+    return format(new ChangeData(rsrc.getControl()));
+  }
+
+  public ChangeInfo format(Change change) throws OrmException {
+    return format(new ChangeData(change));
+  }
+
+  public ChangeInfo format(Change.Id id) throws OrmException {
+    return format(new ChangeData(id));
+  }
+
+  public ChangeInfo format(ChangeData cd) throws OrmException {
+    List<ChangeData> tmp = ImmutableList.of(cd);
+    return formatList2(ImmutableList.of(tmp)).get(0).get(0);
+  }
+
+  public ChangeInfo format(RevisionResource rsrc) throws OrmException {
+    ChangeData cd = new ChangeData(rsrc.getControl());
+    cd.limitToPatchSets(ImmutableList.of(rsrc.getPatchSet().getId()));
+    return format(cd);
+  }
+
+  public List<List<ChangeInfo>> formatList2(List<List<ChangeData>> in)
+      throws OrmException {
+    accountLoader = accountLoaderFactory.create(has(DETAILED_ACCOUNTS));
+    List<List<ChangeInfo>> res = Lists.newArrayListWithCapacity(in.size());
+    for (List<ChangeData> changes : in) {
+      ChangeData.ensureChangeLoaded(db, changes);
+      ChangeData.ensureCurrentPatchSetLoaded(db, changes);
+      ChangeData.ensureCurrentApprovalsLoaded(db, changes);
+      res.add(toChangeInfo(changes));
+    }
+    accountLoader.fill();
+    return res;
+  }
+
+  private boolean has(ListChangesOption option) {
+    return options.contains(option);
+  }
+
+  private List<ChangeInfo> toChangeInfo(List<ChangeData> changes)
+      throws OrmException {
+    List<ChangeInfo> info = Lists.newArrayListWithCapacity(changes.size());
+    for (ChangeData cd : changes) {
+      info.add(toChangeInfo(cd));
+    }
+    return info;
+  }
+
+  private ChangeInfo toChangeInfo(ChangeData cd) throws OrmException {
+    ChangeInfo out = new ChangeInfo();
+    Change in = cd.change(db);
+    out.project = in.getProject().get();
+    out.branch = in.getDest().getShortName();
+    out.topic = in.getTopic();
+    out.changeId = in.getKey().get();
+    out.mergeable = in.getStatus() != Change.Status.MERGED ? in.isMergeable() : null;
+    out.subject = in.getSubject();
+    out.status = in.getStatus();
+    out.owner = accountLoader.get(in.getOwner());
+    out.created = in.getCreatedOn();
+    out.updated = in.getLastUpdatedOn();
+    out._number = in.getId().get();
+    out._sortkey = in.getSortKey();
+    out.starred = user.getStarredChanges().contains(in.getId()) ? true : null;
+    out.reviewed = in.getStatus().isOpen() && isChangeReviewed(cd) ? true : null;
+    out.labels = labelsFor(cd, has(LABELS), has(DETAILED_LABELS));
+
+    Collection<PatchSet.Id> limited = cd.getLimitedPatchSets();
+    if (out.labels != null && has(DETAILED_LABELS)) {
+      // If limited to specific patch sets but not the current patch set, don't
+      // list permitted labels, since users can't vote on those patch sets.
+      if (limited == null || limited.contains(in.currentPatchSetId())) {
+        out.permitted_labels = permittedLabels(cd);
+      }
+      out.removable_reviewers = removableReviewers(cd, out.labels.values());
+    }
+    out.finish();
+
+    if (has(ALL_REVISIONS) || has(CURRENT_REVISION) || limited != null) {
+      out.revisions = revisions(cd);
+      if (out.revisions != null) {
+        for (String commit : out.revisions.keySet()) {
+          if (out.revisions.get(commit).isCurrent) {
+            out.current_revision = commit;
+            break;
+          }
+        }
+      }
+    }
+
+    lastControl = null;
+    return out;
+  }
+
+  private ChangeControl control(ChangeData cd) throws OrmException {
+    ChangeControl ctrl = cd.changeControl();
+    if (ctrl != null && ctrl.getCurrentUser() == user) {
+      return ctrl;
+    } else if (lastControl != null
+        && cd.getId().equals(lastControl.getChange().getId())) {
+      return lastControl;
+    }
+
+    try {
+      if (changeControlUserFactory != null) {
+        ctrl = changeControlUserFactory.controlFor(cd.change(db));
+      } else {
+        ctrl = changeControlGenericFactory.controlFor(cd.change(db), user);
+      }
+    } catch (NoSuchChangeException e) {
+      return null;
+    }
+    lastControl = ctrl;
+    return ctrl;
+  }
+
+  private List<SubmitRecord> submitRecords(ChangeData cd) throws OrmException {
+    if (cd.getSubmitRecords() != null) {
+      return cd.getSubmitRecords();
+    }
+    ChangeControl ctl = control(cd);
+    if (ctl == null) {
+      return ImmutableList.of();
+    }
+    PatchSet ps = cd.currentPatchSet(db);
+    if (ps == null) {
+      return ImmutableList.of();
+    }
+    cd.setSubmitRecords(ctl.canSubmit(db.get(), ps, cd, true, false, true));
+    return cd.getSubmitRecords();
+  }
+
+  private Map<String, LabelInfo> labelsFor(ChangeData cd, boolean standard,
+      boolean detailed) throws OrmException {
+    if (!standard && !detailed) {
+      return null;
+    }
+
+    ChangeControl ctl = control(cd);
+    if (ctl == null) {
+      return null;
+    }
+
+    PatchSet ps = cd.currentPatchSet(db);
+    if (ps == null) {
+      return null;
+    }
+
+    LabelTypes labelTypes = ctl.getLabelTypes();
+    if (cd.getChange().getStatus().isOpen()) {
+      return labelsForOpenChange(cd, labelTypes, standard, detailed);
+    } else {
+      return labelsForClosedChange(cd, labelTypes, standard, detailed);
+    }
+  }
+
+  private Map<String, LabelInfo> labelsForOpenChange(ChangeData cd,
+      LabelTypes labelTypes, boolean standard, boolean detailed)
+      throws OrmException {
+    Map<String, LabelInfo> labels = initLabels(cd, labelTypes, standard);
+    if (detailed) {
+      setAllApprovals(cd, labels);
+    }
+    for (Map.Entry<String, LabelInfo> e : labels.entrySet()) {
+      LabelType type = labelTypes.byLabel(e.getKey());
+      if (type == null) {
+        continue;
+      }
+      if (standard) {
+        setRecommendedAndDisliked(cd, type, e.getValue());
+      }
+      if (detailed) {
+        setLabelValues(type, e.getValue());
+      }
+    }
+    return labels;
+  }
+
+  private Map<String, LabelInfo> initLabels(ChangeData cd,
+      LabelTypes labelTypes, boolean standard) throws OrmException {
+    // Don't use Maps.newTreeMap(Comparator) due to OpenJDK bug 100167.
+    Map<String, LabelInfo> labels =
+        new TreeMap<String, LabelInfo>(labelTypes.nameComparator());
+    for (SubmitRecord rec : submitRecords(cd)) {
+      if (rec.labels == null) {
+        continue;
+      }
+      for (SubmitRecord.Label r : rec.labels) {
+        LabelInfo p = labels.get(r.label);
+        if (p == null || p._status.compareTo(r.status) < 0) {
+          LabelInfo n = new LabelInfo();
+          n._status = r.status;
+          if (standard) {
+            switch (r.status) {
+              case OK:
+                n.approved = accountLoader.get(r.appliedBy);
+                break;
+              case REJECT:
+                n.rejected = accountLoader.get(r.appliedBy);
+                break;
+              default:
+                break;
+            }
+          }
+
+          n.optional = n._status == SubmitRecord.Label.Status.MAY ? true : null;
+          labels.put(r.label, n);
+        }
+      }
+    }
+    return labels;
+  }
+
+  private void setRecommendedAndDisliked(ChangeData cd, LabelType type,
+      LabelInfo label) throws OrmException {
+    if (label.approved != null || label.rejected != null) {
+      return;
+    }
+
+    if (type.getMin() == null || type.getMax() == null) {
+      // Unknown or misconfigured type can't have intermediate scores.
+      return;
+    }
+
+    short min = type.getMin().getValue();
+    short max = type.getMax().getValue();
+    if (-1 <= min && max <= 1) {
+      // Types with a range of -1..+1 can't have intermediate scores.
+      return;
+    }
+
+    for (PatchSetApproval psa : cd.currentApprovals(db)) {
+      short val = psa.getValue();
+      if (val != 0 && min < val && val < max && type.matches(psa)) {
+        if (0 < val) {
+          label.recommended = accountLoader.get(psa.getAccountId());
+          label.value = val != 1 ? val : null;
+        } else {
+          label.disliked = accountLoader.get(psa.getAccountId());
+          label.value = val != -1 ? val : null;
+        }
+      }
+    }
+    return;
+  }
+
+  private void setAllApprovals(ChangeData cd,
+      Map<String, LabelInfo> labels) throws OrmException {
+    ChangeControl baseCtrl = control(cd);
+    if (baseCtrl == null) {
+      return;
+    }
+
+    // All users ever added, even if they can't vote on one or all labels.
+    Set<Account.Id> allUsers = Sets.newHashSet();
+    ListMultimap<PatchSet.Id, PatchSetApproval> allApprovals =
+        cd.allApprovalsMap(db);
+    for (PatchSetApproval psa : allApprovals.values()) {
+      allUsers.add(psa.getAccountId());
+    }
+
+    List<PatchSetApproval> currentList = labelNormalizer.normalize(
+        baseCtrl, allApprovals.get(baseCtrl.getChange().currentPatchSetId()));
+    // Most recent, normalized vote on each label for the current patch set by
+    // each user (may be 0).
+    Table<Account.Id, String, PatchSetApproval> current = HashBasedTable.create(
+        allUsers.size(), baseCtrl.getLabelTypes().getLabelTypes().size());
+    for (PatchSetApproval psa : currentList) {
+      current.put(psa.getAccountId(), psa.getLabel(), psa);
+    }
+
+    for (Account.Id accountId : allUsers) {
+      IdentifiedUser user = userFactory.create(accountId);
+      ChangeControl ctl = baseCtrl.forUser(user);
+      for (Map.Entry<String, LabelInfo> e : labels.entrySet()) {
+        LabelType lt = ctl.getLabelTypes().byLabel(e.getKey());
+        if (lt == null) {
+          // Ignore submit record for undefined label; likely the submit rule
+          // author didn't intend for the label to show up in the table.
+          continue;
+        }
+        Integer value;
+        PatchSetApproval psa = current.get(accountId, lt.getName());
+        if (psa != null) {
+          value = Integer.valueOf(psa.getValue());
+        } else {
+          // Either the user cannot vote on this label, or there just wasn't a
+          // dummy approval for this label. Explicitly check whether the user
+          // can vote on this label.
+          value = labelNormalizer.canVote(ctl, lt, accountId) ? 0 : null;
+        }
+        e.getValue().addApproval(approvalInfo(accountId, value));
+      }
+    }
+  }
+
+  private Map<String, LabelInfo> labelsForClosedChange(ChangeData cd,
+      LabelTypes labelTypes, boolean standard, boolean detailed)
+      throws OrmException {
+    Set<Account.Id> allUsers = Sets.newHashSet();
+    for (PatchSetApproval psa : cd.allApprovals(db)) {
+      allUsers.add(psa.getAccountId());
+    }
+
+    Set<String> labelNames = Sets.newHashSet();
+    Multimap<Account.Id, PatchSetApproval> current = HashMultimap.create();
+    for (PatchSetApproval a : cd.currentApprovals(db)) {
+      LabelType type = labelTypes.byLabel(a.getLabelId());
+      if (type != null && a.getValue() != 0) {
+        labelNames.add(type.getName());
+        current.put(a.getAccountId(), a);
+      }
+    }
+
+    // We can only approximately reconstruct what the submit rule evaluator
+    // would have done. These should really come from a stored submit record.
+    //
+    // Don't use Maps.newTreeMap(Comparator) due to OpenJDK bug 100167.
+    Map<String, LabelInfo> labels =
+        new TreeMap<String, LabelInfo>(labelTypes.nameComparator());
+    for (String name : labelNames) {
+      LabelType type = labelTypes.byLabel(name);
+      LabelInfo li = new LabelInfo();
+      if (detailed) {
+        setLabelValues(type, li);
+      }
+      labels.put(type.getName(), li);
+    }
+
+    for (Account.Id accountId : allUsers) {
+      Map<String, ApprovalInfo> byLabel =
+          Maps.newHashMapWithExpectedSize(labels.size());
+
+      if (detailed) {
+        for (String name : labels.keySet()) {
+          ApprovalInfo ai = approvalInfo(accountId, 0);
+          byLabel.put(name, ai);
+          labels.get(name).addApproval(ai);
+        }
+      }
+      for (PatchSetApproval psa : current.get(accountId)) {
+        LabelType type = labelTypes.byLabel(psa.getLabelId());
+        if (type == null) {
+          continue;
+        }
+
+        short val = psa.getValue();
+        ApprovalInfo info = byLabel.get(type.getName());
+        if (info != null) {
+          info.value = Integer.valueOf(val);
+        }
+
+        LabelInfo li = labels.get(type.getName());
+        if (!standard || li.approved != null || li.rejected != null) {
+          continue;
+        }
+        if (val == type.getMax().getValue()) {
+          li.approved = accountLoader.get(accountId);
+        } else if (val == type.getMin().getValue()
+            // A merged change can't have been rejected.
+            && cd.getChange().getStatus() != Status.MERGED) {
+          li.rejected = accountLoader.get(accountId);
+        } else if (val > 0) {
+          li.recommended = accountLoader.get(accountId);
+          li.value = val;
+        } else if (val < 0) {
+          li.disliked = accountLoader.get(accountId);
+          li.value = val;
+        }
+      }
+    }
+    return labels;
+  }
+
+  private ApprovalInfo approvalInfo(Account.Id id, Integer value) {
+    ApprovalInfo ai = new ApprovalInfo(id);
+    ai.value = value;
+    accountLoader.put(ai);
+    return ai;
+  }
+
+  private static boolean isOnlyZero(Collection<String> values) {
+    return values.isEmpty() || (values.size() == 1 && values.contains(" 0"));
+  }
+
+  private void setLabelValues(LabelType type, LabelInfo label) {
+    label.values = Maps.newLinkedHashMap();
+    for (LabelValue v : type.getValues()) {
+      label.values.put(v.formatValue(), v.getText());
+    }
+    if (isOnlyZero(label.values.keySet())) {
+      label.values = null;
+    }
+  }
+
+  private Map<String, Collection<String>> permittedLabels(ChangeData cd)
+      throws OrmException {
+    ChangeControl ctl = control(cd);
+    if (ctl == null) {
+      return null;
+    }
+
+    LabelTypes labelTypes = ctl.getLabelTypes();
+    ListMultimap<String, String> permitted = LinkedListMultimap.create();
+    for (SubmitRecord rec : submitRecords(cd)) {
+      if (rec.labels == null) {
+        continue;
+      }
+      for (SubmitRecord.Label r : rec.labels) {
+        LabelType type = labelTypes.byLabel(r.label);
+        if (type == null) {
+          continue;
+        }
+        PermissionRange range = ctl.getRange(Permission.forLabel(r.label));
+        for (LabelValue v : type.getValues()) {
+          if (range.contains(v.getValue())) {
+            permitted.put(r.label, v.formatValue());
+          }
+        }
+      }
+    }
+    List<String> toClear =
+      Lists.newArrayListWithCapacity(permitted.keySet().size());
+    for (Map.Entry<String, Collection<String>> e
+        : permitted.asMap().entrySet()) {
+      if (isOnlyZero(e.getValue())) {
+        toClear.add(e.getKey());
+      }
+    }
+    for (String label : toClear) {
+      permitted.removeAll(label);
+    }
+    return permitted.asMap();
+  }
+
+  private Collection<AccountInfo> removableReviewers(ChangeData cd,
+      Collection<LabelInfo> labels) throws OrmException {
+    ChangeControl ctl = control(cd);
+    if (ctl == null) {
+      return null;
+    }
+
+    Set<Account.Id> fixed = Sets.newHashSetWithExpectedSize(labels.size());
+    Set<Account.Id> removable = Sets.newHashSetWithExpectedSize(labels.size());
+    for (LabelInfo label : labels) {
+      if (label.all == null) {
+        continue;
+      }
+      for (ApprovalInfo ai : label.all) {
+        if (ctl.canRemoveReviewer(ai._id, Objects.firstNonNull(ai.value, 0))) {
+          removable.add(ai._id);
+        } else {
+          fixed.add(ai._id);
+        }
+      }
+    }
+    removable.removeAll(fixed);
+
+    List<AccountInfo> result = Lists.newArrayListWithCapacity(removable.size());
+    for (Account.Id id : removable) {
+      result.add(accountLoader.get(id));
+    }
+    return result;
+  }
+
+  private boolean isChangeReviewed(ChangeData cd) throws OrmException {
+    if (user instanceof IdentifiedUser) {
+      PatchSet currentPatchSet = cd.currentPatchSet(db);
+      if (currentPatchSet == null) {
+        return false;
+      }
+
+      List<ChangeMessage> messages =
+          db.get().changeMessages().byPatchSet(currentPatchSet.getId()).toList();
+
+      if (messages.isEmpty()) {
+        return false;
+      }
+
+      // Sort messages to let the most recent ones at the beginning.
+      Collections.sort(messages, new Comparator<ChangeMessage>() {
+        @Override
+        public int compare(ChangeMessage a, ChangeMessage b) {
+          return b.getWrittenOn().compareTo(a.getWrittenOn());
+        }
+      });
+
+      Account.Id currentUserId = ((IdentifiedUser) user).getAccountId();
+      Account.Id changeOwnerId = cd.change(db).getOwner();
+      for (ChangeMessage cm : messages) {
+        if (currentUserId.equals(cm.getAuthor())) {
+          return true;
+        } else if (changeOwnerId.equals(cm.getAuthor())) {
+          return false;
+        }
+      }
+    }
+    return false;
+  }
+
+  private Map<String, RevisionInfo> revisions(ChangeData cd) throws OrmException {
+    ChangeControl ctl = control(cd);
+    if (ctl == null) {
+      return null;
+    }
+
+    Collection<PatchSet> src;
+    if (cd.getLimitedPatchSets() != null || has(ALL_REVISIONS)) {
+      src = cd.patches(db);
+    } else {
+      src = Collections.singletonList(cd.currentPatchSet(db));
+    }
+    Map<String, RevisionInfo> res = Maps.newLinkedHashMap();
+    for (PatchSet in : src) {
+      if (ctl.isPatchVisible(in, db.get())) {
+        res.put(in.getRevision().get(), toRevisionInfo(cd, in));
+      }
+    }
+    return res;
+  }
+
+  private RevisionInfo toRevisionInfo(ChangeData cd, PatchSet in)
+      throws OrmException {
+    RevisionInfo out = new RevisionInfo();
+    out.isCurrent = in.getId().equals(cd.change(db).currentPatchSetId());
+    out._number = in.getId().get();
+    out.draft = in.isDraft() ? true : null;
+    out.fetch = makeFetchMap(cd, in);
+
+    if (has(ALL_COMMITS) || (out.isCurrent && has(CURRENT_COMMIT))) {
+      try {
+        PatchSetInfo info = patchSetInfoFactory.get(db.get(), in.getId());
+        out.commit = new CommitInfo();
+        out.commit.parents = Lists.newArrayListWithCapacity(info.getParents().size());
+        out.commit.author = toGitPerson(info.getAuthor());
+        out.commit.committer = toGitPerson(info.getCommitter());
+        out.commit.subject = info.getSubject();
+        out.commit.message = info.getMessage();
+
+        for (ParentInfo parent : info.getParents()) {
+          CommitInfo i = new CommitInfo();
+          i.commit = parent.id.get();
+          i.subject = parent.shortMessage;
+          out.commit.parents.add(i);
+        }
+      } catch (PatchSetInfoNotAvailableException e) {
+        log.warn("Cannot load PatchSetInfo " + in.getId(), e);
+      }
+    }
+
+    if (has(ALL_FILES) || (out.isCurrent && has(CURRENT_FILES))) {
+      PatchList list;
+      try {
+        list = patchListCache.get(cd.change(db), in);
+      } catch (PatchListNotAvailableException e) {
+        log.warn("Cannot load PatchList " + in.getId(), e);
+        list = null;
+      }
+      if (list != null) {
+        out.files = Maps.newTreeMap();
+        for (PatchListEntry e : list.getPatches()) {
+          if (Patch.COMMIT_MSG.equals(e.getNewName())) {
+            continue;
+          }
+
+          FileInfo d = new FileInfo();
+          d.status = e.getChangeType() != Patch.ChangeType.MODIFIED
+              ? e.getChangeType().getCode()
+              : null;
+          d.oldPath = e.getOldName();
+          if (e.getPatchType() == Patch.PatchType.BINARY) {
+            d.binary = true;
+          } else {
+            d.linesInserted = e.getInsertions() > 0 ? e.getInsertions() : null;
+            d.linesDeleted = e.getDeletions() > 0 ? e.getDeletions() : null;
+          }
+
+          FileInfo o = out.files.put(e.getNewName(), d);
+          if (o != null) {
+            // This should only happen on a delete-add break created by JGit
+            // when the file was rewritten and too little content survived. Write
+            // a single record with data from both sides.
+            d.status = Patch.ChangeType.REWRITE.getCode();
+            if (o.binary != null && o.binary) {
+              d.binary = true;
+            }
+            if (o.linesInserted != null) {
+              d.linesInserted = o.linesInserted;
+            }
+            if (o.linesDeleted != null) {
+              d.linesDeleted = o.linesDeleted;
+            }
+          }
+        }
+      }
+    }
+    return out;
+  }
+
+  private Map<String, FetchInfo> makeFetchMap(ChangeData cd, PatchSet in)
+      throws OrmException {
+    Map<String, FetchInfo> r = Maps.newLinkedHashMap();
+    String refName = in.getRefName();
+    ChangeControl ctl = control(cd);
+    if (ctl != null && ctl.forUser(anonymous).isPatchVisible(in, db.get())) {
+      if (urls.git != null) {
+        r.put("git", new FetchInfo(urls.git
+            + cd.change(db).getProject().get(), refName));
+      }
+    }
+    if (urls.http != null) {
+      r.put("http", new FetchInfo(urls.http
+          + cd.change(db).getProject().get(), refName));
+    } else {
+      String http = urlProvider.get();
+      if (!Strings.isNullOrEmpty(http)) {
+        r.put("http", new FetchInfo(http
+            + cd.change(db).getProject().get(), refName));
+      }
+    }
+    if (sshInfo != null && !sshInfo.getHostKeys().isEmpty()) {
+      HostKey host = sshInfo.getHostKeys().get(0);
+      r.put("ssh", new FetchInfo(String.format(
+          "ssh://%s/%s",
+          host.getHost(), cd.change(db).getProject().get()),
+          refName));
+    }
+
+    return r;
+  }
+
+  private static GitPerson toGitPerson(UserIdentity committer) {
+    GitPerson p = new GitPerson();
+    p.name = committer.getName();
+    p.email = committer.getEmail();
+    p.date = committer.getDate();
+    p.tz = committer.getTimeZone();
+    return p;
+  }
+
+  public static class ChangeInfo {
+    final String kind = "gerritcodereview#change";
+    String id;
+    String project;
+    String branch;
+    String topic;
+    public String changeId;
+    public String subject;
+    Change.Status status;
+    Timestamp created;
+    Timestamp updated;
+    Boolean starred;
+    Boolean reviewed;
+    Boolean mergeable;
+
+    String _sortkey;
+    int _number;
+
+    AccountInfo owner;
+
+    Map<String, LabelInfo> labels;
+    Map<String, Collection<String>> permitted_labels;
+    Collection<AccountInfo> removable_reviewers;
+
+    String current_revision;
+    Map<String, RevisionInfo> revisions;
+    public Boolean _moreChanges;
+
+    void finish() {
+      id = Joiner.on('~').join(
+          Url.encode(project),
+          Url.encode(branch),
+          Url.encode(changeId));
+    }
+  }
+
+  static class RevisionInfo {
+    private transient boolean isCurrent;
+    Boolean draft;
+    int _number;
+    Map<String, FetchInfo> fetch;
+    CommitInfo commit;
+    Map<String, FileInfo> files;
+  }
+
+  static class FetchInfo {
+    String url;
+    String ref;
+
+    FetchInfo(String url, String ref) {
+      this.url = url;
+      this.ref = ref;
+    }
+  }
+
+  static class GitPerson {
+    String name;
+    String email;
+    Timestamp date;
+    int tz;
+  }
+
+  static class CommitInfo {
+    String commit;
+    List<CommitInfo> parents;
+    GitPerson author;
+    GitPerson committer;
+    String subject;
+    String message;
+  }
+
+  static class FileInfo {
+    Character status;
+    Boolean binary;
+    String oldPath;
+    Integer linesInserted;
+    Integer linesDeleted;
+  }
+
+  static class LabelInfo {
+    transient SubmitRecord.Label.Status _status;
+
+    AccountInfo approved;
+    AccountInfo rejected;
+    AccountInfo recommended;
+    AccountInfo disliked;
+    List<ApprovalInfo> all;
+
+    Map<String, String> values;
+
+    Short value;
+    Boolean optional;
+
+    void addApproval(ApprovalInfo ai) {
+      if (all == null) {
+        all = Lists.newArrayList();
+      }
+      all.add(ai);
+    }
+  }
+
+  static class ApprovalInfo extends AccountInfo {
+    Integer value;
+
+    ApprovalInfo(Account.Id id) {
+      super(id);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeMessages.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeMessages.java
new file mode 100644
index 0000000..8236d3d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeMessages.java
@@ -0,0 +1,31 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import org.eclipse.jgit.nls.NLS;
+import org.eclipse.jgit.nls.TranslationBundle;
+
+public class ChangeMessages extends TranslationBundle {
+  public static ChangeMessages get() {
+    return NLS.getBundleFor(ChangeMessages.class);
+  }
+
+  public String revertChangeDefaultMessage;
+  public String reviewerNotFound;
+
+  public String groupIsNotAllowed;
+  public String groupHasTooManyMembers;
+  public String groupManyMembersConfirmation;
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeResource.java
new file mode 100644
index 0000000..be3081a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangeResource.java
@@ -0,0 +1,44 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.gerrit.extensions.restapi.RestResource;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.inject.TypeLiteral;
+
+public class ChangeResource implements RestResource {
+  public static final TypeLiteral<RestView<ChangeResource>> CHANGE_KIND =
+      new TypeLiteral<RestView<ChangeResource>>() {};
+
+  private final ChangeControl control;
+
+  public ChangeResource(ChangeControl control) {
+    this.control = control;
+  }
+
+  protected ChangeResource(ChangeResource copy) {
+    this.control = copy.control;
+  }
+
+  public ChangeControl getControl() {
+    return control;
+  }
+
+  public Change getChange() {
+    return getControl().getChange();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangesCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangesCollection.java
new file mode 100644
index 0000000..0100ebc
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ChangesCollection.java
@@ -0,0 +1,145 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestCollection;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.extensions.restapi.Url;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.query.change.QueryChanges;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.lib.Constants;
+
+import java.io.UnsupportedEncodingException;
+import java.util.Collections;
+import java.util.List;
+
+public class ChangesCollection implements
+    RestCollection<TopLevelResource, ChangeResource> {
+  private final Provider<ReviewDb> db;
+  private final ChangeControl.Factory changeControlFactory;
+  private final Provider<QueryChanges> queryFactory;
+  private final DynamicMap<RestView<ChangeResource>> views;
+
+  @Inject
+  ChangesCollection(
+      Provider<ReviewDb> dbProvider,
+      ChangeControl.Factory changeControlFactory,
+      Provider<QueryChanges> queryFactory,
+      DynamicMap<RestView<ChangeResource>> views) {
+    this.db = dbProvider;
+    this.changeControlFactory = changeControlFactory;
+    this.queryFactory = queryFactory;
+    this.views = views;
+  }
+
+  @Override
+  public RestView<TopLevelResource> list() {
+    return queryFactory.get();
+  }
+
+  @Override
+  public DynamicMap<RestView<ChangeResource>> views() {
+    return views;
+  }
+
+  @Override
+  public ChangeResource parse(TopLevelResource root, IdString id)
+      throws ResourceNotFoundException, OrmException,
+      UnsupportedEncodingException {
+    ParsedId p = new ParsedId(id.encoded());
+    List<Change> changes = findChanges(p);
+    if (changes.size() != 1) {
+      throw new ResourceNotFoundException(id);
+    }
+
+    ChangeControl control;
+    try {
+      control = changeControlFactory.validateFor(changes.get(0));
+    } catch (NoSuchChangeException e) {
+      throw new ResourceNotFoundException(id);
+    }
+    return new ChangeResource(control);
+  }
+
+  private List<Change> findChanges(ParsedId k) throws OrmException {
+    if (k.legacyId != null) {
+      Change c = db.get().changes().get(k.legacyId);
+      if (c != null) {
+        return ImmutableList.of(c);
+      }
+      return Collections.emptyList();
+    } else if (k.project == null && k.branch == null && k.changeId != null) {
+      Change.Key id = new Change.Key(k.changeId);
+      if (id.get().length() == 41) {
+        return db.get().changes().byKey(id).toList();
+      } else {
+        return db.get().changes().byKeyRange(id, id.max()).toList();
+      }
+    }
+    return db.get().changes().byBranchKey(
+        k.branch(),
+        new Change.Key(k.changeId)).toList();
+  }
+
+  private static class ParsedId {
+    Change.Id legacyId;
+    String project;
+    String branch;
+    String changeId;
+
+    ParsedId(String id) throws ResourceNotFoundException {
+      if (id.matches("^[1-9][0-9]*$")) {
+        legacyId = Change.Id.parse(id);
+        return;
+      }
+
+      int t2 = id.lastIndexOf('~');
+      int t1 = id.lastIndexOf('~', t2 - 1);
+      if (t1 < 0 || t2 < 0) {
+        if (!id.matches("^I[0-9a-z]{4,40}$")) {
+          throw new ResourceNotFoundException(id);
+        }
+        changeId = id;
+        return;
+      }
+
+      project = Url.decode(id.substring(0, t1));
+      branch = Url.decode(id.substring(t1 + 1, t2));
+      changeId = Url.decode(id.substring(t2 + 1));
+
+      if (!branch.startsWith(Constants.R_REFS)) {
+        branch = Constants.R_HEADS + branch;
+      }
+    }
+
+    Branch.NameKey branch() {
+      return new Branch.NameKey(new Project.NameKey(project), branch);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraft.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraft.java
new file mode 100644
index 0000000..348be97
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/CreateDraft.java
@@ -0,0 +1,68 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.Url;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.client.PatchLineComment.Status;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.change.PutDraft.Input;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.util.Collections;
+
+class CreateDraft implements RestModifyView<RevisionResource, Input> {
+  private final Provider<ReviewDb> db;
+
+  @Inject
+  CreateDraft(Provider<ReviewDb> db) {
+    this.db = db;
+  }
+
+  @Override
+  public Response<GetDraft.Comment> apply(RevisionResource rsrc, Input in)
+      throws AuthException, BadRequestException, ResourceConflictException, OrmException {
+    if (Strings.isNullOrEmpty(in.path)) {
+      throw new BadRequestException("path must be non-empty");
+    } else if (in.message == null || in.message.trim().isEmpty()) {
+      throw new BadRequestException("message must be non-empty");
+    } else if (in.line != null && in.line <= 0) {
+      throw new BadRequestException("line must be > 0");
+    }
+
+    PatchLineComment c = new PatchLineComment(
+        new PatchLineComment.Key(
+            new Patch.Key(rsrc.getPatchSet().getId(), in.path),
+            ChangeUtil.messageUUID(db.get())),
+        in.line != null ? in.line : 0,
+        rsrc.getAccountId(),
+        Url.decode(in.inReplyTo));
+    c.setStatus(Status.DRAFT);
+    c.setSide(in.side == GetDraft.Side.PARENT ? (short) 0 : (short) 1);
+    c.setMessage(in.message.trim());
+    db.get().patchComments().insert(Collections.singleton(c));
+    return Response.created(new GetDraft.Comment(c));
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraft.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraft.java
new file mode 100644
index 0000000..0d5898e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteDraft.java
@@ -0,0 +1,43 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.change.DeleteDraft.Input;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.util.Collections;
+
+class DeleteDraft implements RestModifyView<DraftResource, Input> {
+  static class Input {
+  }
+
+  private final Provider<ReviewDb> db;
+
+  @Inject
+  DeleteDraft(Provider<ReviewDb> db) {
+    this.db = db;
+  }
+
+  @Override
+  public Object apply(DraftResource rsrc, Input input) throws OrmException {
+    db.get().patchComments().delete(Collections.singleton(rsrc.getComment()));
+    return Response.none();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewer.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewer.java
new file mode 100644
index 0000000..5bfffac
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DeleteReviewer.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.server.change;
+
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+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.change.DeleteReviewer.Input;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.util.List;
+
+public class DeleteReviewer implements RestModifyView<ReviewerResource, Input> {
+  public static class Input {
+  }
+
+  private final Provider<ReviewDb> dbProvider;
+
+  @Inject
+  DeleteReviewer(Provider<ReviewDb> dbProvider) {
+    this.dbProvider = dbProvider;
+  }
+
+  @Override
+  public Object apply(ReviewerResource rsrc, Input input)
+      throws AuthException, ResourceNotFoundException, OrmException {
+    ChangeControl control = rsrc.getControl();
+    Change.Id changeId = rsrc.getChange().getId();
+    ReviewDb db = dbProvider.get();
+    db.changes().beginTransaction(changeId);
+    try {
+      List<PatchSetApproval> del = Lists.newArrayList();
+      for (PatchSetApproval a : approvals(db, rsrc)) {
+        if (control.canRemoveReviewer(a)) {
+          del.add(a);
+        } else {
+          throw new AuthException("delete not permitted");
+        }
+      }
+      if (del.isEmpty()) {
+        throw new ResourceNotFoundException();
+      }
+      db.patchSetApprovals().delete(del);
+      db.commit();
+    } finally {
+      db.rollback();
+    }
+    return Response.none();
+  }
+
+  private Iterable<PatchSetApproval> approvals(ReviewDb db,
+      ReviewerResource rsrc) throws OrmException {
+    final Account.Id user = rsrc.getUser().getAccountId();
+    return Iterables.filter(
+        db.patchSetApprovals().byChange(rsrc.getChange().getId()),
+        new Predicate<PatchSetApproval>() {
+          @Override
+          public boolean apply(PatchSetApproval input) {
+            return user.equals(input.getAccountId());
+          }
+        });
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/DraftResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/DraftResource.java
new file mode 100644
index 0000000..bcd8902
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/DraftResource.java
@@ -0,0 +1,62 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.gerrit.extensions.restapi.RestResource;
+import com.google.gerrit.extensions.restapi.RestView;
+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.server.IdentifiedUser;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.inject.TypeLiteral;
+
+public class DraftResource implements RestResource {
+  public static final TypeLiteral<RestView<DraftResource>> DRAFT_KIND =
+      new TypeLiteral<RestView<DraftResource>>() {};
+
+  private final RevisionResource rev;
+  private final PatchLineComment comment;
+
+  DraftResource(RevisionResource rev, PatchLineComment c) {
+    this.rev = rev;
+    this.comment = c;
+  }
+
+  public ChangeControl getControl() {
+    return rev.getControl();
+  }
+
+  public Change getChange() {
+    return getControl().getChange();
+  }
+
+  public PatchSet getPatchSet() {
+    return rev.getPatchSet();
+  }
+
+  PatchLineComment getComment() {
+    return comment;
+  }
+
+  String getId() {
+    return comment.getKey().get();
+  }
+
+  Account.Id getAuthorId() {
+    return ((IdentifiedUser) getControl().getCurrentUser()).getAccountId();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Drafts.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Drafts.java
new file mode 100644
index 0000000..8282d7c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Drafts.java
@@ -0,0 +1,80 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ChildCollection;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+class Drafts implements ChildCollection<RevisionResource, DraftResource> {
+  private final DynamicMap<RestView<DraftResource>> views;
+  private final Provider<CurrentUser> user;
+  private final Provider<ListDrafts> list;
+  private final Provider<ReviewDb> dbProvider;
+
+  @Inject
+  Drafts(DynamicMap<RestView<DraftResource>> views,
+      Provider<CurrentUser> user,
+      Provider<ListDrafts> list,
+      Provider<ReviewDb> dbProvider) {
+    this.views = views;
+    this.user = user;
+    this.list = list;
+    this.dbProvider = dbProvider;
+  }
+
+  @Override
+  public DynamicMap<RestView<DraftResource>> views() {
+    return views;
+  }
+
+  @Override
+  public RestView<RevisionResource> list() throws AuthException {
+    checkIdentifiedUser();
+    return list.get();
+  }
+
+  @Override
+  public DraftResource parse(RevisionResource rev, IdString id)
+      throws ResourceNotFoundException, OrmException, AuthException {
+    checkIdentifiedUser();
+    String uuid = id.get();
+    for (PatchLineComment c : dbProvider.get().patchComments()
+        .draftByPatchSetAuthor(
+            rev.getPatchSet().getId(),
+            rev.getAccountId())) {
+      if (uuid.equals(c.getKey().get())) {
+        return new DraftResource(rev, c);
+      }
+    }
+    throw new ResourceNotFoundException(id);
+  }
+
+  private void checkIdentifiedUser() throws AuthException {
+    if (!(user.get() instanceof IdentifiedUser)) {
+      throw new AuthException("drafts only available to authenticated users");
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/EmailReviewComments.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/EmailReviewComments.java
new file mode 100644
index 0000000..11a9edd
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/EmailReviewComments.java
@@ -0,0 +1,173 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.common.collect.Lists;
+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.PatchLineComment;
+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.change.PostReview.NotifyHandling;
+import com.google.gerrit.server.git.WorkQueue;
+import com.google.gerrit.server.mail.CommentSender;
+import com.google.gerrit.server.patch.PatchSetInfoFactory;
+import com.google.gerrit.server.util.RequestContext;
+import com.google.gerrit.server.util.ThreadLocalRequestContext;
+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;
+import com.google.inject.assistedinject.Assisted;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+class EmailReviewComments implements Runnable, RequestContext {
+  private static final Logger log = LoggerFactory.getLogger(EmailReviewComments.class);
+
+  interface Factory {
+    EmailReviewComments create(
+        NotifyHandling notify,
+        Change change,
+        PatchSet patchSet,
+        Account.Id authorId,
+        ChangeMessage message,
+        List<PatchLineComment> comments);
+  }
+
+  private final WorkQueue workQueue;
+  private final PatchSetInfoFactory patchSetInfoFactory;
+  private final CommentSender.Factory commentSenderFactory;
+  private final SchemaFactory<ReviewDb> schemaFactory;
+  private final ThreadLocalRequestContext requestContext;
+
+  private final PostReview.NotifyHandling notify;
+  private final Change change;
+  private final PatchSet patchSet;
+  private final Account.Id authorId;
+  private final ChangeMessage message;
+  private List<PatchLineComment> comments;
+  private ReviewDb db;
+
+  @Inject
+  EmailReviewComments (
+      WorkQueue workQueue,
+      PatchSetInfoFactory patchSetInfoFactory,
+      CommentSender.Factory commentSenderFactory,
+      SchemaFactory<ReviewDb> schemaFactory,
+      ThreadLocalRequestContext requestContext,
+      @Assisted NotifyHandling notify,
+      @Assisted Change change,
+      @Assisted PatchSet patchSet,
+      @Assisted Account.Id authorId,
+      @Assisted ChangeMessage message,
+      @Assisted List<PatchLineComment> comments) {
+    this.workQueue = workQueue;
+    this.patchSetInfoFactory = patchSetInfoFactory;
+    this.commentSenderFactory = commentSenderFactory;
+    this.schemaFactory = schemaFactory;
+    this.requestContext = requestContext;
+    this.notify = notify;
+    this.change = change;
+    this.patchSet = patchSet;
+    this.authorId = authorId;
+    this.message = message;
+    this.comments = comments;
+  }
+
+  void sendAsync() {
+    workQueue.getDefaultQueue().submit(this);
+  }
+
+  @Override
+  public void run() {
+    try {
+      requestContext.setContext(this);
+
+      comments = Lists.newArrayList(comments);
+      Collections.sort(comments, new Comparator<PatchLineComment>() {
+        @Override
+        public int compare(PatchLineComment a, PatchLineComment b) {
+          int cmp = path(a).compareTo(path(b));
+          if (cmp != 0) {
+            return cmp;
+          }
+
+          // 0 is ancestor, 1 is revision. Sort ancestor first.
+          cmp = a.getSide() - b.getSide();
+          if (cmp != 0) {
+            return cmp;
+          }
+
+          return a.getLine() - b.getLine();
+        }
+
+        private String path(PatchLineComment c) {
+          return c.getKey().getParentKey().getFileName();
+        }
+      });
+
+      CommentSender cm = commentSenderFactory.create(notify, change);
+      cm.setFrom(authorId);
+      cm.setPatchSet(patchSet, patchSetInfoFactory.get(change, patchSet));
+      cm.setChangeMessage(message);
+      cm.setPatchLineComments(comments);
+      cm.send();
+    } catch (Exception e) {
+      log.error("Cannot email comments for " + patchSet.getId(), e);
+    } finally {
+      requestContext.setContext(null);
+      if (db != null) {
+        db.close();
+        db = null;
+      }
+    }
+  }
+
+  @Override
+  public String toString() {
+    return "send-email comments";
+  }
+
+  @Override
+  public CurrentUser getCurrentUser() {
+    return null;
+  }
+
+  @Override
+  public Provider<ReviewDb> getReviewDbProvider() {
+    return new Provider<ReviewDb>() {
+      @Override
+      public ReviewDb get() {
+        if (db == null) {
+          try {
+            db = schemaFactory.open();
+          } catch (OrmException e) {
+            throw new ProvisionException("Cannot open ReviewDb", e);
+          }
+        }
+        return db;
+      }
+    };
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetChange.java
new file mode 100644
index 0000000..4db5459
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetChange.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.change;
+
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+
+public class GetChange implements RestReadView<ChangeResource> {
+  private final ChangeJson json;
+
+  @Inject
+  GetChange(ChangeJson json) {
+    this.json = json;
+  }
+
+  @Override
+  public Object apply(ChangeResource rsrc) throws OrmException {
+    return json.format(rsrc);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDetail.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDetail.java
new file mode 100644
index 0000000..ae0d9e92
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDetail.java
@@ -0,0 +1,37 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.gerrit.common.changes.ListChangesOption;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+
+public class GetDetail implements RestReadView<ChangeResource> {
+  private final ChangeJson json;
+
+  @Inject
+  GetDetail(ChangeJson json) {
+    this.json = json
+        .addOption(ListChangesOption.LABELS)
+        .addOption(ListChangesOption.DETAILED_LABELS)
+        .addOption(ListChangesOption.DETAILED_ACCOUNTS);
+  }
+
+  @Override
+  public Object apply(ChangeResource rsrc) throws OrmException {
+    return json.format(rsrc);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDraft.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDraft.java
new file mode 100644
index 0000000..596659d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetDraft.java
@@ -0,0 +1,62 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.extensions.restapi.Url;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
+
+import java.sql.Timestamp;
+
+class GetDraft implements RestReadView<DraftResource> {
+  @Override
+  public Object apply(DraftResource rsrc) throws AuthException,
+      BadRequestException, ResourceConflictException, Exception {
+    return new Comment(rsrc.getComment());
+  }
+
+  static enum Side {
+    PARENT, REVISION;
+  }
+
+  static class Comment {
+    final String kind = "gerritcodereview#comment";
+    String id;
+    String path;
+    Side side;
+    Integer line;
+    String inReplyTo;
+    String message;
+    Timestamp updated;
+
+    Comment(PatchLineComment c) {
+      id = Url.encode(c.getKey().get());
+      path = c.getKey().getParentKey().getFileName();
+      if (c.getSide() == 0) {
+        side = Side.PARENT;
+      }
+      if (c.getLine() > 0) {
+        line = c.getLine();
+      }
+      inReplyTo = Url.encode(c.getParentUuid());
+      message = Strings.emptyToNull(c.getMessage());
+      updated = c.getWrittenOn();
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetReview.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetReview.java
new file mode 100644
index 0000000..997f5e7
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetReview.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.gerrit.common.changes.ListChangesOption;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+
+public class GetReview implements RestReadView<RevisionResource> {
+  private final ChangeJson json;
+
+  @Inject
+  GetReview(ChangeJson json) {
+    this.json = json.addOption(ListChangesOption.DETAILED_LABELS)
+        .addOption(ListChangesOption.DETAILED_ACCOUNTS);
+  }
+
+  @Override
+  public Object apply(RevisionResource resource) throws OrmException {
+    return json.format(resource);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetReviewer.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetReviewer.java
new file mode 100644
index 0000000..8c41be8
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetReviewer.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.change;
+
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+
+public class GetReviewer implements RestReadView<ReviewerResource> {
+  private final ReviewerJson json;
+
+  @Inject
+  GetReviewer(ReviewerJson json) {
+    this.json = json;
+  }
+
+  @Override
+  public Object apply(ReviewerResource rsrc) throws OrmException {
+    return json.format(rsrc);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/GetTopic.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetTopic.java
new file mode 100644
index 0000000..96a5c76
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/GetTopic.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gwtorm.server.OrmException;
+
+class GetTopic implements RestReadView<ChangeResource> {
+  @Override
+  public Object apply(ChangeResource rsrc) throws OrmException {
+    return Strings.nullToEmpty(rsrc.getChange().getTopic());
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListDrafts.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListDrafts.java
new file mode 100644
index 0000000..564363e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListDrafts.java
@@ -0,0 +1,80 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import static com.google.common.base.Objects.firstNonNull;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.change.GetDraft.Comment;
+import com.google.gerrit.server.change.GetDraft.Side;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import java.util.Map;
+
+class ListDrafts implements RestReadView<RevisionResource> {
+  private final Provider<ReviewDb> db;
+
+  @Inject
+  ListDrafts(Provider<ReviewDb> db) {
+    this.db = db;
+  }
+
+  @Override
+  public Object apply(RevisionResource rsrc) throws AuthException,
+      BadRequestException, ResourceConflictException, Exception {
+    Map<String, List<Comment>> out = Maps.newTreeMap();
+    for (PatchLineComment c : db.get().patchComments()
+        .draftByPatchSetAuthor(
+            rsrc.getPatchSet().getId(),
+            rsrc.getAccountId())) {
+      Comment o = new Comment(c);
+      List<Comment> list = out.get(o.path);
+      if (list == null) {
+        list = Lists.newArrayList();
+        out.put(o.path, list);
+      }
+      o.path = null;
+      list.add(o);
+    }
+    for (List<Comment> list : out.values()) {
+      Collections.sort(list, new Comparator<Comment>() {
+        @Override
+        public int compare(Comment a, Comment b) {
+          int c = firstNonNull(a.side, Side.REVISION).ordinal()
+                - firstNonNull(b.side, Side.REVISION).ordinal();
+          if (c == 0) {
+            c = firstNonNull(a.line, 0) - firstNonNull(b.line, 0);
+          }
+          if (c == 0) {
+            c = a.id.compareTo(b.id);
+          }
+          return c;
+        }
+      });
+    }
+    return out;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ListReviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListReviewers.java
new file mode 100644
index 0000000..68cc1b4
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ListReviewers.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.change;
+
+import com.google.common.collect.Maps;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+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.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.util.Map;
+
+class ListReviewers implements RestReadView<ChangeResource> {
+  private final Provider<ReviewDb> dbProvider;
+  private final ReviewerJson json;
+  private final ReviewerResource.Factory resourceFactory;
+
+  @Inject
+  ListReviewers(Provider<ReviewDb> dbProvider,
+      ReviewerResource.Factory resourceFactory,
+      ReviewerJson json) {
+    this.dbProvider = dbProvider;
+    this.resourceFactory = resourceFactory;
+    this.json = json;
+  }
+
+  @Override
+  public Object apply(ChangeResource rsrc) throws BadRequestException,
+      OrmException {
+    Map<Account.Id, ReviewerResource> reviewers = Maps.newLinkedHashMap();
+    ReviewDb db = dbProvider.get();
+    Change.Id changeId = rsrc.getChange().getId();
+    for (PatchSetApproval patchSetApproval
+         : db.patchSetApprovals().byChange(changeId)) {
+      Account.Id accountId = patchSetApproval.getAccountId();
+      if (!reviewers.containsKey(accountId)) {
+        reviewers.put(accountId, resourceFactory.create(rsrc, accountId));
+      }
+    }
+    return json.format(reviewers.values());
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.java
new file mode 100644
index 0000000..7e175a7
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Module.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.server.change;
+
+import static com.google.gerrit.server.change.ChangeResource.CHANGE_KIND;
+import static com.google.gerrit.server.change.DraftResource.DRAFT_KIND;
+import static com.google.gerrit.server.change.PatchResource.PATCH_KIND;
+import static com.google.gerrit.server.change.ReviewerResource.REVIEWER_KIND;
+import static com.google.gerrit.server.change.RevisionResource.REVISION_KIND;
+
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.RestApiModule;
+import com.google.gerrit.server.account.AccountInfo;
+import com.google.gerrit.server.change.Reviewed.DeleteReviewed;
+import com.google.gerrit.server.change.Reviewed.PutReviewed;
+import com.google.gerrit.server.config.FactoryModule;
+
+public class Module extends RestApiModule {
+  @Override
+  protected void configure() {
+    bind(Revisions.class);
+    bind(Reviewers.class);
+    bind(Drafts.class);
+    bind(Patches.class);
+
+    DynamicMap.mapOf(binder(), CHANGE_KIND);
+    DynamicMap.mapOf(binder(), DRAFT_KIND);
+    DynamicMap.mapOf(binder(), PATCH_KIND);
+    DynamicMap.mapOf(binder(), REVIEWER_KIND);
+    DynamicMap.mapOf(binder(), REVISION_KIND);
+
+    get(CHANGE_KIND).to(GetChange.class);
+    get(CHANGE_KIND, "detail").to(GetDetail.class);
+    get(CHANGE_KIND, "topic").to(GetTopic.class);
+    put(CHANGE_KIND, "topic").to(PutTopic.class);
+    delete(CHANGE_KIND, "topic").to(PutTopic.class);
+    post(CHANGE_KIND, "abandon").to(Abandon.class);
+    post(CHANGE_KIND, "restore").to(Restore.class);
+    post(CHANGE_KIND, "revert").to(Revert.class);
+    post(CHANGE_KIND, "submit").to(Submit.CurrentRevision.class);
+
+    post(CHANGE_KIND, "reviewers").to(PostReviewers.class);
+    child(CHANGE_KIND, "reviewers").to(Reviewers.class);
+    get(REVIEWER_KIND).to(GetReviewer.class);
+    delete(REVIEWER_KIND).to(DeleteReviewer.class);
+
+    child(CHANGE_KIND, "revisions").to(Revisions.class);
+    get(REVISION_KIND, "review").to(GetReview.class);
+    post(REVISION_KIND, "review").to(PostReview.class);
+    post(REVISION_KIND, "submit").to(Submit.class);
+    get(REVISION_KIND, "submit_type").to(TestSubmitType.Get.class);
+    post(REVISION_KIND, "test.submit_rule").to(TestSubmitRule.class);
+    post(REVISION_KIND, "test.submit_type").to(TestSubmitType.class);
+
+    child(REVISION_KIND, "drafts").to(Drafts.class);
+    put(REVISION_KIND, "drafts").to(CreateDraft.class);
+    get(DRAFT_KIND).to(GetDraft.class);
+    put(DRAFT_KIND).to(PutDraft.class);
+    delete(DRAFT_KIND).to(DeleteDraft.class);
+
+    child(REVISION_KIND, "files").to(Patches.class);
+    put(PATCH_KIND, "reviewed").to(PutReviewed.class);
+    delete(PATCH_KIND, "reviewed").to(DeleteReviewed.class);
+
+    install(new FactoryModule() {
+      @Override
+      protected void configure() {
+        factory(ReviewerResource.Factory.class);
+        factory(AccountInfo.Loader.Factory.class);
+        factory(EmailReviewComments.Factory.class);
+      }
+    });
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchResource.java
new file mode 100644
index 0000000..d3cf5c6
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PatchResource.java
@@ -0,0 +1,42 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.gerrit.extensions.restapi.RestResource;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.inject.TypeLiteral;
+
+public class PatchResource implements RestResource {
+  public static final TypeLiteral<RestView<PatchResource>> PATCH_KIND =
+      new TypeLiteral<RestView<PatchResource>>() {};
+
+  private final RevisionResource rev;
+  private final Patch.Key key;
+
+  PatchResource(RevisionResource rev, String name) {
+    this.rev = rev;
+    this.key = new Patch.Key(rev.getPatchSet().getId(), name);
+  }
+
+  public Patch.Key getPatchKey() {
+    return key;
+  }
+
+  Account.Id getAccountId() {
+    return rev.getAccountId();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Patches.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Patches.java
new file mode 100644
index 0000000..cb0a9bf
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Patches.java
@@ -0,0 +1,49 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ChildCollection;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+
+class Patches implements ChildCollection<RevisionResource, PatchResource> {
+  private final DynamicMap<RestView<PatchResource>> views;
+
+  @Inject
+  Patches(DynamicMap<RestView<PatchResource>> views) {
+    this.views = views;
+  }
+
+  @Override
+  public DynamicMap<RestView<PatchResource>> views() {
+    return views;
+  }
+
+  @Override
+  public RestView<RevisionResource> list() throws AuthException {
+    throw new UnsupportedOperationException();
+  }
+
+  @Override
+  public PatchResource parse(RevisionResource rev, IdString id)
+      throws ResourceNotFoundException, OrmException, AuthException {
+    return new PatchResource(rev, id.get());
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
new file mode 100644
index 0000000..66b939b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReview.java
@@ -0,0 +1,496 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.common.data.LabelTypes;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.common.data.PermissionRange;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.DefaultInput;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.Url;
+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.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.change.PostReview.Input;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.sql.Timestamp;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+
+public class PostReview implements RestModifyView<RevisionResource, Input> {
+  private static final Logger log = LoggerFactory.getLogger(PostReview.class);
+
+  public static class Input {
+    @DefaultInput
+    public String message;
+
+    public Map<String, Short> labels;
+    Map<String, List<Comment>> comments;
+
+    /**
+     * If true require all labels to be within the user's permitted ranges based
+     * on access controls, attempting to use a label not granted to the user
+     * will fail the entire modify operation early. If false the operation will
+     * execute anyway, but the proposed labels given by the user will be
+     * modified to be the "best" value allowed by the access controls, or
+     * ignored if the label does not exist.
+     */
+    public boolean strictLabels = true;
+
+    /**
+     * How to process draft comments already in the database that were not also
+     * described in this input request.
+     */
+    public DraftHandling drafts = DraftHandling.DELETE;
+
+    /** Who to send email notifications to after review is stored. */
+    public NotifyHandling notify = NotifyHandling.ALL;
+  }
+
+  public static enum DraftHandling {
+    DELETE, PUBLISH, KEEP;
+  }
+
+  public static enum NotifyHandling {
+    NONE, OWNER, OWNER_REVIEWERS, ALL;
+  }
+
+  static class Comment {
+    String id;
+    GetDraft.Side side;
+    int line;
+    String inReplyTo;
+    String message;
+  }
+
+  static class Output {
+    Map<String, Short> labels;
+  }
+
+  private final ReviewDb db;
+  private final EmailReviewComments.Factory email;
+  @Deprecated private final ChangeHooks hooks;
+
+  private Change change;
+  private ChangeMessage message;
+  private Timestamp timestamp;
+  private List<PatchLineComment> comments = Lists.newArrayList();
+  private List<String> labelDelta = Lists.newArrayList();
+  private Map<String, Short> categories = Maps.newHashMap();
+
+  @Inject
+  PostReview(ReviewDb db,
+      EmailReviewComments.Factory email,
+      ChangeHooks hooks) {
+    this.db = db;
+    this.email = email;
+    this.hooks = hooks;
+  }
+
+  @Override
+  public Object apply(RevisionResource revision, Input input)
+      throws AuthException, BadRequestException, OrmException {
+    if (input.labels != null) {
+      checkLabels(revision, input.strictLabels, input.labels);
+    }
+    if (input.comments != null) {
+      checkComments(input.comments);
+    }
+    if (input.notify == null) {
+      input.notify = NotifyHandling.NONE;
+    }
+
+    db.changes().beginTransaction(revision.getChange().getId());
+    try {
+      change = db.changes().get(revision.getChange().getId());
+      ChangeUtil.updated(change);
+      timestamp = change.getLastUpdatedOn();
+
+      boolean dirty = false;
+      dirty |= insertComments(revision, input.comments, input.drafts);
+      dirty |= updateLabels(revision, input.labels);
+      dirty |= insertMessage(revision, input.message);
+      if (dirty) {
+        db.changes().update(Collections.singleton(change));
+        db.commit();
+      }
+    } finally {
+      db.rollback();
+    }
+
+    if (input.notify.compareTo(NotifyHandling.NONE) > 0 && message != null) {
+      email.create(
+          input.notify,
+          change,
+          revision.getPatchSet(),
+          revision.getAccountId(),
+          message,
+          comments).sendAsync();
+      fireCommentAddedHook(revision);
+    }
+
+    Output output = new Output();
+    output.labels = input.labels;
+    return output;
+  }
+
+  private void checkLabels(RevisionResource revision, boolean strict,
+      Map<String, Short> labels) throws BadRequestException, AuthException {
+    ChangeControl ctl = revision.getControl();
+    Iterator<Map.Entry<String, Short>> itr = labels.entrySet().iterator();
+    while (itr.hasNext()) {
+      Map.Entry<String, Short> ent = itr.next();
+
+      LabelType lt = revision.getControl().getLabelTypes()
+          .byLabel(ent.getKey());
+      if (lt == null) {
+        if (strict) {
+          throw new BadRequestException(String.format(
+              "label \"%s\" is not a configured label", ent.getKey()));
+        } else {
+          itr.remove();
+          continue;
+        }
+      }
+
+      if (ent.getValue() == null || ent.getValue() == 0) {
+        // Always permit 0, even if it is not within range.
+        // Later null/0 will be deleted and revoke the label.
+        continue;
+      }
+
+      if (lt.getValue(ent.getValue()) == null) {
+        if (strict) {
+          throw new BadRequestException(String.format(
+              "label \"%s\": %d is not a valid value",
+              ent.getKey(), ent.getValue()));
+        } else {
+          itr.remove();
+          continue;
+        }
+      }
+
+      String name = lt.getName();
+      PermissionRange range = ctl.getRange(Permission.forLabel(name));
+      if (range == null || !range.contains(ent.getValue())) {
+        if (strict) {
+          throw new AuthException(String.format(
+              "Applying label \"%s\": %d is restricted",
+              ent.getKey(), ent.getValue()));
+        } else if (range == null || range.isEmpty()) {
+          ent.setValue((short) 0);
+        } else {
+          ent.setValue((short) range.squash(ent.getValue()));
+        }
+      }
+    }
+  }
+
+  private void checkComments(Map<String, List<Comment>> in)
+      throws BadRequestException {
+    Iterator<Map.Entry<String, List<Comment>>> mapItr =
+        in.entrySet().iterator();
+    while (mapItr.hasNext()) {
+      Map.Entry<String, List<Comment>> ent = mapItr.next();
+      String path = ent.getKey();
+      List<Comment> list = ent.getValue();
+      if (list == null) {
+        mapItr.remove();
+        continue;
+      }
+
+      Iterator<Comment> listItr = list.iterator();
+      while (listItr.hasNext()) {
+        Comment c = listItr.next();
+        if (c.line < 0) {
+          throw new BadRequestException(String.format(
+              "negative line number %d not allowed on %s",
+              c.line, path));
+        }
+        c.message = Strings.emptyToNull(c.message).trim();
+        if (c.message.isEmpty()) {
+          listItr.remove();
+        }
+      }
+      if (list.isEmpty()) {
+        mapItr.remove();
+      }
+    }
+  }
+
+  private boolean insertComments(RevisionResource rsrc,
+      Map<String, List<Comment>> in, DraftHandling draftsHandling)
+      throws OrmException {
+    if (in == null) {
+      in = Collections.emptyMap();
+    }
+
+    Map<String, PatchLineComment> drafts = Collections.emptyMap();
+    if (!in.isEmpty() || draftsHandling != DraftHandling.KEEP) {
+      drafts = scanDraftComments(rsrc);
+    }
+
+    List<PatchLineComment> del = Lists.newArrayList();
+    List<PatchLineComment> ins = Lists.newArrayList();
+    List<PatchLineComment> upd = Lists.newArrayList();
+
+    for (Map.Entry<String, List<Comment>> ent : in.entrySet()) {
+      String path = ent.getKey();
+      for (Comment c : ent.getValue()) {
+        String parent = Url.decode(c.inReplyTo);
+        PatchLineComment e = drafts.remove(Url.decode(c.id));
+        boolean create = e == null;
+        if (create) {
+          e = new PatchLineComment(
+              new PatchLineComment.Key(
+                  new Patch.Key(rsrc.getPatchSet().getId(), path),
+                  ChangeUtil.messageUUID(db)),
+              c.line,
+              rsrc.getAccountId(),
+              parent);
+        } else if (parent != null) {
+          e.setParentUuid(parent);
+        }
+        e.setStatus(PatchLineComment.Status.PUBLISHED);
+        e.setWrittenOn(timestamp);
+        e.setSide(c.side == GetDraft.Side.PARENT ? (short) 0 : (short) 1);
+        e.setMessage(c.message);
+        (create ? ins : upd).add(e);
+      }
+    }
+
+    switch (Objects.firstNonNull(draftsHandling, DraftHandling.DELETE)) {
+      case KEEP:
+      default:
+        break;
+      case DELETE:
+        del.addAll(drafts.values());
+        break;
+      case PUBLISH:
+        for (PatchLineComment e : drafts.values()) {
+          e.setStatus(PatchLineComment.Status.PUBLISHED);
+          e.setWrittenOn(timestamp);
+          upd.add(e);
+        }
+        break;
+    }
+    db.patchComments().delete(del);
+    db.patchComments().insert(ins);
+    db.patchComments().update(upd);
+    comments.addAll(ins);
+    comments.addAll(upd);
+    return !del.isEmpty() || !ins.isEmpty() || !upd.isEmpty();
+  }
+
+  private Map<String, PatchLineComment> scanDraftComments(
+      RevisionResource rsrc) throws OrmException {
+    Map<String, PatchLineComment> drafts = Maps.newHashMap();
+    for (PatchLineComment c : db.patchComments().draftByPatchSetAuthor(
+          rsrc.getPatchSet().getId(),
+          rsrc.getAccountId())) {
+      drafts.put(c.getKey().get(), c);
+    }
+    return drafts;
+  }
+
+  private boolean updateLabels(RevisionResource rsrc, Map<String, Short> labels)
+      throws OrmException {
+    if (labels == null) {
+      labels = Collections.emptyMap();
+    }
+
+    List<PatchSetApproval> del = Lists.newArrayList();
+    List<PatchSetApproval> ins = Lists.newArrayList();
+    List<PatchSetApproval> upd = Lists.newArrayList();
+    Map<String, PatchSetApproval> current = scanLabels(rsrc, del);
+
+    LabelTypes labelTypes = rsrc.getControl().getLabelTypes();
+    for (Map.Entry<String, Short> ent : labels.entrySet()) {
+      String name = ent.getKey();
+      LabelType lt = checkNotNull(labelTypes.byLabel(name), name);
+      if (change.getStatus().isClosed()) {
+        // TODO Allow updating some labels even when closed.
+        continue;
+      }
+
+      PatchSetApproval c = current.remove(name);
+      if (ent.getValue() == null || ent.getValue() == 0) {
+        // User requested delete of this label.
+        if (c != null) {
+          if (c.getValue() != 0) {
+            labelDelta.add("-" + name);
+          }
+          del.add(c);
+        }
+      } else if (c != null && c.getValue() != ent.getValue()) {
+        c.setValue(ent.getValue());
+        c.setGranted(timestamp);
+        c.cache(change);
+        upd.add(c);
+        labelDelta.add(format(name, c.getValue()));
+        categories.put(name, c.getValue());
+      } else if (c != null && c.getValue() == ent.getValue()) {
+        current.put(name, c);
+      } else if (c == null) {
+        c = new PatchSetApproval(new PatchSetApproval.Key(
+                rsrc.getPatchSet().getId(),
+                rsrc.getAccountId(),
+                lt.getLabelId()),
+            ent.getValue());
+        c.setGranted(timestamp);
+        c.cache(change);
+        ins.add(c);
+        labelDelta.add(format(name, c.getValue()));
+        categories.put(name, c.getValue());
+      }
+    }
+
+    forceCallerAsReviewer(rsrc, current, ins, upd, del);
+    db.patchSetApprovals().delete(del);
+    db.patchSetApprovals().insert(ins);
+    db.patchSetApprovals().update(upd);
+    return !del.isEmpty() || !ins.isEmpty() || !upd.isEmpty();
+  }
+
+  private void forceCallerAsReviewer(RevisionResource rsrc,
+      Map<String, PatchSetApproval> current, List<PatchSetApproval> ins,
+      List<PatchSetApproval> upd, List<PatchSetApproval> del) {
+    if (current.isEmpty() && ins.isEmpty() && upd.isEmpty()) {
+      // TODO Find another way to link reviewers to changes.
+      if (del.isEmpty()) {
+        // If no existing label is being set to 0, hack in the caller
+        // as a reviewer by picking the first server-wide LabelType.
+        PatchSetApproval c = new PatchSetApproval(new PatchSetApproval.Key(
+            rsrc.getPatchSet().getId(),
+            rsrc.getAccountId(),
+            rsrc.getControl().getLabelTypes().getLabelTypes().get(0)
+                .getLabelId()),
+            (short) 0);
+        c.setGranted(timestamp);
+        c.cache(change);
+        ins.add(c);
+      } else {
+        // Pick a random label that is about to be deleted and keep it.
+        Iterator<PatchSetApproval> i = del.iterator();
+        PatchSetApproval c = i.next();
+        c.setValue((short) 0);
+        c.setGranted(timestamp);
+        c.cache(change);
+        i.remove();
+        upd.add(c);
+      }
+    }
+  }
+
+  private Map<String, PatchSetApproval> scanLabels(RevisionResource rsrc,
+      List<PatchSetApproval> del) throws OrmException {
+    LabelTypes labelTypes = rsrc.getControl().getLabelTypes();
+    Map<String, PatchSetApproval> current = Maps.newHashMap();
+    for (PatchSetApproval a : db.patchSetApprovals().byPatchSetUser(
+          rsrc.getPatchSet().getId(), rsrc.getAccountId())) {
+      if (a.isSubmit()) {
+        continue;
+      }
+
+      LabelType lt = labelTypes.byLabel(a.getLabelId());
+      if (lt != null) {
+        current.put(lt.getName(), a);
+      } else {
+        del.add(a);
+      }
+    }
+    return current;
+  }
+
+  private static String format(String name, short value) {
+    StringBuilder sb = new StringBuilder(name.length() + 2);
+    sb.append(name);
+    if (value >= 0) {
+      sb.append('+');
+    }
+    sb.append(value);
+    return sb.toString();
+  }
+
+  private boolean insertMessage(RevisionResource rsrc, String msg)
+      throws OrmException {
+    msg = Strings.nullToEmpty(msg).trim();
+
+    StringBuilder buf = new StringBuilder();
+    for (String d : labelDelta) {
+      buf.append(" ").append(d);
+    }
+    if (comments.size() == 1) {
+      buf.append("\n\n(1 comment)");
+    } else if (comments.size() > 1) {
+      buf.append(String.format("\n\n(%d comments)", comments.size()));
+    }
+    if (!msg.isEmpty()) {
+      buf.append("\n\n").append(msg);
+    }
+    if (buf.length() == 0) {
+      return false;
+    }
+
+    message = new ChangeMessage(
+        new ChangeMessage.Key(change.getId(), ChangeUtil.messageUUID(db)),
+        rsrc.getAccountId(),
+        timestamp,
+        rsrc.getPatchSet().getId());
+    message.setMessage(String.format(
+        "Patch Set %d:%s",
+        rsrc.getPatchSet().getPatchSetId(),
+        buf.toString()));
+    db.changeMessages().insert(Collections.singleton(message));
+    return true;
+  }
+
+  @Deprecated
+  private void fireCommentAddedHook(RevisionResource rsrc) {
+    IdentifiedUser user = (IdentifiedUser) rsrc.getControl().getCurrentUser();
+    try {
+      hooks.doCommentAddedHook(change,
+          user.getAccount(),
+          rsrc.getPatchSet(),
+          message.getMessage(),
+          categories, db);
+    } catch (OrmException e) {
+      log.warn("ChangeHook.doCommentAddedHook delivery failed", e);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
new file mode 100644
index 0000000..70cf259
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PostReviewers.java
@@ -0,0 +1,290 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.common.base.Objects;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.common.data.GroupDescription;
+import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.DefaultInput;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.PatchSetApproval.LabelId;
+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.AccountInfo;
+import com.google.gerrit.server.account.AccountsCollection;
+import com.google.gerrit.server.account.GroupMembers;
+import com.google.gerrit.server.change.PostReviewers.Input;
+import com.google.gerrit.server.change.ReviewerJson.PostResult;
+import com.google.gerrit.server.change.ReviewerJson.ReviewerInfo;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.group.GroupsCollection;
+import com.google.gerrit.server.mail.AddReviewerSender;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.lib.Config;
+
+import java.text.MessageFormat;
+import java.util.List;
+import java.util.Set;
+
+public class PostReviewers implements RestModifyView<ChangeResource, Input> {
+  public final static int DEFAULT_MAX_REVIEWERS_WITHOUT_CHECK = 10;
+  public final static int DEFAULT_MAX_REVIEWERS = 20;
+
+  public static class Input {
+    @DefaultInput
+    public String reviewer;
+    Boolean confirmed;
+
+    boolean confirmed() {
+      return Objects.firstNonNull(confirmed, false);
+    }
+  }
+
+  private final AccountsCollection accounts;
+  private final ReviewerResource.Factory reviewerFactory;
+  private final AddReviewerSender.Factory addReviewerSenderFactory;
+  private final Provider<GroupsCollection> groupsCollection;
+  private final GroupMembers.Factory groupMembersFactory;
+  private final AccountInfo.Loader.Factory accountLoaderFactory;
+  private final Provider<ReviewDb> db;
+  private final IdentifiedUser currentUser;
+  private final IdentifiedUser.GenericFactory identifiedUserFactory;
+  private final Config cfg;
+  private final ChangeHooks hooks;
+  private final AccountCache accountCache;
+  private final ReviewerJson json;
+
+  @Inject
+  PostReviewers(AccountsCollection accounts,
+      ReviewerResource.Factory reviewerFactory,
+      AddReviewerSender.Factory addReviewerSenderFactory,
+      Provider<GroupsCollection> groupsCollection,
+      GroupMembers.Factory groupMembersFactory,
+      AccountInfo.Loader.Factory accountLoaderFactory,
+      Provider<ReviewDb> db,
+      IdentifiedUser currentUser,
+      IdentifiedUser.GenericFactory identifiedUserFactory,
+      @GerritServerConfig Config cfg,
+      ChangeHooks hooks,
+      AccountCache accountCache,
+      ReviewerJson json) {
+    this.accounts = accounts;
+    this.reviewerFactory = reviewerFactory;
+    this.addReviewerSenderFactory = addReviewerSenderFactory;
+    this.groupsCollection = groupsCollection;
+    this.groupMembersFactory = groupMembersFactory;
+    this.accountLoaderFactory = accountLoaderFactory;
+    this.db = db;
+    this.currentUser = currentUser;
+    this.identifiedUserFactory = identifiedUserFactory;
+    this.cfg = cfg;
+    this.hooks = hooks;
+    this.accountCache = accountCache;
+    this.json = json;
+  }
+
+  @Override
+  public PostResult apply(ChangeResource rsrc, Input input)
+      throws BadRequestException, ResourceNotFoundException, AuthException,
+      UnprocessableEntityException, OrmException, EmailException {
+    if (input.reviewer == null) {
+      throw new BadRequestException("missing reviewer field");
+    }
+
+    try {
+      Account.Id accountId = accounts.parse(input.reviewer).getAccountId();
+      return putAccount(reviewerFactory.create(rsrc, accountId));
+    } catch (UnprocessableEntityException e) {
+      try {
+        return putGroup(rsrc, input);
+      } catch (UnprocessableEntityException e2) {
+        throw new UnprocessableEntityException(MessageFormat.format(
+            ChangeMessages.get().reviewerNotFound,
+            input.reviewer));
+      }
+    }
+  }
+
+  private PostResult putAccount(ReviewerResource rsrc) throws OrmException,
+      EmailException {
+    PostResult result = new PostResult();
+    addReviewers(rsrc, result, ImmutableSet.of(rsrc.getUser()));
+    return result;
+  }
+
+  private PostResult putGroup(ChangeResource rsrc, Input input)
+      throws ResourceNotFoundException, AuthException, BadRequestException,
+      UnprocessableEntityException, OrmException, EmailException {
+    GroupDescription.Basic group = groupsCollection.get().parseInternal(input.reviewer);
+    PostResult result = new PostResult();
+    if (!isLegalReviewerGroup(group.getGroupUUID())) {
+      result.error = MessageFormat.format(
+          ChangeMessages.get().groupIsNotAllowed, group.getName());
+      return result;
+    }
+
+    Set<IdentifiedUser> reviewers = Sets.newLinkedHashSet();
+    ChangeControl control = rsrc.getControl();
+    Set<Account> members;
+    try {
+      members = groupMembersFactory.create(control.getCurrentUser()).listAccounts(
+              group.getGroupUUID(), control.getProject().getNameKey());
+    } catch (NoSuchGroupException e) {
+      throw new UnprocessableEntityException(e.getMessage());
+    } catch (NoSuchProjectException e) {
+      throw new BadRequestException(e.getMessage());
+    }
+
+    // if maxAllowed is set to 0, it is allowed to add any number of
+    // reviewers
+    int maxAllowed =
+        cfg.getInt("addreviewer", "maxAllowed", DEFAULT_MAX_REVIEWERS);
+    if (maxAllowed > 0 && members.size() > maxAllowed) {
+      result.error = MessageFormat.format(
+          ChangeMessages.get().groupHasTooManyMembers, group.getName());
+      return result;
+    }
+
+    // if maxWithoutCheck is set to 0, we never ask for confirmation
+    int maxWithoutConfirmation =
+        cfg.getInt("addreviewer", "maxWithoutConfirmation",
+            DEFAULT_MAX_REVIEWERS_WITHOUT_CHECK);
+    if (!input.confirmed() && maxWithoutConfirmation > 0
+        && members.size() > maxWithoutConfirmation) {
+      result.confirm = true;
+      result.error = MessageFormat.format(
+          ChangeMessages.get().groupManyMembersConfirmation,
+          group.getName(), members.size());
+      return result;
+    }
+
+    for (Account member : members) {
+      if (member.isActive()) {
+        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()) {
+          reviewers.add(user);
+        }
+      }
+    }
+
+    addReviewers(rsrc, result, reviewers);
+    return result;
+  }
+
+  private void addReviewers(ChangeResource rsrc, PostResult result,
+      Set<IdentifiedUser> reviewers) throws OrmException, EmailException {
+    if (reviewers.isEmpty()) {
+      result.reviewers = ImmutableList.of();
+      return;
+    }
+
+    PatchSet.Id psid = rsrc.getChange().currentPatchSetId();
+    Set<Account.Id> existing = Sets.newHashSet();
+    for (PatchSetApproval psa : db.get().patchSetApprovals().byPatchSet(psid)) {
+      existing.add(psa.getAccountId());
+    }
+
+    result.reviewers = Lists.newArrayListWithCapacity(reviewers.size());
+    List<PatchSetApproval> toInsert =
+        Lists.newArrayListWithCapacity(reviewers.size());
+    for (IdentifiedUser user : reviewers) {
+      Account.Id id = user.getAccountId();
+      if (existing.contains(id)) {
+        continue;
+      }
+      ChangeControl control = rsrc.getControl().forUser(user);
+      PatchSetApproval psa = dummyApproval(control, psid, id);
+      result.reviewers.add(json.format(
+          new ReviewerInfo(id), control, ImmutableList.of(psa)));
+      toInsert.add(psa);
+    }
+    db.get().patchSetApprovals().insert(toInsert);
+    accountLoaderFactory.create(true).fill(result.reviewers);
+    postAdd(rsrc.getChange(), result);
+  }
+
+  private void postAdd(Change change, PostResult result)
+      throws OrmException, EmailException {
+    if (result.reviewers.isEmpty()) {
+      return;
+    }
+
+    // Execute hook for added reviewers
+    //
+    PatchSet patchSet = db.get().patchSets().get(change.currentPatchSetId());
+    for (AccountInfo info : result.reviewers) {
+      Account account = accountCache.get(info._id).getAccount();
+      hooks.doReviewerAddedHook(change, account, patchSet, db.get());
+    }
+
+    // Email the reviewers
+    //
+    // The user knows they added themselves, don't bother emailing them.
+    List<Account.Id> added =
+        Lists.newArrayListWithCapacity(result.reviewers.size());
+    for (AccountInfo info : result.reviewers) {
+      if (!info._id.equals(currentUser.getAccountId())) {
+        added.add(info._id);
+      }
+    }
+    if (!added.isEmpty()) {
+      AddReviewerSender cm;
+
+      cm = addReviewerSenderFactory.create(change);
+      cm.setFrom(currentUser.getAccountId());
+      cm.addReviewers(added);
+      cm.send();
+    }
+  }
+
+  public static boolean isLegalReviewerGroup(AccountGroup.UUID groupUUID) {
+    return !(AccountGroup.ANONYMOUS_USERS.equals(groupUUID)
+             || AccountGroup.REGISTERED_USERS.equals(groupUUID));
+  }
+
+  private PatchSetApproval dummyApproval(ChangeControl ctl,
+      PatchSet.Id patchSetId, Account.Id reviewerId) {
+    LabelId id =
+        Iterables.getLast(ctl.getLabelTypes().getLabelTypes()).getLabelId();
+    PatchSetApproval dummyApproval = new PatchSetApproval(
+        new PatchSetApproval.Key(patchSetId, reviewerId, id), (short) 0);
+    dummyApproval.cache(ctl.getChange());
+    return dummyApproval;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraft.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraft.java
new file mode 100644
index 0000000..d5eaa9b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutDraft.java
@@ -0,0 +1,105 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.DefaultInput;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.Url;
+import com.google.gerrit.reviewdb.client.Patch;
+import com.google.gerrit.reviewdb.client.PatchLineComment;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.change.GetDraft.Side;
+import com.google.gerrit.server.change.PutDraft.Input;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.sql.Timestamp;
+import java.util.Collections;
+
+class PutDraft implements RestModifyView<DraftResource, Input> {
+  static class Input {
+    String kind;
+    String id;
+    String path;
+    Side side;
+    Integer line;
+    String inReplyTo;
+    Timestamp updated; // Accepted but ignored.
+
+    @DefaultInput
+    String message;
+  }
+
+  private final Provider<ReviewDb> db;
+  private final Provider<DeleteDraft> delete;
+
+  @Inject
+  PutDraft(Provider<ReviewDb> db, Provider<DeleteDraft> delete) {
+    this.db = db;
+    this.delete = delete;
+  }
+
+  @Override
+  public Object apply(DraftResource rsrc, Input in) throws AuthException,
+      BadRequestException, ResourceConflictException, OrmException {
+    PatchLineComment c = rsrc.getComment();
+    if (in == null || in.message == null || in.message.trim().isEmpty()) {
+      return delete.get().apply(rsrc, null);
+    } else if (in.kind != null && !"gerritcodereview#comment".equals(in.kind)) {
+      throw new BadRequestException("expected kind gerritcodereview#comment");
+    } else if (in.id != null && !rsrc.getId().equals(in.id)) {
+      throw new BadRequestException("id must match URL");
+    } else if (in.line != null && in.line < 0) {
+      throw new BadRequestException("line must be >= 0");
+    }
+
+    if (in.path != null
+        && !in.path.equals(c.getKey().getParentKey().getFileName())) {
+      // Updating the path alters the primary key, which isn't possible.
+      // Delete then recreate the comment instead of an update.
+      db.get().patchComments().delete(Collections.singleton(c));
+      c = new PatchLineComment(
+          new PatchLineComment.Key(
+              new Patch.Key(rsrc.getPatchSet().getId(), in.path),
+              c.getKey().get()),
+          c.getLine(),
+          rsrc.getAuthorId(),
+          c.getParentUuid());
+      db.get().patchComments().insert(Collections.singleton(update(c, in)));
+    } else {
+      db.get().patchComments().update(Collections.singleton(update(c, in)));
+    }
+    return new GetDraft.Comment(c);
+  }
+
+  private PatchLineComment update(PatchLineComment e, Input in) {
+    if (in.side != null) {
+      e.setSide(in.side == GetDraft.Side.PARENT ? (short) 0 : (short) 1);
+    }
+    if (in.line != null) {
+      e.setLine(in.line);
+    }
+    if (in.inReplyTo != null) {
+      e.setParentUuid(Url.decode(in.inReplyTo));
+    }
+    e.setMessage(in.message.trim());
+    e.updated();
+    return e;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/PutTopic.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutTopic.java
new file mode 100644
index 0000000..b96b480
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/PutTopic.java
@@ -0,0 +1,105 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.DefaultInput;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.change.PutTopic.Input;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gwtorm.server.AtomicUpdate;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.util.Collections;
+
+class PutTopic implements RestModifyView<ChangeResource, Input> {
+  private final Provider<ReviewDb> dbProvider;
+
+  static class Input {
+    @DefaultInput
+    String topic;
+    String message;
+  }
+
+  @Inject
+  PutTopic(Provider<ReviewDb> dbProvider) {
+    this.dbProvider = dbProvider;
+  }
+
+  @Override
+  public Object apply(ChangeResource req, Input input)
+      throws BadRequestException, AuthException,
+      ResourceConflictException, Exception {
+    if (input == null) {
+      input = new Input();
+    }
+
+    ChangeControl control = req.getControl();
+    Change change = req.getChange();
+    if (!control.canEditTopicName()) {
+      throw new AuthException("changing topic not permitted");
+    }
+
+    ReviewDb db = dbProvider.get();
+    final String newTopicName = Strings.nullToEmpty(input.topic);
+    String oldTopicName = Strings.nullToEmpty(change.getTopic());
+    if (!oldTopicName.equals(newTopicName)) {
+      String summary;
+      if (oldTopicName.isEmpty()) {
+        summary = "Topic set to \"" + newTopicName + "\".";
+      } else if (newTopicName.isEmpty()) {
+        summary = "Topic \"" + oldTopicName + "\" removed.";
+      } else {
+        summary = String.format(
+            "Topic updated from \"%s\" to \"%s\".",
+            oldTopicName, newTopicName);
+      }
+
+      ChangeMessage cmsg = new ChangeMessage(
+          new ChangeMessage.Key(change.getId(), ChangeUtil.messageUUID(db)),
+          ((IdentifiedUser) control.getCurrentUser()).getAccountId(),
+          change.currentPatchSetId());
+      StringBuilder msgBuf = new StringBuilder(summary);
+      if (!Strings.isNullOrEmpty(input.message)) {
+        msgBuf.append("\n\n");
+        msgBuf.append(input.message);
+      }
+      cmsg.setMessage(msgBuf.toString());
+
+      db.changes().atomicUpdate(change.getId(),
+        new AtomicUpdate<Change>() {
+          @Override
+          public Change update(Change change) {
+            change.setTopic(Strings.emptyToNull(newTopicName));
+            return change;
+          }
+        });
+      db.changeMessages().insert(Collections.singleton(cmsg));
+    }
+    return Strings.isNullOrEmpty(newTopicName)
+        ? Response.none()
+        : newTopicName;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java
new file mode 100644
index 0000000..afb58f9
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Restore.java
@@ -0,0 +1,147 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.DefaultInput;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+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.server.ReviewDb;
+import com.google.gerrit.server.ApprovalsUtil;
+import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.change.Restore.Input;
+import com.google.gerrit.server.mail.ReplyToChangeSender;
+import com.google.gerrit.server.mail.RestoredSender;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gwtorm.server.AtomicUpdate;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collections;
+
+public class Restore implements RestModifyView<ChangeResource, Input> {
+  private static final Logger log = LoggerFactory.getLogger(Restore.class);
+
+  private final ChangeHooks hooks;
+  private final RestoredSender.Factory restoredSenderFactory;
+  private final Provider<ReviewDb> dbProvider;
+  private final ChangeJson json;
+
+  public static class Input {
+    @DefaultInput
+    public String message;
+  }
+
+  @Inject
+  Restore(ChangeHooks hooks,
+      RestoredSender.Factory restoredSenderFactory,
+      Provider<ReviewDb> dbProvider,
+      ChangeJson json) {
+    this.hooks = hooks;
+    this.restoredSenderFactory = restoredSenderFactory;
+    this.dbProvider = dbProvider;
+    this.json = json;
+  }
+
+  @Override
+  public Object apply(ChangeResource req, Input input)
+      throws Exception {
+    ChangeControl control = req.getControl();
+    IdentifiedUser caller = (IdentifiedUser) control.getCurrentUser();
+    Change change = req.getChange();
+    if (!control.canRestore()) {
+      throw new AuthException("restore not permitted");
+    } else if (change.getStatus() != Status.ABANDONED) {
+      throw new ResourceConflictException("change is " + status(change));
+    }
+
+    ChangeMessage message;
+    ReviewDb db = dbProvider.get();
+    db.changes().beginTransaction(change.getId());
+    try {
+      change = db.changes().atomicUpdate(
+        change.getId(),
+        new AtomicUpdate<Change>() {
+          @Override
+          public Change update(Change change) {
+            if (change.getStatus() == Change.Status.ABANDONED) {
+              change.setStatus(Change.Status.NEW);
+              ChangeUtil.updated(change);
+              return change;
+            }
+            return null;
+          }
+        });
+      if (change == null) {
+        throw new ResourceConflictException("change is "
+            + status(db.changes().get(req.getChange().getId())));
+      }
+      message = newMessage(input, caller, change);
+      db.changeMessages().insert(Collections.singleton(message));
+      new ApprovalsUtil(db).syncChangeStatus(change);
+      db.commit();
+    } finally {
+      db.rollback();
+    }
+
+    try {
+      ReplyToChangeSender cm = restoredSenderFactory.create(change);
+      cm.setFrom(caller.getAccountId());
+      cm.setChangeMessage(message);
+      cm.send();
+    } catch (Exception e) {
+      log.error("Cannot email update for change " + change.getChangeId(), e);
+    }
+    hooks.doChangeRestoredHook(change,
+        caller.getAccount(),
+        Strings.emptyToNull(input.message),
+        dbProvider.get());
+    return json.format(change);
+  }
+
+  private ChangeMessage newMessage(Input input, IdentifiedUser caller,
+      Change change) throws OrmException {
+    StringBuilder msg = new StringBuilder();
+    msg.append("Restored");
+    if (!Strings.nullToEmpty(input.message).trim().isEmpty()) {
+      msg.append("\n\n");
+      msg.append(input.message.trim());
+    }
+
+    ChangeMessage message = new ChangeMessage(
+        new ChangeMessage.Key(
+            change.getId(),
+            ChangeUtil.messageUUID(dbProvider.get())),
+        caller.getAccountId(),
+        change.getLastUpdatedOn(),
+        change.currentPatchSetId());
+    message.setMessage(msg.toString());
+    return message;
+  }
+
+  private static String status(Change change) {
+    return change != null ? change.getStatus().name().toLowerCase() : "deleted";
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java
new file mode 100644
index 0000000..c268530
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revert.java
@@ -0,0 +1,120 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Change.Status;
+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.change.Revert.Input;
+import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.validators.CommitValidators;
+import com.google.gerrit.server.mail.RevertedSender;
+import com.google.gerrit.server.patch.PatchSetInfoFactory;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gerrit.server.ssh.NoSshInfo;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+
+import javax.annotation.Nullable;
+
+public class Revert implements RestModifyView<ChangeResource, Input> {
+  private final ChangeHooks hooks;
+  private final RevertedSender.Factory revertedSenderFactory;
+  private final CommitValidators.Factory commitValidatorsFactory;
+  private final Provider<ReviewDb> dbProvider;
+  private final ChangeJson json;
+  private final GitRepositoryManager gitManager;
+  private final PersonIdent myIdent;
+  private final PatchSetInfoFactory patchSetInfoFactory;
+  private final GitReferenceUpdated gitRefUpdated;
+  private final String canonicalWebUrl;
+
+  public static class Input {
+    public String message;
+  }
+
+  @Inject
+  Revert(ChangeHooks hooks,
+      RevertedSender.Factory revertedSenderFactory,
+      final CommitValidators.Factory commitValidatorsFactory,
+      Provider<ReviewDb> dbProvider,
+      ChangeJson json,
+      GitRepositoryManager gitManager,
+      final PatchSetInfoFactory patchSetInfoFactory,
+      final GitReferenceUpdated gitRefUpdated,
+      @GerritPersonIdent final PersonIdent myIdent,
+      @CanonicalWebUrl @Nullable final String canonicalWebUrl) {
+    this.hooks = hooks;
+    this.revertedSenderFactory = revertedSenderFactory;
+    this.commitValidatorsFactory = commitValidatorsFactory;
+    this.dbProvider = dbProvider;
+    this.json = json;
+    this.gitManager = gitManager;
+    this.myIdent = myIdent;
+    this.gitRefUpdated = gitRefUpdated;
+    this.patchSetInfoFactory = patchSetInfoFactory;
+    this.canonicalWebUrl = canonicalWebUrl;
+  }
+
+  @Override
+  public Object apply(ChangeResource req, Input input) throws Exception {
+    ChangeControl control = req.getControl();
+    Change change = req.getChange();
+    if (!control.canAddPatchSet()) {
+      throw new AuthException("revert not permitted");
+    } else if (change.getStatus() != Status.MERGED) {
+      throw new ResourceConflictException("change is " + status(change));
+    }
+
+    final Repository git = gitManager.openRepository(control.getProject().getNameKey());
+    try {
+      CommitValidators commitValidators =
+          commitValidatorsFactory.create(control.getRefControl(), new NoSshInfo(), git);
+
+      Change.Id revertedChangeId =
+          ChangeUtil.revert(control.getRefControl(), change.currentPatchSetId(),
+              (IdentifiedUser) control.getCurrentUser(),
+              commitValidators,
+              Strings.emptyToNull(input.message), dbProvider.get(),
+              revertedSenderFactory, hooks, git, patchSetInfoFactory,
+              gitRefUpdated, myIdent, canonicalWebUrl);
+
+      return json.format(revertedChangeId);
+    } catch (InvalidChangeOperationException e) {
+      throw new BadRequestException(e.getMessage());
+    } finally {
+      git.close();
+    }
+  }
+
+  private static String status(Change change) {
+    return change != null ? change.getStatus().name().toLowerCase() : "deleted";
+   }
+ }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Reviewed.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Reviewed.java
new file mode 100644
index 0000000..0898bce
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Reviewed.java
@@ -0,0 +1,84 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.reviewdb.client.AccountPatchReview;
+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.util.Collections;
+
+class Reviewed {
+  static class Input {
+  }
+
+  static class PutReviewed implements RestModifyView<PatchResource, Input> {
+    private final Provider<ReviewDb> dbProvider;
+
+    @Inject
+    PutReviewed(Provider<ReviewDb> dbProvider) {
+      this.dbProvider = dbProvider;
+    }
+
+    @Override
+    public Object apply(PatchResource resource, Input input)
+        throws OrmException {
+      ReviewDb db = dbProvider.get();
+      AccountPatchReview apr = getExisting(db, resource);
+      if (apr == null) {
+        db.accountPatchReviews().insert(
+            Collections.singleton(new AccountPatchReview(resource.getPatchKey(),
+                resource.getAccountId())));
+        return Response.created("");
+      } else {
+        return Response.ok("");
+      }
+    }
+  }
+
+  static class DeleteReviewed implements RestModifyView<PatchResource, Input> {
+    private final Provider<ReviewDb> dbProvider;
+
+    @Inject
+    DeleteReviewed(Provider<ReviewDb> dbProvider) {
+      this.dbProvider = dbProvider;
+    }
+
+    @Override
+    public Object apply(PatchResource resource, Input input)
+        throws OrmException {
+      ReviewDb db = dbProvider.get();
+      AccountPatchReview apr = getExisting(db, resource);
+      if (apr != null) {
+        db.accountPatchReviews().delete(Collections.singleton(apr));
+      }
+      return Response.none();
+    }
+  }
+
+  private static AccountPatchReview getExisting(ReviewDb db,
+      PatchResource resource) throws OrmException {
+    AccountPatchReview.Key key = new AccountPatchReview.Key(
+        resource.getPatchKey(), resource.getAccountId());
+    return db.accountPatchReviews().get(key);
+  }
+
+  private Reviewed() {
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java
new file mode 100644
index 0000000..734d524
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerJson.java
@@ -0,0 +1,145 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import static com.google.gerrit.common.data.LabelValue.formatValue;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.common.data.LabelTypes;
+import com.google.gerrit.common.data.Permission;
+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.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.account.AccountInfo;
+import com.google.gerrit.server.git.LabelNormalizer;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+import java.util.TreeMap;
+
+public class ReviewerJson {
+  private final Provider<ReviewDb> db;
+  private final LabelNormalizer labelNormalizer;
+  private final AccountInfo.Loader.Factory accountLoaderFactory;
+
+  @Inject
+  ReviewerJson(Provider<ReviewDb> db,
+      LabelNormalizer labelNormalizer,
+      AccountInfo.Loader.Factory accountLoaderFactory) {
+    this.db = db;
+    this.labelNormalizer = labelNormalizer;
+    this.accountLoaderFactory = accountLoaderFactory;
+  }
+
+  public List<ReviewerInfo> format(Collection<ReviewerResource> rsrcs)
+      throws OrmException {
+    List<ReviewerInfo> infos = Lists.newArrayListWithCapacity(rsrcs.size());
+    AccountInfo.Loader loader = accountLoaderFactory.create(true);
+    for (ReviewerResource rsrc : rsrcs) {
+      ReviewerInfo info = format(rsrc, null);
+      loader.put(info);
+      infos.add(info);
+    }
+    loader.fill();
+    return infos;
+  }
+
+  public List<ReviewerInfo> format(ReviewerResource rsrc) throws OrmException {
+    return format(ImmutableList.<ReviewerResource> of(rsrc));
+  }
+
+  public ReviewerInfo format(ReviewerInfo out, ChangeControl ctl,
+      List<PatchSetApproval> approvals) throws OrmException {
+    PatchSet.Id psId = ctl.getChange().currentPatchSetId();
+
+    if (approvals == null) {
+      approvals = ChangeData.sortApprovals(db.get().patchSetApprovals()
+          .byPatchSetUser(psId, out._id));
+    }
+    approvals = labelNormalizer.normalize(ctl, approvals);
+    LabelTypes labelTypes = ctl.getLabelTypes();
+
+    // Don't use Maps.newTreeMap(Comparator) due to OpenJDK bug 100167.
+    out.approvals = new TreeMap<String,String>(labelTypes.nameComparator());
+    for (PatchSetApproval ca : approvals) {
+      for (PermissionRange pr : ctl.getLabelRanges()) {
+        if (!pr.isEmpty()) {
+          LabelType at = labelTypes.byLabel(ca.getLabelId());
+          if (at != null) {
+            out.approvals.put(at.getName(), formatValue(ca.getValue()));
+          }
+        }
+      }
+    }
+
+    // Add dummy approvals for all permitted labels for the user even if they
+    // do not exist in the DB.
+    ChangeData cd = new ChangeData(ctl);
+    PatchSet ps = cd.currentPatchSet(db);
+    if (ps != null) {
+      for (SubmitRecord rec :
+          ctl.canSubmit(db.get(), ps, cd, true, false, true)) {
+        if (rec.labels == null) {
+          continue;
+        }
+        for (SubmitRecord.Label label : rec.labels) {
+          String name = label.label;
+          if (!out.approvals.containsKey(name)
+              && !ctl.getRange(Permission.forLabel(name)).isEmpty()) {
+            out.approvals.put(name, formatValue((short) 0));
+          }
+        }
+      }
+    }
+
+    if (out.approvals.isEmpty()) {
+      out.approvals = null;
+    }
+
+    return out;
+  }
+
+  private ReviewerInfo format(ReviewerResource rsrc,
+      List<PatchSetApproval> approvals) throws OrmException {
+    return format(new ReviewerInfo(rsrc.getUser().getAccountId()),
+        rsrc.getUserControl(), approvals);
+  }
+
+  public static class ReviewerInfo extends AccountInfo {
+    final String kind = "gerritcodereview#reviewer";
+    Map<String, String> approvals;
+
+    protected ReviewerInfo(Account.Id id) {
+      super(id);
+    }
+  }
+
+  public static class PostResult {
+    public List<ReviewerInfo> reviewers;
+    public String error;
+    Boolean confirm;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerResource.java
new file mode 100644
index 0000000..f7b5228
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/ReviewerResource.java
@@ -0,0 +1,61 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.inject.TypeLiteral;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
+
+public class ReviewerResource extends ChangeResource {
+  public static final TypeLiteral<RestView<ReviewerResource>> REVIEWER_KIND =
+      new TypeLiteral<RestView<ReviewerResource>>() {};
+
+  public static interface Factory {
+    ReviewerResource create(ChangeResource rsrc, IdentifiedUser user);
+    ReviewerResource create(ChangeResource rsrc, Account.Id id);
+  }
+
+  private final IdentifiedUser user;
+
+  @AssistedInject
+  ReviewerResource(@Assisted ChangeResource rsrc,
+      @Assisted IdentifiedUser user) {
+    super(rsrc);
+    this.user = user;
+  }
+
+  @AssistedInject
+  ReviewerResource(IdentifiedUser.GenericFactory userFactory,
+      @Assisted ChangeResource rsrc,
+      @Assisted Account.Id id) {
+    this(rsrc, userFactory.create(id));
+  }
+
+  public IdentifiedUser getUser() {
+    return user;
+  }
+
+  /**
+   * @return the control for the reviewer's user (as opposed to the caller's
+   *     user as returned by {@link #getControl()}).
+   */
+  public ChangeControl getUserControl() {
+    return getControl().forUser(user);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Reviewers.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Reviewers.java
new file mode 100644
index 0000000..cf9e089
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Reviewers.java
@@ -0,0 +1,88 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.common.collect.Sets;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ChildCollection;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+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.account.AccountsCollection;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.util.Set;
+
+public class Reviewers implements
+    ChildCollection<ChangeResource, ReviewerResource> {
+  private final DynamicMap<RestView<ReviewerResource>> views;
+  private final Provider<ReviewDb> dbProvider;
+  private final AccountsCollection accounts;
+  private final ReviewerResource.Factory resourceFactory;
+  private final Provider<ListReviewers> list;
+
+  @Inject
+  Reviewers(Provider<ReviewDb> dbProvider,
+      AccountsCollection accounts,
+      ReviewerResource.Factory resourceFactory,
+      DynamicMap<RestView<ReviewerResource>> views,
+      Provider<ListReviewers> list) {
+    this.dbProvider = dbProvider;
+    this.accounts = accounts;
+    this.resourceFactory = resourceFactory;
+    this.views = views;
+    this.list = list;
+  }
+
+  @Override
+  public DynamicMap<RestView<ReviewerResource>> views() {
+    return views;
+  }
+
+  @Override
+  public RestView<ChangeResource> list() {
+    return list.get();
+  }
+
+  @Override
+  public ReviewerResource parse(ChangeResource rsrc, IdString id)
+      throws OrmException, ResourceNotFoundException, AuthException {
+    Account.Id accountId =
+        accounts.parse(TopLevelResource.INSTANCE, id).getUser().getAccountId();
+
+    // See if the id exists as a reviewer for this change
+    if (fetchAccountIds(rsrc).contains(accountId)) {
+      return resourceFactory.create(rsrc, accountId);
+    }
+    throw new ResourceNotFoundException(id);
+  }
+
+  private Set<Account.Id> fetchAccountIds(ChangeResource rsrc)
+      throws OrmException {
+    Set<Account.Id> accountIds = Sets.newHashSet();
+    for (PatchSetApproval a
+         : dbProvider.get().patchSetApprovals().byChange(rsrc.getChange().getId())) {
+      accountIds.add(a.getAccountId());
+    }
+    return accountIds;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/RevisionResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/RevisionResource.java
new file mode 100644
index 0000000..cdd9e0f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/RevisionResource.java
@@ -0,0 +1,53 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.gerrit.extensions.restapi.RestResource;
+import com.google.gerrit.extensions.restapi.RestView;
+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.server.IdentifiedUser;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.inject.TypeLiteral;
+
+public class RevisionResource implements RestResource {
+  public static final TypeLiteral<RestView<RevisionResource>> REVISION_KIND =
+      new TypeLiteral<RestView<RevisionResource>>() {};
+
+  private final ChangeResource change;
+  private final PatchSet ps;
+
+  public RevisionResource(ChangeResource change, PatchSet ps) {
+    this.change = change;
+    this.ps = ps;
+  }
+
+  public ChangeControl getControl() {
+    return change.getControl();
+  }
+
+  public Change getChange() {
+    return getControl().getChange();
+  }
+
+  public PatchSet getPatchSet() {
+    return ps;
+  }
+
+  Account.Id getAccountId() {
+    return ((IdentifiedUser) getControl().getCurrentUser()).getAccountId();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Revisions.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revisions.java
new file mode 100644
index 0000000..68ef63c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Revisions.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.change;
+
+import com.google.common.collect.Lists;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.ChildCollection;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestView;
+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.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.util.Collections;
+import java.util.List;
+
+public class Revisions implements ChildCollection<ChangeResource, RevisionResource> {
+  private final DynamicMap<RestView<RevisionResource>> views;
+  private final Provider<ReviewDb> dbProvider;
+
+  @Inject
+  Revisions(DynamicMap<RestView<RevisionResource>> views,
+      Provider<ReviewDb> dbProvider) {
+    this.views = views;
+    this.dbProvider = dbProvider;
+  }
+
+  @Override
+  public DynamicMap<RestView<RevisionResource>> views() {
+    return views;
+  }
+
+  @Override
+  public RestView<ChangeResource> list() throws ResourceNotFoundException {
+    throw new ResourceNotFoundException();
+  }
+
+  @Override
+  public RevisionResource parse(ChangeResource change, IdString id)
+      throws ResourceNotFoundException, OrmException {
+    if (id.equals("current")) {
+      PatchSet.Id p = change.getChange().currentPatchSetId();
+      PatchSet ps = p != null ? dbProvider.get().patchSets().get(p) : null;
+      if (ps != null && visible(change, ps)) {
+        return new RevisionResource(change, ps);
+      }
+      throw new ResourceNotFoundException(id);
+    }
+    List<PatchSet> match = Lists.newArrayListWithExpectedSize(2);
+    for (PatchSet ps : find(change, id.get())) {
+      Change.Id changeId = ps.getId().getParentKey();
+      if (changeId.equals(change.getChange().getId()) && visible(change, ps)) {
+        match.add(ps);
+      }
+    }
+    if (match.size() != 1) {
+      throw new ResourceNotFoundException(id);
+    }
+    return new RevisionResource(change, match.get(0));
+  }
+
+  private boolean visible(ChangeResource change, PatchSet ps)
+      throws OrmException {
+    return change.getControl().isPatchVisible(ps, dbProvider.get());
+  }
+
+  private List<PatchSet> find(ChangeResource change, String id)
+      throws OrmException {
+    ReviewDb db = dbProvider.get();
+
+    if (id.length() < 6 && id.matches("^[1-9][0-9]{0,4}$")) {
+      // Legacy patch set number syntax.
+      PatchSet ps = dbProvider.get().patchSets().get(new PatchSet.Id(
+          change.getChange().getId(),
+          Integer.parseInt(id)));
+      if (ps != null) {
+        return Collections.singletonList(ps);
+      }
+      return Collections.emptyList();
+    } else if (id.length() < 4 || id.length() > RevId.LEN) {
+      // Require a minimum of 4 digits.
+      // Impossibly long identifier will never match.
+      return Collections.emptyList();
+    } else if (id.length() >= 8) {
+      // Commit names are rather unique. Query for the commit and later
+      // match to the change. This is most likely going to identify 1 or
+      // at most 2 patch sets to consider, which is smaller than looking
+      // for all patch sets in the change.
+      RevId revid = new RevId(id);
+      if (revid.isComplete()) {
+        return db.patchSets().byRevision(revid).toList();
+      } else {
+        return db.patchSets().byRevisionRange(revid, revid.max()).toList();
+      }
+    } else {
+      // Chance of collision rises; look at all patch sets on the change.
+      List<PatchSet> out = Lists.newArrayList();
+      for (PatchSet ps : db.patchSets().byChange(change.getChange().getId())) {
+        if (ps.getRevision() != null && ps.getRevision().get().startsWith(id)) {
+          out.add(ps);
+        }
+      }
+      return out;
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
new file mode 100644
index 0000000..c63bf5d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/Submit.java
@@ -0,0 +1,318 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import static com.google.gerrit.common.data.SubmitRecord.Status.OK;
+
+import com.google.common.base.Optional;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.PatchSetApproval.LabelId;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.ProjectUtil;
+import com.google.gerrit.server.change.Submit.Input;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MergeQueue;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gwtorm.server.AtomicUpdate;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+
+import java.io.IOException;
+import java.sql.Timestamp;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+public class Submit implements RestModifyView<RevisionResource, Input> {
+  public static class Input {
+    public boolean waitForMerge;
+  }
+
+  public enum Status {
+    SUBMITTED, MERGED;
+  }
+
+  public static class Output {
+    public Status status;
+    transient Change change;
+
+    private Output(Status s, Change c) {
+      status = s;
+      change = c;
+    }
+  }
+
+  private final Provider<ReviewDb> dbProvider;
+  private final GitRepositoryManager repoManager;
+  private final MergeQueue mergeQueue;
+
+  @Inject
+  Submit(Provider<ReviewDb> dbProvider,
+      GitRepositoryManager repoManager,
+      MergeQueue mergeQueue) {
+    this.dbProvider = dbProvider;
+    this.repoManager = repoManager;
+    this.mergeQueue = mergeQueue;
+  }
+
+  @Override
+  public Output apply(RevisionResource rsrc, Input input) throws AuthException,
+      ResourceConflictException, RepositoryNotFoundException, IOException,
+      OrmException {
+    ChangeControl control = rsrc.getControl();
+    IdentifiedUser caller = (IdentifiedUser) control.getCurrentUser();
+    Change change = rsrc.getChange();
+    if (!control.canSubmit()) {
+      throw new AuthException("submit not permitted");
+    } else if (!change.getStatus().isOpen()) {
+      throw new ResourceConflictException("change is " + status(change));
+    } else if (!ProjectUtil.branchExists(repoManager, change.getDest())) {
+      throw new ResourceConflictException(String.format(
+          "destination branch \"%s\" not found.",
+          change.getDest().get()));
+    } else if (!rsrc.getPatchSet().getId().equals(change.currentPatchSetId())) {
+      // TODO Allow submitting non-current revision by changing the current.
+      throw new ResourceConflictException(String.format(
+          "revision %s is not current revision",
+          rsrc.getPatchSet().getRevision().get()));
+    }
+
+    checkSubmitRule(rsrc);
+    change = submit(rsrc, caller);
+
+    if (input.waitForMerge) {
+      mergeQueue.merge(change.getDest());
+      change = dbProvider.get().changes().get(change.getId());
+    } else {
+      mergeQueue.schedule(change.getDest());
+    }
+
+    if (change == null) {
+      throw new ResourceConflictException("change is deleted");
+    }
+    switch (change.getStatus()) {
+      case SUBMITTED:
+        return new Output(Status.SUBMITTED, change);
+      case MERGED:
+        return new Output(Status.MERGED, change);
+      case NEW:
+        // If the merge was attempted and it failed the system usually
+        // writes a comment as a ChangeMessage and sets status to NEW.
+        // Find the relevant message and report that as the conflict.
+        final Timestamp before = rsrc.getChange().getLastUpdatedOn();
+        ChangeMessage msg = Iterables.getFirst(Iterables.filter(
+          Lists.reverse(dbProvider.get().changeMessages()
+              .byChange(change.getId())
+              .toList()),
+          new Predicate<ChangeMessage>() {
+            @Override
+            public boolean apply(ChangeMessage input) {
+              return input.getAuthor() == null
+                  && input.getWrittenOn().getTime() >= before.getTime();
+            }
+          }), null);
+        if (msg != null) {
+          throw new ResourceConflictException(msg.getMessage());
+        }
+      default:
+        throw new ResourceConflictException("change is " + status(change));
+    }
+  }
+
+  private Change submit(RevisionResource rsrc, IdentifiedUser caller)
+      throws OrmException, ResourceConflictException {
+    final Timestamp timestamp = new Timestamp(System.currentTimeMillis());
+    Change change = rsrc.getChange();
+    ReviewDb db = dbProvider.get();
+    db.changes().beginTransaction(change.getId());
+    try {
+      approve(rsrc.getPatchSet(), caller, timestamp);
+      change = db.changes().atomicUpdate(
+        change.getId(),
+        new AtomicUpdate<Change>() {
+          @Override
+          public Change update(Change change) {
+            if (change.getStatus().isOpen()) {
+              change.setStatus(Change.Status.SUBMITTED);
+              change.setLastUpdatedOn(timestamp);
+              ChangeUtil.computeSortKey(change);
+              return change;
+            }
+            return null;
+          }
+        });
+      if (change == null) {
+        throw new ResourceConflictException("change is "
+            + status(db.changes().get(rsrc.getChange().getId())));
+      }
+      db.commit();
+    } finally {
+      db.rollback();
+    }
+    return change;
+  }
+
+  private void approve(PatchSet rev, IdentifiedUser caller, Timestamp timestamp)
+      throws OrmException {
+    PatchSetApproval submit = Iterables.getFirst(Iterables.filter(
+      dbProvider.get().patchSetApprovals()
+        .byPatchSetUser(rev.getId(), caller.getAccountId()),
+      new Predicate<PatchSetApproval>() {
+        @Override
+        public boolean apply(PatchSetApproval input) {
+          return input.isSubmit();
+        }
+      }), null);
+    if (submit == null) {
+      submit = new PatchSetApproval(
+          new PatchSetApproval.Key(
+              rev.getId(),
+              caller.getAccountId(),
+              LabelId.SUBMIT),
+          (short) 1);
+    }
+    submit.setValue((short) 1);
+    submit.setGranted(timestamp);
+    dbProvider.get().patchSetApprovals().upsert(Collections.singleton(submit));
+  }
+
+  private void checkSubmitRule(RevisionResource rsrc)
+      throws ResourceConflictException {
+  List<SubmitRecord> results = rsrc.getControl().canSubmit(
+        dbProvider.get(),
+        rsrc.getPatchSet());
+    Optional<SubmitRecord> ok = findOkRecord(results);
+    if (ok.isPresent()) {
+      // Rules supplied a valid solution.
+      return;
+    } else if (results.isEmpty()) {
+      throw new IllegalStateException(String.format(
+          "ChangeControl.canSubmit returned empty list for %s in %s",
+          rsrc.getPatchSet().getId(),
+          rsrc.getChange().getProject().get()));
+    }
+
+    for (SubmitRecord record : results) {
+      switch (record.status) {
+        case CLOSED:
+          throw new ResourceConflictException("change is closed");
+
+        case RULE_ERROR:
+          throw new ResourceConflictException(String.format(
+              "rule error: %s",
+              record.errorMessage));
+
+        case NOT_READY:
+          StringBuilder msg = new StringBuilder();
+          for (SubmitRecord.Label lbl : record.labels) {
+            switch (lbl.status) {
+              case OK:
+              case MAY:
+                continue;
+
+              case REJECT:
+                if (msg.length() > 0) msg.append("; ");
+                msg.append("blocked by " + lbl.label);
+                continue;
+
+              case NEED:
+                if (msg.length() > 0) msg.append("; ");
+                msg.append("needs " + lbl.label);
+                continue;
+
+              case IMPOSSIBLE:
+                if (msg.length() > 0) msg.append("; ");
+                msg.append("needs " + lbl.label + " (check project access)");
+                continue;
+
+              default:
+                throw new IllegalStateException(String.format(
+                    "Unsupported SubmitRecord.Label %s for %s in %s",
+                    lbl.toString(),
+                    rsrc.getPatchSet().getId(),
+                    rsrc.getChange().getProject().get()));
+            }
+          }
+          throw new ResourceConflictException(msg.toString());
+
+        default:
+          throw new IllegalStateException(String.format(
+              "Unsupported SubmitRecord %s for %s in %s",
+              record,
+              rsrc.getPatchSet().getId(),
+              rsrc.getChange().getProject().get()));
+      }
+    }
+  }
+
+  private static Optional<SubmitRecord> findOkRecord(Collection<SubmitRecord> in) {
+    return Iterables.tryFind(in, new Predicate<SubmitRecord>() {
+      @Override
+      public boolean apply(SubmitRecord input) {
+        return input.status == OK;
+      }
+    });
+  }
+
+  private static String status(Change change) {
+    return change != null ? change.getStatus().name().toLowerCase() : "deleted";
+  }
+
+  public static class CurrentRevision implements
+      RestModifyView<ChangeResource, Input> {
+    private final Provider<ReviewDb> dbProvider;
+    private final Submit submit;
+    private final ChangeJson json;
+
+    @Inject
+    CurrentRevision(Provider<ReviewDb> dbProvider,
+        Submit submit,
+        ChangeJson json) {
+      this.dbProvider = dbProvider;
+      this.submit = submit;
+      this.json = json;
+    }
+
+    @Override
+    public Object apply(ChangeResource rsrc, Input input) throws AuthException,
+        ResourceConflictException, RepositoryNotFoundException, IOException,
+        OrmException {
+      PatchSet ps = dbProvider.get().patchSets()
+        .get(rsrc.getChange().currentPatchSetId());
+      if (ps == null) {
+        throw new ResourceConflictException("current revision is missing");
+      } else if (!rsrc.getControl().isPatchVisible(ps, dbProvider.get())) {
+        throw new AuthException("current revision not accessible");
+      }
+      Output out = submit.apply(new RevisionResource(rsrc, ps), input);
+      return json.format(out.change);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitRule.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitRule.java
new file mode 100644
index 0000000..db18f0d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitRule.java
@@ -0,0 +1,198 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.common.base.Charsets;
+import com.google.common.base.Function;
+import com.google.common.base.Joiner;
+import com.google.common.base.Objects;
+import com.google.common.base.Throwables;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.DefaultInput;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.rules.RulesCache;
+import com.google.gerrit.server.account.AccountInfo;
+import com.google.gerrit.server.change.TestSubmitRule.Input;
+import com.google.gerrit.server.project.RuleEvalException;
+import com.google.gerrit.server.project.SubmitRuleEvaluator;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+
+import com.googlecode.prolog_cafe.lang.Term;
+
+import org.kohsuke.args4j.Option;
+
+import java.io.ByteArrayInputStream;
+import java.util.List;
+import java.util.Map;
+
+public class TestSubmitRule implements RestModifyView<RevisionResource, Input> {
+  public enum Filters {
+    RUN, SKIP;
+  }
+
+  public static class Input {
+    @DefaultInput
+    public String rule;
+    public Filters filters;
+  }
+
+  private final ReviewDb db;
+  private final RulesCache rules;
+  private final AccountInfo.Loader.Factory accountInfoFactory;
+
+  @Option(name = "--filters", usage = "impact of filters in parent projects")
+  private Filters filters = Filters.RUN;
+
+  @Inject
+  TestSubmitRule(ReviewDb db, RulesCache rules,
+      AccountInfo.Loader.Factory infoFactory) {
+    this.db = db;
+    this.rules = rules;
+    this.accountInfoFactory = infoFactory;
+  }
+
+  @Override
+  public Object apply(RevisionResource rsrc, Input input) throws OrmException,
+      BadRequestException, AuthException {
+    if (input == null) {
+      input = new Input();
+    }
+    if (input.rule != null && !rules.isProjectRulesEnabled()) {
+      throw new AuthException("project rules are disabled");
+    }
+    input.filters = Objects.firstNonNull(input.filters, filters);
+
+    SubmitRuleEvaluator evaluator = new SubmitRuleEvaluator(
+        db,
+        rsrc.getPatchSet(),
+        rsrc.getControl().getProjectControl(),
+        rsrc.getControl(),
+        rsrc.getChange(),
+        new ChangeData(rsrc.getChange()),
+        false,
+        "locate_submit_rule", "can_submit",
+        "locate_submit_filter", "filter_submit_results",
+        input.filters == Filters.SKIP,
+        input.rule != null
+          ? new ByteArrayInputStream(input.rule.getBytes(Charsets.UTF_8))
+          : null);
+
+    List<Term> results;
+    try {
+      results = eval(evaluator);
+    } catch (RuleEvalException e) {
+      String msg = Joiner.on(": ").skipNulls().join(Iterables.transform(
+          Throwables.getCausalChain(e),
+          new Function<Throwable, String>() {
+            @Override
+            public String apply(Throwable in) {
+              return in.getMessage();
+            }
+          }));
+      throw new BadRequestException("rule failed: " + msg);
+    }
+    if (results.isEmpty()) {
+      throw new BadRequestException(String.format(
+          "rule %s has no solutions",
+          evaluator.getSubmitRule().toString()));
+    }
+
+    List<SubmitRecord> records = rsrc.getControl().resultsToSubmitRecord(
+        evaluator.getSubmitRule(),
+        results);
+    List<Record> out = Lists.newArrayListWithCapacity(records.size());
+    AccountInfo.Loader accounts = accountInfoFactory.create(true);
+    for (SubmitRecord r : records) {
+      out.add(new Record(r, accounts));
+    }
+    accounts.fill();
+    return out;
+  }
+
+  private static List<Term> eval(SubmitRuleEvaluator evaluator)
+      throws RuleEvalException {
+    return evaluator.evaluate();
+  }
+
+  static class Record {
+    SubmitRecord.Status status;
+    String errorMessage;
+    Map<String, AccountInfo> ok;
+    Map<String, AccountInfo> reject;
+    Map<String, None> need;
+    Map<String, AccountInfo> may;
+    Map<String, None> impossible;
+
+    Record(SubmitRecord r, AccountInfo.Loader accounts) {
+      this.status = r.status;
+      this.errorMessage = r.errorMessage;
+
+      if (r.labels != null) {
+        for (SubmitRecord.Label n : r.labels) {
+          AccountInfo who = n.appliedBy != null
+              ? accounts.get(n.appliedBy)
+              : new AccountInfo(null);
+          label(n, who);
+        }
+      }
+    }
+
+    private void label(SubmitRecord.Label n, AccountInfo who) {
+      switch (n.status) {
+        case OK:
+          if (ok == null) {
+            ok = Maps.newLinkedHashMap();
+          }
+          ok.put(n.label, who);
+          break;
+        case REJECT:
+          if (reject == null) {
+            reject = Maps.newLinkedHashMap();
+          }
+          reject.put(n.label, who);
+          break;
+        case NEED:
+          if (need == null) {
+            need = Maps.newLinkedHashMap();
+          }
+          need.put(n.label, new None());
+          break;
+        case MAY:
+          if (may == null) {
+            may = Maps.newLinkedHashMap();
+          }
+          may.put(n.label, who);
+          break;
+        case IMPOSSIBLE:
+          if (impossible == null) {
+            impossible = Maps.newLinkedHashMap();
+          }
+          impossible.put(n.label, new None());
+          break;
+      }
+    }
+  }
+
+  static class None {
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitType.java b/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitType.java
new file mode 100644
index 0000000..e7e1f32
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/change/TestSubmitType.java
@@ -0,0 +1,116 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.change;
+
+import com.google.common.base.Charsets;
+import com.google.common.base.Objects;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.rules.RulesCache;
+import com.google.gerrit.server.change.TestSubmitRule.Filters;
+import com.google.gerrit.server.change.TestSubmitRule.Input;
+import com.google.gerrit.server.project.RuleEvalException;
+import com.google.gerrit.server.project.SubmitRuleEvaluator;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+
+import com.googlecode.prolog_cafe.lang.Term;
+
+import org.kohsuke.args4j.Option;
+
+import java.io.ByteArrayInputStream;
+import java.util.List;
+
+public class TestSubmitType implements RestModifyView<RevisionResource, Input> {
+  private final ReviewDb db;
+  private final RulesCache rules;
+
+  @Option(name = "--filters", usage = "impact of filters in parent projects")
+  private Filters filters = Filters.RUN;
+
+  @Inject
+  TestSubmitType(ReviewDb db, RulesCache rules) {
+    this.db = db;
+    this.rules = rules;
+  }
+
+  @Override
+  public String apply(RevisionResource rsrc, Input input) throws OrmException,
+      BadRequestException, AuthException {
+    if (input == null) {
+      input = new Input();
+    }
+    if (input.rule != null && !rules.isProjectRulesEnabled()) {
+      throw new AuthException("project rules are disabled");
+    }
+    input.filters = Objects.firstNonNull(input.filters, filters);
+
+    SubmitRuleEvaluator evaluator = new SubmitRuleEvaluator(
+        db,
+        rsrc.getPatchSet(),
+        rsrc.getControl().getProjectControl(),
+        rsrc.getControl(),
+        rsrc.getChange(),
+        new ChangeData(rsrc.getChange()),
+        false,
+        "locate_submit_type", "get_submit_type",
+        "locate_submit_type_filter", "filter_submit_type_results",
+        input.filters == Filters.SKIP,
+        input.rule != null
+          ? new ByteArrayInputStream(input.rule.getBytes(Charsets.UTF_8))
+          : null);
+
+    List<Term> results;
+    try {
+      results = evaluator.evaluate();
+    } catch (RuleEvalException e) {
+      throw new BadRequestException(String.format(
+          "rule failed with exception: %s",
+          e.getMessage()));
+    }
+    if (results.isEmpty()) {
+      throw new BadRequestException(String.format(
+          "rule %s has no solution",
+          evaluator.getSubmitRule()));
+    }
+    Term type = results.get(0);
+    if (!type.isSymbol()) {
+      throw new BadRequestException(String.format(
+          "rule %s produced invalid result: %s",
+          evaluator.getSubmitRule().toString(),
+          type));
+    }
+    return type.toString();
+  }
+
+  static class Get implements RestReadView<RevisionResource> {
+    private final TestSubmitType test;
+
+    @Inject
+    Get(TestSubmitType test) {
+      this.test = test;
+    }
+
+    @Override
+    public String apply(RevisionResource resource) throws BadRequestException,
+        OrmException, AuthException {
+      return test.apply(resource, null);
+    }
+  }
+}
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
deleted file mode 100644
index dca4a83..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/AbandonChange.java
+++ /dev/null
@@ -1,139 +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.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 java.util.concurrent.Callable;
-
-import org.kohsuke.args4j.Argument;
-import org.kohsuke.args4j.Option;
-
-public class AbandonChange implements Callable<ReviewResult> {
-
-  private final AbandonedSender.Factory abandonedSenderFactory;
-  private final ChangeControl.Factory changeControlFactory;
-  private final ReviewDb db;
-  private final IdentifiedUser currentUser;
-  private final ChangeHooks hooks;
-
-  @Argument(index = 0, required = true, multiValued = false, usage = "change to abandon")
-  private Change.Id changeId;
-
-  public void setChangeId(final Change.Id changeId) {
-    this.changeId = changeId;
-  }
-
-  @Option(name = "--message", aliases = {"-m"},
-          usage = "optional message to append to change")
-  private String message;
-
-  public void setMessage(final String message) {
-    this.message = message;
-  }
-
-  @Inject
-  AbandonChange(final AbandonedSender.Factory abandonedSenderFactory,
-      final ChangeControl.Factory changeControlFactory, final ReviewDb db,
-      final IdentifiedUser currentUser, final ChangeHooks hooks) {
-    this.abandonedSenderFactory = abandonedSenderFactory;
-    this.changeControlFactory = changeControlFactory;
-    this.db = db;
-    this.currentUser = currentUser;
-    this.hooks = hooks;
-
-    changeId = null;
-    message = null;
-  }
-
-  @Override
-  public ReviewResult call() throws EmailException,
-      InvalidChangeOperationException, NoSuchChangeException, OrmException {
-    if (changeId == null) {
-      throw new InvalidChangeOperationException("changeId is required");
-    }
-
-    final ReviewResult result = new ReviewResult();
-    result.setChangeId(changeId);
-
-    final ChangeControl control = changeControlFactory.validateFor(changeId);
-    final Change change = db.changes().get(changeId);
-    final PatchSet.Id patchSetId = change.currentPatchSetId();
-    final PatchSet patch = db.patchSets().get(patchSetId);
-    if (!control.canAbandon()) {
-      result.addError(new ReviewResult.Error(
-          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 (message != null && message.length() > 0) {
-        msgBuf.append("\n\n");
-        msgBuf.append(message);
-      }
-      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.setStatus(Change.Status.ABANDONED);
-            ChangeUtil.updated(change);
-            return change;
-          } else {
-            return null;
-          }
-        }
-      });
-
-      if (updatedChange == null) {
-        result.addError(new ReviewResult.Error(
-            ReviewResult.Error.Type.CHANGE_IS_CLOSED));
-        return result;
-      }
-
-      ChangeUtil.updatedChange(db, currentUser, updatedChange, cmsg,
-                               abandonedSenderFactory);
-      hooks.doChangeAbandonedHook(updatedChange, currentUser.getAccount(),
-                                  message, db);
-    }
-
-    return result;
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/DeleteDraftPatchSet.java b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/DeleteDraftPatchSet.java
index f466231..adec292 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/DeleteDraftPatchSet.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/DeleteDraftPatchSet.java
@@ -44,7 +44,7 @@
   private final ChangeControl.Factory changeControlFactory;
   private final ReviewDb db;
   private final GitRepositoryManager gitManager;
-  private final GitReferenceUpdated replication;
+  private final GitReferenceUpdated gitRefUpdated;
   private final PatchSetInfoFactory patchSetInfoFactory;
 
   private final PatchSet.Id patchSetId;
@@ -52,12 +52,12 @@
   @Inject
   DeleteDraftPatchSet(ChangeControl.Factory changeControlFactory,
       ReviewDb db, GitRepositoryManager gitManager,
-      GitReferenceUpdated replication, PatchSetInfoFactory patchSetInfoFactory,
+      GitReferenceUpdated gitRefUpdated, PatchSetInfoFactory patchSetInfoFactory,
       @Assisted final PatchSet.Id patchSetId) {
     this.changeControlFactory = changeControlFactory;
     this.db = db;
     this.gitManager = gitManager;
-    this.replication = replication;
+    this.gitRefUpdated = gitRefUpdated;
     this.patchSetInfoFactory = patchSetInfoFactory;
 
     this.patchSetId = patchSetId;
@@ -88,7 +88,7 @@
     final Change change = control.getChange();
 
     try {
-      ChangeUtil.deleteOnlyDraftPatchSet(patch, change, gitManager, replication, db);
+      ChangeUtil.deleteOnlyDraftPatchSet(patch, change, gitManager, gitRefUpdated, db);
     } catch (IOException e) {
       result.addError(new ReviewResult.Error(
           ReviewResult.Error.Type.GIT_ERROR, e.getMessage()));
@@ -97,7 +97,7 @@
     List<PatchSet> restOfPatches = db.patchSets().byChange(changeId).toList();
     if (restOfPatches.size() == 0) {
       try {
-        ChangeUtil.deleteDraftChange(patchSetId, gitManager, replication, db);
+        ChangeUtil.deleteDraftChange(patchSetId, gitManager, gitRefUpdated, db);
         result.setChangeId(null);
       } catch (IOException e) {
         result.addError(new ReviewResult.Error(
@@ -111,9 +111,10 @@
         }
       }
       if (change.currentPatchSetId().equals(patchSetId)) {
-        change.removeLastPatchSetId();
         try {
-          change.setCurrentPatchSet(patchSetInfoFactory.get(db, change.currPatchSetId()));
+          PatchSet.Id id =
+              new PatchSet.Id(patchSetId.getParentKey(), patchSetId.get() - 1);
+          change.setCurrentPatchSet(patchSetInfoFactory.get(db, id));
         } catch (PatchSetInfoNotAvailableException e) {
           throw new NoSuchChangeException(changeId);
         }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/PathConflictException.java b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/PathConflictException.java
new file mode 100644
index 0000000..7e2f2d7
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/PathConflictException.java
@@ -0,0 +1,24 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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;
+
+/** Indicates a path conflict during rebase or merge */
+public class PathConflictException extends Exception {
+  private static final long serialVersionUID = 1L;
+
+  public PathConflictException(String msg) {
+    super(msg);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/PublishDraft.java b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/PublishDraft.java
index a71e12e..22eae2d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/PublishDraft.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/PublishDraft.java
@@ -15,12 +15,29 @@
 
 package com.google.gerrit.server.changedetail;
 
+import static com.google.gerrit.server.mail.MailUtil.getRecipientsFromApprovals;
+import static com.google.gerrit.server.mail.MailUtil.getRecipientsFromFooters;
+
 import com.google.gerrit.common.ChangeHooks;
+import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.common.data.ReviewResult;
+import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.PatchSetInfo;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.ChangeUtil;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountResolver;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.mail.CreateChangeSender;
+import com.google.gerrit.server.mail.MailUtil.MailRecipients;
+import com.google.gerrit.server.mail.ReplacePatchSetSender;
+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.AtomicUpdate;
@@ -28,9 +45,22 @@
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.FooterLine;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
 import java.util.concurrent.Callable;
 
 public class PublishDraft implements Callable<ReviewResult> {
+  private static final Logger log =
+      LoggerFactory.getLogger(PublishDraft.class);
 
   public interface Factory {
     PublishDraft create(PatchSet.Id patchSetId);
@@ -39,27 +69,47 @@
   private final ChangeControl.Factory changeControlFactory;
   private final ReviewDb db;
   private final ChangeHooks hooks;
+  private final GitRepositoryManager repoManager;
+  private final PatchSetInfoFactory patchSetInfoFactory;
+  private final ApprovalsUtil approvalsUtil;
+  private final AccountResolver accountResolver;
+  private final CreateChangeSender.Factory createChangeSenderFactory;
+  private final ReplacePatchSetSender.Factory replacePatchSetFactory;
 
   private final PatchSet.Id patchSetId;
 
   @Inject
-  PublishDraft(ChangeControl.Factory changeControlFactory,
-      ReviewDb db, @Assisted final PatchSet.Id patchSetId,
-      final ChangeHooks hooks) {
+  PublishDraft(final ChangeControl.Factory changeControlFactory,
+      final ReviewDb db, final ChangeHooks hooks,
+      final GitRepositoryManager repoManager,
+      final PatchSetInfoFactory patchSetInfoFactory,
+      final ApprovalsUtil approvalsUtil,
+      final AccountResolver accountResolver,
+      final CreateChangeSender.Factory createChangeSenderFactory,
+      final ReplacePatchSetSender.Factory replacePatchSetFactory,
+      @Assisted final PatchSet.Id patchSetId) {
     this.changeControlFactory = changeControlFactory;
     this.db = db;
     this.hooks = hooks;
+    this.repoManager = repoManager;
+    this.patchSetInfoFactory = patchSetInfoFactory;
+    this.approvalsUtil = approvalsUtil;
+    this.accountResolver = accountResolver;
+    this.createChangeSenderFactory = createChangeSenderFactory;
+    this.replacePatchSetFactory = replacePatchSetFactory;
 
     this.patchSetId = patchSetId;
   }
 
   @Override
-  public ReviewResult call() throws NoSuchChangeException, OrmException {
+  public ReviewResult call() throws NoSuchChangeException, OrmException,
+      IOException, PatchSetInfoNotAvailableException {
     final ReviewResult result = new ReviewResult();
 
     final Change.Id changeId = patchSetId.getParentKey();
     result.setChangeId(changeId);
     final ChangeControl control = changeControlFactory.validateFor(changeId);
+    final LabelTypes labelTypes = control.getLabelTypes();
     final PatchSet patch = db.patchSets().get(patchSetId);
     if (patch == null) {
       throw new NoSuchChangeException(changeId);
@@ -74,7 +124,7 @@
       result.addError(new ReviewResult.Error(
           ReviewResult.Error.Type.PUBLISH_NOT_PERMITTED));
     } else {
-      final PatchSet updatedPatch = db.patchSets().atomicUpdate(patchSetId,
+      final PatchSet updatedPatchSet = db.patchSets().atomicUpdate(patchSetId,
           new AtomicUpdate<PatchSet>() {
         @Override
         public PatchSet update(PatchSet patchset) {
@@ -95,11 +145,77 @@
         }
       });
 
-      if (!updatedPatch.isDraft() || updatedChange.getStatus() == Change.Status.NEW) {
-        hooks.doDraftPublishedHook(updatedChange, updatedPatch, db);
+      if (!updatedPatchSet.isDraft() || updatedChange.getStatus() == Change.Status.NEW) {
+        hooks.doDraftPublishedHook(updatedChange, updatedPatchSet, db);
+
+        sendNotifications(control.getChange().getStatus() == Change.Status.DRAFT,
+            (IdentifiedUser) control.getCurrentUser(), updatedChange, updatedPatchSet,
+            labelTypes);
       }
     }
 
     return result;
   }
+
+  private void sendNotifications(final boolean newChange,
+      final IdentifiedUser currentUser, final Change updatedChange,
+      final PatchSet updatedPatchSet, final LabelTypes labelTypes)
+      throws OrmException, IOException, PatchSetInfoNotAvailableException {
+    final Repository git = repoManager.openRepository(updatedChange.getProject());
+    try {
+      final RevWalk revWalk = new RevWalk(git);
+      final RevCommit commit;
+      try {
+        commit = revWalk.parseCommit(ObjectId.fromString(updatedPatchSet.getRevision().get()));
+      } finally {
+        revWalk.release();
+      }
+      final PatchSetInfo info = patchSetInfoFactory.get(commit, updatedPatchSet.getId());
+      final List<FooterLine> footerLines = commit.getFooterLines();
+      final Account.Id me = currentUser.getAccountId();
+      final MailRecipients recipients =
+          getRecipientsFromFooters(accountResolver, updatedPatchSet, footerLines);
+      recipients.remove(me);
+
+      if (newChange) {
+        approvalsUtil.addReviewers(db, labelTypes, updatedChange, updatedPatchSet, info,
+            recipients.getReviewers(), Collections.<Account.Id> emptySet());
+        try {
+          CreateChangeSender cm = createChangeSenderFactory.create(updatedChange);
+          cm.setFrom(me);
+          cm.setPatchSet(updatedPatchSet, info);
+          cm.addReviewers(recipients.getReviewers());
+          cm.addExtraCC(recipients.getCcOnly());
+          cm.send();
+        } catch (Exception e) {
+          log.error("Cannot send email for new change " + updatedChange.getId(), e);
+        }
+      } else {
+        final List<PatchSetApproval> patchSetApprovals =
+            db.patchSetApprovals().byChange(updatedChange.getId()).toList();
+        final MailRecipients oldRecipients =
+            getRecipientsFromApprovals(patchSetApprovals);
+        approvalsUtil.addReviewers(db, labelTypes, updatedChange, updatedPatchSet, info,
+            recipients.getReviewers(), oldRecipients.getAll());
+        final ChangeMessage msg =
+            new ChangeMessage(new ChangeMessage.Key(updatedChange.getId(),
+                ChangeUtil.messageUUID(db)), me,
+                updatedPatchSet.getCreatedOn(), updatedPatchSet.getId());
+        msg.setMessage("Uploaded patch set " + updatedPatchSet.getPatchSetId() + ".");
+        try {
+          ReplacePatchSetSender cm = replacePatchSetFactory.create(updatedChange);
+          cm.setFrom(me);
+          cm.setPatchSet(updatedPatchSet, info);
+          cm.setChangeMessage(msg);
+          cm.addReviewers(recipients.getReviewers());
+          cm.addExtraCC(recipients.getCcOnly());
+          cm.send();
+        } catch (Exception e) {
+          log.error("Cannot send email for new patch set " + updatedPatchSet.getId(), e);
+        }
+      }
+    } finally {
+      git.close();
+    }
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RebaseChange.java b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RebaseChange.java
new file mode 100644
index 0000000..9089710
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RebaseChange.java
@@ -0,0 +1,467 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.common.collect.Sets;
+import com.google.gerrit.common.ChangeHookRunner;
+import com.google.gerrit.common.data.LabelTypes;
+import com.google.gerrit.common.errors.EmailException;
+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.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.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.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MergeUtil;
+import com.google.gerrit.server.mail.RebasedPatchSetSender;
+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.InvalidChangeOperationException;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gwtorm.server.AtomicUpdate;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.CommitBuilder;
+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.ThreeWayMerger;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+import java.io.IOException;
+import java.sql.Timestamp;
+import java.util.Collections;
+import java.util.List;
+import java.util.Set;
+
+public class RebaseChange {
+  private final ChangeControl.Factory changeControlFactory;
+  private final PatchSetInfoFactory patchSetInfoFactory;
+  private final ReviewDb db;
+  private final GitRepositoryManager gitManager;
+  private final PersonIdent myIdent;
+  private final GitReferenceUpdated gitRefUpdated;
+  private final RebasedPatchSetSender.Factory rebasedPatchSetSenderFactory;
+  private final ChangeHookRunner hooks;
+  private final ApprovalsUtil approvalsUtil;
+  private final MergeUtil.Factory mergeUtilFactory;
+  private final ProjectCache projectCache;
+
+  @Inject
+  RebaseChange(final ChangeControl.Factory changeControlFactory,
+      final PatchSetInfoFactory patchSetInfoFactory, final ReviewDb db,
+      @GerritPersonIdent final PersonIdent myIdent,
+      final GitRepositoryManager gitManager,
+      final GitReferenceUpdated gitRefUpdated,
+      final RebasedPatchSetSender.Factory rebasedPatchSetSenderFactory,
+      final ChangeHookRunner hooks, final ApprovalsUtil approvalsUtil,
+      final MergeUtil.Factory mergeUtilFactory,
+      final ProjectCache projectCache) {
+    this.changeControlFactory = changeControlFactory;
+    this.patchSetInfoFactory = patchSetInfoFactory;
+    this.db = db;
+    this.gitManager = gitManager;
+    this.myIdent = myIdent;
+    this.gitRefUpdated = gitRefUpdated;
+    this.rebasedPatchSetSenderFactory = rebasedPatchSetSenderFactory;
+    this.hooks = hooks;
+    this.approvalsUtil = approvalsUtil;
+    this.mergeUtilFactory = mergeUtilFactory;
+    this.projectCache = projectCache;
+  }
+
+  /**
+   * Rebases the change of the given patch set.
+   *
+   * It is verified that the current user is allowed to do the rebase.
+   *
+   * If the patch set has no dependency to an open change, then the change is
+   * rebased on the tip of the destination branch.
+   *
+   * If the patch set depends on an open change, it is rebased on the latest
+   * patch set of this change.
+   *
+   * The rebased commit is added as new patch set to the change.
+   *
+   * E-mail notification and triggering of hooks happens for the creation of the
+   * new patch set.
+   *
+   * @param patchSetId the id of the patch set
+   * @param uploader the user that creates the rebased patch set
+   * @throws NoSuchChangeException thrown if the change to which the patch set
+   *         belongs does not exist or is not visible to the user
+   * @throws EmailException thrown if sending the e-mail to notify about the new
+   *         patch set fails
+   * @throws OrmException thrown in case accessing the database fails
+   * @throws IOException thrown if rebase is not possible or not needed
+   * @throws InvalidChangeOperationException thrown if rebase is not allowed
+   */
+  public void rebase(final PatchSet.Id patchSetId, final Account.Id uploader)
+      throws NoSuchChangeException, EmailException, OrmException, IOException,
+      InvalidChangeOperationException {
+    final Change.Id changeId = patchSetId.getParentKey();
+    final ChangeControl changeControl =
+        changeControlFactory.validateFor(changeId);
+    if (!changeControl.canRebase()) {
+      throw new InvalidChangeOperationException(
+          "Cannot rebase: New patch sets are not allowed to be added to change: "
+              + changeId.toString());
+    }
+    final Change change = changeControl.getChange();
+    Repository git = null;
+    RevWalk rw = null;
+    ObjectInserter inserter = null;
+    try {
+      git = gitManager.openRepository(change.getProject());
+      rw = new RevWalk(git);
+      inserter = git.newObjectInserter();
+
+      final List<PatchSetApproval> oldPatchSetApprovals =
+          db.patchSetApprovals().byChange(change.getId()).toList();
+
+      final String baseRev = findBaseRevision(patchSetId, db,
+          change.getDest(), git, null, null, null);
+      final RevCommit baseCommit =
+          rw.parseCommit(ObjectId.fromString(baseRev));
+
+      final PatchSet newPatchSet =
+          rebase(git, rw, inserter, patchSetId, change, uploader, baseCommit,
+              mergeUtilFactory.create(
+                  changeControl.getProjectControl().getProjectState(), true));
+
+      final Set<Account.Id> oldReviewers = Sets.newHashSet();
+      final Set<Account.Id> oldCC = Sets.newHashSet();
+      for (PatchSetApproval a : oldPatchSetApprovals) {
+        if (a.getValue() != 0) {
+          oldReviewers.add(a.getAccountId());
+        } else {
+          oldCC.add(a.getAccountId());
+        }
+      }
+      final ReplacePatchSetSender cm =
+          rebasedPatchSetSenderFactory.create(change);
+      cm.setFrom(uploader);
+      cm.setPatchSet(newPatchSet);
+      cm.addReviewers(oldReviewers);
+      cm.addExtraCC(oldCC);
+      cm.send();
+
+      hooks.doPatchsetCreatedHook(change, newPatchSet, db);
+    } catch (PathConflictException e) {
+      throw new IOException(e.getMessage());
+    } finally {
+      if (inserter != null) {
+        inserter.release();
+      }
+      if (rw != null) {
+        rw.release();
+      }
+      if (git != null) {
+        git.close();
+      }
+    }
+  }
+
+  /**
+   * Finds the revision of commit on which the given patch set should be based.
+   *
+   * @param patchSetId the id of the patch set for which the new base commit
+   *        should be found
+   * @param db the ReviewDb
+   * @param destBranch the destination branch
+   * @param git the repository
+   * @param patchSetAncestors the original PatchSetAncestor of the given patch
+   *        set that should be based
+   * @param depPatchSetList the original patch set list on which the rebased
+   *        patch set depends
+   * @param depChangeList the original change list on whose patch set the
+   *        rebased patch set depends
+   * @return the revision of commit on which the given patch set should be based
+   * @throws IOException thrown if rebase is not possible or not needed
+   * @throws OrmException thrown in case accessing the database fails
+   */
+    private static String findBaseRevision(final PatchSet.Id patchSetId,
+        final ReviewDb db, final Branch.NameKey destBranch, final Repository git,
+        List<PatchSetAncestor> patchSetAncestors, List<PatchSet> depPatchSetList,
+        List<Change> depChangeList) throws IOException, OrmException {
+
+      String baseRev = null;
+
+      if (patchSetAncestors == null) {
+        patchSetAncestors =
+            db.patchSetAncestors().ancestorsOf(patchSetId).toList();
+      }
+
+      if (patchSetAncestors.size() > 1) {
+        throw new IOException(
+            "Cannot rebase a change with multiple parents. Parents commits: "
+                + patchSetAncestors.toString());
+      }
+      if (patchSetAncestors.size() == 0) {
+        throw new IOException(
+            "Cannot rebase a change without any parents (is this the initial commit?).");
+      }
+
+      RevId ancestorRev = patchSetAncestors.get(0).getAncestorRevision();
+      if (depPatchSetList == null || depPatchSetList.size() != 1 ||
+          !depPatchSetList.get(0).getRevision().equals(ancestorRev)) {
+        depPatchSetList = db.patchSets().byRevision(ancestorRev).toList();
+      }
+
+      if (!depPatchSetList.isEmpty()) {
+        PatchSet depPatchSet = depPatchSetList.get(0);
+
+        Change.Id depChangeId = depPatchSet.getId().getParentKey();
+        Change depChange;
+        if (depChangeList == null || depChangeList.size() != 1 ||
+            !depChangeList.get(0).getId().equals(depChangeId)) {
+          depChange = db.changes().get(depChangeId);
+        } else {
+          depChange = depChangeList.get(0);
+        }
+
+        if (depChange.getStatus() == Status.ABANDONED) {
+          throw new IOException("Cannot rebase a change with an abandoned parent: "
+              + depChange.getKey().toString());
+        }
+
+        if (depChange.getStatus().isOpen()) {
+          if (depPatchSet.getId().equals(depChange.currentPatchSetId())) {
+            throw new IOException(
+                "Change is already based on the latest patch set of the dependent change.");
+          }
+          PatchSet latestDepPatchSet =
+              db.patchSets().get(depChange.currentPatchSetId());
+          baseRev = latestDepPatchSet.getRevision().get();
+        }
+      }
+
+      if (baseRev == null) {
+        // We are dependent on a merged PatchSet or have no PatchSet
+        // dependencies at all.
+        Ref destRef = git.getRef(destBranch.get());
+        if (destRef == null) {
+          throw new IOException(
+              "The destination branch does not exist: "
+                  + destBranch.get());
+        }
+        baseRev = destRef.getObjectId().getName();
+        if (baseRev.equals(ancestorRev.get())) {
+          throw new IOException("Change is already up to date.");
+        }
+      }
+      return baseRev;
+    }
+
+  /**
+   * Rebases the change of the given patch set on the given base commit.
+   *
+   * The rebased commit is added as new patch set to the change.
+   *
+   * E-mail notification and triggering of hooks is NOT done for the creation of
+   * the new patch set.
+   *
+   * @param git the repository
+   * @param revWalk the RevWalk
+   * @param inserter the object inserter
+   * @param patchSetId the id of the patch set
+   * @param chg the change that should be rebased
+   * @param uploader the user that creates the rebased patch set
+   * @param baseCommit the commit that should be the new base
+   * @param mergeUtil merge utilities for the destination project
+   * @return the new patch set which is based on the given base commit
+   * @throws NoSuchChangeException thrown if the change to which the patch set
+   *         belongs does not exist or is not visible to the user
+   * @throws OrmException thrown in case accessing the database fails
+   * @throws IOException thrown if rebase is not possible or not needed
+   * @throws InvalidChangeOperationException thrown if rebase is not allowed
+   */
+  public PatchSet rebase(final Repository git, final RevWalk revWalk,
+      final ObjectInserter inserter, final PatchSet.Id patchSetId,
+      final Change chg, final Account.Id uploader, final RevCommit baseCommit,
+      final MergeUtil mergeUtil) throws NoSuchChangeException,
+      OrmException, IOException, InvalidChangeOperationException,
+      PathConflictException {
+    Change change = chg;
+    final PatchSet originalPatchSet = db.patchSets().get(patchSetId);
+
+    final RevCommit rebasedCommit;
+    ObjectId oldId = ObjectId.fromString(originalPatchSet.getRevision().get());
+    ObjectId newId = rebaseCommit(git, inserter, revWalk.parseCommit(oldId),
+        baseCommit, mergeUtil, myIdent);
+
+    rebasedCommit = revWalk.parseCommit(newId);
+
+    PatchSet.Id id = ChangeUtil.nextPatchSetId(git, change.currentPatchSetId());
+    final PatchSet newPatchSet = new PatchSet(id);
+    newPatchSet.setCreatedOn(new Timestamp(System.currentTimeMillis()));
+    newPatchSet.setUploader(uploader);
+    newPatchSet.setRevision(new RevId(rebasedCommit.name()));
+    newPatchSet.setDraft(originalPatchSet.isDraft());
+
+    final PatchSetInfo info =
+        patchSetInfoFactory.get(rebasedCommit, newPatchSet.getId());
+
+    final RefUpdate ru = git.updateRef(newPatchSet.getRefName());
+    ru.setExpectedOldObjectId(ObjectId.zeroId());
+    ru.setNewObjectId(rebasedCommit);
+    ru.disableRefLog();
+    if (ru.update(revWalk) != RefUpdate.Result.NEW) {
+      throw new IOException(String.format("Failed to create ref %s in %s: %s",
+          newPatchSet.getRefName(), change.getDest().getParentKey().get(),
+          ru.getResult()));
+    }
+    gitRefUpdated.fire(change.getProject(), ru);
+
+    db.changes().beginTransaction(change.getId());
+    try {
+      Change updatedChange = db.changes().get(change.getId());
+      if (updatedChange != null && change.getStatus().isOpen()) {
+        change = updatedChange;
+      } else {
+        throw new InvalidChangeOperationException(String.format(
+            "Change %s is closed", change.getId()));
+      }
+
+      ChangeUtil.insertAncestors(db, newPatchSet.getId(), rebasedCommit);
+      db.patchSets().insert(Collections.singleton(newPatchSet));
+      updatedChange =
+          db.changes().atomicUpdate(change.getId(), new AtomicUpdate<Change>() {
+            @Override
+            public Change update(Change change) {
+              if (change.getStatus().isClosed()) {
+                return null;
+              }
+              if (!change.currentPatchSetId().equals(patchSetId)) {
+                return null;
+              }
+              if (change.getStatus() != Change.Status.DRAFT) {
+                change.setStatus(Change.Status.NEW);
+              }
+              change.setLastSha1MergeTested(null);
+              change.setCurrentPatchSet(info);
+              ChangeUtil.updated(change);
+              return change;
+            }
+          });
+      if (updatedChange != null) {
+        change = updatedChange;
+      } else {
+        throw new InvalidChangeOperationException(String.format(
+            "Change %s was modified", change.getId()));
+      }
+
+      final LabelTypes labelTypes =
+          projectCache.get(change.getProject()).getLabelTypes();
+      approvalsUtil.copyVetosToPatchSet(db, labelTypes,
+          change.currentPatchSetId());
+
+      final ChangeMessage cmsg =
+          new ChangeMessage(new ChangeMessage.Key(change.getId(),
+              ChangeUtil.messageUUID(db)), uploader, patchSetId);
+      cmsg.setMessage("Patch Set " + change.currentPatchSetId().get()
+          + ": Patch Set " + patchSetId.get() + " was rebased");
+      db.changeMessages().insert(Collections.singleton(cmsg));
+      db.commit();
+    } finally {
+      db.rollback();
+    }
+
+    return newPatchSet;
+  }
+
+  /**
+   * Rebases a commit.
+   *
+   * @param git repository to find commits in
+   * @param inserter inserter to handle new trees and blobs
+   * @param original The commit to rebase
+   * @param base Base to rebase against
+   * @param mergeUtil merge utilities for the destination project
+   * @param committerIdent committer identity
+   * @return the id of the rebased commit
+   * @throws IOException Merged failed
+   * @throws PathConflictException the rebase failed due to a path conflict
+   */
+  private ObjectId rebaseCommit(final Repository git,
+      final ObjectInserter inserter, final RevCommit original,
+      final RevCommit base, final MergeUtil mergeUtil,
+      final PersonIdent committerIdent) throws IOException,
+      PathConflictException {
+
+    final RevCommit parentCommit = original.getParent(0);
+
+    if (base.equals(parentCommit)) {
+      throw new IOException("Change is already up to date.");
+    }
+
+    final ThreeWayMerger merger = mergeUtil.newThreeWayMerger(git, inserter);
+    merger.setBase(parentCommit);
+    merger.merge(original, base);
+
+    if (merger.getResultTreeId() == null) {
+      throw new PathConflictException(
+          "The change could not be rebased due to a path conflict during merge.");
+    }
+
+    final CommitBuilder cb = new CommitBuilder();
+    cb.setTreeId(merger.getResultTreeId());
+    cb.setParentId(base);
+    cb.setAuthor(original.getAuthorIdent());
+    cb.setMessage(original.getFullMessage());
+    cb.setCommitter(committerIdent);
+    final ObjectId objectId = inserter.insert(cb);
+    inserter.flush();
+    return objectId;
+  }
+
+  public static boolean canDoRebase(final ReviewDb db,
+      final Change change, final GitRepositoryManager gitManager,
+      List<PatchSetAncestor> patchSetAncestors,
+      List<PatchSet> depPatchSetList, List<Change> depChangeList)
+      throws OrmException, RepositoryNotFoundException, IOException {
+
+    final Repository git = gitManager.openRepository(change.getProject());
+
+    try {
+      // If no exception is thrown, then we can do a rebase.
+      findBaseRevision(change.currentPatchSetId(), db, change.getDest(), git,
+          patchSetAncestors, depPatchSetList, depChangeList);
+      return true;
+    } catch (IOException e) {
+      return false;
+    } finally {
+      git.close();
+    }
+  }
+}
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
deleted file mode 100644
index 53da2b6..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/RestoreChange.java
+++ /dev/null
@@ -1,157 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-
-package com.google.gerrit.server.changedetail;
-
-import com.google.gerrit.common.ChangeHooks;
-import com.google.gerrit.common.data.ReviewResult;
-import com.google.gerrit.reviewdb.client.Branch;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.ChangeMessage;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.ChangeUtil;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.ProjectUtil;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.mail.EmailException;
-import com.google.gerrit.server.mail.RestoredSender;
-import com.google.gerrit.server.project.ChangeControl;
-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 org.eclipse.jgit.errors.RepositoryNotFoundException;
-
-import java.io.IOException;
-import java.util.concurrent.Callable;
-
-import org.kohsuke.args4j.Argument;
-import org.kohsuke.args4j.Option;
-
-public class RestoreChange implements Callable<ReviewResult> {
-
-  private final RestoredSender.Factory restoredSenderFactory;
-  private final ChangeControl.Factory changeControlFactory;
-  private final ReviewDb db;
-  private final GitRepositoryManager repoManager;
-  private final IdentifiedUser currentUser;
-  private final ChangeHooks hooks;
-
-  @Argument(index = 0, required = true, multiValued = false,
-            usage = "change to restore", metaVar = "CHANGE")
-  private Change.Id changeId;
-  public void setChangeId(final Change.Id changeId) {
-    this.changeId = changeId;
-  }
-
-  @Option(name = "--message", aliases = {"-m"},
-          usage = "optional message to append to change")
-  private String message;
-  public void setMessage(final String message) {
-    this.message = message;
-  }
-
-  @Inject
-  RestoreChange(final RestoredSender.Factory restoredSenderFactory,
-      final ChangeControl.Factory changeControlFactory, final ReviewDb db,
-      final GitRepositoryManager repoManager, final IdentifiedUser currentUser,
-      final ChangeHooks hooks) {
-    this.restoredSenderFactory = restoredSenderFactory;
-    this.changeControlFactory = changeControlFactory;
-    this.db = db;
-    this.repoManager = repoManager;
-    this.currentUser = currentUser;
-    this.hooks = hooks;
-
-    changeId = null;
-    message = null;
-  }
-
-  @Override
-  public ReviewResult call() throws EmailException, NoSuchChangeException,
-      InvalidChangeOperationException, OrmException,
-      RepositoryNotFoundException, IOException {
-    if (changeId == null) {
-      throw new InvalidChangeOperationException("changeId is required");
-    }
-
-    final ReviewResult result = new ReviewResult();
-    result.setChangeId(changeId);
-
-    final ChangeControl control = changeControlFactory.validateFor(changeId);
-    final Change change = db.changes().get(changeId);
-    final PatchSet.Id patchSetId = change.currentPatchSetId();
-    if (!control.canRestore()) {
-      result.addError(new ReviewResult.Error(
-          ReviewResult.Error.Type.RESTORE_NOT_PERMITTED));
-      return result;
-    }
-
-    final PatchSet patch = db.patchSets().get(patchSetId);
-    if (patch == null) {
-      throw new NoSuchChangeException(changeId);
-    }
-
-    final Branch.NameKey destBranch = control.getChange().getDest();
-    if (!ProjectUtil.branchExists(repoManager, destBranch)) {
-      result.addError(new ReviewResult.Error(
-          ReviewResult.Error.Type.DEST_BRANCH_NOT_FOUND, destBranch.get()));
-      return result;
-    }
-
-    // Create a message to accompany the restored change
-    final ChangeMessage cmsg =
-        new ChangeMessage(new ChangeMessage.Key(changeId, ChangeUtil
-            .messageUUID(db)), currentUser.getAccountId(), patchSetId);
-    final StringBuilder msgBuf =
-        new StringBuilder("Patch Set " + patchSetId.get() + ": Restored");
-    if (message != null && message.length() > 0) {
-      msgBuf.append("\n\n");
-      msgBuf.append(message);
-    }
-    cmsg.setMessage(msgBuf.toString());
-
-    // Restore the change
-    final Change updatedChange = db.changes().atomicUpdate(changeId,
-        new AtomicUpdate<Change>() {
-          @Override
-          public Change update(Change change) {
-            if (change.getStatus() == Change.Status.ABANDONED) {
-              change.setStatus(Change.Status.NEW);
-              ChangeUtil.updated(change);
-              return change;
-            } else {
-              return null;
-            }
-          }
-        });
-
-    if (updatedChange == null) {
-      result.addError(new ReviewResult.Error(
-          ReviewResult.Error.Type.CHANGE_NOT_ABANDONED));
-      return result;
-    }
-
-    ChangeUtil.updatedChange(db, currentUser, updatedChange, cmsg,
-                             restoredSenderFactory);
-    hooks.doChangeRestoredHook(updatedChange, currentUser.getAccount(),
-                               message, db);
-
-    return result;
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/Submit.java b/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/Submit.java
deleted file mode 100644
index 3287aa1..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/changedetail/Submit.java
+++ /dev/null
@@ -1,207 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.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.ProjectUtil;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.MergeOp;
-import com.google.gerrit.server.git.MergeQueue;
-import com.google.gerrit.server.project.ChangeControl;
-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.io.IOException;
-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 GitRepositoryManager repoManager;
-  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 GitRepositoryManager repoManager,
-      final IdentifiedUser currentUser, @Assisted final PatchSet.Id patchSetId) {
-    this.changeControlFactory = changeControlFactory;
-    this.opFactory = opFactory;
-    this.merger = merger;
-    this.db = db;
-    this.repoManager = repoManager;
-    this.currentUser = currentUser;
-
-    this.patchSetId = patchSetId;
-  }
-
-  @Override
-  public ReviewResult call() throws IllegalStateException,
-      InvalidChangeOperationException, NoSuchChangeException, OrmException,
-      IOException {
-    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, patch);
-    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 MAY:
-                // The MAY label didn't cause the NOT_READY status
-                break;
-
-              case IMPOSSIBLE:
-                if (errMsg.length() > 0) errMsg.append("; ");
-                errMsg.append("change " + changeId + ": needs " + lbl.label
-                    + " (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
-              + ")");
-      }
-    }
-
-    if (!ProjectUtil.branchExists(repoManager, control.getChange().getDest())) {
-      result.addError(new ReviewResult.Error(
-          ReviewResult.Error.Type.DEST_BRANCH_NOT_FOUND,
-          "Destination branch \"" + control.getChange().getDest().get()
-              + "\" not found."));
-      return result;
-    }
-
-    // Submit the change if we can
-    if (result.getErrors().isEmpty()) {
-      final List<PatchSetApproval> allApprovals =
-          new ArrayList<PatchSetApproval>(db.patchSetApprovals().byPatchSet(
-              patchSetId).toList());
-
-      final PatchSetApproval.Key akey =
-          new PatchSetApproval.Key(patchSetId, currentUser.getAccountId(),
-                                   SUBMIT);
-
-      PatchSetApproval approval = new PatchSetApproval(akey, (short) 1);
-      for (final PatchSetApproval candidateApproval : allApprovals) {
-        if (akey.equals(candidateApproval.getKey())) {
-          candidateApproval.setValue((short) 1);
-          candidateApproval.setGranted();
-          approval = candidateApproval;
-          break;
-        }
-      }
-      db.patchSetApprovals().upsert(Collections.singleton(approval));
-
-      final Change updatedChange = db.changes().atomicUpdate(changeId,
-          new AtomicUpdate<Change>() {
-        @Override
-        public Change update(Change change) {
-          if (change.getStatus() == Change.Status.NEW) {
-            change.setStatus(Change.Status.SUBMITTED);
-            ChangeUtil.updated(change);
-          }
-          return change;
-        }
-      });
-
-      if (updatedChange.getStatus() == Change.Status.SUBMITTED) {
-        merger.merge(opFactory, updatedChange.getDest());
-      }
-    }
-    return result;
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ApprovalTypesProvider.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ApprovalTypesProvider.java
deleted file mode 100644
index ed9416d..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/ApprovalTypesProvider.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.config;
-
-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.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;
-
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.List;
-
-public class ApprovalTypesProvider implements Provider<ApprovalTypes> {
-  private final SchemaFactory<ReviewDb> schema;
-
-  @Inject
-  ApprovalTypesProvider(final SchemaFactory<ReviewDb> sf) {
-    schema = sf;
-  }
-
-  @Override
-  public ApprovalTypes get() {
-    List<ApprovalType> types = new ArrayList<ApprovalType>(2);
-
-    try {
-      final ReviewDb db = schema.open();
-      try {
-        for (final ApprovalCategory c : db.approvalCategories().all()) {
-          final List<ApprovalCategoryValue> values =
-              db.approvalCategoryValues().byCategory(c.getId()).toList();
-          types.add(new ApprovalType(c, values));
-        }
-      } finally {
-        db.close();
-      }
-    } catch (OrmException e) {
-      throw new ProvisionException("Cannot query approval categories", e);
-    }
-
-    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 9916257..9a804c1 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
@@ -14,9 +14,9 @@
 
 package com.google.gerrit.server.config;
 
-import com.google.gerrit.common.auth.openid.OpenIdProviderPattern;
 import com.google.gerrit.reviewdb.client.AccountExternalId;
 import com.google.gerrit.reviewdb.client.AuthType;
+import com.google.gerrit.server.auth.openid.OpenIdProviderPattern;
 import com.google.gwtjsonrpc.server.SignedToken;
 import com.google.gwtjsonrpc.server.XsrfException;
 import com.google.inject.Inject;
@@ -25,6 +25,7 @@
 import org.eclipse.jgit.lib.Config;
 
 import java.util.ArrayList;
+import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.List;
@@ -40,6 +41,7 @@
   private final boolean gitBasicAuth;
   private final String logoutUrl;
   private final String openIdSsoUrl;
+  private final List<String> openIdDomains;
   private final List<OpenIdProviderPattern> trustedOpenIDs;
   private final List<OpenIdProviderPattern> allowedOpenIDs;
   private final String cookiePath;
@@ -56,6 +58,7 @@
     httpHeader = cfg.getString("auth", null, "httpheader");
     logoutUrl = cfg.getString("auth", null, "logouturl");
     openIdSsoUrl = cfg.getString("auth", null, "openidssourl");
+    openIdDomains = Arrays.asList(cfg.getStringList("auth", null, "openIdDomain"));
     trustedOpenIDs = toPatterns(cfg, "trustedOpenID");
     allowedOpenIDs = toPatterns(cfg, "allowedOpenID");
     cookiePath = cfg.getString("auth", null, "cookiepath");
@@ -127,6 +130,10 @@
     return openIdSsoUrl;
   }
 
+  public List<String> getOpenIdDomains() {
+    return openIdDomains;
+  }
+
   public String getCookiePath() {
     return cookiePath;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigSection.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigSection.java
new file mode 100644
index 0000000..057ce99
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/ConfigSection.java
@@ -0,0 +1,37 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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 org.eclipse.jgit.lib.Config;
+
+/** Provides access to one section from {@link Config} */
+public class ConfigSection {
+
+  private final Config cfg;
+  private final String section;
+
+  public ConfigSection(Config cfg, String section) {
+    this.cfg = cfg;
+    this.section = section;
+  }
+
+  public String optional(String name) {
+    return cfg.getString(section, null, name);
+  }
+
+  public String required(String name) {
+    return ConfigUtil.getRequired(cfg, section, name);
+  }
+}
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 cc54054..95c1500 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
@@ -102,7 +102,7 @@
       final String subsection, final String setting, String valueString,
       final T[] all) {
 
-    String n = valueString.replace(' ', '_');
+    String n = valueString.replace(' ', '_').replace('-', '_');
     for (final T e : all) {
       if (equalsIgnoreCase(e.name(), n)) {
         return e;
@@ -286,6 +286,15 @@
     }
   }
 
+  public static String getRequired(Config cfg, String section, String name) {
+    final String v = cfg.getString(section, null, name);
+    if (v == null || "".equals(v)) {
+      throw new IllegalArgumentException("No " + section + "." + name
+          + " configured");
+    }
+    return v;
+  }
+
   private static boolean match(final String a, final String... cases) {
     for (final String b : cases) {
       if (equalsIgnoreCase(a, b)) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/GerritGlobalModule.java
index 2a66706..1da611c 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
@@ -18,9 +18,10 @@
 
 import com.google.common.cache.Cache;
 import com.google.gerrit.audit.AuditModule;
-import com.google.gerrit.common.data.ApprovalTypes;
+import com.google.gerrit.common.ChangeListener;
 import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
 import com.google.gerrit.extensions.events.NewProjectCreatedListener;
+import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.reviewdb.client.AuthType;
@@ -31,50 +32,80 @@
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.InternalUser;
 import com.google.gerrit.server.MimeUtilFileTypeRegistry;
+import com.google.gerrit.server.PluginUser;
 import com.google.gerrit.server.account.AccountByEmailCacheImpl;
 import com.google.gerrit.server.account.AccountCacheImpl;
+import com.google.gerrit.server.account.AccountControl;
 import com.google.gerrit.server.account.AccountInfoCacheFactory;
+import com.google.gerrit.server.account.AccountManager;
+import com.google.gerrit.server.account.AccountResolver;
 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.ChangeUserName;
 import com.google.gerrit.server.account.DefaultRealm;
 import com.google.gerrit.server.account.EmailExpander;
 import com.google.gerrit.server.account.GroupBackend;
 import com.google.gerrit.server.account.GroupCacheImpl;
 import com.google.gerrit.server.account.GroupControl;
+import com.google.gerrit.server.account.GroupDetailFactory;
 import com.google.gerrit.server.account.GroupIncludeCacheImpl;
 import com.google.gerrit.server.account.GroupInfoCacheFactory;
+import com.google.gerrit.server.account.GroupMembers;
 import com.google.gerrit.server.account.IncludingGroupMembership;
 import com.google.gerrit.server.account.InternalGroupBackend;
+import com.google.gerrit.server.account.PerformCreateGroup;
+import com.google.gerrit.server.account.PerformRenameGroup;
 import com.google.gerrit.server.account.Realm;
 import com.google.gerrit.server.account.UniversalGroupBackend;
+import com.google.gerrit.server.auth.AuthBackend;
+import com.google.gerrit.server.auth.InternalAuthBackend;
+import com.google.gerrit.server.auth.UniversalAuthBackend;
 import com.google.gerrit.server.auth.ldap.LdapModule;
+import com.google.gerrit.server.avatar.AvatarProvider;
 import com.google.gerrit.server.cache.CacheRemovalListener;
 import com.google.gerrit.server.events.EventFactory;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.ChangeCache;
 import com.google.gerrit.server.git.ChangeMergeQueue;
+import com.google.gerrit.server.git.GarbageCollection;
 import com.google.gerrit.server.git.GitModule;
 import com.google.gerrit.server.git.MergeQueue;
+import com.google.gerrit.server.git.MergeUtil;
+import com.google.gerrit.server.git.NotesBranchUtil;
 import com.google.gerrit.server.git.ReloadSubmitQueueOp;
 import com.google.gerrit.server.git.TagCache;
 import com.google.gerrit.server.git.TransferConfig;
+import com.google.gerrit.server.git.validators.CommitValidationListener;
+import com.google.gerrit.server.git.validators.CommitValidators;
+import com.google.gerrit.server.mail.AddReviewerSender;
+import com.google.gerrit.server.mail.CommitMessageEditedSender;
+import com.google.gerrit.server.mail.CreateChangeSender;
+import com.google.gerrit.server.mail.EmailModule;
 import com.google.gerrit.server.mail.FromAddressGenerator;
 import com.google.gerrit.server.mail.FromAddressGeneratorProvider;
+import com.google.gerrit.server.mail.MergeFailSender;
+import com.google.gerrit.server.mail.MergedSender;
+import com.google.gerrit.server.mail.RebasedPatchSetSender;
+import com.google.gerrit.server.mail.ReplacePatchSetSender;
 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.PerformCreateProject;
 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.SectionSortCache;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder;
+import com.google.gerrit.server.query.change.ChangeQueryRewriter;
+import com.google.gerrit.server.ssh.SshAddressesModule;
 import com.google.gerrit.server.tools.ToolsCatalog;
 import com.google.gerrit.server.util.IdGenerator;
 import com.google.gerrit.server.util.ThreadLocalRequestContext;
-import com.google.gerrit.server.workflow.FunctionState;
 import com.google.inject.Inject;
 import com.google.inject.TypeLiteral;
 
@@ -107,11 +138,10 @@
 
       default:
         bind(Realm.class).to(DefaultRealm.class);
+        DynamicSet.bind(binder(), AuthBackend.class).to(InternalAuthBackend.class);
         break;
     }
 
-    bind(ApprovalTypes.class).toProvider(ApprovalTypesProvider.class).in(
-        SINGLETON);
     bind(EmailExpander.class).toProvider(EmailExpanderProvider.class).in(
         SINGLETON);
 
@@ -125,27 +155,55 @@
     install(ProjectCacheImpl.module());
     install(SectionSortCache.module());
     install(TagCache.module());
+    install(ChangeCache.module());
+
     install(new AccessControlModule());
+    install(new EmailModule());
     install(new GitModule());
     install(new PrologModule());
+    install(new SshAddressesModule());
     install(ThreadLocalRequestContext.module());
 
+    bind(AccountResolver.class);
+    bind(ChangeQueryRewriter.class);
+
     factory(AccountInfoCacheFactory.Factory.class);
+    factory(AddReviewerSender.Factory.class);
     factory(CapabilityControl.Factory.class);
+    factory(ChangeQueryBuilder.Factory.class);
+    factory(CommitMessageEditedSender.Factory.class);
+    factory(CreateChangeSender.Factory.class);
+    factory(GroupDetailFactory.Factory.class);
     factory(GroupInfoCacheFactory.Factory.class);
+    factory(GroupMembers.Factory.class);
     factory(InternalUser.Factory.class);
+    factory(MergedSender.Factory.class);
+    factory(MergeFailSender.Factory.class);
+    factory(MergeUtil.Factory.class);
+    factory(PerformCreateGroup.Factory.class);
+    factory(PerformRenameGroup.Factory.class);
+    factory(PluginUser.Factory.class);
     factory(ProjectNode.Factory.class);
     factory(ProjectState.Factory.class);
+    factory(RebasedPatchSetSender.Factory.class);
+    factory(ReplacePatchSetSender.Factory.class);
+    factory(PerformCreateProject.Factory.class);
+    factory(GarbageCollection.Factory.class);
     bind(PermissionCollection.Factory.class);
     bind(AccountVisibility.class)
         .toProvider(AccountVisibilityProvider.class)
         .in(SINGLETON);
 
+    bind(AuthBackend.class).to(UniversalAuthBackend.class).in(SINGLETON);
+    DynamicSet.setOf(binder(), AuthBackend.class);
+
     bind(GroupControl.Factory.class).in(SINGLETON);
+    bind(GroupControl.GenericFactory.class).in(SINGLETON);
     factory(IncludingGroupMembership.Factory.class);
-    bind(InternalGroupBackend.class).in(SINGLETON);
     bind(GroupBackend.class).to(UniversalGroupBackend.class).in(SINGLETON);
     DynamicSet.setOf(binder(), GroupBackend.class);
+
+    bind(InternalGroupBackend.class).in(SINGLETON);
     DynamicSet.bind(binder(), GroupBackend.class).to(InternalGroupBackend.class);
 
     bind(FileTypeRegistry.class).to(MimeUtilFileTypeRegistry.class);
@@ -167,16 +225,31 @@
     bind(IdentifiedUser.GenericFactory.class).in(SINGLETON);
     bind(ChangeControl.GenericFactory.class);
     bind(ProjectControl.GenericFactory.class);
-    factory(FunctionState.Factory.class);
+    bind(AccountControl.Factory.class);
 
     install(new AuditModule());
+    install(new com.google.gerrit.server.account.Module());
+    install(new com.google.gerrit.server.change.Module());
+    install(new com.google.gerrit.server.group.Module());
+    install(new com.google.gerrit.server.project.Module());
 
     bind(GitReferenceUpdated.class);
     DynamicMap.mapOf(binder(), new TypeLiteral<Cache<?, ?>>() {});
     DynamicSet.setOf(binder(), CacheRemovalListener.class);
     DynamicSet.setOf(binder(), GitReferenceUpdatedListener.class);
     DynamicSet.setOf(binder(), NewProjectCreatedListener.class);
+    DynamicSet.bind(binder(), GitReferenceUpdatedListener.class).to(ChangeCache.class);
+    DynamicSet.setOf(binder(), ChangeListener.class);
+    DynamicSet.setOf(binder(), CommitValidationListener.class);
+    DynamicItem.itemOf(binder(), AvatarProvider.class);
 
     bind(AnonymousUser.class);
+
+    factory(CommitValidators.Factory.class);
+    factory(NotesBranchUtil.Factory.class);
+
+    bind(AccountManager.class);
+    bind(ChangeUserName.CurrentUser.class);
+    factory(ChangeUserName.Factory.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 ba54c56..ec14883 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,48 +16,20 @@
 
 import static com.google.inject.Scopes.SINGLETON;
 
-import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.ApprovalsUtil;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.RequestCleanup;
-import com.google.gerrit.server.account.AccountControl;
-import com.google.gerrit.server.account.AccountResolver;
-import com.google.gerrit.server.account.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.change.ChangeInserter;
 import com.google.gerrit.server.changedetail.DeleteDraftPatchSet;
 import com.google.gerrit.server.changedetail.PublishDraft;
-import com.google.gerrit.server.changedetail.Submit;
-import com.google.gerrit.server.git.AsyncReceiveCommits;
 import com.google.gerrit.server.git.BanCommit;
-import com.google.gerrit.server.git.CreateCodeReviewNotes;
 import com.google.gerrit.server.git.MergeOp;
-import com.google.gerrit.server.git.MetaDataUpdate;
-import com.google.gerrit.server.git.NotesBranchUtil;
 import com.google.gerrit.server.git.SubmoduleOp;
-import com.google.gerrit.server.mail.AbandonedSender;
-import com.google.gerrit.server.mail.AddReviewerSender;
-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.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;
 
 /** Bindings for {@link RequestScoped} entities. */
@@ -65,52 +37,24 @@
   @Override
   protected void configure() {
     bind(RequestCleanup.class).in(RequestScoped.class);
-    bind(ReviewDb.class).toProvider(RequestScopedReviewDbProvider.class).in(
-        RequestScoped.class);
+    bind(RequestScopedReviewDbProvider.class);
     bind(IdentifiedUser.RequestFactory.class).in(SINGLETON);
-    bind(MetaDataUpdate.User.class).in(RequestScoped.class);
-    bind(AccountResolver.class);
-    bind(ChangeQueryRewriter.class);
-    bind(ListProjects.class);
     bind(ApprovalsUtil.class);
+    bind(ChangeInserter.class);
 
     bind(PerRequestProjectControlCache.class).in(RequestScoped.class);
     bind(ChangeControl.Factory.class).in(SINGLETON);
     bind(ProjectControl.Factory.class).in(SINGLETON);
-    bind(AccountControl.Factory.class).in(SINGLETON);
 
-    factory(ChangeQueryBuilder.Factory.class);
     factory(SubmoduleOp.Factory.class);
     factory(MergeOp.Factory.class);
-    factory(CreateCodeReviewNotes.Factory.class);
-    factory(NotesBranchUtil.Factory.class);
-    install(new AsyncReceiveCommits.Module());
 
     // Not really per-request, but dammit, I don't know where else to
     // easily park this stuff.
     //
-    factory(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(RestoredSender.Factory.class);
-    factory(RevertedSender.Factory.class);
-    factory(CommentSender.Factory.class);
-    factory(MergedSender.Factory.class);
-    factory(MergeFailSender.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);
     factory(BanCommit.Factory.class);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/config/MasterNodeStartup.java b/gerrit-server/src/main/java/com/google/gerrit/server/config/MasterNodeStartup.java
index bb95dca..bf96a22 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/config/MasterNodeStartup.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/config/MasterNodeStartup.java
@@ -14,35 +14,60 @@
 
 package com.google.gerrit.server.config;
 
+import static java.util.concurrent.TimeUnit.MINUTES;
+import static java.util.concurrent.TimeUnit.SECONDS;
+
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.server.git.ReloadSubmitQueueOp;
 import com.google.inject.Inject;
+import com.google.inject.Singleton;
 
-import java.util.concurrent.TimeUnit;
+import org.eclipse.jgit.lib.Config;
+
+import java.util.concurrent.ScheduledFuture;
 
 /** Configuration for a master node in a cluster of servers. */
 public class MasterNodeStartup extends LifecycleModule {
   @Override
   public void configure() {
-    listener().to(OnStart.class);
+    listener().to(Lifecycle.class);
   }
 
-  static class OnStart implements LifecycleListener {
+  @Singleton
+  static class Lifecycle implements LifecycleListener {
+    private static final int INITIAL_DELAY_S = 15;
+
     private final ReloadSubmitQueueOp.Factory submit;
+    private final long delay;
+    private volatile ScheduledFuture<?> handle;
 
     @Inject
-    OnStart(final ReloadSubmitQueueOp.Factory submit) {
+    Lifecycle(ReloadSubmitQueueOp.Factory submit,
+        @GerritServerConfig Config config) {
       this.submit = submit;
+      this.delay = ConfigUtil.getTimeUnit(config,
+          "changeMerge", null, "checkFrequency",
+          SECONDS.convert(5, MINUTES), SECONDS);
     }
 
     @Override
     public void start() {
-      submit.create().start(15, TimeUnit.SECONDS);
+      if (delay > 0) {
+        handle = submit.create()
+            .startWithFixedDelay(INITIAL_DELAY_S, delay, SECONDS);
+      } else {
+        handle = submit.create().start(INITIAL_DELAY_S, SECONDS);
+      }
     }
 
     @Override
     public void stop() {
+      ScheduledFuture<?> f = handle;
+      if (f != null) {
+        handle = null;
+        f.cancel(true);
+      }
     }
   }
 }
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 4518a2e..4ada7b1 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
@@ -21,16 +21,17 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 import com.google.inject.ProvisionException;
-import com.google.inject.Singleton;
+import com.google.inject.servlet.RequestScoped;
 
 /** Provides {@link ReviewDb} database handle live only for this request. */
-@Singleton
-final class RequestScopedReviewDbProvider implements Provider<ReviewDb> {
+@RequestScoped
+public class RequestScopedReviewDbProvider implements Provider<ReviewDb> {
   private final SchemaFactory<ReviewDb> schema;
   private final Provider<RequestCleanup> cleanup;
+  private ReviewDb db;
 
   @Inject
-  RequestScopedReviewDbProvider(final SchemaFactory<ReviewDb> schema,
+  public RequestScopedReviewDbProvider(final SchemaFactory<ReviewDb> schema,
       final Provider<RequestCleanup> cleanup) {
     this.schema = schema;
     this.cleanup = cleanup;
@@ -38,26 +39,27 @@
 
   @Override
   public ReviewDb get() {
-    final ReviewDb c;
-    try {
-      c = schema.open();
-    } catch (OrmException e) {
-      throw new ProvisionException("Cannot open ReviewDb", e);
+    if (db == null) {
+      final ReviewDb c;
+      try {
+        c = schema.open();
+      } catch (OrmException e) {
+        throw new ProvisionException("Cannot open ReviewDb", e);
+      }
+      try {
+        cleanup.get().add(new Runnable() {
+          @Override
+          public void run() {
+            c.close();
+            db = null;
+          }
+        });
+      } catch (Throwable e) {
+        c.close();
+        throw new ProvisionException("Cannot defer cleanup of ReviewDb", e);
+      }
+      db = c;
     }
-    try {
-      cleanup.get().add(new Runnable() {
-        @Override
-        public void run() {
-          c.close();
-        }
-      });
-      return c;
-    } catch (Error e) {
-      c.close();
-      throw e;
-    } catch (RuntimeException e) {
-      c.close();
-      throw e;
-    }
+    return db;
   }
 }
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 bc88cd2..bd1c0d7 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
@@ -107,6 +107,7 @@
     return true;
   }
 
+  @SuppressWarnings("resource")
   private static PGPPublicKeyRingCollection readPubRing(final File pub) {
     try {
       InputStream in = new FileInputStream(pub);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/documentation/MarkdownFormatter.java b/gerrit-server/src/main/java/com/google/gerrit/server/documentation/MarkdownFormatter.java
index 6bb7ba8..af08d1e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/documentation/MarkdownFormatter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/documentation/MarkdownFormatter.java
@@ -136,8 +136,12 @@
     InputStream in = url.openStream();
     try {
       TemporaryBuffer.Heap tmp = new TemporaryBuffer.Heap(128 * 1024);
-      tmp.copy(in);
-      return new String(tmp.toByteArray(), "UTF-8");
+      try {
+        tmp.copy(in);
+        return new String(tmp.toByteArray(), "UTF-8");
+      } finally {
+        tmp.close();
+      }
     } finally {
       in.close();
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/CommitReceivedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/CommitReceivedEvent.java
new file mode 100644
index 0000000..8dd1084
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/CommitReceivedEvent.java
@@ -0,0 +1,38 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.events;
+
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.IdentifiedUser;
+
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.transport.ReceiveCommand;
+
+public class CommitReceivedEvent extends ChangeEvent {
+  public final ReceiveCommand command;
+  public final Project project;
+  public final String refName;
+  public final RevCommit commit;
+  public final IdentifiedUser user;
+
+  public CommitReceivedEvent(ReceiveCommand command, Project project,
+      String refName, RevCommit commit, IdentifiedUser user) {
+    this.command = command;
+    this.project = project;
+    this.refName = refName;
+    this.commit = commit;
+    this.user = user;
+  }
+}
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 f07ae0c..d556e73 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
@@ -14,19 +14,21 @@
 
 package com.google.gerrit.server.events;
 
-import com.google.gerrit.common.data.ApprovalType;
-import com.google.gerrit.common.data.ApprovalTypes;
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Change;
 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.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.client.UserIdentity;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.account.AccountCache;
@@ -35,6 +37,8 @@
 import com.google.gerrit.server.patch.PatchListCache;
 import com.google.gerrit.server.patch.PatchListEntry;
 import com.google.gerrit.server.patch.PatchListNotAvailableException;
+import com.google.gerrit.server.patch.PatchSetInfoFactory;
+import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
@@ -48,8 +52,8 @@
 
 import java.util.ArrayList;
 import java.util.Collection;
-import java.util.Map;
 import java.util.List;
+import java.util.Map;
 
 import javax.annotation.Nullable;
 
@@ -58,22 +62,22 @@
   private static final Logger log = LoggerFactory.getLogger(EventFactory.class);
   private final AccountCache accountCache;
   private final Provider<String> urlProvider;
-  private final ApprovalTypes approvalTypes;
   private final PatchListCache patchListCache;
   private final SchemaFactory<ReviewDb> schema;
+  private final PatchSetInfoFactory psInfoFactory;
   private final PersonIdent myIdent;
 
   @Inject
   EventFactory(AccountCache accountCache,
       @CanonicalWebUrl @Nullable Provider<String> urlProvider,
-      ApprovalTypes approvalTypes,
+      PatchSetInfoFactory psif,
       PatchListCache patchListCache, SchemaFactory<ReviewDb> schema,
       @GerritPersonIdent PersonIdent myIdent) {
     this.accountCache = accountCache;
     this.urlProvider = urlProvider;
-    this.approvalTypes = approvalTypes;
     this.patchListCache = patchListCache;
     this.schema = schema;
+    this.psInfoFactory = psif;
     this.myIdent = myIdent;
   }
 
@@ -184,11 +188,24 @@
           }
         }
 
-        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));
+        final PatchSet ps = db.patchSets().get(psId);
+        if (ps == null) {
+          log.error("Error while generating the list of descendants for"
+              + " PatchSet " + psId + ": Cannot find PatchSet entry in"
+              + " database.");
+        } else {
+          final RevId revId = ps.getRevision();
+          for (PatchSetAncestor a : db.patchSetAncestors().descendantsOf(revId)) {
+            final PatchSet p = db.patchSets().get(a.getPatchSet());
+            if (p == null) {
+              log.error("Error while generating the list of descendants for"
+                  + " revision " + revId.get() + ": Cannot find PatchSet entry in"
+                  + " database for " + a.getPatchSet());
+              continue;
+            }
+            final Change c = db.changes().get(p.getId().getParentKey());
+            ca.neededBy.add(newNeededBy(c, p));
+          }
         }
       } finally {
         db.close();
@@ -207,7 +224,7 @@
 
   private DependencyAttribute newDependsOn(Change c, PatchSet ps) {
     DependencyAttribute d = newDependencyAttribute(c, ps);
-    d.isCurrentPatchSet = c.currPatchSetId().equals(ps.getId());
+    d.isCurrentPatchSet = ps.getId().equals(c.currentPatchSetId());
     return d;
   }
 
@@ -237,25 +254,26 @@
     a.commitMessage = commitMessage;
   }
 
-  public void addPatchSets(ChangeAttribute a, Collection<PatchSet> ps) {
-    addPatchSets(a, ps, null, false, null);
+  public void addPatchSets(ChangeAttribute a, Collection<PatchSet> ps,
+      LabelTypes labelTypes) {
+    addPatchSets(a, ps, null, false, null, labelTypes);
   }
 
   public void addPatchSets(ChangeAttribute ca, Collection<PatchSet> ps,
-      Map<PatchSet.Id,Collection<PatchSetApproval>> approvals) {
-    addPatchSets(ca, ps, approvals, false, null);
+      Map<PatchSet.Id, Collection<PatchSetApproval>> approvals,
+      LabelTypes labelTypes) {
+    addPatchSets(ca, ps, approvals, false, null, labelTypes);
   }
 
   public void addPatchSets(ChangeAttribute ca, Collection<PatchSet> ps,
-      Map<PatchSet.Id,Collection<PatchSetApproval>> approvals,
-      boolean includeFiles, Change change) {
-
+      Map<PatchSet.Id, Collection<PatchSetApproval>> approvals,
+      boolean includeFiles, Change change, LabelTypes labelTypes) {
     if (!ps.isEmpty()) {
       ca.patchSets = new ArrayList<PatchSetAttribute>(ps.size());
       for (PatchSet p : ps) {
         PatchSetAttribute psa = asPatchSetAttribute(p);
         if (approvals != null) {
-          addApprovals(psa, p.getId(), approvals);
+          addApprovals(psa, p.getId(), approvals, labelTypes);
         }
         ca.patchSets.add(psa);
         if (includeFiles && change != null) {
@@ -290,7 +308,10 @@
 
         PatchAttribute p = new PatchAttribute();
         p.file = patch.getNewName();
+        p.fileOld = patch.getOldName();
         p.type = patch.getChangeType();
+        p.deletions -= patch.getDeletions();
+        p.insertions = patch.getInsertions();
         patchSetAttribute.files.add(p);
       }
     } catch (PatchListNotAvailableException e) {
@@ -328,6 +349,7 @@
     p.ref = patchSet.getRefName();
     p.uploader = asAccountAttribute(patchSet.getUploader());
     p.createdOn = patchSet.getCreatedOn().getTime() / 1000L;
+    final PatchSet.Id pId = patchSet.getId();
     try {
       final ReviewDb db = schema.open();
       try {
@@ -336,30 +358,55 @@
             patchSet.getId())) {
           p.parents.add(a.getAncestorRevision().get());
         }
+
+        UserIdentity author = psInfoFactory.get(db, pId).getAuthor();
+        if (author.getAccount() == null) {
+          p.author = new AccountAttribute();
+          p.author.email = author.getEmail();
+          p.author.name = author.getName();
+          p.author.username = "";
+        } else {
+          p.author = asAccountAttribute(author.getAccount());
+        }
+
+        Change change = db.changes().get(pId.getParentKey());
+        List<Patch> list =
+            patchListCache.get(change, patchSet).toPatchList(pId);
+        for (Patch pe : list) {
+          if (!Patch.COMMIT_MSG.equals(pe.getFileName())) {
+            p.sizeDeletions -= pe.getDeletions();
+            p.sizeInsertions += pe.getInsertions();
+          }
+        }
       } finally {
         db.close();
       }
     } catch (OrmException e) {
       log.error("Cannot load patch set data for " + patchSet.getId(), e);
+    } catch (PatchSetInfoNotAvailableException e) {
+      log.error(String.format("Cannot get authorEmail for %s.", pId), e);
+    } catch (PatchListNotAvailableException e) {
+      log.error(String.format("Cannot get size information for %s.", pId), e);
     }
     return p;
   }
 
   public void addApprovals(PatchSetAttribute p, PatchSet.Id id,
-      Map<PatchSet.Id,Collection<PatchSetApproval>> all) {
+      Map<PatchSet.Id, Collection<PatchSetApproval>> all,
+      LabelTypes labelTypes) {
     Collection<PatchSetApproval> list = all.get(id);
     if (list != null) {
-      addApprovals(p, list);
+      addApprovals(p, list, labelTypes);
     }
   }
 
   public void addApprovals(PatchSetAttribute p,
-      Collection<PatchSetApproval> list) {
+      Collection<PatchSetApproval> list, LabelTypes labelTypes) {
     if (!list.isEmpty()) {
       p.approvals = new ArrayList<ApprovalAttribute>(list.size());
       for (PatchSetApproval a : list) {
         if (a.getValue() != 0) {
-          p.approvals.add(asApprovalAttribute(a));
+          p.approvals.add(asApprovalAttribute(a, labelTypes));
         }
       }
       if (p.approvals.isEmpty()) {
@@ -376,6 +423,9 @@
    * @return object suitable for serialization to JSON
    */
   public AccountAttribute asAccountAttribute(Account.Id id) {
+    if (id == null) {
+      return null;
+    }
     return asAccountAttribute(accountCache.get(id).getAccount());
   }
 
@@ -413,18 +463,20 @@
    * serialization to JSON.
    *
    * @param approval
+   * @param labelTypes label types for the containing project
    * @return object suitable for serialization to JSON
    */
-  public ApprovalAttribute asApprovalAttribute(PatchSetApproval approval) {
+  public ApprovalAttribute asApprovalAttribute(PatchSetApproval approval,
+      LabelTypes labelTypes) {
     ApprovalAttribute a = new ApprovalAttribute();
-    a.type = approval.getCategoryId().get();
+    a.type = approval.getLabelId().get();
     a.value = Short.toString(approval.getValue());
     a.by = asAccountAttribute(approval.getAccountId());
     a.grantedOn = approval.getGranted().getTime() / 1000L;
 
-    ApprovalType at = approvalTypes.byId(approval.getCategoryId());
-    if (at != null) {
-      a.description = at.getCategory().getName();
+    LabelType lt = labelTypes.byLabel(approval.getLabelId());
+    if (lt != null) {
+      a.description = lt.getName();
     }
     return a;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/MergeFailedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/MergeFailedEvent.java
new file mode 100644
index 0000000..e6ff525
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/MergeFailedEvent.java
@@ -0,0 +1,23 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.events;
+
+public class MergeFailedEvent extends ChangeEvent {
+    public final String type = "merge-failed";
+    public ChangeAttribute change;
+    public PatchSetAttribute patchSet;
+    public AccountAttribute submitter;
+    public String reason;
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/PatchAttribute.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/PatchAttribute.java
index 3802fdd..82f44a1 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/PatchAttribute.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/PatchAttribute.java
@@ -18,5 +18,8 @@
 
 public class PatchAttribute {
     public String file;
+    public String fileOld;
     public ChangeType type;
+    public int insertions;
+    public int deletions;
 }
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 f726ce3..1123e5f 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
@@ -23,8 +23,11 @@
   public String ref;
   public AccountAttribute uploader;
   public Long createdOn;
+  public AccountAttribute author;
 
   public List<ApprovalAttribute> approvals;
   public List<PatchSetCommentAttribute> comments;
   public List<PatchAttribute> files;
+  public int sizeInsertions;
+  public int sizeDeletions;
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/QueryStats.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/QueryStats.java
index 1c5e7d7..ecf2b9a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/events/QueryStats.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/QueryStats.java
@@ -18,4 +18,5 @@
   public final String type = "stats";
   public int rowCount;
   public long runTimeMilliseconds;
+  public String resumeSortKey;
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/events/ReviewerAddedEvent.java b/gerrit-server/src/main/java/com/google/gerrit/server/events/ReviewerAddedEvent.java
new file mode 100644
index 0000000..a881d8d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/events/ReviewerAddedEvent.java
@@ -0,0 +1,22 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.events;
+
+public class ReviewerAddedEvent extends ChangeEvent {
+    public final String type = "reviewer-added";
+    public ChangeAttribute change;
+    public PatchSetAttribute patchSet;
+    public AccountAttribute reviewer;
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java
index 833b611..49c90bd 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/extensions/events/GitReferenceUpdated.java
@@ -20,6 +20,9 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.inject.Inject;
 
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.RefUpdate;
+
 import java.util.Collections;
 import java.util.List;
 
@@ -38,8 +41,16 @@
     this.listeners = listeners;
   }
 
-  public void fire(Project.NameKey project, String ref) {
-    Event event = new Event(project, ref);
+  public void fire(Project.NameKey project, RefUpdate refUpdate) {
+    fire(project, refUpdate.getName(), refUpdate.getOldObjectId(),
+        refUpdate.getNewObjectId());
+  }
+
+  public void fire(Project.NameKey project, String ref,
+      ObjectId oldObjectId, ObjectId newObjectId) {
+    ObjectId o = oldObjectId != null ? oldObjectId : ObjectId.zeroId();
+    ObjectId n = newObjectId != null ? newObjectId : ObjectId.zeroId();
+    Event event = new Event(project, ref, o.name(), n.name());
     for (GitReferenceUpdatedListener l : listeners) {
       l.onGitReferenceUpdated(event);
     }
@@ -48,10 +59,15 @@
   private static class Event implements GitReferenceUpdatedListener.Event {
     private final String projectName;
     private final String ref;
+    private final String oldObjectId;
+    private final String newObjectId;
 
-    Event(Project.NameKey project, String ref) {
+    Event(Project.NameKey project, String ref,
+        String oldObjectId, String newObjectId) {
       this.projectName = project.get();
       this.ref = ref;
+      this.oldObjectId = oldObjectId;
+      this.newObjectId = newObjectId;
     }
 
     @Override
@@ -63,9 +79,20 @@
     public List<GitReferenceUpdatedListener.Update> getUpdates() {
       GitReferenceUpdatedListener.Update update =
           new GitReferenceUpdatedListener.Update() {
+            @Override
             public String getRefName() {
               return ref;
             }
+
+            @Override
+            public String getOldObjectId() {
+              return oldObjectId;
+            }
+
+            @Override
+            public String getNewObjectId() {
+              return newObjectId;
+            }
           };
       return ImmutableList.of(update);
     }
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
index c8d741b..aaf0273 100644
--- 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
@@ -165,7 +165,7 @@
           timeoutMillis, TimeUnit.MILLISECONDS);
     } catch (ExecutionException e) {
       log.warn("Error in ReceiveCommits", e);
-      rc.addError("internal error while processing changes");
+      rc.addError("internal error while processing changes " + e.getMessage());
       // ReceiveCommits has tried its best to catch errors, so anything at this
       // point is very bad.
       for (final ReceiveCommand c : commands) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommit.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommit.java
index c0e00aa..6859262 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommit.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/BanCommit.java
@@ -17,6 +17,7 @@
 import static com.google.gerrit.server.git.GitRepositoryManager.REF_REJECT_COMMITS;
 
 import com.google.gerrit.common.errors.PermissionDeniedException;
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.project.ProjectControl;
@@ -74,8 +75,8 @@
     final BanCommitResult result = new BanCommitResult();
     NoteMap banCommitNotes = NoteMap.newEmptyMap();
     // add a note for each banned commit to notes
-    final Repository repo =
-        repoManager.openRepository(projectControl.getProject().getNameKey());
+    final Project.NameKey project = projectControl.getProject().getNameKey();
+    final Repository repo = repoManager.openRepository(project);
     try {
       final RevWalk revWalk = new RevWalk(repo);
       final ObjectInserter inserter = repo.newObjectInserter();
@@ -92,7 +93,8 @@
           banCommitNotes.set(commitToBan, createNoteContent(reason, inserter));
         }
         inserter.flush();
-        NotesBranchUtil notesBranchUtil = notesBranchUtilFactory.create(repo);
+        NotesBranchUtil notesBranchUtil = notesBranchUtilFactory.create(project,
+            repo, inserter);
         NoteMap newlyCreated =
             notesBranchUtil.commitNewNotes(banCommitNotes, REF_REJECT_COMMITS,
                 createPersonIdent(), buildCommitMessage(commitsToBan, reason));
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeCache.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeCache.java
new file mode 100644
index 0000000..6f71936
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeCache.java
@@ -0,0 +1,101 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.package com.google.gerrit.server.git;
+
+package com.google.gerrit.server.git;
+
+import com.google.common.cache.CacheLoader;
+import com.google.common.cache.LoadingCache;
+import com.google.gerrit.extensions.events.GitReferenceUpdatedListener;
+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.cache.CacheModule;
+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 org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.concurrent.ExecutionException;
+
+@Singleton
+public class ChangeCache implements GitReferenceUpdatedListener {
+  private static final Logger log =
+      LoggerFactory.getLogger(ChangeCache.class);
+  private static final String ID_CACHE = "changes";
+
+  public static Module module() {
+    return new CacheModule() {
+      @Override
+      protected void configure() {
+        cache(ID_CACHE,
+            Project.NameKey.class,
+            new TypeLiteral<List<Change>>() {})
+          .maximumWeight(0)
+          .loader(Loader.class);
+      }
+    };
+  }
+
+  private final LoadingCache<Project.NameKey, List<Change>> cache;
+
+  @Inject
+  ChangeCache(@Named(ID_CACHE) LoadingCache<Project.NameKey, List<Change>> cache) {
+    this.cache = cache;
+  }
+
+  List<Change> get(Project.NameKey name) {
+    try {
+      return cache.get(name);
+    } catch (ExecutionException e) {
+      log.warn("Cannot fetch changes for " + name, e);
+      return Collections.emptyList();
+    }
+  }
+
+  @Override
+  public void onGitReferenceUpdated(GitReferenceUpdatedListener.Event event) {
+    for (GitReferenceUpdatedListener.Update u : event.getUpdates()) {
+      if (u.getRefName().startsWith("refs/changes/")) {
+        cache.invalidate(new Project.NameKey(event.getProjectName()));
+        break;
+      }
+    }
+  }
+
+  static class Loader extends CacheLoader<Project.NameKey, List<Change>> {
+    private final SchemaFactory<ReviewDb> schema;
+
+    @Inject
+    Loader(SchemaFactory<ReviewDb> schema) {
+      this.schema = schema;
+    }
+
+    @Override
+    public List<Change> load(Project.NameKey key) throws Exception {
+      final ReviewDb db = schema.open();
+      try {
+        return Collections.unmodifiableList(db.changes().byProject(key).toList());
+      } finally {
+        db.close();
+      }
+    }
+  }
+}
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 6d1b155..12219e2 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
@@ -19,9 +19,11 @@
 
 import com.google.gerrit.reviewdb.client.Branch;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.RemotePeer;
 import com.google.gerrit.server.config.GerritRequestModule;
+import com.google.gerrit.server.config.RequestScopedReviewDbProvider;
 import com.google.gerrit.server.ssh.SshInfo;
 import com.google.gerrit.server.util.RequestContext;
 import com.google.gerrit.server.util.RequestScopePropagator;
@@ -91,12 +93,18 @@
 
       @Provides
       public PerThreadRequestScope.Scoper provideScoper(
-          final PerThreadRequestScope.Propagator propagator) {
+          final PerThreadRequestScope.Propagator propagator,
+          final Provider<RequestScopedReviewDbProvider> dbProvider) {
         final RequestContext requestContext = new RequestContext() {
           @Override
           public CurrentUser getCurrentUser() {
             throw new OutOfScopeException("No user on merge thread");
           }
+
+          @Override
+          public Provider<ReviewDb> getReviewDbProvider() {
+            return dbProvider.get();
+          }
         };
         return new PerThreadRequestScope.Scoper() {
           @Override
@@ -111,9 +119,9 @@
   }
 
   @Override
-  public void merge(MergeOp.Factory mof, Branch.NameKey branch) {
+  public void merge(Branch.NameKey branch) {
     if (start(branch)) {
-      mergeImpl(mof, branch);
+      mergeImpl(branch);
     }
   }
 
@@ -191,16 +199,6 @@
     e.needMerge = false;
   }
 
-  private void mergeImpl(MergeOp.Factory opFactory, Branch.NameKey branch) {
-    try {
-      opFactory.create(branch).merge();
-    } catch (Throwable e) {
-      log.error("Merge attempt for " + branch + " failed", e);
-    } finally {
-      finish(branch);
-    }
-  }
-
   private void mergeImpl(final Branch.NameKey branch) {
     try {
       threadScoper.scope(new Callable<Void>(){
@@ -243,6 +241,7 @@
       dest = d;
     }
 
+    @Override
     public void run() {
       unschedule(this);
       mergeImpl(dest);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeUpdateExecutor.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeUpdateExecutor.java
new file mode 100644
index 0000000..3452bb0
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ChangeUpdateExecutor.java
@@ -0,0 +1,31 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.inject.BindingAnnotation;
+
+import java.lang.annotation.Retention;
+
+/**
+ * Marker on the global {@link ListeningExecutorService} used by
+ * {@link ReceiveCommits} to create or replace changes.
+ */
+@Retention(RUNTIME)
+@BindingAnnotation
+public @interface ChangeUpdateExecutor {
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/CherryPick.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/CherryPick.java
new file mode 100644
index 0000000..ce5308f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/CherryPick.java
@@ -0,0 +1,220 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git;
+
+import com.google.common.collect.Lists;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetAncestor;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+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.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.patch.PatchSetInfoFactory;
+import com.google.gwtorm.server.OrmException;
+
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.revwalk.RevCommit;
+
+import java.io.IOException;
+import java.sql.Timestamp;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class CherryPick extends SubmitStrategy {
+  private final PatchSetInfoFactory patchSetInfoFactory;
+  private final GitReferenceUpdated gitRefUpdated;
+  private final Map<Change.Id, CodeReviewCommit> newCommits;
+
+  CherryPick(final SubmitStrategy.Arguments args,
+      final PatchSetInfoFactory patchSetInfoFactory,
+      final GitReferenceUpdated gitRefUpdated) {
+    super(args);
+
+    this.patchSetInfoFactory = patchSetInfoFactory;
+    this.gitRefUpdated = gitRefUpdated;
+    this.newCommits = new HashMap<Change.Id, CodeReviewCommit>();
+  }
+
+  @Override
+  protected CodeReviewCommit _run(final CodeReviewCommit mergeTip,
+      final List<CodeReviewCommit> toMerge) throws MergeException {
+    CodeReviewCommit newMergeTip = mergeTip;
+    while (!toMerge.isEmpty()) {
+      final CodeReviewCommit n = toMerge.remove(0);
+
+      try {
+        if (newMergeTip == null) {
+          // The branch is unborn. Take a fast-forward resolution to
+          // create the branch.
+          //
+          newMergeTip = n;
+          n.statusCode = CommitMergeStatus.CLEAN_MERGE;
+
+        } else if (n.getParentCount() == 0) {
+          // Refuse to merge a root commit into an existing branch,
+          // we cannot obtain a delta for the cherry-pick to apply.
+          //
+          n.statusCode = CommitMergeStatus.CANNOT_CHERRY_PICK_ROOT;
+
+        } else if (n.getParentCount() == 1) {
+          // If there is only one parent, a cherry-pick can be done by
+          // taking the delta relative to that one parent and redoing
+          // that on the current merge tip.
+          //
+
+          newMergeTip = writeCherryPickCommit(mergeTip, n);
+
+          if (newMergeTip != null) {
+            newCommits.put(newMergeTip.patchsetId.getParentKey(), newMergeTip);
+          } else {
+            n.statusCode = CommitMergeStatus.PATH_CONFLICT;
+          }
+
+        } else {
+          // There are multiple parents, so this is a merge commit. We
+          // don't want to cherry-pick the merge as clients can't easily
+          // rebase their history with that merge present and replaced
+          // by an equivalent merge with a different first parent. So
+          // instead behave as though MERGE_IF_NECESSARY was configured.
+          //
+          if (!args.mergeUtil.hasMissingDependencies(args.mergeSorter, n)) {
+            if (args.rw.isMergedInto(newMergeTip, n)) {
+              newMergeTip = n;
+            } else {
+              newMergeTip =
+                  args.mergeUtil.mergeOneCommit(args.myIdent, args.repo,
+                      args.rw, args.inserter, args.canMergeFlag,
+                      args.destBranch, newMergeTip, n);
+           }
+            final PatchSetApproval submitApproval =
+                args.mergeUtil.markCleanMerges(args.rw, args.canMergeFlag,
+                    newMergeTip, args.alreadyAccepted);
+            setRefLogIdent(submitApproval);
+
+          } else {
+            // One or more dependencies were not met. The status was
+            // already marked on the commit so we have nothing further
+            // to perform at this time.
+            //
+          }
+        }
+
+      } catch (IOException e) {
+        throw new MergeException("Cannot merge " + n.name(), e);
+      } catch (OrmException e) {
+        throw new MergeException("Cannot merge " + n.name(), e);
+      }
+    }
+    return newMergeTip;
+  }
+
+  private CodeReviewCommit writeCherryPickCommit(final CodeReviewCommit mergeTip, final CodeReviewCommit n)
+      throws IOException, OrmException {
+
+    args.rw.parseBody(n);
+
+    final PatchSetApproval submitAudit =
+        args.mergeUtil.getSubmitter(n.change.currentPatchSetId());
+
+    PersonIdent cherryPickCommitterIdent = null;
+    if (submitAudit != null) {
+      cherryPickCommitterIdent =
+          args.identifiedUserFactory.create(submitAudit.getAccountId())
+              .newCommitterIdent(submitAudit.getGranted(),
+                  args.myIdent.getTimeZone());
+    } else {
+      cherryPickCommitterIdent = args.myIdent;
+    }
+
+    final String cherryPickCmtMsg = args.mergeUtil.createCherryPickCommitMessage(n);
+
+    final CodeReviewCommit newCommit =
+        args.mergeUtil.createCherryPickFromCommit(args.repo, args.inserter, mergeTip, n,
+            cherryPickCommitterIdent, cherryPickCmtMsg, args.rw);
+
+    if (newCommit == null) {
+        return null;
+    }
+
+    PatchSet.Id id =
+        ChangeUtil.nextPatchSetId(args.repo, n.change.currentPatchSetId());
+    final PatchSet ps = new PatchSet(id);
+    ps.setCreatedOn(new Timestamp(System.currentTimeMillis()));
+    ps.setUploader(submitAudit.getAccountId());
+    ps.setRevision(new RevId(newCommit.getId().getName()));
+    insertAncestors(args.db, ps.getId(), newCommit);
+    args.db.patchSets().insert(Collections.singleton(ps));
+
+    n.change.setCurrentPatchSet(patchSetInfoFactory.get(newCommit, ps.getId()));
+    args.db.changes().update(Collections.singletonList(n.change));
+
+    final List<PatchSetApproval> approvals = Lists.newArrayList();
+    for (PatchSetApproval a : args.mergeUtil.getApprovalsForCommit(n)) {
+      approvals.add(new PatchSetApproval(ps.getId(), a));
+    }
+    args.db.patchSetApprovals().insert(approvals);
+
+    final RefUpdate ru = args.repo.updateRef(ps.getRefName());
+    ru.setExpectedOldObjectId(ObjectId.zeroId());
+    ru.setNewObjectId(newCommit);
+    ru.disableRefLog();
+    if (ru.update(args.rw) != RefUpdate.Result.NEW) {
+      throw new IOException(String.format("Failed to create ref %s in %s: %s",
+          ps.getRefName(), n.change.getDest().getParentKey().get(),
+          ru.getResult()));
+    }
+
+    gitRefUpdated.fire(n.change.getProject(), ru);
+
+    newCommit.copyFrom(n);
+    newCommit.statusCode = CommitMergeStatus.CLEAN_PICK;
+    newCommits.put(newCommit.patchsetId.getParentKey(), newCommit);
+    setRefLogIdent(submitAudit);
+    return newCommit;
+  }
+
+  private 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;
+
+      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);
+  }
+
+  @Override
+  public Map<Change.Id, CodeReviewCommit> getNewCommits() {
+    return newCommits;
+  }
+
+  @Override
+  public boolean dryRun(final CodeReviewCommit mergeTip,
+      final CodeReviewCommit toMerge) throws MergeException {
+    return args.mergeUtil.canCherryPick(args.mergeSorter, args.repo,
+        mergeTip, args.rw, toMerge);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/CodeReviewNoteCreationException.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/CodeReviewNoteCreationException.java
deleted file mode 100644
index 367ed56..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/CodeReviewNoteCreationException.java
+++ /dev/null
@@ -1,37 +0,0 @@
-// Copyright (C) 2011 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-package com.google.gerrit.server.git;
-
-import org.eclipse.jgit.revwalk.RevCommit;
-
-/**
- * Thrown when creation of a code review note fails.
- */
-public class CodeReviewNoteCreationException extends Exception {
-  private static final long serialVersionUID = 1L;
-
-  public CodeReviewNoteCreationException(final String msg) {
-    super(msg);
-  }
-
-  public CodeReviewNoteCreationException(final Throwable why) {
-    super(why);
-  }
-
-  public CodeReviewNoteCreationException(final RevCommit commit,
-      final Throwable cause) {
-    super("Couldn't create code review note for the following commit: "
-        + commit.name(), cause);
-  }
-}
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 6b53121..69dcb15 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
@@ -22,12 +22,15 @@
   CLEAN_PICK("Change has been successfully cherry-picked"),
 
   /** */
+  CLEAN_REBASE("Change has been successfully rebased"),
+
+  /** */
   ALREADY_MERGED(""),
 
   /** */
-  PATH_CONFLICT("Your change could not be merged due to a path conflict.\n"
+  PATH_CONFLICT("The 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."),
+                  + "Please rebase the change locally and upload the rebased commit for review."),
 
   /** */
   MISSING_DEPENDENCY(""),
@@ -39,9 +42,12 @@
   REVISION_GONE(""),
 
   /** */
-  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."),
+  NO_SUBMIT_TYPE(""),
+
+  /** */
+  MANUAL_RECURSIVE_MERGE("The change requires a local 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 an initial commit onto an existing branch.\n"
@@ -49,6 +55,11 @@
                   + "Please merge the change locally and upload the merge commit for review."),
 
   /** */
+  CANNOT_REBASE_ROOT("Cannot rebase an initial commit onto an existing branch.\n"
+                   + "\n"
+                   + "Please merge the change locally and upload the merge commit for review."),
+
+  /** */
   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."),
@@ -81,4 +92,4 @@
   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
deleted file mode 100644
index 9a0fe17..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/CreateCodeReviewNotes.java
+++ /dev/null
@@ -1,214 +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 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.client.ApprovalCategory;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.GerritPersonIdent;
-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.server.OrmException;
-import com.google.gwtorm.server.ResultSet;
-import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
-
-import org.eclipse.jgit.api.errors.ConcurrentRefUpdateException;
-import org.eclipse.jgit.lib.Constants;
-import org.eclipse.jgit.lib.NullProgressMonitor;
-import org.eclipse.jgit.lib.ObjectId;
-import org.eclipse.jgit.lib.ObjectInserter;
-import org.eclipse.jgit.lib.PersonIdent;
-import org.eclipse.jgit.lib.ProgressMonitor;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.notes.NoteMap;
-import org.eclipse.jgit.revwalk.FooterKey;
-import org.eclipse.jgit.revwalk.RevCommit;
-import org.eclipse.jgit.revwalk.RevWalk;
-
-import java.io.IOException;
-import java.util.List;
-
-import javax.annotation.Nullable;
-
-/**
- * This class create code review notes for given {@link CodeReviewCommit}s.
- * <p>
- * After the {@link #create(List, PersonIdent)} method is invoked once this
- * instance must not be reused. Create a new instance of this class if needed.
- */
-public class CreateCodeReviewNotes {
-  public interface Factory {
-    CreateCodeReviewNotes create(ReviewDb reviewDb, Repository db);
-  }
-
-  private static final FooterKey CHANGE_ID = new FooterKey("Change-Id");
-
-  private final AccountCache accountCache;
-  private final ApprovalTypes approvalTypes;
-  private final String canonicalWebUrl;
-  private final String anonymousCowardName;
-  private final ReviewDb schema;
-  private final Repository db;
-
-  private PersonIdent author;
-
-  private RevWalk revWalk;
-  private ObjectInserter inserter;
-
-  private final NotesBranchUtil.Factory notesBranchUtilFactory;
-
-  @Inject
-  CreateCodeReviewNotes(
-      @GerritPersonIdent final PersonIdent gerritIdent,
-      final AccountCache accountCache,
-      final ApprovalTypes approvalTypes,
-      final @Nullable @CanonicalWebUrl String canonicalWebUrl,
-      final @AnonymousCowardName String anonymousCowardName,
-      final NotesBranchUtil.Factory notesBranchUtilFactory,
-      final @Assisted  ReviewDb reviewDb,
-      final @Assisted  Repository db) {
-    this.author = gerritIdent;
-    this.accountCache = accountCache;
-    this.approvalTypes = approvalTypes;
-    this.canonicalWebUrl = canonicalWebUrl;
-    this.anonymousCowardName = anonymousCowardName;
-    this.notesBranchUtilFactory = notesBranchUtilFactory;
-    schema = reviewDb;
-    this.db = db;
-  }
-
-  public void create(List<CodeReviewCommit> commits, PersonIdent author)
-      throws CodeReviewNoteCreationException {
-    try {
-      revWalk = new RevWalk(db);
-      inserter = db.newObjectInserter();
-      if (author != null) {
-        this.author = author;
-      }
-
-      NoteMap notes = NoteMap.newEmptyMap();
-      StringBuilder message =
-          new StringBuilder("Update notes for submitted changes\n\n");
-      for (CodeReviewCommit c : commits) {
-        notes.set(c, createNoteContent(c.change, c));
-        message.append("* ").append(c.getShortMessage()).append("\n");
-      }
-
-      NotesBranchUtil notesBranchUtil = notesBranchUtilFactory.create(db);
-      notesBranchUtil.commitAllNotes(notes, REFS_NOTES_REVIEW, author,
-          message.toString());
-      inserter.flush();
-    } catch (IOException e) {
-      throw new CodeReviewNoteCreationException(e);
-    } catch (ConcurrentRefUpdateException e) {
-      throw new CodeReviewNoteCreationException(e);
-    } finally {
-      revWalk.release();
-      inserter.release();
-    }
-  }
-
-  public void create(List<Change> changes, PersonIdent author,
-      String commitMessage, ProgressMonitor monitor) throws OrmException,
-      IOException, CodeReviewNoteCreationException {
-    try {
-      revWalk = new RevWalk(db);
-      inserter = db.newObjectInserter();
-      if (author != null) {
-        this.author = author;
-      }
-      if (monitor == null) {
-        monitor = NullProgressMonitor.INSTANCE;
-      }
-
-      NoteMap notes = NoteMap.newEmptyMap();
-      for (Change c : changes) {
-        monitor.update(1);
-        PatchSet ps = schema.patchSets().get(c.currentPatchSetId());
-        ObjectId commitId = ObjectId.fromString(ps.getRevision().get());
-        notes.set(commitId, createNoteContent(c, commitId));
-      }
-
-      NotesBranchUtil notesBranchUtil = notesBranchUtilFactory.create(db);
-      notesBranchUtil.commitAllNotes(notes, REFS_NOTES_REVIEW, author,
-          commitMessage);
-      inserter.flush();
-    } catch (ConcurrentRefUpdateException e) {
-      throw new CodeReviewNoteCreationException(e);
-    } finally {
-      revWalk.release();
-      inserter.release();
-    }
-  }
-
-  private ObjectId createNoteContent(Change change, ObjectId commit)
-      throws CodeReviewNoteCreationException, IOException  {
-    if (!(commit instanceof RevCommit)) {
-      commit = revWalk.parseCommit(commit);
-    }
-    return createNoteContent(change, (RevCommit) commit);
-  }
-
-  private ObjectId createNoteContent(Change change, RevCommit commit)
-      throws CodeReviewNoteCreationException, IOException {
-    try {
-      ReviewNoteHeaderFormatter formatter =
-          new ReviewNoteHeaderFormatter(author.getTimeZone(),
-              anonymousCowardName);
-      final List<String> idList = commit.getFooterLines(CHANGE_ID);
-      if (idList.isEmpty())
-        formatter.appendChangeId(change.getKey());
-      ResultSet<PatchSetApproval> approvals =
-        schema.patchSetApprovals().byPatchSet(change.currentPatchSetId());
-      PatchSetApproval submit = null;
-      for (PatchSetApproval a : approvals) {
-        if (a.getValue() == 0) {
-          // Ignore 0 values.
-        } else if (ApprovalCategory.SUBMIT.equals(a.getCategoryId())) {
-          submit = a;
-        } else {
-          ApprovalType type = approvalTypes.byId(a.getCategoryId());
-          if (type != null) {
-            formatter.appendApproval(
-                type.getCategory(),
-                a.getValue(),
-                accountCache.get(a.getAccountId()).getAccount());
-          }
-        }
-      }
-
-      if (submit != null) {
-        formatter.appendSubmittedBy(accountCache.get(submit.getAccountId()).getAccount());
-        formatter.appendSubmittedAt(submit.getGranted());
-      }
-      if (canonicalWebUrl != null) {
-        formatter.appendReviewedOn(canonicalWebUrl, change.getId());
-      }
-      formatter.appendProject(change.getProject().get());
-      formatter.appendBranch(change.getDest());
-      return inserter.insert(Constants.OBJ_BLOB, formatter.toString().getBytes("UTF-8"));
-    } catch (OrmException e) {
-      throw new CodeReviewNoteCreationException(commit, e);
-    }
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/DefaultQueueOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/DefaultQueueOp.java
index 82d0493..cff69c6 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/DefaultQueueOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/DefaultQueueOp.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.git;
 
+import java.util.concurrent.ScheduledFuture;
 import java.util.concurrent.TimeUnit;
 
 public abstract class DefaultQueueOp implements Runnable {
@@ -23,7 +24,13 @@
     workQueue = wq;
   }
 
-  public void start(final int delay, final TimeUnit unit) {
-    workQueue.getDefaultQueue().schedule(this, delay, unit);
+  public ScheduledFuture<?> start(long delay, TimeUnit unit) {
+    return workQueue.getDefaultQueue().schedule(this, delay, unit);
+  }
+
+  public ScheduledFuture<?> startWithFixedDelay(long initialDelay, long delay,
+      TimeUnit unit) {
+    return workQueue.getDefaultQueue()
+        .scheduleWithFixedDelay(this, initialDelay, delay, unit);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/FastForwardOnly.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/FastForwardOnly.java
new file mode 100644
index 0000000..7adf4d5
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/FastForwardOnly.java
@@ -0,0 +1,57 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git;
+
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+
+import java.util.List;
+
+public class FastForwardOnly extends SubmitStrategy {
+
+  FastForwardOnly(final SubmitStrategy.Arguments args) {
+    super(args);
+  }
+
+  @Override
+  protected CodeReviewCommit _run(final CodeReviewCommit mergeTip,
+      final List<CodeReviewCommit> toMerge) throws MergeException {
+    args.mergeUtil.reduceToMinimalMerge(args.mergeSorter, toMerge);
+    final CodeReviewCommit newMergeTip =
+        args.mergeUtil.getFirstFastForward(mergeTip, args.rw, toMerge);
+
+    while (!toMerge.isEmpty()) {
+      final CodeReviewCommit n = toMerge.remove(0);
+      n.statusCode = CommitMergeStatus.NOT_FAST_FORWARD;
+    }
+
+    final PatchSetApproval submitApproval =
+        args.mergeUtil.markCleanMerges(args.rw, args.canMergeFlag, newMergeTip,
+            args.alreadyAccepted);
+    setRefLogIdent(submitApproval);
+
+    return newMergeTip;
+  }
+
+  @Override
+  public boolean retryOnLockFailure() {
+    return false;
+  }
+
+  public boolean dryRun(final CodeReviewCommit mergeTip,
+      final CodeReviewCommit toMerge) throws MergeException {
+    return args.mergeUtil.canFastForward(args.mergeSorter, mergeTip, args.rw,
+        toMerge);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/GarbageCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/GarbageCollection.java
new file mode 100644
index 0000000..685e87c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/GarbageCollection.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.common.collect.Sets;
+import com.google.gerrit.common.data.GarbageCollectionResult;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.api.GarbageCollectCommand;
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.api.errors.JGitInternalException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ConfigConstants;
+import org.eclipse.jgit.lib.NullProgressMonitor;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.lib.TextProgressMonitor;
+import org.eclipse.jgit.storage.pack.PackConfig;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.List;
+import java.util.Properties;
+import java.util.Set;
+
+public class GarbageCollection {
+  private static final Logger log = LoggerFactory
+      .getLogger(GarbageCollection.class);
+
+  public static final String LOG_NAME = "gc_log";
+  private static final Logger gcLog = LoggerFactory.getLogger(LOG_NAME);
+
+
+  private final GitRepositoryManager repoManager;
+  private final GarbageCollectionQueue gcQueue;
+
+  public interface Factory {
+    GarbageCollection create();
+  }
+
+  @Inject
+  GarbageCollection(GitRepositoryManager repoManager, GarbageCollectionQueue gcQueue) {
+    this.repoManager = repoManager;
+    this.gcQueue = gcQueue;
+  }
+
+  public GarbageCollectionResult run(List<Project.NameKey> projectNames) {
+    return run(projectNames, null);
+  }
+
+  public GarbageCollectionResult run(List<Project.NameKey> projectNames,
+      PrintWriter writer) {
+    GarbageCollectionResult result = new GarbageCollectionResult();
+    Set<Project.NameKey> projectsToGc = gcQueue.addAll(projectNames);
+    for (Project.NameKey projectName : Sets.difference(
+        Sets.newHashSet(projectNames), projectsToGc)) {
+      result.addError(new GarbageCollectionResult.Error(
+          GarbageCollectionResult.Error.Type.GC_ALREADY_SCHEDULED, projectName));
+    }
+    for (Project.NameKey p : projectsToGc) {
+      Repository repo = null;
+      try {
+        repo = repoManager.openRepository(p);
+        logGcConfiguration(p, repo);
+        print(writer, "collecting garbage for \"" + p + "\":\n");
+        GarbageCollectCommand gc = Git.wrap(repo).gc();
+        logGcInfo(p, "before:", gc.getStatistics());
+        gc.setProgressMonitor(writer != null ? new TextProgressMonitor(writer)
+            : NullProgressMonitor.INSTANCE);
+        Properties statistics = gc.call();
+        logGcInfo(p, "after: ", statistics);
+        print(writer, "done.\n\n");
+      } catch (RepositoryNotFoundException e) {
+        logGcError(writer, p, e);
+        result.addError(new GarbageCollectionResult.Error(
+            GarbageCollectionResult.Error.Type.REPOSITORY_NOT_FOUND,
+            p));
+      } catch (IOException e) {
+        logGcError(writer, p, e);
+        result.addError(new GarbageCollectionResult.Error(
+            GarbageCollectionResult.Error.Type.GC_FAILED, p));
+      } catch (GitAPIException e) {
+        logGcError(writer, p, e);
+        result.addError(new GarbageCollectionResult.Error(
+            GarbageCollectionResult.Error.Type.GC_FAILED, p));
+      } catch (JGitInternalException e) {
+        logGcError(writer, p, e);
+        result.addError(new GarbageCollectionResult.Error(
+            GarbageCollectionResult.Error.Type.GC_FAILED, p));
+      } finally {
+        if (repo != null) {
+          repo.close();
+        }
+        gcQueue.gcFinished(p);
+      }
+    }
+    return result;
+  }
+
+  private static void logGcInfo(Project.NameKey projectName, String msg) {
+    logGcInfo(projectName, msg, null);
+  }
+
+  private static void logGcInfo(Project.NameKey projectName, String msg,
+      Properties statistics) {
+    StringBuilder b = new StringBuilder();
+    b.append("[").append(projectName.get()).append("] ");
+    b.append(msg);
+    if (statistics != null) {
+      b.append(" ");
+      String s = statistics.toString();
+      if (s.startsWith("{") && s.endsWith("}")) {
+        s = s.substring(1, s.length() - 1);
+      }
+      b.append(s);
+    }
+    gcLog.info(b.toString());
+  }
+
+  private static void logGcConfiguration(Project.NameKey projectName,
+      Repository repo) {
+    StringBuilder b = new StringBuilder();
+    Config cfg = repo.getConfig();
+    b.append(formatConfigValues(cfg, ConfigConstants.CONFIG_GC_SECTION, null));
+    for (String subsection : cfg.getSubsections(ConfigConstants.CONFIG_GC_SECTION)) {
+      b.append(formatConfigValues(cfg, ConfigConstants.CONFIG_GC_SECTION,
+          subsection));
+    }
+    if (b.length() == 0) {
+      b.append("no set");
+    }
+
+    logGcInfo(projectName, "gc config: " + b.toString());
+    logGcInfo(projectName, "pack config: " + (new PackConfig(repo)).toString());
+  }
+
+  private static String formatConfigValues(Config config, String section,
+      String subsection) {
+    StringBuilder b = new StringBuilder();
+    Set<String> names = config.getNames(section, subsection);
+    for (String name : names) {
+      String value = config.getString(section, subsection, name);
+      b.append(section);
+      if (subsection != null) {
+        b.append(".").append(subsection);
+      }
+      b.append(".");
+      b.append(name).append("=").append(value);
+      b.append("; ");
+    }
+    return b.toString();
+  }
+
+  private static void logGcError(PrintWriter writer,
+      Project.NameKey projectName, Exception e) {
+    print(writer, "failed.\n\n");
+    StringBuilder b = new StringBuilder();
+    b.append("[").append(projectName.get()).append("]");
+    gcLog.error(b.toString(), e);
+    log.error(b.toString(), e);
+  }
+
+  private static void print(PrintWriter writer, String message) {
+    if (writer != null) {
+      writer.print(message);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/GarbageCollectionQueue.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/GarbageCollectionQueue.java
new file mode 100644
index 0000000..42dc505
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/GarbageCollectionQueue.java
@@ -0,0 +1,42 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git;
+
+import com.google.common.collect.Sets;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.inject.Singleton;
+
+import java.util.Collection;
+import java.util.Set;
+
+@Singleton
+public class GarbageCollectionQueue {
+  private final Set<Project.NameKey> projectsScheduledForGc = Sets.newHashSet();
+
+  public synchronized Set<Project.NameKey> addAll(Collection<Project.NameKey> projects) {
+    Set<Project.NameKey> added =
+        Sets.newLinkedHashSetWithExpectedSize(projects.size());
+    for (Project.NameKey p : projects) {
+      if (projectsScheduledForGc.add(p)) {
+        added.add(p);
+      }
+    }
+    return added;
+  }
+
+  public synchronized void gcFinished(Project.NameKey project) {
+    projectsScheduledForGc.remove(project);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/GitModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/GitModule.java
index 7947d30..9ef7256 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/GitModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/GitModule.java
@@ -23,5 +23,6 @@
     factory(RenameGroupOp.Factory.class);
     factory(MetaDataUpdate.InternalFactory.class);
     bind(MetaDataUpdate.Server.class);
+    bind(ReceiveConfig.class);
   }
 }
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 1bf157b..0118404 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
@@ -31,15 +31,15 @@
  * environment.
  */
 public interface GitRepositoryManager {
-  /** Notes branch successful reviews are written to after being merged. */
-  public static final String REFS_NOTES_REVIEW = "refs/notes/review";
-
   /** 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";
 
+  /** Configurations of project-specific dashboards (canned search queries). */
+  public static String REFS_DASHBOARDS = "refs/meta/dashboards/";
+
   /**
    * Prefix applied to merge commit base nodes.
    * <p>
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/LabelNormalizer.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/LabelNormalizer.java
new file mode 100644
index 0000000..3f09916
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/LabelNormalizer.java
@@ -0,0 +1,141 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.common.base.Preconditions.checkArgument;
+
+import com.google.common.collect.Lists;
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.common.data.LabelTypes;
+import com.google.gerrit.common.data.LabelValue;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.common.data.PermissionRange;
+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.server.IdentifiedUser;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.inject.Inject;
+
+import java.util.Collection;
+import java.util.List;
+
+public class LabelNormalizer {
+  private final ChangeControl.GenericFactory changeFactory;
+  private final IdentifiedUser.GenericFactory userFactory;
+
+  @Inject
+  LabelNormalizer(ChangeControl.GenericFactory changeFactory,
+      IdentifiedUser.GenericFactory userFactory) {
+    this.changeFactory = changeFactory;
+    this.userFactory = userFactory;
+  }
+
+  /**
+   * @param change change containing the given approvals.
+   * @param approvals list of approvals.
+   * @return copies of approvals normalized to the defined ranges for the label
+   *     type and permissions for the user. Approvals for unknown labels are not
+   *     included in the output, nor are approvals where the user has no
+   *     permissions for that label.
+   * @throws NoSuchChangeException
+   */
+  public List<PatchSetApproval> normalize(Change change,
+      Collection<PatchSetApproval> approvals) throws NoSuchChangeException {
+    return normalize(
+        changeFactory.controlFor(change, userFactory.create(change.getOwner())),
+        approvals);
+  }
+
+  /**
+   * @param ctl change control containing the given approvals.
+   * @param approvals list of approvals.
+   * @return copies of approvals normalized to the defined ranges for the label
+   *     type and permissions for the user. Approvals for unknown labels are not
+   *     included in the output, nor are approvals where the user has no
+   *     permissions for that label.
+   */
+  public List<PatchSetApproval> normalize(ChangeControl ctl,
+      Collection<PatchSetApproval> approvals) {
+    List<PatchSetApproval> result =
+        Lists.newArrayListWithCapacity(approvals.size());
+    LabelTypes labelTypes = ctl.getLabelTypes();
+    for (PatchSetApproval psa : approvals) {
+      Change.Id changeId = psa.getKey().getParentKey().getParentKey();
+      checkArgument(changeId.equals(ctl.getChange().getId()),
+          "Approval %s does not match change %s",
+          psa.getKey(), ctl.getChange().getKey());
+      if (psa.isSubmit()) {
+        result.add(copy(psa, ctl));
+        continue;
+      }
+      LabelType label = labelTypes.byLabel(psa.getLabelId());
+      if (label != null) {
+        psa = copy(psa, ctl);
+        applyTypeFloor(label, psa);
+        if (applyRightFloor(ctl, label, psa)) {
+          result.add(psa);
+        }
+      }
+    }
+    return result;
+  }
+
+  /**
+   * @param ctl change control (for any user).
+   * @param lt label type.
+   * @param id account ID.
+   * @return whether the given account ID has any permissions to vote on this
+   *     label for this change.
+   */
+  public boolean canVote(ChangeControl ctl, LabelType lt, Account.Id id) {
+    return !getRange(ctl, lt, id).isEmpty();
+  }
+
+  private PatchSetApproval copy(PatchSetApproval src, ChangeControl ctl) {
+    PatchSetApproval dest = new PatchSetApproval(src.getPatchSetId(), src);
+    dest.cache(ctl.getChange());
+    return dest;
+  }
+
+  private PermissionRange getRange(ChangeControl ctl, LabelType lt,
+      Account.Id id) {
+    String permission = Permission.forLabel(lt.getName());
+    IdentifiedUser user = userFactory.create(id);
+    return ctl.forUser(user).getRange(permission);
+  }
+
+  private boolean applyRightFloor(ChangeControl ctl, LabelType lt,
+      PatchSetApproval a) {
+    PermissionRange range = getRange(ctl, lt, a.getAccountId());
+    if (range.isEmpty()) {
+      return false;
+    }
+    a.setValue((short) range.squash(a.getValue()));
+    return true;
+  }
+
+  private void applyTypeFloor(LabelType lt, PatchSetApproval a) {
+    LabelValue atMin = lt.getMin();
+    if (atMin != null && a.getValue() < atMin.getValue()) {
+      a.setValue(atMin.getValue());
+    }
+    LabelValue atMax = lt.getMax();
+    if (atMax != null && a.getValue() > atMax.getValue()) {
+      a.setValue(atMax.getValue());
+    }
+  }
+}
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 4e3f324..1ca74b1 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
@@ -26,6 +26,7 @@
 import com.jcraft.jsch.Session;
 
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.internal.storage.file.LockFile;
 import org.eclipse.jgit.lib.Config;
 import org.eclipse.jgit.lib.ConfigConstants;
 import org.eclipse.jgit.lib.Constants;
@@ -33,8 +34,6 @@
 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;
 import org.eclipse.jgit.transport.JschConfigSessionFactory;
 import org.eclipse.jgit.transport.OpenSshConfig;
@@ -78,11 +77,11 @@
   }
 
   public static class Lifecycle implements LifecycleListener {
-    private final Config cfg;
+    private final Config serverConfig;
 
     @Inject
     Lifecycle(@GerritServerConfig final Config cfg) {
-      this.cfg = cfg;
+      this.serverConfig = cfg;
     }
 
     @Override
@@ -96,9 +95,34 @@
         }
       });
 
-      final WindowCacheConfig c = new WindowCacheConfig();
-      c.fromConfig(cfg);
-      WindowCache.reconfigure(c);
+      WindowCacheConfig cfg = new WindowCacheConfig();
+      cfg.fromConfig(serverConfig);
+      if (serverConfig.getString("core", null, "streamFileThreshold") == null) {
+        long mx = Runtime.getRuntime().maxMemory();
+        int limit = (int) Math.min(
+            mx / 4, // don't use more than 1/4 of the heap.
+            2047 << 20); // cannot exceed array length
+        if ((5 << 20) < limit && limit % (1 << 20) != 0) {
+          // If the limit is at least 5 MiB but is not a whole multiple
+          // of MiB round up to the next one full megabyte. This is a very
+          // tiny memory increase in exchange for nice round units.
+          limit = ((limit / (1 << 20)) + 1) << 20;
+        }
+
+        String desc;
+        if (limit % (1 << 20) == 0) {
+          desc = String.format("%dm", limit / (1 << 20));
+        } else if (limit % (1 << 10) == 0) {
+          desc = String.format("%dk", limit / (1 << 10));
+        } else {
+          desc = String.format("%d", limit);
+        }
+        log.info(String.format(
+            "Defaulting core.streamFileThreshold to %s",
+            desc));
+        cfg.setStreamFileThreshold(limit);
+      }
+      cfg.install();
     }
 
     @Override
@@ -177,10 +201,7 @@
       // 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;
-      }
+      String n = name.get() + Constants.DOT_GIT_EXT;
       loc = FileKey.exact(new File(basePath, n), FS.DETECTED);
     }
 
@@ -290,7 +311,7 @@
     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.indexOf('\\') >= 0) return true; // no windows/dos style paths
     if (name.charAt(0) == '/') return true; // no absolute paths
     if (new File(name).isAbsolute()) return true; // no absolute paths
 
@@ -298,6 +319,15 @@
     if (name.contains("/../")) return true; // no "foo/../etc/passwd"
     if (name.contains("/./")) return true; // "foo/./foo" is insane to ask
     if (name.contains("//")) return true; // windows UNC path can be "//..."
+    if (name.contains("?")) return true; // common unix wildcard
+    if (name.contains("%")) return true; // wildcard or string parameter
+    if (name.contains("*")) return true; // wildcard
+    if (name.contains(":")) return true; // Could be used for absolute paths in windows?
+    if (name.contains("<")) return true; // redirect input
+    if (name.contains(">")) return true; // redirect output
+    if (name.contains("|")) return true; // pipe
+    if (name.contains("$")) return true; // dollar sign
+    if (name.contains("\r")) return true; // carriage return
 
     return false; // is a reasonable name
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeAlways.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeAlways.java
new file mode 100644
index 0000000..493a40f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeAlways.java
@@ -0,0 +1,54 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git;
+
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+
+import java.util.List;
+
+public class MergeAlways extends SubmitStrategy {
+
+  MergeAlways(final SubmitStrategy.Arguments args) {
+    super(args);
+  }
+
+  @Override
+  protected CodeReviewCommit _run(final CodeReviewCommit mergeTip,
+      final List<CodeReviewCommit> toMerge) throws MergeException {
+    args.mergeUtil.reduceToMinimalMerge(args.mergeSorter, toMerge);
+
+    CodeReviewCommit newMergeTip = mergeTip;
+    while (!toMerge.isEmpty()) {
+      newMergeTip =
+          args.mergeUtil.mergeOneCommit(args.myIdent, args.repo, args.rw,
+              args.inserter, args.canMergeFlag, args.destBranch, mergeTip,
+              toMerge.remove(0));
+    }
+
+    final PatchSetApproval submitApproval =
+        args.mergeUtil.markCleanMerges(args.rw, args.canMergeFlag, newMergeTip,
+            args.alreadyAccepted);
+    setRefLogIdent(submitApproval);
+
+    return newMergeTip;
+  }
+
+  @Override
+  public boolean dryRun(final CodeReviewCommit mergeTip,
+      final CodeReviewCommit toMerge) throws MergeException {
+    return args.mergeUtil.canMerge(args.mergeSorter, args.repo, mergeTip,
+        toMerge);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeException.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeException.java
index 1997c13..54cd329 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeException.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeException.java
@@ -18,11 +18,11 @@
 public class MergeException extends Exception {
   private static final long serialVersionUID = 1L;
 
-  MergeException(final String msg) {
+  public MergeException(final String msg) {
     super(msg, null);
   }
 
-  MergeException(final String msg, final Throwable why) {
+  public MergeException(final String msg, final Throwable why) {
     super(msg, why);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeIfNecessary.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeIfNecessary.java
new file mode 100644
index 0000000..c31edb2
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeIfNecessary.java
@@ -0,0 +1,57 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git;
+
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+
+import java.util.List;
+
+public class MergeIfNecessary extends SubmitStrategy {
+
+  MergeIfNecessary(final SubmitStrategy.Arguments args) {
+    super(args);
+  }
+
+  @Override
+  protected CodeReviewCommit _run(final CodeReviewCommit mergeTip,
+      final List<CodeReviewCommit> toMerge) throws MergeException {
+    args.mergeUtil.reduceToMinimalMerge(args.mergeSorter, toMerge);
+    CodeReviewCommit newMergeTip =
+        args.mergeUtil.getFirstFastForward(mergeTip, args.rw, toMerge);
+
+    // For every other commit do a pair-wise merge.
+    while (!toMerge.isEmpty()) {
+      newMergeTip =
+          args.mergeUtil.mergeOneCommit(args.myIdent, args.repo, args.rw,
+              args.inserter, args.canMergeFlag, args.destBranch, mergeTip,
+              toMerge.remove(0));
+    }
+
+    final PatchSetApproval submitApproval = args.mergeUtil.markCleanMerges(
+        args.rw, args.canMergeFlag, newMergeTip, args.alreadyAccepted);
+    setRefLogIdent(submitApproval);
+
+    return newMergeTip;
+  }
+
+  @Override
+  public boolean dryRun(final CodeReviewCommit mergeTip,
+      final CodeReviewCommit toMerge) throws MergeException {
+    return args.mergeUtil.canFastForward(
+          args.mergeSorter, mergeTip, args.rw, toMerge)
+        || args.mergeUtil.canMerge(
+          args.mergeSorter, args.repo, mergeTip, toMerge);
+  }
+}
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 2e8f183..dace0ae 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
@@ -14,30 +14,35 @@
 
 package com.google.gerrit.server.git;
 
+import static com.google.gerrit.server.git.MergeUtil.getSubmitter;
+import static java.util.concurrent.TimeUnit.DAYS;
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static java.util.concurrent.TimeUnit.MINUTES;
+import static java.util.concurrent.TimeUnit.SECONDS;
 
+import com.google.common.base.Objects;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.ListMultimap;
+import com.google.common.collect.Sets;
 import com.google.gerrit.common.ChangeHooks;
-import com.google.gerrit.common.data.ApprovalType;
-import com.google.gerrit.common.data.ApprovalTypes;
 import com.google.gerrit.common.data.Capable;
+import com.google.gerrit.common.data.LabelTypes;
+import com.google.gerrit.common.data.SubmitTypeRecord;
 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.Project.SubmitType;
 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.AllProjectsName;
-import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.mail.MergeFailSender;
 import com.google.gerrit.server.mail.MergedSender;
@@ -45,24 +50,20 @@
 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.NoSuchProjectException;
 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.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;
 
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
-import org.eclipse.jgit.errors.MissingObjectException;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.AnyObjectId;
-import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
 import org.eclipse.jgit.lib.ObjectInserter;
@@ -70,11 +71,6 @@
 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.Merger;
-import org.eclipse.jgit.merge.ThreeWayMerger;
-import org.eclipse.jgit.revwalk.FooterKey;
-import org.eclipse.jgit.revwalk.FooterLine;
 import org.eclipse.jgit.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevFlag;
 import org.eclipse.jgit.revwalk.RevSort;
@@ -83,21 +79,15 @@
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
-import java.io.UnsupportedEncodingException;
-import java.sql.Timestamp;
 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.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.Map.Entry;
 import java.util.Set;
-import java.util.TimeZone;
-
-import javax.annotation.Nullable;
 
 /**
  * Merges changes in submission order into a single branch.
@@ -119,53 +109,47 @@
   }
 
   private static final Logger log = LoggerFactory.getLogger(MergeOp.class);
-  private static final String R_HEADS_MASTER =
-      Constants.R_HEADS + Constants.MASTER;
-  private static final ApprovalCategory.Id CRVW =
-      new ApprovalCategory.Id("CRVW");
-  private static final ApprovalCategory.Id VRIF =
-      new ApprovalCategory.Id("VRIF");
-  private static final FooterKey REVIEWED_ON = new FooterKey("Reviewed-on");
-  private static final FooterKey CHANGE_ID = new FooterKey("Change-Id");
 
   /** Amount of time to wait between submit and checking for missing deps. */
   private static final long DEPENDENCY_DELAY =
       MILLISECONDS.convert(15, MINUTES);
 
+  private static final long LOCK_FAILURE_RETRY_DELAY =
+      MILLISECONDS.convert(15, SECONDS);
+
+  private static final long DUPLICATE_MESSAGE_INTERVAL =
+      MILLISECONDS.convert(1, DAYS);
+
   private final GitRepositoryManager repoManager;
   private final SchemaFactory<ReviewDb> schemaFactory;
   private final ProjectCache projectCache;
-  private final FunctionState.Factory functionState;
-  private final GitReferenceUpdated replication;
+  private final LabelNormalizer labelNormalizer;
+  private final GitReferenceUpdated gitRefUpdated;
   private final MergedSender.Factory mergedSenderFactory;
   private final MergeFailSender.Factory mergeFailSenderFactory;
-  private final Provider<String> urlProvider;
-  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;
   private final Branch.NameKey destBranch;
-  private Project destProject;
-  private final List<CodeReviewCommit> toMerge;
-  private List<Change> submitted;
+  private ProjectState destProject;
+  private final ListMultimap<SubmitType, CodeReviewCommit> toMerge;
+  private final List<CodeReviewCommit> potentiallyStillSubmittable;
   private final Map<Change.Id, CodeReviewCommit> commits;
   private ReviewDb db;
   private Repository repo;
   private RevWalk rw;
-  private RevFlag CAN_MERGE;
+  private RevFlag canMergeFlag;
   private CodeReviewCommit branchTip;
   private CodeReviewCommit mergeTip;
-  private Set<RevCommit> alreadyAccepted;
-  private RefUpdate branchUpdate;
   private ObjectInserter inserter;
+  private PersonIdent refLogIdent;
 
   private final ChangeHooks hooks;
   private final AccountCache accountCache;
   private final TagCache tagCache;
-  private final CreateCodeReviewNotes.Factory codeReviewNotesFactory;
+  private final SubmitStrategyFactory submitStrategyFactory;
   private final SubmoduleOp.Factory subOpFactory;
   private final WorkQueue workQueue;
   private final RequestScopePropagator requestScopePropagator;
@@ -173,30 +157,27 @@
 
   @Inject
   MergeOp(final GitRepositoryManager grm, final SchemaFactory<ReviewDb> sf,
-      final ProjectCache pc, final FunctionState.Factory fs,
-      final GitReferenceUpdated rq, final MergedSender.Factory msf,
+      final ProjectCache pc, final LabelNormalizer fs,
+      final GitReferenceUpdated gru, final MergedSender.Factory msf,
       final MergeFailSender.Factory mfsf,
-      @CanonicalWebUrl @Nullable final Provider<String> cwu,
-      final ApprovalTypes approvalTypes, final PatchSetInfoFactory psif,
+      final LabelTypes labelTypes, final PatchSetInfoFactory psif,
       final IdentifiedUser.GenericFactory iuf,
       final ChangeControl.GenericFactory changeControlFactory,
-      @GerritPersonIdent final PersonIdent myIdent,
       final MergeQueue mergeQueue, @Assisted final Branch.NameKey branch,
       final ChangeHooks hooks, final AccountCache accountCache,
-      final TagCache tagCache, final CreateCodeReviewNotes.Factory crnf,
+      final TagCache tagCache,
+      final SubmitStrategyFactory submitStrategyFactory,
       final SubmoduleOp.Factory subOpFactory,
       final WorkQueue workQueue,
       final RequestScopePropagator requestScopePropagator,
       final AllProjectsName allProjectsName) {
     repoManager = grm;
     schemaFactory = sf;
-    functionState = fs;
+    labelNormalizer = fs;
     projectCache = pc;
-    replication = rq;
+    gitRefUpdated = gru;
     mergedSenderFactory = msf;
     mergeFailSenderFactory = mfsf;
-    urlProvider = cwu;
-    this.approvalTypes = approvalTypes;
     patchSetInfoFactory = psif;
     identifiedUserFactory = iuf;
     this.changeControlFactory = changeControlFactory;
@@ -204,24 +185,22 @@
     this.hooks = hooks;
     this.accountCache = accountCache;
     this.tagCache = tagCache;
-    codeReviewNotesFactory = crnf;
+    this.submitStrategyFactory = submitStrategyFactory;
     this.subOpFactory = subOpFactory;
     this.workQueue = workQueue;
     this.requestScopePropagator = requestScopePropagator;
     this.allProjectsName = allProjectsName;
-    this.myIdent = myIdent;
     destBranch = branch;
-    toMerge = new ArrayList<CodeReviewCommit>();
+    toMerge = ArrayListMultimap.create();
+    potentiallyStillSubmittable = new ArrayList<CodeReviewCommit>();
     commits = new HashMap<Change.Id, CodeReviewCommit>();
   }
 
-  public void verifyMergeability(Change change) {
+  public void verifyMergeability(Change change) throws NoSuchProjectException {
     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
@@ -231,17 +210,27 @@
           || (destBranchRef != null && !destBranchRef.getObjectId().getName()
               .equals(change.getLastSha1MergeTested().get()))) {
         openSchema();
-        preMerge();
+        openBranch();
+        validateChangeList(Collections.singletonList(change));
+        if (!toMerge.isEmpty()) {
+          final Entry<SubmitType, CodeReviewCommit> e =
+              toMerge.entries().iterator().next();
+          final boolean isMergeable =
+              createStrategy(e.getKey()).dryRun(branchTip, e.getValue());
 
-        // update sha1 tested merge.
-        if (destBranchRef != null) {
-          change.setLastSha1MergeTested(new RevId(destBranchRef
-              .getObjectId().getName()));
+          // update sha1 tested merge.
+          if (destBranchRef != null) {
+            change.setLastSha1MergeTested(new RevId(destBranchRef
+                .getObjectId().getName()));
+          } else {
+            change.setLastSha1MergeTested(new RevId(""));
+          }
+          change.setMergeable(isMergeable);
+          db.changes().update(Collections.singleton(change));
         } else {
-          change.setLastSha1MergeTested(new RevId(""));
+          log.error("Test merge attempt for change: " + change.getId()
+              + " failed");
         }
-        change.setMergeable(isMergeable(change));
-        db.changes().update(Collections.singleton(change));
       }
     } catch (MergeException e) {
       log.error("Test merge attempt for change: " + change.getId()
@@ -263,11 +252,10 @@
   }
 
   private void setDestProject() throws MergeException {
-    final ProjectState pe = projectCache.get(destBranch.getParentKey());
-    if (pe == null) {
+    destProject = projectCache.get(destBranch.getParentKey());
+    if (destProject == null) {
       throw new MergeException("No such project: " + destBranch.getParentKey());
     }
-    destProject = pe.getProject();
   }
 
   private void openSchema() throws OrmException {
@@ -276,16 +264,59 @@
     }
   }
 
-  public void merge() throws MergeException {
+  public void merge() throws MergeException, NoSuchProjectException {
     setDestProject();
     try {
       openSchema();
       openRepository();
-      submitted = db.changes().submitted(destBranch).toList();
-      preMerge();
-      updateBranch();
-      updateChangeStatus();
-      updateSubscriptions();
+      openBranch();
+      final ListMultimap<SubmitType, Change> toSubmit =
+          validateChangeList(db.changes().submitted(destBranch).toList());
+
+      final ListMultimap<SubmitType, CodeReviewCommit> toMergeNextTurn =
+          ArrayListMultimap.create();
+      final List<CodeReviewCommit> potentiallyStillSubmittableOnNextRun =
+          new ArrayList<CodeReviewCommit>();
+      while (!toMerge.isEmpty()) {
+        toMergeNextTurn.clear();
+        final Set<SubmitType> submitTypes =
+            new HashSet<Project.SubmitType>(toMerge.keySet());
+        for (final SubmitType submitType : submitTypes) {
+          final RefUpdate branchUpdate = openBranch();
+          final SubmitStrategy strategy = createStrategy(submitType);
+          preMerge(strategy, toMerge.get(submitType));
+          updateBranch(strategy, branchUpdate);
+          updateChangeStatus(toSubmit.get(submitType));
+          updateSubscriptions(toSubmit.get(submitType));
+
+          for (final Iterator<CodeReviewCommit> it =
+              potentiallyStillSubmittable.iterator(); it.hasNext();) {
+            final CodeReviewCommit commit = it.next();
+            if (containsMissingCommits(toMerge, commit)
+                || containsMissingCommits(toMergeNextTurn, commit)) {
+              // change has missing dependencies, but all commits which are
+              // missing are still attempted to be merged with another submit
+              // strategy, retry to merge this commit in the next turn
+              it.remove();
+              commit.statusCode = null;
+              commit.missing = null;
+              toMergeNextTurn.put(submitType, commit);
+            }
+          }
+          potentiallyStillSubmittableOnNextRun.addAll(potentiallyStillSubmittable);
+          potentiallyStillSubmittable.clear();
+        }
+        toMerge.clear();
+        toMerge.putAll(toMergeNextTurn);
+      }
+
+      for (final CodeReviewCommit commit : potentiallyStillSubmittableOnNextRun) {
+        final Capable capable = isSubmitStillPossible(commit);
+        if (capable != Capable.OK) {
+          sendMergeFail(commit.change,
+              message(commit.change, capable.getMessage()), false);
+        }
+      }
     } catch (OrmException e) {
       throw new MergeException("Cannot query the database", e);
     } finally {
@@ -304,24 +335,59 @@
     }
   }
 
-  private void preMerge() throws MergeException, OrmException {
-    openBranch();
-    validateChangeList();
-    mergeTip = branchTip;
-    switch (destProject.getSubmitType()) {
-      case CHERRY_PICK:
-        cherryPickChanges();
-        break;
-
-      case FAST_FORWARD_ONLY:
-      case MERGE_ALWAYS:
-      case MERGE_IF_NECESSARY:
-      default:
-        reduceToMinimalMerge();
-        mergeTopics();
-        markCleanMerges();
-        break;
+  private boolean containsMissingCommits(
+      final ListMultimap<SubmitType, CodeReviewCommit> map,
+      final CodeReviewCommit commit) {
+    if (!isSubmitForMissingCommitsStillPossible(commit)) {
+      return false;
     }
+
+    for (final CodeReviewCommit missingCommit : commit.missing) {
+      if (!map.containsValue(missingCommit)) {
+        return false;
+      }
+    }
+    return true;
+  }
+
+  private boolean isSubmitForMissingCommitsStillPossible(final CodeReviewCommit commit) {
+    if (commit.missing == null || commit.missing.isEmpty()) {
+      return false;
+    }
+
+    for (CodeReviewCommit missingCommit : commit.missing) {
+      loadChangeInfo(missingCommit);
+
+      if (missingCommit.patchsetId == null) {
+        // The commit doesn't have a patch set, so it cannot be
+        // submitted to the branch.
+        //
+        return false;
+      }
+
+      if (!missingCommit.change.currentPatchSetId().equals(
+          missingCommit.patchsetId)) {
+        // If the missing commit is not the current patch set,
+        // the change must be rebased to use the proper parent.
+        //
+        return false;
+      }
+    }
+
+    return true;
+  }
+
+  private void preMerge(final SubmitStrategy strategy,
+      final List<CodeReviewCommit> toMerge) throws MergeException {
+    mergeTip = strategy.run(branchTip, toMerge);
+    refLogIdent = strategy.getRefLogIdent();
+    commits.putAll(strategy.getNewCommits());
+  }
+
+  private SubmitStrategy createStrategy(final SubmitType submitType)
+      throws MergeException, NoSuchProjectException {
+    return submitStrategyFactory.create(submitType, db, repo, rw, inserter,
+        canMergeFlag, getAlreadyAccepted(branchTip), destBranch);
   }
 
   private void openRepository() throws MergeException {
@@ -344,20 +410,17 @@
     };
     rw.sort(RevSort.TOPO);
     rw.sort(RevSort.COMMIT_TIME_DESC, true);
-    CAN_MERGE = rw.newFlag("CAN_MERGE");
+    canMergeFlag = rw.newFlag("CAN_MERGE");
 
     inserter = repo.newObjectInserter();
   }
 
-  private void openBranch() throws MergeException {
-    alreadyAccepted = new HashSet<RevCommit>();
-
+  private RefUpdate openBranch() throws MergeException, OrmException {
     try {
-      branchUpdate = repo.updateRef(destBranch.get());
+      final RefUpdate branchUpdate = repo.updateRef(destBranch.get());
       if (branchUpdate.getOldObjectId() != null) {
         branchTip =
             (CodeReviewCommit) rw.parseCommit(branchUpdate.getOldObjectId());
-        alreadyAccepted.add(branchTip);
       } else {
         branchTip = null;
       }
@@ -369,14 +432,31 @@
         } else if (repo.getFullBranch().equals(destBranch.get())) {
           branchUpdate.setExpectedOldObjectId(ObjectId.zeroId());
         } else {
-          throw new MergeException("Destination branch \""
-              + branchUpdate.getRef().getName() + "\" does not exist");
+          for (final Change c : db.changes().submitted(destBranch).toList()) {
+            setNew(c, message(c, "Your change could not be merged, "
+                + "because the destination branch does not exist anymore."));
+          }
         }
       } catch (IOException e) {
         throw new MergeException(
             "Failed to check existence of destination branch", e);
       }
 
+      return branchUpdate;
+    } catch (IOException e) {
+      throw new MergeException("Cannot open branch", e);
+    }
+  }
+
+  private Set<RevCommit> getAlreadyAccepted(final CodeReviewCommit branchTip)
+      throws MergeException {
+    final Set<RevCommit> alreadyAccepted = new HashSet<RevCommit>();
+
+    if (branchTip != null) {
+      alreadyAccepted.add(branchTip);
+    }
+
+    try {
       for (final Ref r : repo.getAllRefs().values()) {
         if (r.getName().startsWith(Constants.R_HEADS)
             || r.getName().startsWith(Constants.R_TAGS)) {
@@ -388,11 +468,17 @@
         }
       }
     } catch (IOException e) {
-      throw new MergeException("Cannot open branch", e);
+      throw new MergeException("Failed to determine already accepted commits.", e);
     }
+
+    return alreadyAccepted;
   }
 
-  private void validateChangeList() throws MergeException {
+  private ListMultimap<SubmitType, Change> validateChangeList(
+      final List<Change> submitted) throws MergeException {
+    final ListMultimap<SubmitType, Change> toSubmit =
+        ArrayListMultimap.create();
+
     final Set<ObjectId> tips = new HashSet<ObjectId>();
     for (final Ref r : repo.getAllRefs().values()) {
       tips.add(r.getObjectId());
@@ -455,10 +541,11 @@
         continue;
       }
 
-      if (GitRepositoryManager.REF_CONFIG.equals(branchUpdate.getName())) {
+      if (GitRepositoryManager.REF_CONFIG.equals(destBranch.get())) {
         final Project.NameKey newParent;
         try {
-          ProjectConfig cfg = new ProjectConfig(destProject.getNameKey());
+          ProjectConfig cfg =
+              new ProjectConfig(destProject.getProject().getNameKey());
           cfg.load(repo, commit);
           newParent = cfg.getProject().getParent(allProjectsName);
         } catch (Exception e) {
@@ -466,7 +553,8 @@
               .error(CommitMergeStatus.INVALID_PROJECT_CONFIGURATION));
           continue;
         }
-        final Project.NameKey oldParent = destProject.getParent(allProjectsName);
+        final Project.NameKey oldParent =
+            destProject.getProject().getParent(allProjectsName);
         if (oldParent == null) {
           // update of the 'All-Projects' project
           if (newParent != null) {
@@ -506,7 +594,7 @@
 
       if (branchTip != null) {
         // If this commit is already merged its a bug in the queuing code
-        // that we got back here. Just mark it complete and move on. Its
+        // that we got back here. Just mark it complete and move on. It's
         // merged and that is all that mattered to the requestor.
         //
         try {
@@ -519,593 +607,58 @@
         }
       }
 
-      commit.add(CAN_MERGE);
-      toMerge.add(commit);
+      final SubmitType submitType = getSubmitType(chg, ps);
+      if (submitType == null) {
+        commits.put(changeId,
+            CodeReviewCommit.error(CommitMergeStatus.NO_SUBMIT_TYPE));
+        continue;
+      }
+
+      commit.add(canMergeFlag);
+      toMerge.put(submitType, commit);
+      toSubmit.put(submitType, chg);
     }
+    return toSubmit;
   }
 
-  private void reduceToMinimalMerge() throws MergeException {
-    final Collection<CodeReviewCommit> heads;
+  private SubmitType getSubmitType(final Change change, final PatchSet ps) {
     try {
-      heads = new MergeSorter(rw, alreadyAccepted, CAN_MERGE).sort(toMerge);
-    } catch (IOException e) {
-      throw new MergeException("Branch head sorting failed", e);
-    }
-
-    toMerge.clear();
-    toMerge.addAll(heads);
-    Collections.sort(toMerge, new Comparator<CodeReviewCommit>() {
-      public int compare(final CodeReviewCommit a, final CodeReviewCommit b) {
-        return a.originalOrder - b.originalOrder;
+      final SubmitTypeRecord r =
+          changeControlFactory.controlFor(change,
+              identifiedUserFactory.create(change.getOwner()))
+              .getSubmitTypeRecord(db, ps);
+      if (r.status != SubmitTypeRecord.Status.OK) {
+        log.error("Failed to get submit type for " + change.getKey());
+        return null;
       }
-    });
-  }
-
-  private void mergeTopics() throws MergeException {
-    // Take the first fast-forward available, if any is available in the set.
-    //
-    if (destProject.getSubmitType() != Project.SubmitType.MERGE_ALWAYS) {
-      for (final Iterator<CodeReviewCommit> i = toMerge.iterator(); i.hasNext();) {
-        try {
-          final CodeReviewCommit n = i.next();
-          if (mergeTip == null || rw.isMergedInto(mergeTip, n)) {
-            mergeTip = n;
-            i.remove();
-            break;
-          }
-        } catch (IOException e) {
-          throw new MergeException("Cannot fast-forward test during merge", e);
-        }
-      }
-    }
-
-    if (destProject.getSubmitType() == Project.SubmitType.FAST_FORWARD_ONLY) {
-      // If this project only permits fast-forwards, abort everything else.
-      //
-      while (!toMerge.isEmpty()) {
-        final CodeReviewCommit n = toMerge.remove(0);
-        n.statusCode = CommitMergeStatus.NOT_FAST_FORWARD;
-      }
-
-    } else {
-      // For every other commit do a pair-wise merge.
-      //
-      while (!toMerge.isEmpty()) {
-        mergeOneCommit(toMerge.remove(0));
-      }
+      return r.type;
+    } catch (NoSuchChangeException e) {
+      log.error("Failed to get submit type for " + change.getKey(), e);
+      return null;
     }
   }
 
-  private void mergeOneCommit(final CodeReviewCommit n) throws MergeException {
-    final ThreeWayMerger m = newThreeWayMerger();
-    try {
-      if (m.merge(new AnyObjectId[] {mergeTip, n})) {
-        writeMergeCommit(m.getResultTreeId(), n);
-
-      } else {
-        failed(n, CommitMergeStatus.PATH_CONFLICT);
-      }
-    } catch (IOException e) {
-      if (e.getMessage().startsWith("Multiple merge bases for")) {
-        try {
-          failed(n, CommitMergeStatus.CRISS_CROSS_MERGE);
-        } catch (IOException e2) {
-          throw new MergeException("Cannot merge " + n.name(), e);
-        }
-      } else {
-        throw new MergeException("Cannot merge " + n.name(), e);
-      }
-    }
-  }
-
-  private ThreeWayMerger newThreeWayMerger() {
-    ThreeWayMerger m;
-    if (destProject.isUseContentMerge()) {
-      // Settings for this project allow us to try and
-      // automatically resolve conflicts within files if needed.
-      // Use ResolveMerge and instruct to operate in core.
-      m = MergeStrategy.RESOLVE.newMerger(repo, true);
-    } else {
-      // No auto conflict resolving allowed. If any of the
-      // affected files was modified, merge will fail.
-      m = MergeStrategy.SIMPLE_TWO_WAY_IN_CORE.newMerger(repo);
-    }
-    m.setObjectInserter(new ObjectInserter.Filter() {
-      @Override
-      protected ObjectInserter delegate() {
-        return inserter;
-      }
-
-      @Override
-      public void flush() {
-      }
-
-      @Override
-      public void release() {
-      }
-    });
-    return m;
-  }
-
-  private CodeReviewCommit failed(final CodeReviewCommit n,
-      final CommitMergeStatus failure) throws MissingObjectException,
-      IncorrectObjectTypeException, IOException {
-    rw.reset();
-    rw.markStart(n);
-    rw.markUninteresting(mergeTip);
-    CodeReviewCommit failed;
-    while ((failed = (CodeReviewCommit) rw.next()) != null) {
-      failed.statusCode = failure;
-    }
-    return failed;
-  }
-
-  private void writeMergeCommit(ObjectId treeId, CodeReviewCommit n)
-      throws IOException, MissingObjectException, IncorrectObjectTypeException {
-    final List<CodeReviewCommit> merged = new ArrayList<CodeReviewCommit>();
-    rw.reset();
-    rw.markStart(n);
-    rw.markUninteresting(mergeTip);
-    for (final RevCommit c : rw) {
-      final CodeReviewCommit crc = (CodeReviewCommit) c;
-      if (crc.patchsetId != null) {
-        merged.add(crc);
-      }
-    }
-
-    final StringBuilder msgbuf = new StringBuilder();
-    if (merged.size() == 1) {
-      final CodeReviewCommit c = merged.get(0);
-      rw.parseBody(c);
-      msgbuf.append("Merge \"");
-      msgbuf.append(c.getShortMessage());
-      msgbuf.append("\"");
-
-    } else {
-      msgbuf.append("Merge changes ");
-      for (final Iterator<CodeReviewCommit> i = merged.iterator(); i.hasNext();) {
-        msgbuf.append(i.next().change.getKey().abbreviate());
-        if (i.hasNext()) {
-          msgbuf.append(',');
-        }
-      }
-    }
-
-    if (!R_HEADS_MASTER.equals(destBranch.get())) {
-      msgbuf.append(" into ");
-      msgbuf.append(destBranch.getShortName());
-    }
-
-    if (merged.size() > 1) {
-      msgbuf.append("\n\n* changes:\n");
-      for (final CodeReviewCommit c : merged) {
-        rw.parseBody(c);
-        msgbuf.append("  ");
-        msgbuf.append(c.getShortMessage());
-        msgbuf.append("\n");
-      }
-    }
-
-    PersonIdent authorIdent = computeAuthor(merged);
-
-    final CommitBuilder mergeCommit = new CommitBuilder();
-    mergeCommit.setTreeId(treeId);
-    mergeCommit.setParentIds(mergeTip, n);
-    mergeCommit.setAuthor(authorIdent);
-    mergeCommit.setCommitter(myIdent);
-    mergeCommit.setMessage(msgbuf.toString());
-
-    mergeTip = (CodeReviewCommit) rw.parseCommit(commit(mergeCommit));
-  }
-
-  private PersonIdent computeAuthor(
-      final List<CodeReviewCommit> codeReviewCommits) {
-    PatchSetApproval submitter = null;
-    for (final CodeReviewCommit c : codeReviewCommits) {
-      PatchSetApproval s = getSubmitter(db, c.patchsetId);
-      if (submitter == null
-          || (s != null && s.getGranted().compareTo(submitter.getGranted()) > 0)) {
-        submitter = s;
-      }
-    }
-
-    // Try to use the submitter's identity for the merge commit author.
-    // If all of the commits being merged are created by the submitter,
-    // prefer the identity line they used in the commits rather than the
-    // preferred identity stored in the user account. This way the Git
-    // commit records are more consistent internally.
-    //
-    PersonIdent authorIdent;
-    if (submitter != null) {
-      IdentifiedUser who =
-          identifiedUserFactory.create(submitter.getAccountId());
-      Set<String> emails = new HashSet<String>();
-      for (RevCommit c : codeReviewCommits) {
-        try {
-          rw.parseBody(c);
-        } catch (IOException e) {
-          log.warn("Cannot parse commit " + c.name() + " in " + destBranch, e);
-          continue;
-        }
-        emails.add(c.getAuthorIdent().getEmailAddress());
-      }
-
-      final Timestamp dt = submitter.getGranted();
-      final TimeZone tz = myIdent.getTimeZone();
-      if (emails.size() == 1
-          && who.getEmailAddresses().contains(emails.iterator().next())) {
-        authorIdent =
-            new PersonIdent(codeReviewCommits.get(0).getAuthorIdent(), dt, tz);
-      } else {
-        authorIdent = who.newCommitterIdent(dt, tz);
-      }
-    } else {
-      authorIdent = myIdent;
-    }
-    return authorIdent;
-  }
-
-  private void markCleanMerges() throws MergeException {
-    if (mergeTip == null) {
-      // If mergeTip is null here, branchTip was null, indicating a new branch
-      // at the start of the merge process. We also elected to merge nothing,
-      // probably due to missing dependencies. Nothing was cleanly merged.
-      //
+  private void updateBranch(final SubmitStrategy strategy,
+      final RefUpdate branchUpdate) throws MergeException {
+    if ((branchTip == null && mergeTip == null) || branchTip == mergeTip) {
+      // nothing to do
       return;
     }
 
-    try {
-      rw.reset();
-      rw.sort(RevSort.TOPO);
-      rw.sort(RevSort.REVERSE, true);
-      rw.markStart(mergeTip);
-      for (RevCommit c : alreadyAccepted) {
-        rw.markUninteresting(c);
-      }
-
-      CodeReviewCommit c;
-      while ((c = (CodeReviewCommit) rw.next()) != null) {
-        if (c.patchsetId != null) {
-          c.statusCode = CommitMergeStatus.CLEAN_MERGE;
-          if (branchUpdate.getRefLogIdent() == null) {
-            setRefLogIdent(getSubmitter(db, c.patchsetId));
-          }
-        }
-      }
-    } catch (IOException e) {
-      throw new MergeException("Cannot mark clean merges", e);
-    }
-  }
-
-  private void setRefLogIdent(final PatchSetApproval submitAudit) {
-    if (submitAudit != null) {
-      branchUpdate.setRefLogIdent(identifiedUserFactory.create(
-          submitAudit.getAccountId()).newRefLogIdent());
-    }
-  }
-
-  private void cherryPickChanges() throws MergeException, OrmException {
-    while (!toMerge.isEmpty()) {
-      final CodeReviewCommit n = toMerge.remove(0);
-      final ThreeWayMerger m = newThreeWayMerger();
-      try {
-        if (mergeTip == null) {
-          // The branch is unborn. Take a fast-forward resolution to
-          // create the branch.
-          //
-          mergeTip = n;
-          n.statusCode = CommitMergeStatus.CLEAN_MERGE;
-
-        } else if (n.getParentCount() == 0) {
-          // Refuse to merge a root commit into an existing branch,
-          // we cannot obtain a delta for the cherry-pick to apply.
-          //
-          n.statusCode = CommitMergeStatus.CANNOT_CHERRY_PICK_ROOT;
-
-        } else if (n.getParentCount() == 1) {
-          // If there is only one parent, a cherry-pick can be done by
-          // taking the delta relative to that one parent and redoing
-          // that on the current merge tip.
-          //
-          m.setBase(n.getParent(0));
-          if (m.merge(mergeTip, n)) {
-            writeCherryPickCommit(m, n);
-
-          } else {
-            n.statusCode = CommitMergeStatus.PATH_CONFLICT;
-          }
-
-        } else {
-          // There are multiple parents, so this is a merge commit. We
-          // don't want to cherry-pick the merge as clients can't easily
-          // rebase their history with that merge present and replaced
-          // by an equivalent merge with a different first parent. So
-          // instead behave as though MERGE_IF_NECESSARY was configured.
-          //
-          if (hasDependenciesMet(n)) {
-            if (rw.isMergedInto(mergeTip, n)) {
-              mergeTip = n;
-            } else {
-              mergeOneCommit(n);
-            }
-            markCleanMerges();
-
-          } else {
-            // One or more dependencies were not met. The status was
-            // already marked on the commit so we have nothing further
-            // to perform at this time.
-            //
-          }
-        }
-
-      } catch (IOException e) {
-        throw new MergeException("Cannot merge " + n.name(), e);
-      }
-    }
-  }
-
-  private boolean hasDependenciesMet(final CodeReviewCommit n)
-      throws IOException {
-    // Oddly we can determine this by running the merge sorter and
-    // look for the one commit to come out as a result. This works
-    // as the merge sorter checks the dependency chain as part of
-    // its logic trying to find a minimal merge path.
-    //
-    return new MergeSorter(rw, alreadyAccepted, CAN_MERGE).sort(
-        Collections.singleton(n)).contains(n);
-  }
-
-  private void writeCherryPickCommit(final Merger m, final CodeReviewCommit n)
-      throws IOException, OrmException {
-    rw.parseBody(n);
-
-    final List<FooterLine> footers = n.getFooterLines();
-    final StringBuilder msgbuf = new StringBuilder();
-    msgbuf.append(n.getFullMessage());
-
-    if (msgbuf.length() == 0) {
-      // WTF, an empty commit message?
-      msgbuf.append("<no commit message provided>");
-    }
-    if (msgbuf.charAt(msgbuf.length() - 1) != '\n') {
-      // Missing a trailing LF? Correct it (perhaps the editor was broken).
-      msgbuf.append('\n');
-    }
-    if (footers.isEmpty()) {
-      // Doesn't end in a "Signed-off-by: ..." style line? Add another line
-      // break to start a new paragraph for the reviewed-by tag lines.
-      //
-      msgbuf.append('\n');
-    }
-
-    if (!contains(footers, CHANGE_ID, n.change.getKey().get())) {
-      msgbuf.append(CHANGE_ID.getName());
-      msgbuf.append(": ");
-      msgbuf.append(n.change.getKey().get());
-      msgbuf.append('\n');
-    }
-
-    final String siteUrl = urlProvider.get();
-    if (siteUrl != null) {
-      final String url = siteUrl + n.patchsetId.getParentKey().get();
-      if (!contains(footers, REVIEWED_ON, url)) {
-        msgbuf.append(REVIEWED_ON.getName());
-        msgbuf.append(": ");
-        msgbuf.append(url);
-        msgbuf.append('\n');
-      }
-    }
-
-    PatchSetApproval submitAudit = null;
-    List<PatchSetApproval> approvalList = null;
-    try {
-      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());
-        }
-      });
-
-      for (final PatchSetApproval a : approvalList) {
-        if (a.getValue() <= 0) {
-          // Negative votes aren't counted.
-          continue;
-        }
-
-        if (ApprovalCategory.SUBMIT.equals(a.getCategoryId())) {
-          // Submit is treated specially, below (becomes committer)
-          //
-          if (submitAudit == null
-              || a.getGranted().compareTo(submitAudit.getGranted()) > 0) {
-            submitAudit = a;
-          }
-          continue;
-        }
-
-        final Account acc =
-            identifiedUserFactory.create(a.getAccountId()).getAccount();
-        final StringBuilder identbuf = new StringBuilder();
-        if (acc.getFullName() != null && acc.getFullName().length() > 0) {
-          if (identbuf.length() > 0) {
-            identbuf.append(' ');
-          }
-          identbuf.append(acc.getFullName());
-        }
-        if (acc.getPreferredEmail() != null
-            && acc.getPreferredEmail().length() > 0) {
-          if (isSignedOffBy(footers, acc.getPreferredEmail())) {
-            continue;
-          }
-          if (identbuf.length() > 0) {
-            identbuf.append(' ');
-          }
-          identbuf.append('<');
-          identbuf.append(acc.getPreferredEmail());
-          identbuf.append('>');
-        }
-        if (identbuf.length() == 0) {
-          // Nothing reasonable to describe them by? Ignore them.
-          continue;
-        }
-
-        final String tag;
-        if (CRVW.equals(a.getCategoryId())) {
-          tag = "Reviewed-by";
-        } else if (VRIF.equals(a.getCategoryId())) {
-          tag = "Tested-by";
-        } else {
-          final ApprovalType at =
-              approvalTypes.byId(a.getCategoryId());
-          if (at == null) {
-            // A deprecated/deleted approval type, ignore it.
-            continue;
-          }
-          tag = at.getCategory().getName().replace(' ', '-');
-        }
-
-        if (!contains(footers, new FooterKey(tag), identbuf.toString())) {
-          msgbuf.append(tag);
-          msgbuf.append(": ");
-          msgbuf.append(identbuf);
-          msgbuf.append('\n');
-        }
-      }
-    } catch (OrmException e) {
-      log.error("Can't read approval records for " + n.patchsetId, e);
-    }
-
-    final CommitBuilder mergeCommit = new CommitBuilder();
-    mergeCommit.setTreeId(m.getResultTreeId());
-    mergeCommit.setParentId(mergeTip);
-    mergeCommit.setAuthor(n.getAuthorIdent());
-    mergeCommit.setCommitter(toCommitterIdent(submitAudit));
-    mergeCommit.setMessage(msgbuf.toString());
-
-    final ObjectId id = commit(mergeCommit);
-    final CodeReviewCommit newCommit = (CodeReviewCommit) rw.parseCommit(id);
-
-    if (submitAudit != null) {
-      final Change oldChange = n.change;
-
-      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;
-                }
-              });
-
-      this.submitted.remove(oldChange);
-      this.submitted.add(n.change);
-
-      if (approvalList != null) {
-        for (PatchSetApproval a : approvalList) {
-          db.patchSetApprovals().insert(
-              Collections.singleton(new PatchSetApproval(ps.getId(), a)));
-        }
-      }
-
-      final RefUpdate ru = repo.updateRef(ps.getRefName());
-      ru.setExpectedOldObjectId(ObjectId.zeroId());
-      ru.setNewObjectId(newCommit);
-      ru.disableRefLog();
-      if (ru.update(rw) != RefUpdate.Result.NEW) {
-        throw new IOException(String.format(
-            "Failed to create ref %s in %s: %s", ps.getRefName(), n.change
-                .getDest().getParentKey().get(), ru.getResult()));
-      }
-      replication.fire(n.change.getProject(), ru.getName());
-    }
-
-    newCommit.copyFrom(n);
-    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(CommitBuilder mergeCommit)
-      throws IOException, UnsupportedEncodingException {
-    ObjectId id = inserter.insert(mergeCommit);
-    inserter.flush();
-    return id;
-  }
-
-  private boolean contains(List<FooterLine> footers, FooterKey key, String val) {
-    for (final FooterLine line : footers) {
-      if (line.matches(key) && val.equals(line.getValue())) {
-        return true;
-      }
-    }
-    return false;
-  }
-
-  private boolean isSignedOffBy(List<FooterLine> footers, String email) {
-    for (final FooterLine line : footers) {
-      if (line.matches(FooterKey.SIGNED_OFF_BY)
-          && email.equals(line.getEmailAddress())) {
-        return true;
-      }
-    }
-    return false;
-  }
-
-  private PersonIdent toCommitterIdent(final PatchSetApproval audit) {
-    if (audit != null) {
-      return identifiedUserFactory.create(audit.getAccountId())
-          .newCommitterIdent(audit.getGranted(), myIdent.getTimeZone());
-    }
-    return myIdent;
-  }
-
-  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());
+          ProjectConfig cfg =
+              new ProjectConfig(destProject.getProject().getNameKey());
           cfg.load(repo, mergeTip);
         } catch (Exception e) {
           throw new MergeException("Submit would store invalid"
               + " project configuration " + mergeTip.name() + " for "
-              + destProject.getName(), e);
+              + destProject.getProject().getName(), e);
         }
       }
 
+      branchUpdate.setRefLogIdent(refLogIdent);
       branchUpdate.setForceUpdate(false);
       branchUpdate.setNewObjectId(mergeTip);
       branchUpdate.setRefLogMessage("merged", true);
@@ -1121,13 +674,14 @@
             }
 
             if (GitRepositoryManager.REF_CONFIG.equals(branchUpdate.getName())) {
-              projectCache.evict(destProject);
-              ProjectState ps = projectCache.get(destProject.getNameKey());
-              repoManager.setProjectDescription(destProject.getNameKey(), //
-                  ps.getProject().getDescription());
+              projectCache.evict(destProject.getProject());
+              destProject = projectCache.get(destProject.getProject().getNameKey());
+              repoManager.setProjectDescription(
+                  destProject.getProject().getNameKey(),
+                  destProject.getProject().getDescription());
             }
 
-            replication.fire(destBranch.getParentKey(), branchUpdate.getName());
+            gitRefUpdated.fire(destBranch.getParentKey(), branchUpdate);
 
             Account account = null;
             final PatchSetApproval submitter = getSubmitter(db, mergeTip.patchsetId);
@@ -1137,6 +691,16 @@
             hooks.doRefUpdatedHook(destBranch, branchUpdate, account);
             break;
 
+          case LOCK_FAILURE:
+            String msg;
+            if (strategy.retryOnLockFailure()) {
+              mergeQueue.recheckAfter(destBranch, LOCK_FAILURE_RETRY_DELAY,
+                  MILLISECONDS);
+              msg = "will retry";
+            } else {
+              msg = "will not retry";
+            }
+            throw new IOException(branchUpdate.getResult().name() + ", " + msg);
           default:
             throw new IOException(branchUpdate.getResult().name());
         }
@@ -1146,23 +710,7 @@
     }
   }
 
-  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>();
-
+  private void updateChangeStatus(final List<Change> submitted) {
     for (final Change c : submitted) {
       final CodeReviewCommit commit = commits.get(c.getId());
       final CommitMergeStatus s = commit != null ? commit.statusCode : null;
@@ -1175,66 +723,51 @@
 
       final String txt = s.getMessage();
 
-      switch (s) {
-        case CLEAN_MERGE: {
-          setMerged(c, message(c, txt));
-          merged.add(commit);
-          break;
+      try {
+        switch (s) {
+          case CLEAN_MERGE:
+            setMerged(c, message(c, txt));
+            break;
+
+          case CLEAN_REBASE:
+          case CLEAN_PICK:
+            setMerged(c, message(c, txt + " as " + commit.name()));
+            break;
+
+          case ALREADY_MERGED:
+            setMerged(c, null);
+            break;
+
+          case PATH_CONFLICT:
+          case MANUAL_RECURSIVE_MERGE:
+          case CANNOT_CHERRY_PICK_ROOT:
+          case NOT_FAST_FORWARD:
+          case INVALID_PROJECT_CONFIGURATION:
+          case INVALID_PROJECT_CONFIGURATION_PARENT_PROJECT_NOT_FOUND:
+          case INVALID_PROJECT_CONFIGURATION_ROOT_PROJECT_CANNOT_HAVE_PARENT:
+          case SETTING_PARENT_PROJECT_ONLY_ALLOWED_BY_ADMIN:
+            setNew(c, message(c, txt));
+            break;
+
+          case MISSING_DEPENDENCY:
+            potentiallyStillSubmittable.add(commit);
+            break;
+
+          default:
+            setNew(c, message(c, "Unspecified merge failure: " + s.name()));
+            break;
         }
-
-        case CLEAN_PICK: {
-          setMerged(c, message(c, txt + " as " + commit.name()));
-          merged.add(commit);
-          break;
-        }
-
-        case ALREADY_MERGED:
-          setMerged(c, null);
-          merged.add(commit);
-          break;
-
-        case PATH_CONFLICT:
-        case CRISS_CROSS_MERGE:
-        case CANNOT_CHERRY_PICK_ROOT:
-        case NOT_FAST_FORWARD:
-        case INVALID_PROJECT_CONFIGURATION:
-        case INVALID_PROJECT_CONFIGURATION_PARENT_PROJECT_NOT_FOUND:
-        case INVALID_PROJECT_CONFIGURATION_ROOT_PROJECT_CANNOT_HAVE_PARENT:
-        case SETTING_PARENT_PROJECT_ONLY_ALLOWED_BY_ADMIN: {
-          setNew(c, message(c, txt));
-          break;
-        }
-
-        case MISSING_DEPENDENCY: {
-          final Capable capable = isSubmitStillPossible(commit);
-          if (capable != Capable.OK) {
-            sendMergeFail(c, message(c, capable.getMessage()), false);
-          }
-          break;
-        }
-
-        default:
-          setNew(c, message(c, "Unspecified merge failure: " + s.name()));
-          break;
+      } catch (OrmException err) {
+        log.warn("Error updating change status for " + c.getId(), err);
       }
     }
-
-    CreateCodeReviewNotes codeReviewNotes =
-        codeReviewNotesFactory.create(db, repo);
-    try {
-      codeReviewNotes.create(merged, computeAuthor(merged));
-    } catch (CodeReviewNoteCreationException e) {
-      log.error(e.getMessage());
-    }
-    replication.fire(destBranch.getParentKey(),
-        GitRepositoryManager.REFS_NOTES_REVIEW);
   }
 
-  private void updateSubscriptions() {
+  private void updateSubscriptions(final List<Change> submitted) {
     if (mergeTip != null && (branchTip == null || branchTip != mergeTip)) {
       SubmoduleOp subOp =
-          subOpFactory.create(destBranch, mergeTip, rw, repo, destProject,
-              submitted, commits);
+          subOpFactory.create(destBranch, mergeTip, rw, repo,
+              destProject.getProject(), submitted, commits);
       try {
         subOp.update();
       } catch (SubmoduleException e) {
@@ -1248,32 +781,7 @@
   private Capable isSubmitStillPossible(final CodeReviewCommit commit) {
     final Capable capable;
     final Change c = commit.change;
-    if (commit.missing == null) {
-      commit.missing = new ArrayList<CodeReviewCommit>();
-    }
-
-    boolean submitStillPossible = commit.missing.size() > 0;
-    for (CodeReviewCommit missingCommit : commit.missing) {
-      loadChangeInfo(missingCommit);
-
-      if (missingCommit.patchsetId == null) {
-        // The commit doesn't have a patch set, so it cannot be
-        // submitted to the branch.
-        //
-        submitStillPossible = false;
-        break;
-      }
-
-      if (!missingCommit.change.currentPatchSetId().equals(
-          missingCommit.patchsetId)) {
-        // If the missing commit is not the current patch set,
-        // the change must be rebased to use the proper parent.
-        //
-        submitStillPossible = false;
-        break;
-      }
-    }
-
+    final boolean submitStillPossible = isSubmitForMissingCommitsStillPossible(commit);
     final long now = System.currentTimeMillis();
     final long waitUntil = c.getLastUpdatedOn().getTime() + DEPENDENCY_DELAY;
     if (submitStillPossible && now < waitUntil) {
@@ -1287,25 +795,20 @@
       // dependencies are also submitted. Perhaps the user just
       // forgot to submit those.
       //
-      String txt =
-          "Change could not be merged because of a missing dependency.";
-      if (!isAlreadySent(c, txt)) {
-        StringBuilder m = new StringBuilder();
-        m.append(txt);
-        m.append("\n");
+      StringBuilder m = new StringBuilder();
+      m.append("Change could not be merged because of a missing dependency.");
+      m.append("\n");
 
-        m.append("\n");
+      m.append("\n");
 
-        m.append("The following changes must also be submitted:\n");
+      m.append("The following changes must also be submitted:\n");
+      m.append("\n");
+      for (CodeReviewCommit missingCommit : commit.missing) {
+        m.append("* ");
+        m.append(missingCommit.change.getKey().get());
         m.append("\n");
-        for (CodeReviewCommit missingCommit : commit.missing) {
-          m.append("* ");
-          m.append(missingCommit.change.getKey().get());
-          m.append("\n");
-        }
-        txt = m.toString();
       }
-      capable = new Capable(txt);
+      capable = new Capable(m.toString());
     } else {
       // It is impossible to submit this change as-is. The author
       // needs to rebase it in order to work around the missing
@@ -1323,8 +826,10 @@
           m.append(missingCommit.patchsetId.get());
           m.append(" of ");
           m.append(missingCommit.change.getKey().abbreviate());
-          m.append(", however the current patch set is ");
-          m.append(missingCommit.change.currentPatchSetId().get());
+          if (missingCommit.patchsetId.get() != missingCommit.change.currentPatchSetId().get()) {
+            m.append(", however the current patch set is ");
+            m.append(missingCommit.change.currentPatchSetId().get());
+          }
           m.append(".\n");
 
         } else {
@@ -1356,30 +861,6 @@
     }
   }
 
-  private boolean isAlreadySent(final Change c, final String prefix) {
-    try {
-      final List<ChangeMessage> msgList =
-          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)) {
-          // The last message was written by us, and it said this
-          // same message already. Its unlikely anything has changed
-          // that would cause us to need to repeat ourselves.
-          //
-          return true;
-        }
-      }
-
-      // The last message was not sent by us, or doesn't match the text
-      // we are about to send.
-      //
-      return false;
-    } catch (OrmException e) {
-      return true;
-    }
-  }
-
   private ChangeMessage message(final Change c, final String body) {
     final String uuid;
     try {
@@ -1394,87 +875,87 @@
     return m;
   }
 
-  private static PatchSetApproval getSubmitter(ReviewDb reviewDb,
-      PatchSet.Id c) {
-    if (c == null) {
-      return null;
-    }
-    PatchSetApproval submitter = null;
+  private void setMerged(final Change c, final ChangeMessage msg)
+      throws OrmException {
     try {
-      final List<PatchSetApproval> approvals =
-          reviewDb.patchSetApprovals().byPatchSet(c).toList();
-      for (PatchSetApproval a : approvals) {
-        if (a.getValue() > 0
-            && ApprovalCategory.SUBMIT.equals(a.getCategoryId())) {
-          if (submitter == null
-              || a.getGranted().compareTo(submitter.getGranted()) > 0) {
-            submitter = a;
-          }
+      db.changes().beginTransaction(c.getId());
+
+      // We must pull the patchset out of commits, because the patchset ID is
+      // modified when using the cherry-pick merge strategy.
+      CodeReviewCommit commit = commits.get(c.getId());
+      PatchSet.Id merged = commit.change.currentPatchSetId();
+      setMergedPatchSet(c.getId(), merged);
+      PatchSetApproval submitter = saveApprovals(c, merged);
+      addMergedMessage(submitter, msg);
+
+      db.commit();
+
+      sendMergedEmail(c, submitter);
+      if (submitter != null) {
+        try {
+          hooks.doChangeMergedHook(c,
+              accountCache.get(submitter.getAccountId()).getAccount(),
+              db.patchSets().get(c.currentPatchSetId()), db);
+        } catch (OrmException ex) {
+          log.error("Cannot run hook for submitted patch set " + c.getId(), ex);
         }
       }
-    } catch (OrmException e) {
+    } finally {
+      db.rollback();
     }
-    return submitter;
   }
 
-  private void setMerged(final Change c, final ChangeMessage msg) {
-    final Change.Id changeId = c.getId();
-    // 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 {
-      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(db, merged));
-            } catch (PatchSetInfoNotAvailableException e1) {
-              log.error("Cannot read merged patch set " + merged, e1);
-            }
+  private void setMergedPatchSet(Change.Id changeId, final PatchSet.Id merged)
+      throws OrmException {
+    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(db, merged));
+          } catch (PatchSetInfoNotAvailableException e1) {
+            log.error("Cannot read merged patch set " + merged, e1);
           }
-          ChangeUtil.updated(c);
-          return c;
         }
-      });
-    } catch (OrmConcurrencyException err) {
-    } catch (OrmException err) {
-      log.warn("Cannot update change status", err);
-    }
+        ChangeUtil.updated(c);
+        return c;
+      }
+    });
+  }
 
-    // Flatten out all existing approvals based upon the current
-    // permissions. Once the change is closed the approvals are
-    // not updated at presentation view time, so we need to make.
-    // sure they are accurate now. This way if permissions get
-    // modified in the future, historical records stay accurate.
-    //
+  private PatchSetApproval saveApprovals(Change c, PatchSet.Id merged)
+      throws OrmException {
+    // Flatten out existing approvals for this patch set based upon the current
+    // permissions. Once the change is closed the approvals are not updated at
+    // presentation view time, except for zero votes used to indicate a reviewer
+    // was added. So we need to make sure votes are accurate now. This way if
+    // permissions get modified in the future, historical records stay accurate.
     PatchSetApproval submitter = null;
     try {
       c.setStatus(Change.Status.MERGED);
-      final List<PatchSetApproval> 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);
-      }
+
+      List<PatchSetApproval> approvals =
+          db.patchSetApprovals().byPatchSet(merged).toList();
+      Set<PatchSetApproval.Key> toDelete =
+          Sets.newHashSetWithExpectedSize(approvals.size());
       for (PatchSetApproval a : approvals) {
-        if (a.getValue() > 0
-            && ApprovalCategory.SUBMIT.equals(a.getCategoryId())
-            && a.getPatchSetId().equals(merged)) {
+        if (a.getValue() != 0) {
+          toDelete.add(a.getKey());
+        }
+      }
+
+      approvals = labelNormalizer.normalize(c, approvals);
+      for (PatchSetApproval a : approvals) {
+        toDelete.remove(a.getKey());
+        if (a.getValue() > 0 && a.isSubmit()) {
           if (submitter == null
               || a.getGranted().compareTo(submitter.getGranted()) > 0) {
             submitter = a;
@@ -1483,24 +964,24 @@
         a.cache(c);
       }
       db.patchSetApprovals().update(approvals);
+      db.patchSetApprovals().deleteKeys(toDelete);
     } catch (NoSuchChangeException err) {
-      log.warn("Cannot normalize approvals for change " + changeId, err);
-    } catch (OrmException err) {
-      log.warn("Cannot normalize approvals for change " + changeId, err);
+      throw new OrmException(err);
     }
+    return submitter;
+  }
 
+  private void addMergedMessage(PatchSetApproval submitter, ChangeMessage msg)
+      throws OrmException {
     if (msg != null) {
       if (submitter != null && msg.getAuthor() == null) {
         msg.setAuthor(submitter.getAccountId());
       }
-      try {
-        db.changeMessages().insert(Collections.singleton(msg));
-      } catch (OrmException err) {
-        log.warn("Cannot store message on change", err);
-      }
+      db.changeMessages().insert(Collections.singleton(msg));
     }
+  }
 
-    final PatchSetApproval from = submitter;
+  private void sendMergedEmail(final Change c, final PatchSetApproval from) {
     workQueue.getDefaultQueue()
         .submit(requestScopePropagator.wrap(new Runnable() {
       @Override
@@ -1519,7 +1000,9 @@
         }
 
         try {
-          final MergedSender cm = mergedSenderFactory.create(c);
+          final ChangeControl control = changeControlFactory.controlFor(c,
+              identifiedUserFactory.create(c.getOwner()));
+          final MergedSender cm = mergedSenderFactory.create(control);
           if (from != null) {
             cm.setFrom(from.getAccountId());
           }
@@ -1535,23 +1018,37 @@
         return "send-email merged";
       }
     }));
-
-
-    try {
-      hooks.doChangeMergedHook(c, //
-          accountCache.get(submitter.getAccountId()).getAccount(), //
-          db.patchSets().get(c.currentPatchSetId()), db);
-    } catch (OrmException ex) {
-      log.error("Cannot run hook for submitted patch set " + c.getId(), ex);
-    }
   }
 
   private void setNew(Change c, ChangeMessage msg) {
     sendMergeFail(c, msg, true);
   }
 
+  private boolean isDuplicate(ChangeMessage msg) {
+    try {
+      ChangeMessage last = Iterables.getLast(db.changeMessages().byChange(
+          msg.getPatchSetId().getParentKey()), null);
+      if (last != null) {
+        long lastMs = last.getWrittenOn().getTime();
+        long msgMs = msg.getWrittenOn().getTime();
+        if (Objects.equal(last.getAuthor(), msg.getAuthor())
+            && Objects.equal(last.getMessage(), msg.getMessage())
+            && msgMs - lastMs < DUPLICATE_MESSAGE_INTERVAL) {
+          return true;
+        }
+      }
+    } catch (OrmException err) {
+      log.warn("Cannot check previous merge failure message", err);
+    }
+    return false;
+  }
+
   private void sendMergeFail(final Change c, final ChangeMessage msg,
       final boolean makeNew) {
+    if (isDuplicate(msg)) {
+      return;
+    }
+
     try {
       db.changeMessages().insert(Collections.singleton(msg));
     } catch (OrmException err) {
@@ -1582,17 +1079,23 @@
       }
     }
 
+    PatchSetApproval submitter = null;
+    try {
+      submitter = getSubmitter(db, c.currentPatchSetId());
+    } catch (Exception e) {
+      log.error("Cannot get submitter", e);
+    }
+
+    final PatchSetApproval from = submitter;
     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();
           }
@@ -1603,8 +1106,8 @@
 
         try {
           final MergeFailSender cm = mergeFailSenderFactory.create(c);
-          if (submitter != null) {
-            cm.setFrom(submitter.getAccountId());
+          if (from != null) {
+            cm.setFrom(from.getAccountId());
           }
           cm.setPatchSet(patchSet);
           cm.setChangeMessage(msg);
@@ -1619,5 +1122,15 @@
         return "send-email merge-failed";
       }
     }));
+
+    if (submitter != null) {
+      try {
+        hooks.doMergeFailedHook(c,
+            accountCache.get(submitter.getAccountId()).getAccount(),
+            db.patchSets().get(c.currentPatchSetId()), msg.getMessage(), db);
+      } catch (OrmException ex) {
+        log.error("Cannot run hook for merge failed " + c.getId(), ex);
+      }
+    }
   }
 }
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 1071768..a53b04c 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
@@ -19,9 +19,7 @@
 import java.util.concurrent.TimeUnit;
 
 public interface MergeQueue {
-  void merge(MergeOp.Factory mof, Branch.NameKey branch);
-
+  void merge(Branch.NameKey branch);
   void schedule(Branch.NameKey branch);
-
   void recheckAfter(Branch.NameKey branch, long delay, TimeUnit delayUnit);
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSorter.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSorter.java
index fe87480..3911d96 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSorter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeSorter.java
@@ -28,14 +28,14 @@
 
 class MergeSorter {
   private final RevWalk rw;
-  private final RevFlag CAN_MERGE;
+  private final RevFlag canMergeFlag;
   private final Set<RevCommit> accepted;
 
-  MergeSorter(final RevWalk walk, final Set<RevCommit> alreadyAccepted,
-      final RevFlag flagCAN_MERGE) {
-    rw = walk;
-    CAN_MERGE = flagCAN_MERGE;
-    accepted = alreadyAccepted;
+  MergeSorter(final RevWalk rw, final Set<RevCommit> alreadyAccepted,
+      final RevFlag canMergeFlag) {
+    this.rw = rw;
+    this.canMergeFlag = canMergeFlag;
+    this.accepted = alreadyAccepted;
   }
 
   Collection<CodeReviewCommit> sort(final Collection<CodeReviewCommit> incoming)
@@ -45,7 +45,7 @@
     while (!sort.isEmpty()) {
       final CodeReviewCommit n = removeOne(sort);
 
-      rw.resetRetain(CAN_MERGE);
+      rw.resetRetain(canMergeFlag);
       rw.markStart(n);
       for (RevCommit c : accepted) {
         rw.markUninteresting(c);
@@ -54,7 +54,7 @@
       RevCommit c;
       final RevCommitList<RevCommit> contents = new RevCommitList<RevCommit>();
       while ((c = rw.next()) != null) {
-        if (!c.has(CAN_MERGE)) {
+        if (!c.has(canMergeFlag) || !incoming.contains(c)) {
           // We cannot merge n as it would bring something we
           // aren't permitted to merge at this time. Drop n.
           //
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
new file mode 100644
index 0000000..862eb2b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MergeUtil.java
@@ -0,0 +1,713 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.LabelType;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.PatchSetApproval.LabelId;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Provider;
+import com.google.inject.assistedinject.Assisted;
+import com.google.inject.assistedinject.AssistedInject;
+
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.MissingObjectException;
+import org.eclipse.jgit.errors.NoMergeBaseException;
+import org.eclipse.jgit.errors.NoMergeBaseException.MergeBaseFailureReason;
+import org.eclipse.jgit.lib.AnyObjectId;
+import org.eclipse.jgit.lib.CommitBuilder;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.MutableObjectId;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.ObjectInserter;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.merge.MergeStrategy;
+import org.eclipse.jgit.merge.ThreeWayMerger;
+import org.eclipse.jgit.revwalk.FooterKey;
+import org.eclipse.jgit.revwalk.FooterLine;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevFlag;
+import org.eclipse.jgit.revwalk.RevSort;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.transport.PackParser;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.UnsupportedEncodingException;
+import java.sql.Timestamp;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Set;
+import java.util.TimeZone;
+
+import javax.annotation.Nullable;
+
+public class MergeUtil {
+  private static final Logger log = LoggerFactory.getLogger(MergeUtil.class);
+
+  public static interface Factory {
+    MergeUtil create(ProjectState project);
+    MergeUtil create(ProjectState project, boolean useContentMerge);
+  }
+
+  private static final String R_HEADS_MASTER =
+      Constants.R_HEADS + Constants.MASTER;
+
+  private static final FooterKey REVIEWED_ON = new FooterKey("Reviewed-on");
+  private static final FooterKey CHANGE_ID = new FooterKey("Change-Id");
+
+  private final Provider<ReviewDb> db;
+  private final IdentifiedUser.GenericFactory identifiedUserFactory;
+  private final Provider<String> urlProvider;
+  private final ProjectState project;
+  private final boolean useContentMerge;
+  private final boolean useRecursiveMerge;
+
+  @AssistedInject
+  MergeUtil(@GerritServerConfig Config serverConfig,
+      final Provider<ReviewDb> db,
+      final IdentifiedUser.GenericFactory identifiedUserFactory,
+      @CanonicalWebUrl @Nullable final Provider<String> urlProvider,
+      @Assisted final ProjectState project) {
+    this(serverConfig, db, identifiedUserFactory, urlProvider, project,
+        project.isUseContentMerge());
+  }
+
+  @AssistedInject
+  MergeUtil(@GerritServerConfig Config serverConfig,
+      final Provider<ReviewDb> db,
+      final IdentifiedUser.GenericFactory identifiedUserFactory,
+      @CanonicalWebUrl @Nullable final Provider<String> urlProvider,
+      @Assisted final ProjectState project,
+      @Assisted boolean useContentMerge) {
+    this.db = db;
+    this.identifiedUserFactory = identifiedUserFactory;
+    this.urlProvider = urlProvider;
+    this.project = project;
+    this.useContentMerge = useContentMerge;
+    this.useRecursiveMerge =
+        serverConfig.getBoolean("core", null, "useRecursiveMerge", false);
+  }
+
+  public CodeReviewCommit getFirstFastForward(
+      final CodeReviewCommit mergeTip, final RevWalk rw,
+      final List<CodeReviewCommit> toMerge) throws MergeException {
+    for (final Iterator<CodeReviewCommit> i = toMerge.iterator(); i.hasNext();) {
+      try {
+        final CodeReviewCommit n = i.next();
+        if (mergeTip == null || rw.isMergedInto(mergeTip, n)) {
+          i.remove();
+          return n;
+        }
+      } catch (IOException e) {
+        throw new MergeException("Cannot fast-forward test during merge", e);
+      }
+    }
+    return mergeTip;
+  }
+
+  public void reduceToMinimalMerge(final MergeSorter mergeSorter,
+      final List<CodeReviewCommit> toSort) throws MergeException {
+    final Collection<CodeReviewCommit> heads;
+    try {
+      heads = mergeSorter.sort(toSort);
+    } catch (IOException e) {
+      throw new MergeException("Branch head sorting failed", e);
+    }
+
+    toSort.clear();
+    toSort.addAll(heads);
+    Collections.sort(toSort, new Comparator<CodeReviewCommit>() {
+      @Override
+      public int compare(final CodeReviewCommit a, final CodeReviewCommit b) {
+        return a.originalOrder - b.originalOrder;
+      }
+    });
+  }
+
+  public PatchSetApproval getSubmitter(final PatchSet.Id c) {
+    return getSubmitter(db.get(), c);
+  }
+
+  public static PatchSetApproval getSubmitter(final ReviewDb reviewDb,
+      final PatchSet.Id c) {
+    if (c == null) {
+      return null;
+    }
+    PatchSetApproval submitter = null;
+    try {
+      final List<PatchSetApproval> approvals =
+          reviewDb.patchSetApprovals().byPatchSet(c).toList();
+      for (PatchSetApproval a : approvals) {
+        if (a.getValue() > 0 && a.isSubmit()) {
+          if (submitter == null
+              || a.getGranted().compareTo(submitter.getGranted()) > 0) {
+            submitter = a;
+          }
+        }
+      }
+    } catch (OrmException e) {
+    }
+    return submitter;
+  }
+
+  public CodeReviewCommit createCherryPickFromCommit(Repository repo,
+      ObjectInserter inserter, CodeReviewCommit mergeTip, CodeReviewCommit originalCommit,
+      PersonIdent cherryPickCommitterIdent, String commitMsg, RevWalk rw)
+      throws MissingObjectException, IncorrectObjectTypeException, IOException {
+
+    final ThreeWayMerger m = newThreeWayMerger(repo, inserter);
+
+    m.setBase(originalCommit.getParent(0));
+    if (m.merge(mergeTip, originalCommit)) {
+
+      final CommitBuilder mergeCommit = new CommitBuilder();
+
+      mergeCommit.setTreeId(m.getResultTreeId());
+      mergeCommit.setParentId(mergeTip);
+      mergeCommit.setAuthor(originalCommit.getAuthorIdent());
+      mergeCommit.setCommitter(cherryPickCommitterIdent);
+      mergeCommit.setMessage(commitMsg);
+
+      final ObjectId id = commit(inserter, mergeCommit);
+      final CodeReviewCommit newCommit =
+          (CodeReviewCommit) rw.parseCommit(id);
+
+      return newCommit;
+    } else {
+      return null;
+    }
+  }
+
+  public String createCherryPickCommitMessage(final CodeReviewCommit n) {
+    final List<FooterLine> footers = n.getFooterLines();
+    final StringBuilder msgbuf = new StringBuilder();
+    msgbuf.append(n.getFullMessage());
+
+    if (msgbuf.length() == 0) {
+      // WTF, an empty commit message?
+      msgbuf.append("<no commit message provided>");
+    }
+    if (msgbuf.charAt(msgbuf.length() - 1) != '\n') {
+      // Missing a trailing LF? Correct it (perhaps the editor was broken).
+      msgbuf.append('\n');
+    }
+    if (footers.isEmpty()) {
+      // Doesn't end in a "Signed-off-by: ..." style line? Add another line
+      // break to start a new paragraph for the reviewed-by tag lines.
+      //
+      msgbuf.append('\n');
+    }
+
+    if (!contains(footers, CHANGE_ID, n.change.getKey().get())) {
+      msgbuf.append(CHANGE_ID.getName());
+      msgbuf.append(": ");
+      msgbuf.append(n.change.getKey().get());
+      msgbuf.append('\n');
+    }
+
+    final String siteUrl = urlProvider.get();
+    if (siteUrl != null) {
+      final String url = siteUrl + n.patchsetId.getParentKey().get();
+      if (!contains(footers, REVIEWED_ON, url)) {
+        msgbuf.append(REVIEWED_ON.getName());
+        msgbuf.append(": ");
+        msgbuf.append(url);
+        msgbuf.append('\n');
+      }
+    }
+
+    PatchSetApproval submitAudit = null;
+
+    for (final PatchSetApproval a : getApprovalsForCommit(n)) {
+      if (a.getValue() <= 0) {
+        // Negative votes aren't counted.
+        continue;
+      }
+
+      if (a.isSubmit()) {
+        // Submit is treated specially, below (becomes committer)
+        //
+        if (submitAudit == null
+            || a.getGranted().compareTo(submitAudit.getGranted()) > 0) {
+          submitAudit = a;
+        }
+        continue;
+      }
+
+      final Account acc =
+          identifiedUserFactory.create(a.getAccountId()).getAccount();
+      final StringBuilder identbuf = new StringBuilder();
+      if (acc.getFullName() != null && acc.getFullName().length() > 0) {
+        if (identbuf.length() > 0) {
+          identbuf.append(' ');
+        }
+        identbuf.append(acc.getFullName());
+      }
+      if (acc.getPreferredEmail() != null
+          && acc.getPreferredEmail().length() > 0) {
+        if (isSignedOffBy(footers, acc.getPreferredEmail())) {
+          continue;
+        }
+        if (identbuf.length() > 0) {
+          identbuf.append(' ');
+        }
+        identbuf.append('<');
+        identbuf.append(acc.getPreferredEmail());
+        identbuf.append('>');
+      }
+      if (identbuf.length() == 0) {
+        // Nothing reasonable to describe them by? Ignore them.
+        continue;
+      }
+
+      final String tag;
+      if (isCodeReview(a.getLabelId())) {
+        tag = "Reviewed-by";
+      } else if (isVerified(a.getLabelId())) {
+        tag = "Tested-by";
+      } else {
+        final LabelType lt = project.getLabelTypes().byLabel(a.getLabelId());
+        if (lt == null) {
+          continue;
+        }
+        tag = lt.getName();
+      }
+
+      if (!contains(footers, new FooterKey(tag), identbuf.toString())) {
+        msgbuf.append(tag);
+        msgbuf.append(": ");
+        msgbuf.append(identbuf);
+        msgbuf.append('\n');
+      }
+    }
+
+    return msgbuf.toString();
+  }
+
+  private static boolean isCodeReview(LabelId id) {
+    return "Code-Review".equalsIgnoreCase(id.get());
+  }
+
+  private static boolean isVerified(LabelId id) {
+    return "Verified".equalsIgnoreCase(id.get());
+  }
+
+  public List<PatchSetApproval> getApprovalsForCommit(final CodeReviewCommit n) {
+    try {
+      List<PatchSetApproval> approvalList =
+          db.get().patchSetApprovals().byPatchSet(n.patchsetId).toList();
+      Collections.sort(approvalList, new Comparator<PatchSetApproval>() {
+        @Override
+        public int compare(final PatchSetApproval a, final PatchSetApproval b) {
+          return a.getGranted().compareTo(b.getGranted());
+        }
+      });
+      return approvalList;
+    } catch (OrmException e) {
+      log.error("Can't read approval records for " + n.patchsetId, e);
+      return Collections.emptyList();
+    }
+  }
+
+  private static boolean contains(List<FooterLine> footers, FooterKey key, String val) {
+    for (final FooterLine line : footers) {
+      if (line.matches(key) && val.equals(line.getValue())) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  private static boolean isSignedOffBy(List<FooterLine> footers, String email) {
+    for (final FooterLine line : footers) {
+      if (line.matches(FooterKey.SIGNED_OFF_BY)
+          && email.equals(line.getEmailAddress())) {
+        return true;
+      }
+    }
+    return false;
+  }
+
+  public PersonIdent computeMergeCommitAuthor(final PersonIdent myIdent,
+      final RevWalk rw, final List<CodeReviewCommit> codeReviewCommits) {
+    PatchSetApproval submitter = null;
+    for (final CodeReviewCommit c : codeReviewCommits) {
+      PatchSetApproval s = getSubmitter(c.patchsetId);
+      if (submitter == null
+          || (s != null && s.getGranted().compareTo(submitter.getGranted()) > 0)) {
+        submitter = s;
+      }
+    }
+
+    // Try to use the submitter's identity for the merge commit author.
+    // If all of the commits being merged are created by the submitter,
+    // prefer the identity line they used in the commits rather than the
+    // preferred identity stored in the user account. This way the Git
+    // commit records are more consistent internally.
+    //
+    PersonIdent authorIdent;
+    if (submitter != null) {
+      IdentifiedUser who =
+          identifiedUserFactory.create(submitter.getAccountId());
+      Set<String> emails = new HashSet<String>();
+      for (RevCommit c : codeReviewCommits) {
+        try {
+          rw.parseBody(c);
+        } catch (IOException e) {
+          log.warn("Cannot parse commit " + c.name(), e);
+          continue;
+        }
+        emails.add(c.getAuthorIdent().getEmailAddress());
+      }
+
+      final Timestamp dt = submitter.getGranted();
+      final TimeZone tz = myIdent.getTimeZone();
+      if (emails.size() == 1
+          && who.getEmailAddresses().contains(emails.iterator().next())) {
+        authorIdent =
+            new PersonIdent(codeReviewCommits.get(0).getAuthorIdent(), dt, tz);
+      } else {
+        authorIdent = who.newCommitterIdent(dt, tz);
+      }
+    } else {
+      authorIdent = myIdent;
+    }
+    return authorIdent;
+  }
+
+  public boolean canMerge(final MergeSorter mergeSorter,
+      final Repository repo, final CodeReviewCommit mergeTip,
+      final CodeReviewCommit toMerge)
+      throws MergeException {
+    if (hasMissingDependencies(mergeSorter, toMerge)) {
+      return false;
+    }
+
+    final ThreeWayMerger m = newThreeWayMerger(repo, createDryRunInserter());
+    try {
+      return m.merge(new AnyObjectId[] {mergeTip, toMerge});
+    } catch (NoMergeBaseException e) {
+      return false;
+    } catch (IOException e) {
+      throw new MergeException("Cannot merge " + toMerge.name(), e);
+    }
+  }
+
+  public boolean canFastForward(final MergeSorter mergeSorter,
+      final CodeReviewCommit mergeTip, final RevWalk rw,
+      final CodeReviewCommit toMerge) throws MergeException {
+    if (hasMissingDependencies(mergeSorter, toMerge)) {
+      return false;
+    }
+
+    try {
+      return mergeTip == null || rw.isMergedInto(mergeTip, toMerge);
+    } catch (IOException e) {
+      throw new MergeException("Cannot fast-forward test during merge", e);
+    }
+  }
+
+  public boolean canCherryPick(final MergeSorter mergeSorter,
+      final Repository repo, final CodeReviewCommit mergeTip, final RevWalk rw,
+      final CodeReviewCommit toMerge) throws MergeException {
+    if (mergeTip == null) {
+      // The branch is unborn. Fast-forward is possible.
+      //
+      return true;
+    }
+
+    if (toMerge.getParentCount() == 0) {
+      // Refuse to merge a root commit into an existing branch,
+      // we cannot obtain a delta for the cherry-pick to apply.
+      //
+      return false;
+    }
+
+    if (toMerge.getParentCount() == 1) {
+      // If there is only one parent, a cherry-pick can be done by
+      // taking the delta relative to that one parent and redoing
+      // that on the current merge tip.
+      //
+      try {
+        final ThreeWayMerger m =
+            newThreeWayMerger(repo, createDryRunInserter());
+        m.setBase(toMerge.getParent(0));
+        return m.merge(mergeTip, toMerge);
+      } catch (IOException e) {
+        throw new MergeException("Cannot merge " + toMerge.name(), e);
+      }
+    }
+
+    // There are multiple parents, so this is a merge commit. We
+    // don't want to cherry-pick the merge as clients can't easily
+    // rebase their history with that merge present and replaced
+    // by an equivalent merge with a different first parent. So
+    // instead behave as though MERGE_IF_NECESSARY was configured.
+    //
+    return canFastForward(mergeSorter, mergeTip, rw, toMerge)
+        || canMerge(mergeSorter, repo, mergeTip, toMerge);
+  }
+
+  public boolean hasMissingDependencies(final MergeSorter mergeSorter,
+      final CodeReviewCommit toMerge) throws MergeException {
+    try {
+      return !mergeSorter.sort(Collections.singleton(toMerge)).contains(toMerge);
+    } catch (IOException e) {
+      throw new MergeException("Branch head sorting failed", e);
+    }
+  }
+
+  public ObjectInserter createDryRunInserter() {
+    return new ObjectInserter() {
+      private final MutableObjectId buf = new MutableObjectId();
+      private final static int LAST_BYTE = Constants.OBJECT_ID_LENGTH - 1;
+
+      @Override
+      public ObjectId insert(int objectType, long length, InputStream in)
+          throws IOException {
+        // create non-existing dummy ID
+        buf.setByte(LAST_BYTE, buf.getByte(LAST_BYTE) + 1);
+        return buf.copy();
+      }
+
+      @Override
+      public PackParser newPackParser(InputStream in) throws IOException {
+        throw new UnsupportedOperationException();
+      }
+
+      @Override
+      public void flush() throws IOException {
+        // Do nothing.
+      }
+
+      @Override
+      public void release() {
+        // Do nothing.
+      }
+    };
+  }
+
+  public CodeReviewCommit mergeOneCommit(final PersonIdent myIdent,
+      final Repository repo, final RevWalk rw, final ObjectInserter inserter,
+      final RevFlag canMergeFlag, final Branch.NameKey destBranch,
+      final CodeReviewCommit mergeTip, final CodeReviewCommit n)
+      throws MergeException {
+    final ThreeWayMerger m = newThreeWayMerger(repo, inserter);
+    try {
+      if (m.merge(new AnyObjectId[] {mergeTip, n})) {
+        return writeMergeCommit(myIdent, rw, inserter, canMergeFlag, destBranch,
+            mergeTip, m.getResultTreeId(), n);
+      } else {
+        failed(rw, canMergeFlag, mergeTip, n, CommitMergeStatus.PATH_CONFLICT);
+      }
+    } catch (NoMergeBaseException e) {
+      try {
+        failed(rw, canMergeFlag, mergeTip, n,
+            getCommitMergeStatus(e.getReason()));
+      } catch (IOException e2) {
+        throw new MergeException("Cannot merge " + n.name(), e);
+      }
+    } catch (IOException e) {
+      throw new MergeException("Cannot merge " + n.name(), e);
+    }
+    return mergeTip;
+  }
+
+  private static CommitMergeStatus getCommitMergeStatus(
+      MergeBaseFailureReason reason) {
+    switch (reason) {
+      case MULTIPLE_MERGE_BASES_NOT_SUPPORTED:
+      case TOO_MANY_MERGE_BASES:
+      default:
+        return CommitMergeStatus.MANUAL_RECURSIVE_MERGE;
+      case CONFLICTS_DURING_MERGE_BASE_CALCULATION:
+        return CommitMergeStatus.PATH_CONFLICT;
+    }
+  }
+
+  private static CodeReviewCommit failed(final RevWalk rw,
+      final RevFlag canMergeFlag, final CodeReviewCommit mergeTip,
+      final CodeReviewCommit n, final CommitMergeStatus failure)
+      throws MissingObjectException, IncorrectObjectTypeException, IOException {
+    rw.resetRetain(canMergeFlag);
+    rw.markStart(n);
+    rw.markUninteresting(mergeTip);
+    CodeReviewCommit failed;
+    while ((failed = (CodeReviewCommit) rw.next()) != null) {
+      failed.statusCode = failure;
+    }
+    return failed;
+  }
+
+  public CodeReviewCommit writeMergeCommit(final PersonIdent myIdent,
+      final RevWalk rw, final ObjectInserter inserter,
+      final RevFlag canMergeFlag, final Branch.NameKey destBranch,
+      final CodeReviewCommit mergeTip, final ObjectId treeId,
+      final CodeReviewCommit n) throws IOException,
+      MissingObjectException, IncorrectObjectTypeException {
+    final List<CodeReviewCommit> merged = new ArrayList<CodeReviewCommit>();
+    rw.resetRetain(canMergeFlag);
+    rw.markStart(n);
+    rw.markUninteresting(mergeTip);
+    for (final RevCommit c : rw) {
+      final CodeReviewCommit crc = (CodeReviewCommit) c;
+      if (crc.patchsetId != null) {
+        merged.add(crc);
+      }
+    }
+
+    final StringBuilder msgbuf = new StringBuilder();
+    if (merged.size() == 1) {
+      final CodeReviewCommit c = merged.get(0);
+      rw.parseBody(c);
+      msgbuf.append("Merge \"");
+      msgbuf.append(c.getShortMessage());
+      msgbuf.append("\"");
+
+    } else {
+      msgbuf.append("Merge changes ");
+      for (final Iterator<CodeReviewCommit> i = merged.iterator(); i.hasNext();) {
+        msgbuf.append(i.next().change.getKey().abbreviate());
+        if (i.hasNext()) {
+          msgbuf.append(',');
+        }
+      }
+    }
+
+    if (!R_HEADS_MASTER.equals(destBranch.get())) {
+      msgbuf.append(" into ");
+      msgbuf.append(destBranch.getShortName());
+    }
+
+    if (merged.size() > 1) {
+      msgbuf.append("\n\n* changes:\n");
+      for (final CodeReviewCommit c : merged) {
+        rw.parseBody(c);
+        msgbuf.append("  ");
+        msgbuf.append(c.getShortMessage());
+        msgbuf.append("\n");
+      }
+    }
+
+    PersonIdent authorIdent = computeMergeCommitAuthor(myIdent, rw, merged);
+
+    final CommitBuilder mergeCommit = new CommitBuilder();
+    mergeCommit.setTreeId(treeId);
+    mergeCommit.setParentIds(mergeTip, n);
+    mergeCommit.setAuthor(authorIdent);
+    mergeCommit.setCommitter(myIdent);
+    mergeCommit.setMessage(msgbuf.toString());
+
+    return (CodeReviewCommit) rw.parseCommit(commit(inserter, mergeCommit));
+  }
+
+  public ThreeWayMerger newThreeWayMerger(final Repository repo,
+      final ObjectInserter inserter) {
+    ThreeWayMerger m;
+    if (useContentMerge) {
+      // Settings for this project allow us to try and automatically resolve
+      // conflicts within files if needed. Use either the old resolve merger or
+      // new recursive merger, and instruct to operate in core.
+      if (useRecursiveMerge) {
+        m = MergeStrategy.RECURSIVE.newMerger(repo, true);
+      } else {
+        m = MergeStrategy.RESOLVE.newMerger(repo, true);
+      }
+    } else {
+      // No auto conflict resolving allowed. If any of the
+      // affected files was modified, merge will fail.
+      m = MergeStrategy.SIMPLE_TWO_WAY_IN_CORE.newMerger(repo);
+    }
+    m.setObjectInserter(new ObjectInserter.Filter() {
+      @Override
+      protected ObjectInserter delegate() {
+        return inserter;
+      }
+
+      @Override
+      public void flush() {
+      }
+
+      @Override
+      public void release() {
+      }
+    });
+    return m;
+  }
+
+  public ObjectId commit(final ObjectInserter inserter,
+      final CommitBuilder mergeCommit) throws IOException,
+      UnsupportedEncodingException {
+    ObjectId id = inserter.insert(mergeCommit);
+    inserter.flush();
+    return id;
+  }
+
+  public PatchSetApproval markCleanMerges(final RevWalk rw,
+      final RevFlag canMergeFlag, final CodeReviewCommit mergeTip,
+      final Set<RevCommit> alreadyAccepted) throws MergeException {
+    if (mergeTip == null) {
+      // If mergeTip is null here, branchTip was null, indicating a new branch
+      // at the start of the merge process. We also elected to merge nothing,
+      // probably due to missing dependencies. Nothing was cleanly merged.
+      //
+      return null;
+    }
+
+    try {
+      PatchSetApproval submitApproval = null;
+
+      rw.resetRetain(canMergeFlag);
+      rw.sort(RevSort.TOPO);
+      rw.sort(RevSort.REVERSE, true);
+      rw.markStart(mergeTip);
+      for (RevCommit c : alreadyAccepted) {
+        rw.markUninteresting(c);
+      }
+
+      CodeReviewCommit c;
+      while ((c = (CodeReviewCommit) rw.next()) != null) {
+        if (c.patchsetId != null) {
+          c.statusCode = CommitMergeStatus.CLEAN_MERGE;
+          if (submitApproval == null) {
+            submitApproval = getSubmitter(c.patchsetId);
+          }
+        }
+      }
+
+      return submitApproval;
+    } catch (IOException e) {
+      throw new MergeException("Cannot mark clean merges", e);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MetaDataUpdate.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MetaDataUpdate.java
index 86dc19c..dc08a6c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MetaDataUpdate.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MetaDataUpdate.java
@@ -24,6 +24,7 @@
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.eclipse.jgit.lib.CommitBuilder;
 import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.RefUpdate;
 import org.eclipse.jgit.lib.Repository;
 
 import java.io.IOException;
@@ -87,15 +88,15 @@
         @Assisted Repository db);
   }
 
-  private final GitReferenceUpdated replication;
+  private final GitReferenceUpdated gitRefUpdated;
   private final Project.NameKey projectName;
   private final Repository db;
   private final CommitBuilder commit;
 
   @Inject
-  public MetaDataUpdate(GitReferenceUpdated replication,
+  public MetaDataUpdate(GitReferenceUpdated gitRefUpdated,
       @Assisted Project.NameKey projectName, @Assisted Repository db) {
-    this.replication = replication;
+    this.gitRefUpdated = gitRefUpdated;
     this.projectName = projectName;
     this.db = db;
     this.commit = new CommitBuilder();
@@ -106,6 +107,12 @@
     getCommitBuilder().setMessage(message);
   }
 
+  public void setAuthor(IdentifiedUser user) {
+    getCommitBuilder().setAuthor(user.newCommitterIdent(
+        getCommitBuilder().getCommitter().getWhen(),
+        getCommitBuilder().getCommitter().getTimeZone()));
+  }
+
   /** Close the cached Repository handle. */
   public void close() {
     getRepository().close();
@@ -123,7 +130,7 @@
     return commit;
   }
 
-  void replicate(String ref) {
-    replication.fire(projectName, ref);
+  void fireGitRefUpdatedEvent(RefUpdate ru) {
+    gitRefUpdated.fire(projectName, ru);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/MultiProgressMonitor.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/MultiProgressMonitor.java
index 23d8dad..9c5633b 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/MultiProgressMonitor.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/MultiProgressMonitor.java
@@ -183,6 +183,7 @@
       final TimeUnit timeoutUnit) throws ExecutionException {
     long overallStart = System.nanoTime();
     long deadline;
+    String detailMessage = "";
     if (timeoutTime > 0) {
       deadline = overallStart + NANOSECONDS.convert(timeoutTime, timeoutUnit);
     } else {
@@ -204,10 +205,15 @@
         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);
+          if (workerFuture.isCancelled()) {
+            detailMessage = String.format(
+                    "(timeout %sms, cancelled)",
+                    TimeUnit.MILLISECONDS.convert(now - deadline, NANOSECONDS));
+            log.warn(String.format(
+                    "MultiProgressMonitor worker killed after %sms" + detailMessage, //
+                    TimeUnit.MILLISECONDS.convert(now - overallStart, NANOSECONDS)));
+          }
           break;
         }
 
@@ -235,7 +241,7 @@
     } catch (InterruptedException e) {
       throw new ExecutionException(e);
     } catch (CancellationException e) {
-      throw new ExecutionException(e);
+      throw new ExecutionException(detailMessage, e);
     } catch (TimeoutException e) {
       workerFuture.cancel(true);
       throw new ExecutionException(e);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/NotesBranchUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/NotesBranchUtil.java
index 17cfea8..2c6c7b8 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/NotesBranchUtil.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/NotesBranchUtil.java
@@ -14,7 +14,9 @@
 
 package com.google.gerrit.server.git;
 
+import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -47,14 +49,18 @@
  */
 public class NotesBranchUtil {
   public interface Factory {
-    NotesBranchUtil create(Repository db);
+    NotesBranchUtil create(Project.NameKey project, Repository db,
+        ObjectInserter inserter);
   }
 
   private static final int MAX_LOCK_FAILURE_CALLS = 10;
   private static final int SLEEP_ON_LOCK_FAILURE_MS = 25;
 
-  private PersonIdent gerritIdent;
+  private final PersonIdent gerritIdent;
+  private final GitReferenceUpdated gitRefUpdated;
+  private final Project.NameKey project;
   private final Repository db;
+  private final ObjectInserter inserter;
 
   private RevCommit baseCommit;
   private NoteMap base;
@@ -63,7 +69,6 @@
   private NoteMap ours;
 
   private RevWalk revWalk;
-  private ObjectInserter inserter;
   private ObjectReader reader;
   private boolean overwrite;
 
@@ -71,9 +76,15 @@
 
   @Inject
   public NotesBranchUtil(@GerritPersonIdent final PersonIdent gerritIdent,
-      @Assisted Repository db) {
+      final GitReferenceUpdated gitRefUpdated,
+      @Assisted Project.NameKey project,
+      @Assisted Repository db,
+      @Assisted ObjectInserter inserter) {
     this.gerritIdent = gerritIdent;
+    this.gitRefUpdated = gitRefUpdated;
+    this.project = project;
     this.db = db;
+    this.inserter = inserter;
   }
 
   /**
@@ -128,7 +139,6 @@
       ConcurrentRefUpdateException {
     try {
       revWalk = new RevWalk(db);
-      inserter = db.newObjectInserter();
       reader = db.newObjectReader();
       loadBase(notesBranch);
       if (overwrite) {
@@ -144,7 +154,6 @@
       updateRef(notesBranch);
     } finally {
       revWalk.release();
-      inserter.release();
       reader.release();
     }
   }
@@ -252,6 +261,7 @@
         throw new IOException("Couldn't update " + notesBranch + ". "
             + result.name());
       } else {
+        gitRefUpdated.fire(project, refUpdate);
         break;
       }
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/NotifyConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/NotifyConfig.java
index ba2833d..3a0c278 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/NotifyConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/NotifyConfig.java
@@ -24,10 +24,15 @@
 import java.util.Set;
 
 public class NotifyConfig implements Comparable<NotifyConfig> {
+  public static enum Header {
+    TO, CC, BCC;
+  }
+
   private String name;
   private EnumSet<NotifyType> types = EnumSet.of(NotifyType.ALL);
   private String filter;
 
+  private Header header;
   private Set<GroupReference> groups = Sets.newHashSet();
   private Set<Address> addresses = Sets.newHashSet();
 
@@ -63,6 +68,14 @@
     }
   }
 
+  public Header getHeader() {
+    return header;
+  }
+
+  public void setHeader(Header hdr) {
+    header = hdr;
+  }
+
   public Set<GroupReference> getGroups() {
     return groups;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/PerThreadRequestScope.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/PerThreadRequestScope.java
index 8aea73a..e7bb0ae 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/PerThreadRequestScope.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/PerThreadRequestScope.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.git;
 
 import com.google.common.collect.Maps;
+import com.google.gerrit.server.config.RequestScopedReviewDbProvider;
 import com.google.gerrit.server.util.RequestContext;
 import com.google.gerrit.server.util.ThreadLocalRequestContext;
 import com.google.gerrit.server.util.ThreadLocalRequestScopePropagator;
@@ -52,8 +53,9 @@
 
   public static class Propagator extends ThreadLocalRequestScopePropagator<Context> {
     @Inject
-    Propagator(ThreadLocalRequestContext local) {
-      super(REQUEST, current, local);
+    Propagator(ThreadLocalRequestContext local,
+        Provider<RequestScopedReviewDbProvider> dbProviderProvider) {
+      super(REQUEST, current, local, dbProviderProvider);
     }
 
     @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
index 13e9967..2a260e4d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ProjectConfig.java
@@ -16,13 +16,23 @@
 
 import static com.google.gerrit.common.data.Permission.isPermission;
 
+import com.google.common.base.CharMatcher;
+import com.google.common.base.Joiner;
+import com.google.common.base.Objects;
+import com.google.common.base.Splitter;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ImmutableSet;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.common.primitives.Shorts;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.ContributorAgreement;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.data.GroupDescription;
 import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.common.data.LabelValue;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.common.data.PermissionRule.Action;
@@ -79,6 +89,7 @@
   private static final String KEY_EMAIL = "email";
   private static final String KEY_FILTER = "filter";
   private static final String KEY_TYPE = "type";
+  private static final String KEY_HEADER = "header";
 
   private static final String CAPABILITY = "capability";
 
@@ -93,6 +104,19 @@
   private static final String KEY_MERGE_CONTENT = "mergeContent";
   private static final String KEY_STATE = "state";
 
+  private static final String DASHBOARD = "dashboard";
+  private static final String KEY_DEFAULT = "default";
+  private static final String KEY_LOCAL_DEFAULT = "local-default";
+
+  private static final String LABEL = "label";
+  private static final String KEY_ABBREVIATION = "abbreviation";
+  private static final String KEY_FUNCTION = "function";
+  private static final String KEY_COPY_MIN_SCORE = "copyMinScore";
+  private static final String KEY_VALUE = "value";
+  private static final String KEY_CAN_OVERRIDE = "canOverride";
+  private static final Set<String> LABEL_FUNCTIONS = ImmutableSet.of(
+      "MaxWithBlock", "MaxNoBlock", "NoBlock", "NoOp");
+
   private static final SubmitType defaultSubmitAction =
       SubmitType.MERGE_IF_NECESSARY;
   private static final State defaultStateValue =
@@ -105,6 +129,7 @@
   private Map<String, AccessSection> accessSections;
   private Map<String, ContributorAgreement> contributorAgreements;
   private Map<String, NotifyConfig> notifySections;
+  private Map<String, LabelType> labelSections;
   private List<ValidationError> validationErrors;
   private ObjectId rulesId;
 
@@ -203,6 +228,10 @@
     return notifySections.values();
   }
 
+  public Map<String, LabelType> getLabelSections() {
+    return labelSections;
+  }
+
   public GroupReference resolve(AccountGroup group) {
     return resolve(GroupReference.forGroup(group));
   }
@@ -223,6 +252,11 @@
     return groupsByUUID.get(uuid);
   }
 
+  /** @return set of all groups used by this configuration. */
+  public Set<AccountGroup.UUID> getAllGroupUUIDs() {
+    return Collections.unmodifiableSet(groupsByUUID.keySet());
+  }
+
   /**
    * @return the project's rules.pl ObjectId, if present in the branch.
    *    Null if it doesn't exist.
@@ -282,18 +316,22 @@
     }
     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.setUseContributorAgreements(getEnum(rc, RECEIVE, null, KEY_REQUIRE_CONTRIBUTOR_AGREEMENT, Project.InheritableBoolean.INHERIT));
+    p.setUseSignedOffBy(getEnum(rc, RECEIVE, null, KEY_REQUIRE_SIGNED_OFF_BY, Project.InheritableBoolean.INHERIT));
+    p.setRequireChangeID(getEnum(rc, RECEIVE, null, KEY_REQUIRE_CHANGE_ID, Project.InheritableBoolean.INHERIT));
 
     p.setSubmitType(getEnum(rc, SUBMIT, null, KEY_ACTION, defaultSubmitAction));
-    p.setUseContentMerge(getBoolean(rc, SUBMIT, KEY_MERGE_CONTENT, false));
+    p.setUseContentMerge(getEnum(rc, SUBMIT, null, KEY_MERGE_CONTENT, Project.InheritableBoolean.INHERIT));
     p.setState(getEnum(rc, PROJECT, null, KEY_STATE, defaultStateValue));
 
+    p.setDefaultDashboard(rc.getString(DASHBOARD, null, KEY_DEFAULT));
+    p.setLocalDefaultDashboard(rc.getString(DASHBOARD, null, KEY_LOCAL_DEFAULT));
+
     loadAccountsSection(rc, groupsByName);
     loadContributorAgreements(rc, groupsByName);
     loadAccessSections(rc, groupsByName);
     loadNotifySections(rc, groupsByName);
+    loadLabelSections(rc);
   }
 
   private void loadAccountsSection(
@@ -368,6 +406,9 @@
           NOTIFY, sectionName, KEY_TYPE,
           NotifyType.ALL));
       n.setTypes(types);
+      n.setHeader(ConfigUtil.getEnum(rc,
+          NOTIFY, sectionName, KEY_HEADER,
+          NotifyConfig.Header.BCC));
 
       for (String dst : rc.getStringList(NOTIFY, sectionName, KEY_EMAIL)) {
         if (dst.startsWith("group ")) {
@@ -480,6 +521,72 @@
     }
   }
 
+  private static LabelValue parseLabelValue(String src) {
+    List<String> parts = ImmutableList.copyOf(
+        Splitter.on(CharMatcher.WHITESPACE).omitEmptyStrings().limit(2)
+        .split(src));
+    if (parts.isEmpty()) {
+      throw new IllegalArgumentException("empty value");
+    }
+    return new LabelValue(
+        Shorts.checkedCast(PermissionRule.parseInt(parts.get(0))),
+        parts.get(1));
+  }
+
+  private void loadLabelSections(Config rc) throws IOException {
+    Map<String, String> lowerNames = Maps.newHashMapWithExpectedSize(2);
+    labelSections = Maps.newLinkedHashMap();
+    for (String name : rc.getSubsections(LABEL)) {
+      String lower = name.toLowerCase();
+      if (lowerNames.containsKey(lower)) {
+        error(new ValidationError(PROJECT_CONFIG, String.format(
+            "Label \"%s\" conflicts with \"%s\"",
+            name, lowerNames.get(lower))));
+      }
+      lowerNames.put(lower, name);
+
+      List<LabelValue> values = Lists.newArrayList();
+      for (String value : rc.getStringList(LABEL, name, KEY_VALUE)) {
+        try {
+          values.add(parseLabelValue(value));
+        } catch (IllegalArgumentException notValue) {
+          error(new ValidationError(PROJECT_CONFIG, String.format(
+              "Invalid %s \"%s\" for label \"%s\": %s",
+              KEY_VALUE, value, name, notValue.getMessage())));
+        }
+      }
+
+      LabelType label;
+      try {
+        label = new LabelType(name, values);
+      } catch (IllegalArgumentException badName) {
+        error(new ValidationError(PROJECT_CONFIG, String.format(
+            "Invalid label \"%s\"", name)));
+        continue;
+      }
+      String abbr = rc.getString(LABEL, name, KEY_ABBREVIATION);
+      if (abbr != null) {
+        label.setAbbreviatedName(abbr);
+      }
+
+      String functionName = Objects.firstNonNull(
+          rc.getString(LABEL, name, KEY_FUNCTION), "MaxWithBlock");
+      if (LABEL_FUNCTIONS.contains(functionName)) {
+        label.setFunctionName(functionName);
+      } else {
+        error(new ValidationError(PROJECT_CONFIG, String.format(
+            "Invalid %s for label \"%s\". Valid names are: %s",
+            KEY_FUNCTION, name, Joiner.on(", ").join(LABEL_FUNCTIONS))));
+        label.setFunctionName(null);
+      }
+      label.setCopyMinScore(
+          rc.getBoolean(LABEL, name, KEY_COPY_MIN_SCORE, false));
+      label.setCanOverride(
+          rc.getBoolean(LABEL, name, KEY_CAN_OVERRIDE, true));
+      labelSections.put(name, label);
+    }
+  }
+
   private Map<String, GroupReference> readGroupList() throws IOException {
     groupsByUUID = new HashMap<AccountGroup.UUID, GroupReference>();
     Map<String, GroupReference> groupsByName =
@@ -525,21 +632,25 @@
     }
     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, RECEIVE, null, KEY_REQUIRE_CONTRIBUTOR_AGREEMENT, p.getUseContributorAgreements(), Project.InheritableBoolean.INHERIT);
+    set(rc, RECEIVE, null, KEY_REQUIRE_SIGNED_OFF_BY, p.getUseSignedOffBy(), Project.InheritableBoolean.INHERIT);
+    set(rc, RECEIVE, null, KEY_REQUIRE_CHANGE_ID, p.getRequireChangeID(), Project.InheritableBoolean.INHERIT);
 
     set(rc, SUBMIT, null, KEY_ACTION, p.getSubmitType(), defaultSubmitAction);
-    set(rc, SUBMIT, null, KEY_MERGE_CONTENT, p.isUseContentMerge());
+    set(rc, SUBMIT, null, KEY_MERGE_CONTENT, p.getUseContentMerge(), Project.InheritableBoolean.INHERIT);
 
     set(rc, PROJECT, null, KEY_STATE, p.getState(), null);
 
+    set(rc, DASHBOARD, null, KEY_DEFAULT, p.getDefaultDashboard());
+    set(rc, DASHBOARD, null, KEY_LOCAL_DEFAULT, p.getLocalDefaultDashboard());
+
     Set<AccountGroup.UUID> keepGroups = new HashSet<AccountGroup.UUID>();
     saveAccountsSection(rc, keepGroups);
     saveContributorAgreements(rc, keepGroups);
     saveAccessSections(rc, keepGroups);
     saveNotifySections(rc, keepGroups);
     groupsByUUID.keySet().retainAll(keepGroups);
+    saveLabelSections(rc);
 
     saveConfig(PROJECT_CONFIG, rc);
     saveGroupList();
@@ -593,6 +704,8 @@
       Collections.sort(addrs);
       email.addAll(addrs);
 
+      set(rc, NOTIFY, nc.getName(), KEY_HEADER,
+          nc.getHeader(), NotifyConfig.Header.BCC);
       if (email.isEmpty()) {
         rc.unset(NOTIFY, nc.getName(), KEY_EMAIL);
       } else {
@@ -707,6 +820,53 @@
     }
   }
 
+  private void saveLabelSections(Config rc) {
+    List<String> existing = Lists.newArrayList(rc.getSubsections(LABEL));
+    if (!Lists.newArrayList(labelSections.keySet()).equals(existing)) {
+      // Order of sections changed, remove and rewrite them all.
+      for (String name : existing) {
+        rc.unsetSection(LABEL, name);
+      }
+    }
+
+    Set<String> toUnset = Sets.newHashSet(existing);
+    for (Map.Entry<String, LabelType> e : labelSections.entrySet()) {
+      String name = e.getKey();
+      LabelType label = e.getValue();
+      toUnset.remove(name);
+      rc.setString(LABEL, name, KEY_FUNCTION, label.getFunctionName());
+
+      if (!LabelType.defaultAbbreviation(name)
+          .equals(label.getAbbreviatedName())) {
+        rc.setString(
+            LABEL, name, KEY_ABBREVIATION, label.getAbbreviatedName());
+      } else {
+        rc.unset(LABEL, name, KEY_ABBREVIATION);
+      }
+      if (label.isCopyMinScore()) {
+        rc.setBoolean(LABEL, name, KEY_COPY_MIN_SCORE, true);
+      } else {
+        rc.unset(LABEL, name, KEY_COPY_MIN_SCORE);
+      }
+      if (!label.canOverride()) {
+        rc.setBoolean(LABEL, name, KEY_CAN_OVERRIDE, false);
+      } else {
+        rc.unset(LABEL, name, KEY_CAN_OVERRIDE);
+      }
+
+      List<String> values =
+          Lists.newArrayListWithCapacity(label.getValues().size());
+      for (LabelValue value : label.getValues()) {
+        values.add(value.format());
+      }
+      rc.setStringList(LABEL, name, KEY_VALUE, values);
+    }
+
+    for (String name : toUnset) {
+      rc.unsetSection(LABEL, name);
+    }
+  }
+
   private void saveGroupList() throws IOException {
     if (groupsByUUID.isEmpty()) {
       saveFile(GROUP_LIST, null);
@@ -734,16 +894,6 @@
     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 {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/RebaseIfNecessary.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/RebaseIfNecessary.java
new file mode 100644
index 0000000..8490ea1
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/RebaseIfNecessary.java
@@ -0,0 +1,162 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git;
+
+import com.google.common.collect.Lists;
+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.server.changedetail.PathConflictException;
+import com.google.gerrit.server.changedetail.RebaseChange;
+import com.google.gerrit.server.project.InvalidChangeOperationException;
+import com.google.gerrit.server.project.NoSuchChangeException;
+import com.google.gwtorm.server.OrmException;
+
+import org.eclipse.jgit.lib.ObjectId;
+
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+public class RebaseIfNecessary extends SubmitStrategy {
+
+  private final RebaseChange rebaseChange;
+  private final Map<Change.Id, CodeReviewCommit> newCommits;
+
+  RebaseIfNecessary(final SubmitStrategy.Arguments args,
+      final RebaseChange rebaseChange) {
+    super(args);
+    this.rebaseChange = rebaseChange;
+    this.newCommits = new HashMap<Change.Id, CodeReviewCommit>();
+  }
+
+  @Override
+  protected CodeReviewCommit _run(final CodeReviewCommit mergeTip,
+      final List<CodeReviewCommit> toMerge) throws MergeException {
+    CodeReviewCommit newMergeTip = mergeTip;
+    sort(toMerge);
+
+    while (!toMerge.isEmpty()) {
+      final CodeReviewCommit n = toMerge.remove(0);
+
+      if (newMergeTip == null) {
+        // The branch is unborn. Take a fast-forward resolution to
+        // create the branch.
+        //
+        newMergeTip = n;
+        n.statusCode = CommitMergeStatus.CLEAN_MERGE;
+
+      } else if (n.getParentCount() == 0) {
+        // Refuse to merge a root commit into an existing branch,
+        // we cannot obtain a delta for the rebase to apply.
+        //
+        n.statusCode = CommitMergeStatus.CANNOT_REBASE_ROOT;
+
+      } else if (n.getParentCount() == 1) {
+        if (args.mergeUtil.canFastForward(
+            args.mergeSorter, newMergeTip, args.rw, n)) {
+          newMergeTip = n;
+          n.statusCode = CommitMergeStatus.CLEAN_MERGE;
+
+        } else {
+          try {
+            final PatchSet newPatchSet =
+                rebaseChange.rebase(args.repo, args.rw, args.inserter,
+                    n.patchsetId, n.change,
+                    args.mergeUtil.getSubmitter(n.patchsetId).getAccountId(),
+                    newMergeTip, args.mergeUtil);
+            List<PatchSetApproval> approvals = Lists.newArrayList();
+            for (PatchSetApproval a : args.mergeUtil.getApprovalsForCommit(n)) {
+              approvals.add(new PatchSetApproval(newPatchSet.getId(), a));
+            }
+            args.db.patchSetApprovals().insert(approvals);
+            newMergeTip =
+                (CodeReviewCommit) args.rw.parseCommit(ObjectId
+                    .fromString(newPatchSet.getRevision().get()));
+            newMergeTip.copyFrom(n);
+            newMergeTip.patchsetId = newPatchSet.getId();
+            newMergeTip.change =
+                args.db.changes().get(newPatchSet.getId().getParentKey());
+            newMergeTip.statusCode = CommitMergeStatus.CLEAN_REBASE;
+            newCommits.put(newPatchSet.getId().getParentKey(), newMergeTip);
+            setRefLogIdent(args.mergeUtil.getSubmitter(n.patchsetId));
+          } catch (PathConflictException e) {
+            n.statusCode = CommitMergeStatus.PATH_CONFLICT;
+          } catch (NoSuchChangeException e) {
+            throw new MergeException("Cannot rebase " + n.name(), e);
+          } catch (OrmException e) {
+            throw new MergeException("Cannot rebase " + n.name(), e);
+          } catch (IOException e) {
+            throw new MergeException("Cannot rebase " + n.name(), e);
+          } catch (InvalidChangeOperationException e) {
+            throw new MergeException("Cannot rebase " + n.name(), e);
+          }
+        }
+
+      } else if (n.getParentCount() > 1) {
+        // There are multiple parents, so this is a merge commit. We
+        // don't want to rebase the merge as clients can't easily
+        // rebase their history with that merge present and replaced
+        // by an equivalent merge with a different first parent. So
+        // instead behave as though MERGE_IF_NECESSARY was configured.
+        //
+        try {
+          if (args.rw.isMergedInto(newMergeTip, n)) {
+            newMergeTip = n;
+          } else {
+            newMergeTip = args.mergeUtil.mergeOneCommit(
+                args.myIdent, args.repo, args.rw, args.inserter,
+                args.canMergeFlag, args.destBranch, newMergeTip, n);
+          }
+          final PatchSetApproval submitApproval = args.mergeUtil.markCleanMerges(
+              args.rw, args.canMergeFlag, newMergeTip, args.alreadyAccepted);
+          setRefLogIdent(submitApproval);
+        } catch (IOException e) {
+          throw new MergeException("Cannot merge " + n.name(), e);
+        }
+      }
+
+      args.alreadyAccepted.add(newMergeTip);
+    }
+
+    return newMergeTip;
+  }
+
+  private void sort(final List<CodeReviewCommit> toSort) throws MergeException {
+    try {
+      final List<CodeReviewCommit> sorted =
+          new RebaseSorter(args.rw, args.alreadyAccepted, args.canMergeFlag)
+              .sort(toSort);
+      toSort.clear();
+      toSort.addAll(sorted);
+    } catch (IOException e) {
+      throw new MergeException("Commit sorting failed", e);
+    }
+  }
+
+  @Override
+  public Map<Change.Id, CodeReviewCommit> getNewCommits() {
+    return newCommits;
+  }
+
+  @Override
+  public boolean dryRun(final CodeReviewCommit mergeTip,
+      final CodeReviewCommit toMerge) throws MergeException {
+    return !args.mergeUtil.hasMissingDependencies(args.mergeSorter, toMerge)
+        && args.mergeUtil.canCherryPick(args.mergeSorter, args.repo, mergeTip,
+            args.rw, toMerge);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/RebaseSorter.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/RebaseSorter.java
new file mode 100644
index 0000000..ac1929b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/RebaseSorter.java
@@ -0,0 +1,91 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevFlag;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+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.List;
+import java.util.Set;
+
+class RebaseSorter {
+
+  private final RevWalk rw;
+  private final RevFlag canMergeFlag;
+  private final Set<RevCommit> accepted;
+
+  RebaseSorter(final RevWalk rw, final Set<RevCommit> alreadyAccepted,
+      final RevFlag canMergeFlag) {
+    this.rw = rw;
+    this.canMergeFlag = canMergeFlag;
+    this.accepted = alreadyAccepted;
+  }
+
+  List<CodeReviewCommit> sort(Collection<CodeReviewCommit> incoming)
+      throws IOException {
+    final List<CodeReviewCommit> sorted = new ArrayList<CodeReviewCommit>();
+    final Set<CodeReviewCommit> sort = new HashSet<CodeReviewCommit>(incoming);
+    while (!sort.isEmpty()) {
+      final CodeReviewCommit n = removeOne(sort);
+
+      rw.resetRetain(canMergeFlag);
+      rw.markStart(n);
+      for (RevCommit c : accepted) {
+        rw.markUninteresting(c);
+      }
+
+      CodeReviewCommit c;
+      final List<CodeReviewCommit> contents = new ArrayList<CodeReviewCommit>();
+      while ((c = (CodeReviewCommit) rw.next()) != null) {
+        if (!c.has(canMergeFlag) || !incoming.contains(c)) {
+          // We cannot merge n as it would bring something we
+          // aren't permitted to merge at this time. Drop n.
+          //
+          if (n.missing == null) {
+            n.statusCode = CommitMergeStatus.MISSING_DEPENDENCY;
+            n.missing = new ArrayList<CodeReviewCommit>();
+          }
+          n.missing.add((CodeReviewCommit) c);
+        } else {
+          contents.add(c);
+        }
+      }
+
+      if (n.statusCode == CommitMergeStatus.MISSING_DEPENDENCY) {
+        continue;
+      }
+
+      sort.removeAll(contents);
+      Collections.reverse(contents);
+      sorted.removeAll(contents);
+      sorted.addAll(contents);
+    }
+    return sorted;
+  }
+
+  private static <T> T removeOne(final Collection<T> c) {
+    final Iterator<T> i = c.iterator();
+    final T r = i.next();
+    i.remove();
+    return r;
+  }
+}
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 a2a809b..a5478ab 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,15 +14,22 @@
 
 package com.google.gerrit.server.git;
 
-import static org.eclipse.jgit.lib.Constants.R_HEADS;
+import static com.google.gerrit.reviewdb.client.Change.INITIAL_PATCH_SET_ID;
 import static com.google.gerrit.server.git.MultiProgressMonitor.UNKNOWN;
+import static com.google.gerrit.server.mail.MailUtil.getRecipientsFromApprovals;
+import static com.google.gerrit.server.mail.MailUtil.getRecipientsFromFooters;
+
+import static org.eclipse.jgit.lib.Constants.R_HEADS;
 import static org.eclipse.jgit.transport.ReceiveCommand.Result.NOT_ATTEMPTED;
 import static org.eclipse.jgit.transport.ReceiveCommand.Result.OK;
 import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_MISSING_OBJECT;
 import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_NONFASTFORWARD;
 import static org.eclipse.jgit.transport.ReceiveCommand.Result.REJECTED_OTHER_REASON;
 
+import com.google.common.base.Function;
 import com.google.common.base.Predicate;
+import com.google.common.base.Splitter;
+import com.google.common.base.Strings;
 import com.google.common.collect.Iterables;
 import com.google.common.collect.LinkedListMultimap;
 import com.google.common.collect.ListMultimap;
@@ -30,17 +37,19 @@
 import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
 import com.google.common.util.concurrent.CheckedFuture;
+import com.google.common.util.concurrent.Futures;
+import com.google.common.util.concurrent.ListenableFuture;
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.gerrit.common.ChangeHookRunner.HookResult;
 import com.google.gerrit.common.ChangeHooks;
-import com.google.gerrit.common.PageLinks;
 import com.google.gerrit.common.data.Capable;
+import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.common.data.PermissionRule;
-import com.google.gerrit.common.errors.NoSuchAccountException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.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;
@@ -51,12 +60,18 @@
 import com.google.gerrit.server.GerritPersonIdent;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.account.AccountResolver;
+import com.google.gerrit.server.change.ChangeInserter;
 import com.google.gerrit.server.config.AllProjectsName;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.config.TrackingFooters;
+import com.google.gerrit.server.events.CommitReceivedEvent;
 import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
 import com.google.gerrit.server.git.MultiProgressMonitor.Task;
+import com.google.gerrit.server.git.validators.CommitValidationException;
+import com.google.gerrit.server.git.validators.CommitValidationMessage;
+import com.google.gerrit.server.git.validators.CommitValidators;
 import com.google.gerrit.server.mail.CreateChangeSender;
+import com.google.gerrit.server.mail.MailUtil.MailRecipients;
 import com.google.gerrit.server.mail.MergedSender;
 import com.google.gerrit.server.mail.ReplacePatchSetSender;
 import com.google.gerrit.server.patch.PatchSetInfoFactory;
@@ -65,17 +80,19 @@
 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.ssh.SshInfo;
 import com.google.gerrit.server.util.MagicBranch;
 import com.google.gerrit.server.util.RequestScopePropagator;
+import com.google.gerrit.util.cli.CmdLineParser;
 import com.google.gwtorm.server.AtomicUpdate;
 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.assistedinject.Assisted;
 
 import org.eclipse.jgit.errors.IncorrectObjectTypeException;
 import org.eclipse.jgit.errors.MissingObjectException;
-import org.eclipse.jgit.lib.AbbreviatedObjectId;
 import org.eclipse.jgit.lib.BatchRefUpdate;
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.ObjectId;
@@ -93,13 +110,18 @@
 import org.eclipse.jgit.revwalk.filter.RevFilter;
 import org.eclipse.jgit.transport.AdvertiseRefsHook;
 import org.eclipse.jgit.transport.AdvertiseRefsHookChain;
+import org.eclipse.jgit.transport.BaseReceivePack;
 import org.eclipse.jgit.transport.ReceiveCommand;
 import org.eclipse.jgit.transport.ReceiveCommand.Result;
 import org.eclipse.jgit.transport.ReceivePack;
+import org.eclipse.jgit.transport.UploadPack;
+import org.kohsuke.args4j.CmdLineException;
+import org.kohsuke.args4j.Option;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.io.IOException;
+import java.io.StringWriter;
 import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.Collection;
@@ -109,6 +131,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Set;
+import java.util.concurrent.Callable;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -122,8 +145,6 @@
   private static final Pattern NEW_PATCHSET =
       Pattern.compile("^refs/changes/(?:[0-9][0-9]/)?([1-9][0-9]*)(?:/new)?$");
 
-  private static final FooterKey REVIEWED_BY = new FooterKey("Reviewed-by");
-  private static final FooterKey TESTED_BY = new FooterKey("Tested-by");
   private static final FooterKey CHANGE_ID = new FooterKey("Change-Id");
 
   private static final String COMMAND_REJECTION_MESSAGE_FOOTER =
@@ -138,6 +159,7 @@
         + "To push into this reference you need 'Push' rights."),
         DELETE("You need 'Push' rights with the 'Force Push'\n"
             + "flag set to delete references."),
+        DELETE_CHANGES("Cannot delete from 'refs/changes'"),
         CODE_REVIEW("You need 'Push' rights to upload code review requests.\n"
             + "Verify that you are pushing to the right branch.");
 
@@ -199,61 +221,67 @@
     }
   }
 
-  private static class Message {
-    private final String message;
-    private final boolean isError;
+  private static final Function<Exception, OrmException> ORM_EXCEPTION =
+      new Function<Exception, OrmException>() {
+        @Override
+        public OrmException apply(Exception input) {
+          if (input instanceof OrmException) {
+            return (OrmException) input;
+          }
+          return new OrmException("Error updating database", input);
+        }
+      };
 
-    private Message(final String message, final boolean isError) {
-      this.message = message;
-      this.isError = isError;
-    }
-  }
-
-  private final Set<Account.Id> reviewerId = new HashSet<Account.Id>();
-  private final Set<Account.Id> ccId = new HashSet<Account.Id>();
+  private Set<Account.Id> reviewersFromCommandLine = Sets.newLinkedHashSet();
+  private Set<Account.Id> ccFromCommandLine = Sets.newLinkedHashSet();
 
   private final IdentifiedUser currentUser;
   private final ReviewDb db;
+  private final SchemaFactory<ReviewDb> schemaFactory;
   private final AccountResolver accountResolver;
+  private final CmdLineParser.Factory optionParserFactory;
   private final CreateChangeSender.Factory createChangeSenderFactory;
   private final MergedSender.Factory mergedSenderFactory;
   private final ReplacePatchSetSender.Factory replacePatchSetFactory;
-  private final GitReferenceUpdated replication;
+  private final GitReferenceUpdated gitRefUpdated;
   private final PatchSetInfoFactory patchSetInfoFactory;
   private final ChangeHooks hooks;
   private final ApprovalsUtil approvalsUtil;
   private final GitRepositoryManager repoManager;
   private final ProjectCache projectCache;
   private final String canonicalWebUrl;
-  private final PersonIdent gerritIdent;
+  private final CommitValidators.Factory commitValidatorsFactory;
   private final TrackingFooters trackingFooters;
   private final TagCache tagCache;
+  private final ChangeInserter changeInserter;
   private final WorkQueue workQueue;
+  private final ListeningExecutorService changeUpdateExector;
   private final RequestScopePropagator requestScopePropagator;
+  private final SshInfo sshInfo;
   private final AllProjectsName allProjectsName;
+  private final ReceiveConfig receiveConfig;
 
   private final ProjectControl projectControl;
   private final Project project;
+  private final LabelTypes labelTypes;
   private final Repository repo;
   private final ReceivePack rp;
   private final NoteMap rejectCommits;
-  private ReceiveCommand newChange;
-  private Branch.NameKey destBranch;
-  private RefControl destBranchCtl;
+  private MagicBranchInput magicBranch;
 
   private List<CreateRequest> newChanges = Collections.emptyList();
   private final Map<Change.Id, ReplaceRequest> replaceByChange =
       new HashMap<Change.Id, ReplaceRequest>();
   private final Map<RevCommit, ReplaceRequest> replaceByCommit =
       new HashMap<RevCommit, ReplaceRequest>();
+  private final Set<RevCommit> validCommits = new HashSet<RevCommit>();
 
   private Map<ObjectId, Ref> refsById;
-
-  private String destTopicName;
+  private Map<String, Ref> allRefs;
 
   private final SubmoduleOp.Factory subOpFactory;
 
-  private final List<Message> messages = new ArrayList<Message>();
+  private final List<CommitValidationMessage> messages = new ArrayList<CommitValidationMessage>();
   private ListMultimap<Error, String> errors = LinkedListMultimap.create();
   private Task newProgress;
   private Task replaceProgress;
@@ -264,48 +292,62 @@
 
   @Inject
   ReceiveCommits(final ReviewDb db,
+      final SchemaFactory<ReviewDb> schemaFactory,
       final AccountResolver accountResolver,
+      final CmdLineParser.Factory optionParserFactory,
       final CreateChangeSender.Factory createChangeSenderFactory,
       final MergedSender.Factory mergedSenderFactory,
       final ReplacePatchSetSender.Factory replacePatchSetFactory,
-      final GitReferenceUpdated replication,
+      final GitReferenceUpdated gitRefUpdated,
       final PatchSetInfoFactory patchSetInfoFactory,
       final ChangeHooks hooks,
       final ApprovalsUtil approvalsUtil,
       final ProjectCache projectCache,
       final GitRepositoryManager repoManager,
       final TagCache tagCache,
+      final ChangeCache changeCache,
+      final ChangeInserter changeInserter,
+      final CommitValidators.Factory commitValidatorsFactory,
       @CanonicalWebUrl @Nullable final String canonicalWebUrl,
       @GerritPersonIdent final PersonIdent gerritIdent,
       final TrackingFooters trackingFooters,
       final WorkQueue workQueue,
+      @ChangeUpdateExecutor ListeningExecutorService changeUpdateExector,
       final RequestScopePropagator requestScopePropagator,
+      final SshInfo sshInfo,
       final AllProjectsName allProjectsName,
-
+      ReceiveConfig config,
       @Assisted final ProjectControl projectControl,
       @Assisted final Repository repo,
       final SubmoduleOp.Factory subOpFactory) throws IOException {
     this.currentUser = (IdentifiedUser) projectControl.getCurrentUser();
     this.db = db;
+    this.schemaFactory = schemaFactory;
     this.accountResolver = accountResolver;
+    this.optionParserFactory = optionParserFactory;
     this.createChangeSenderFactory = createChangeSenderFactory;
     this.mergedSenderFactory = mergedSenderFactory;
     this.replacePatchSetFactory = replacePatchSetFactory;
-    this.replication = replication;
+    this.gitRefUpdated = gitRefUpdated;
     this.patchSetInfoFactory = patchSetInfoFactory;
     this.hooks = hooks;
     this.approvalsUtil = approvalsUtil;
     this.projectCache = projectCache;
     this.repoManager = repoManager;
     this.canonicalWebUrl = canonicalWebUrl;
-    this.gerritIdent = gerritIdent;
     this.trackingFooters = trackingFooters;
     this.tagCache = tagCache;
+    this.changeInserter = changeInserter;
+    this.commitValidatorsFactory = commitValidatorsFactory;
     this.workQueue = workQueue;
+    this.changeUpdateExector = changeUpdateExector;
     this.requestScopePropagator = requestScopePropagator;
+    this.sshInfo = sshInfo;
     this.allProjectsName = allProjectsName;
+    this.receiveConfig = config;
 
     this.projectControl = projectControl;
+    this.labelTypes = projectControl.getLabelTypes();
     this.project = projectControl.getProject();
     this.repo = repo;
     this.rp = new ReceivePack(repo);
@@ -321,10 +363,24 @@
     rp.setCheckReceivedObjects(true);
 
     if (!projectControl.allRefsAreVisible()) {
-      rp.setCheckReferencedObjectsAreReachable(true);
-      rp.setAdvertiseRefsHook(new VisibleRefFilter(tagCache, repo, projectControl, db, false));
+      rp.setCheckReferencedObjectsAreReachable(config.checkReferencedObjectsAreReachable);
+      rp.setAdvertiseRefsHook(new VisibleRefFilter(tagCache, changeCache, repo, projectControl, db, false));
     }
-    List<AdvertiseRefsHook> advHooks = new ArrayList<AdvertiseRefsHook>(2);
+    List<AdvertiseRefsHook> advHooks = new ArrayList<AdvertiseRefsHook>(3);
+    advHooks.add(new AdvertiseRefsHook() {
+      @Override
+      public void advertiseRefs(BaseReceivePack rp) {
+        allRefs = rp.getAdvertisedRefs();
+        if (allRefs == null) {
+          allRefs = rp.getRepository().getAllRefs();
+        }
+        rp.setAdvertisedRefs(allRefs, rp.getAdvertisedObjects());
+      }
+
+      @Override
+      public void advertiseRefs(UploadPack uploadPack) {
+      }
+    });
     advHooks.add(rp.getAdvertiseRefsHook());
     advHooks.add(new ReceiveCommitsAdvertiseRefsHook());
     rp.setAdvertiseRefsHook(AdvertiseRefsHookChain.newChain(advHooks));
@@ -332,12 +388,12 @@
 
   /** Add reviewers for new (or updated) changes. */
   public void addReviewers(Collection<Account.Id> who) {
-    reviewerId.addAll(who);
+    reviewersFromCommandLine.addAll(who);
   }
 
   /** Add reviewers for new (or updated) changes. */
   public void addExtraCC(Collection<Account.Id> who) {
-    ccId.addAll(who);
+    ccFromCommandLine.addAll(who);
   }
 
   /** Set a message sender for this operation. */
@@ -441,24 +497,26 @@
     if (result != Capable.OK) {
       return result;
     }
-
-    return MagicBranch.checkMagicBranchRefs(repo, project);
+    if (receiveConfig.checkMagicRefs) {
+      result = MagicBranch.checkMagicBranchRefs(repo, project);
+    }
+    return result;
   }
 
   private void addMessage(String message) {
-    messages.add(new Message(message, false));
+    messages.add(new CommitValidationMessage(message, false));
   }
 
   void addError(String error) {
-    messages.add(new Message(error, true));
+    messages.add(new CommitValidationMessage(error, true));
   }
 
   void sendMessages() {
-    for (Message m : messages) {
-      if (m.isError) {
-        messageSender.sendError(m.message);
+    for (CommitValidationMessage m : messages) {
+      if (m.isError()) {
+        messageSender.sendError(m.getMessage());
       } else {
-        messageSender.sendMessage(m.message);
+        messageSender.sendMessage(m.getMessage());
       }
     }
   }
@@ -475,7 +533,7 @@
     batch.setRefLogMessage("push", true);
 
     parseCommands(commands);
-    if (newChange != null && newChange.getResult() == NOT_ATTEMPTED) {
+    if (magicBranch != null && magicBranch.cmd.getResult() == NOT_ATTEMPTED) {
       newChanges = selectNewChanges();
     }
     preparePatchSetsForReplace();
@@ -532,6 +590,9 @@
               autoCloseChanges(c);
             }
             break;
+
+          case DELETE:
+            break;
         }
 
         if (isConfig(c)) {
@@ -542,10 +603,11 @@
         }
 
         if (!MagicBranch.isMagicBranch(c.getRefName())) {
-          // We only schedule direct refs updates for replication.
-          // Change refs are scheduled when they are created.
+          // We only fire gitRefUpdated for direct refs updates.
+          // Events for change refs are fired when they are created.
           //
-          replication.fire(project.getNameKey(), c.getRefName());
+          gitRefUpdated.fire(project.getNameKey(), c.getRefName(),
+              c.getOldId(), c.getNewId());
           hooks.doRefUpdatedHook(
               new Branch.NameKey(project.getNameKey(), c.getRefName()),
               c.getOldId(),
@@ -587,8 +649,9 @@
     int replaceCount = 0;
     int okToInsert = 0;
 
-    for (ReplaceRequest replace : replaceByChange.values()) {
-      if (replace.inputCommand == newChange) {
+    for (Map.Entry<Change.Id, ReplaceRequest> e : replaceByChange.entrySet()) {
+      ReplaceRequest replace = e.getValue();
+      if (magicBranch != null && replace.inputCommand == magicBranch.cmd) {
         replaceCount++;
 
         if (replace.cmd != null && replace.cmd.getResult() == OK) {
@@ -596,26 +659,26 @@
         }
       } else if (replace.cmd != null && replace.cmd.getResult() == OK) {
         try {
-          if (replace.insertPatchSet() != null) {
+          if (replace.insertPatchSet().checkedGet() != null) {
             replace.inputCommand.setResult(OK);
           }
         } catch (IOException err) {
           reject(replace.inputCommand, "internal server error");
           log.error(String.format(
               "Cannot add patch set to %d of %s",
-              replace.newPatchSet.getId(), project.getName()), err);
+              e.getKey().get(), project.getName()), err);
         } catch (OrmException err) {
           reject(replace.inputCommand, "internal server error");
           log.error(String.format(
               "Cannot add patch set to %d of %s",
-              replace.newPatchSet.getId(), project.getName()), err);
+              e.getKey().get(), project.getName()), err);
         }
-      } else {
+      } else if (replace.inputCommand.getResult() == NOT_ATTEMPTED) {
         reject(replace.inputCommand, "internal server error");
       }
     }
 
-    if (newChange == null || newChange.getResult() != NOT_ATTEMPTED) {
+    if (magicBranch == null || magicBranch.cmd.getResult() != NOT_ATTEMPTED) {
       // refs/for/ or refs/drafts/ not used, or it already failed earlier.
       // No need to continue.
       return;
@@ -630,30 +693,35 @@
     if (okToInsert != replaceCount + newChanges.size()) {
       // One or more new references failed to create. Assume the
       // system isn't working correctly anymore and abort.
-      reject(newChange, "internal server error");
+      reject(magicBranch.cmd, "internal server error");
       log.error(String.format(
           "Only %d of %d new change refs created in %s; aborting",
-          okToInsert, newChanges.size(), project.getName()));
+          okToInsert, replaceCount + newChanges.size(), project.getName()));
       return;
     }
 
     try {
+      List<CheckedFuture<?, OrmException>> futures = Lists.newArrayList();
       for (ReplaceRequest replace : replaceByChange.values()) {
-        if (replace.inputCommand == newChange) {
-          replace.insertPatchSet();
+        if (magicBranch != null && replace.inputCommand == magicBranch.cmd) {
+          futures.add(replace.insertPatchSet());
         }
       }
 
       for (CreateRequest create : newChanges) {
-        create.insertChange();
+        futures.add(create.insertChange());
       }
-      newChange.setResult(OK);
+
+      for (CheckedFuture<?, OrmException> f : futures) {
+        f.checkedGet();
+      }
+      magicBranch.cmd.setResult(OK);
     } catch (OrmException err) {
       log.error("Can't insert changes for " + project.getName(), err);
-      reject(newChange, "internal server error");
+      reject(magicBranch.cmd, "internal server error");
     } catch (IOException err) {
       log.error("Can't read commits for " + project.getName(), err);
-      reject(newChange, "internal server error");
+      reject(magicBranch.cmd, "internal server error");
     }
   }
 
@@ -681,16 +749,6 @@
     return displayName;
   }
 
-  private Account.Id toAccountId(final String nameOrEmail) throws OrmException,
-      NoSuchAccountException {
-    final Account a = accountResolver.findByNameOrEmail(nameOrEmail);
-    if (a == null) {
-      throw new NoSuchAccountException("\"" + nameOrEmail
-          + "\" is not registered");
-    }
-    return a.getId();
-  }
-
   private void parseCommands(final Collection<ReceiveCommand> commands) {
     for (final ReceiveCommand cmd : commands) {
       if (cmd.getResult() != NOT_ATTEMPTED) {
@@ -705,8 +763,21 @@
         continue;
       }
 
+      HookResult result = hooks.doRefUpdateHook(project, cmd.getRefName(),
+                              currentUser.getAccount(), cmd.getOldId(),
+                              cmd.getNewId());
+
+      if (result != null) {
+        final String message = result.toString().trim();
+        if (result.getExitValue() != 0) {
+          reject(cmd, message);
+          continue;
+        }
+        rp.sendMessage(message);
+      }
+
       if (MagicBranch.isMagicBranch(cmd.getRefName())) {
-        parseNewChangeCommand(cmd);
+        parseMagicBranch(cmd);
         continue;
       }
 
@@ -873,14 +944,17 @@
 
   private void parseDelete(final ReceiveCommand cmd) {
     RefControl ctl = projectControl.controlForRef(cmd.getRefName());
-    if (ctl.canDelete()) {
+    if (ctl.getRefName().startsWith("refs/changes/")) {
+      errors.put(Error.DELETE_CHANGES, ctl.getRefName());
+      reject(cmd, "cannot delete changes");
+    } else if (ctl.canDelete()) {
       batch.addCommand(cmd);
     } else {
       if (GitRepositoryManager.REF_CONFIG.equals(ctl.getRefName())) {
         reject(cmd, "cannot delete project configuration");
       } else {
         errors.put(Error.DELETE, ctl.getRefName());
-        reject(cmd, "can not delete references");
+        reject(cmd, "cannot delete references");
       }
     }
   }
@@ -914,72 +988,138 @@
     }
   }
 
-  private void parseNewChangeCommand(final ReceiveCommand cmd) {
+  private static class MagicBranchInput {
+    private static final Splitter COMMAS = Splitter.on(',').omitEmptyStrings();
+
+    final ReceiveCommand cmd;
+    Branch.NameKey dest;
+    RefControl ctl;
+    Set<Account.Id> reviewer = Sets.newLinkedHashSet();
+    Set<Account.Id> cc = Sets.newLinkedHashSet();
+
+    @Option(name = "--topic", metaVar = "NAME", usage = "attach topic to changes")
+    String topic;
+
+    @Option(name = "--draft", usage = "mark new/updated changes as draft")
+    boolean draft;
+
+    @Option(name = "-r", metaVar = "EMAIL", usage = "add reviewer to changes")
+    void reviewer(Account.Id id) {
+      reviewer.add(id);
+    }
+
+    @Option(name = "--cc", metaVar = "EMAIL", usage = "notify user by CC")
+    void cc(Account.Id id) {
+      cc.add(id);
+    }
+
+    @Option(name = "--publish", usage = "publish new/updated changes")
+    void publish(boolean publish) {
+      draft = !publish;
+    }
+
+    MagicBranchInput(ReceiveCommand cmd) {
+      this.cmd = cmd;
+      this.draft = cmd.getRefName().startsWith(MagicBranch.NEW_DRAFT_CHANGE);
+    }
+
+    boolean isDraft() {
+      return draft;
+    }
+
+    MailRecipients getMailRecipients() {
+      return new MailRecipients(reviewer, cc);
+    }
+
+    String parse(CmdLineParser clp, Repository repo, Set<String> refs)
+        throws CmdLineException {
+      String ref = MagicBranch.getDestBranchName(cmd.getRefName());
+      if (!ref.startsWith(Constants.R_REFS)) {
+        ref = Constants.R_HEADS + ref;
+      }
+
+      int optionStart = ref.indexOf('%');
+      if (0 < optionStart) {
+        ListMultimap<String, String> options = LinkedListMultimap.create();
+        for (String s : COMMAS.split(ref.substring(optionStart + 1))) {
+          int e = s.indexOf('=');
+          if (0 < e) {
+            options.put(s.substring(0, e), s.substring(e + 1));
+          } else {
+            options.put(s, "");
+          }
+        }
+        clp.parseOptionMap(options);
+        ref = ref.substring(0, optionStart);
+      }
+
+      // Split the destination branch by branch and topic. The topic
+      // suffix is entirely optional, so it might not even exist.
+      String head = readHEAD(repo);
+      int split = ref.length();
+      for (;;) {
+        String name = ref.substring(0, split);
+        if (refs.contains(name) || name.equals(head)) {
+          break;
+        }
+
+        split = name.lastIndexOf('/', split - 1);
+        if (split <= Constants.R_REFS.length()) {
+          return ref;
+        }
+      }
+      if (split < ref.length()) {
+        topic = Strings.emptyToNull(ref.substring(split + 1));
+      }
+      return ref.substring(0, split);
+    }
+  }
+
+  private void parseMagicBranch(final ReceiveCommand cmd) {
     // Permit exactly one new change request per push.
-    //
-    if (newChange != null) {
+    if (magicBranch != null) {
       reject(cmd, "duplicate request");
       return;
     }
 
-    newChange = cmd;
-    String destBranchName = MagicBranch.getDestBranchName(cmd.getRefName());
-    if (!destBranchName.startsWith(Constants.R_REFS)) {
-      destBranchName = Constants.R_HEADS + destBranchName;
-    }
+    magicBranch = new MagicBranchInput(cmd);
+    magicBranch.reviewer.addAll(reviewersFromCommandLine);
+    magicBranch.cc.addAll(ccFromCommandLine);
 
-    final String head;
+    String ref;
+    CmdLineParser clp = optionParserFactory.create(magicBranch);
     try {
-      head = repo.getFullBranch();
-    } catch (IOException e) {
-      log.error("Cannot read HEAD symref", e);
-      reject(cmd, "internal error");
+      ref = magicBranch.parse(clp, repo, rp.getAdvertisedRefs().keySet());
+    } catch (CmdLineException e) {
+      if (!clp.wasHelpRequestedByOption()) {
+        reject(cmd, e.getMessage());
+        return;
+      }
+      ref = null; // never happen
+    }
+    if (clp.wasHelpRequestedByOption()) {
+      StringWriter w = new StringWriter();
+      w.write("\nHelp for refs/for/branch:\n\n");
+      clp.printUsage(w, null);
+      addMessage(w.toString());
+      reject(cmd, "see help");
+      return;
+    }
+    if (!rp.getAdvertisedRefs().containsKey(ref) && !ref.equals(readHEAD(repo))) {
+      if (ref.startsWith(Constants.R_HEADS)) {
+        String n = ref.substring(Constants.R_HEADS.length());
+        reject(cmd, "branch " + n + " not found");
+      } else {
+        reject(cmd, ref + " not found");
+      }
       return;
     }
 
-    // Split the destination branch by branch and topic.  The topic
-    // suffix is entirely optional, so it might not even exist.
-    //
-    int split = destBranchName.length();
-    for (;;) {
-      String name = destBranchName.substring(0, split);
-
-      if (rp.getAdvertisedRefs().containsKey(name)) {
-        // We advertised the branch to the client so we know
-        // the branch exists. Target this branch for the upload.
-        //
-        break;
-      } else if (head.equals(name)) {
-        // We didn't advertise the branch, because it doesn't exist yet.
-        // Allow it anyway as HEAD is a symbolic reference to the name.
-        //
-        break;
-      }
-
-      split = name.lastIndexOf('/', split - 1);
-      if (split <= Constants.R_REFS.length()) {
-        String n = destBranchName;
-        if (n.startsWith(Constants.R_HEADS))
-          n = n.substring(Constants.R_HEADS.length());
-        reject(cmd, "branch " + n + " not found");
-        return;
-      }
-    }
-
-    if (split < destBranchName.length()) {
-      destTopicName = destBranchName.substring(split + 1);
-      if (destTopicName.isEmpty()) {
-        destTopicName = null;
-      }
-    } else {
-      destTopicName = null;
-    }
-    destBranch = new Branch.NameKey(project.getNameKey(), //
-        destBranchName.substring(0, split));
-    destBranchCtl = projectControl.controlForRef(destBranch);
-    if (!destBranchCtl.canUpload()) {
-      errors.put(Error.CODE_REVIEW, cmd.getRefName());
-      reject(cmd, "can not upload review");
+    magicBranch.dest = new Branch.NameKey(project.getNameKey(), ref);
+    magicBranch.ctl = projectControl.controlForRef(ref);
+    if (!magicBranch.ctl.canUpload()) {
+      errors.put(Error.CODE_REVIEW, ref);
+      reject(cmd, "cannot upload review");
       return;
     }
 
@@ -990,9 +1130,8 @@
     //
     try {
       final RevWalk walk = rp.getRevWalk();
-
-      final RevCommit tip = walk.parseCommit(newChange.getNewId());
-      Ref targetRef = rp.getAdvertisedRefs().get(destBranchName);
+      final RevCommit tip = walk.parseCommit(magicBranch.cmd.getNewId());
+      Ref targetRef = rp.getAdvertisedRefs().get(magicBranch.ctl.getRefName());
       if (targetRef == null || targetRef.getObjectId() == null) {
         // The destination branch does not yet exist. Assume the
         // history being sent for review will start it and thus
@@ -1000,7 +1139,6 @@
         return;
       }
       final RevCommit h = walk.parseCommit(targetRef.getObjectId());
-
       final RevFilter oldRevFilter = walk.getRevFilter();
       try {
         walk.reset();
@@ -1008,7 +1146,7 @@
         walk.markStart(tip);
         walk.markStart(h);
         if (walk.next() == null) {
-          reject(newChange, "no common ancestry");
+          reject(magicBranch.cmd, "no common ancestry");
           return;
         }
       } finally {
@@ -1016,12 +1154,21 @@
         walk.setRevFilter(oldRevFilter);
       }
     } catch (IOException e) {
-      newChange.setResult(REJECTED_MISSING_OBJECT);
+      magicBranch.cmd.setResult(REJECTED_MISSING_OBJECT);
       log.error("Invalid pack upload; one or more objects weren't sent", e);
       return;
     }
   }
 
+  private static String readHEAD(Repository repo) {
+    try {
+      return repo.getFullBranch();
+    } catch (IOException e) {
+      log.error("Cannot read HEAD symref", e);
+      return null;
+    }
+  }
+
   /**
    * Loads a list of commits to reject from {@code refs/meta/reject-commits}.
    *
@@ -1111,8 +1258,11 @@
     walk.sort(RevSort.REVERSE, true);
     try {
       Set<ObjectId> existing = Sets.newHashSet();
-      walk.markStart(walk.parseCommit(newChange.getNewId()));
-      markHeadsAsUninteresting(walk, existing);
+      walk.markStart(walk.parseCommit(magicBranch.cmd.getNewId()));
+      markHeadsAsUninteresting(
+          walk,
+          existing,
+          magicBranch.ctl != null ? magicBranch.ctl.getRefName() : null);
 
       List<ChangeLookup> pending = Lists.newArrayList();
       final Set<Change.Key> newChangeIds = new HashSet<Change.Key>();
@@ -1126,7 +1276,8 @@
           //
           continue;
         }
-        if (!validCommit(destBranchCtl, newChange, c)) {
+
+        if (!validCommit(magicBranch.ctl, magicBranch.cmd, c)) {
           // Not a change the user can propose? Abort as early as possible.
           //
           return Collections.emptyList();
@@ -1142,7 +1293,7 @@
         final String idStr = idList.get(idList.size() - 1).trim();
         if (idStr.matches("^I00*$")) {
           // Reject this invalid line from EGit.
-          reject(newChange, "invalid Change-Id");
+          reject(magicBranch.cmd, "invalid Change-Id");
           return Collections.emptyList();
         }
 
@@ -1152,7 +1303,7 @@
 
       for (ChangeLookup p : pending) {
         if (newChangeIds.contains(p.changeKey)) {
-          reject(newChange, "squash commits first");
+          reject(magicBranch.cmd, "squash commits first");
           return Collections.emptyList();
         }
 
@@ -1163,14 +1314,14 @@
           // a different Change-Id. In practice, we should never see
           // this error message as Change-Id should be unique.
           //
-          reject(newChange, p.changeKey.get() + " has duplicates");
+          reject(magicBranch.cmd, p.changeKey.get() + " has duplicates");
           return Collections.emptyList();
         }
 
         if (changes.size() == 1) {
           // Schedule as a replacement to this one matching change.
           //
-          if (requestReplace(newChange, false, changes.get(0), p.commit)) {
+          if (requestReplace(magicBranch.cmd, false, changes.get(0), p.commit)) {
             continue;
           } else {
             return Collections.emptyList();
@@ -1179,7 +1330,7 @@
 
         if (changes.size() == 0) {
           if (!isValidChangeId(p.changeKey.get())) {
-            reject(newChange, "invalid Change-Id");
+            reject(magicBranch.cmd, "invalid Change-Id");
             return Collections.emptyList();
           }
 
@@ -1191,17 +1342,17 @@
       // Should never happen, the core receive process would have
       // identified the missing object earlier before we got control.
       //
-      newChange.setResult(REJECTED_MISSING_OBJECT);
+      magicBranch.cmd.setResult(REJECTED_MISSING_OBJECT);
       log.error("Invalid pack upload; one or more objects weren't sent", e);
       return Collections.emptyList();
     } catch (OrmException e) {
       log.error("Cannot query database to locate prior changes", e);
-      reject(newChange, "database error");
+      reject(magicBranch.cmd, "database error");
       return Collections.emptyList();
     }
 
     if (newChanges.isEmpty() && replaceByChange.isEmpty()) {
-      reject(newChange, "no new changes");
+      reject(magicBranch.cmd, "no new changes");
       return Collections.emptyList();
     }
     for (CreateRequest create : newChanges) {
@@ -1210,15 +1361,17 @@
     return newChanges;
   }
 
-
-  private void markHeadsAsUninteresting(final RevWalk walk, Set<ObjectId> existing) {
-    for (Ref ref : repo.getAllRefs().values()) {
+  private void markHeadsAsUninteresting(
+      final RevWalk walk,
+      Set<ObjectId> existing,
+      @Nullable String forRef) {
+    for (Ref ref : allRefs.values()) {
       if (ref.getObjectId() == null) {
         continue;
       } else if (ref.getName().startsWith("refs/changes/")) {
         existing.add(ref.getObjectId());
       } else if (ref.getName().startsWith(R_HEADS)
-          || (destBranchCtl != null && ref.getName().equals(destBranchCtl.getRefName()))) {
+          || (forRef != null && forRef.equals(ref.getName()))) {
         try {
           walk.markUninteresting(walk.parseCommit(ref.getObjectId()));
         } catch (IOException e) {
@@ -1242,7 +1395,7 @@
     ChangeLookup(RevCommit c, Change.Key key) throws OrmException {
       commit = c;
       changeKey = key;
-      changes = db.changes().byBranchKey(destBranch, key);
+      changes = db.changes().byBranchKey(magicBranch.dest, key);
     }
   }
 
@@ -1260,16 +1413,15 @@
       change = new Change(changeKey,
           new Change.Id(db.nextChangeId()),
           currentUser.getAccountId(),
-          destBranch);
-      change.setTopic(destTopicName);
-      change.nextPatchSetId();
+          magicBranch.dest);
+      change.setTopic(magicBranch.topic);
 
-      ps = new PatchSet(change.currPatchSetId());
+      ps = new PatchSet(new PatchSet.Id(change.getId(), INITIAL_PATCH_SET_ID));
       ps.setCreatedOn(change.getCreatedOn());
       ps.setUploader(change.getOwner());
       ps.setRevision(toRevId(c));
 
-      if (MagicBranch.isDraft(newChange.getRefName())) {
+      if (magicBranch.isDraft()) {
         change.setStatus(Change.Status.DRAFT);
         ps.setDraft(true);
       }
@@ -1280,48 +1432,48 @@
       cmd = new ReceiveCommand(ObjectId.zeroId(), c, ps.getRefName());
     }
 
-    void insertChange() throws IOException, OrmException {
+    CheckedFuture<Void, OrmException> insertChange() throws IOException {
       rp.getRevWalk().parseBody(commit);
-      warnMalformedMessage(commit);
 
-      final Account.Id me = currentUser.getAccountId();
-      final Set<Account.Id> reviewers = new HashSet<Account.Id>(reviewerId);
-      final Set<Account.Id> cc = new HashSet<Account.Id>(ccId);
-      final List<FooterLine> footerLines = commit.getFooterLines();
-      for (final FooterLine footerLine : footerLines) {
-        try {
-          if (ps.isDraft()) {
-            continue;
+      final Thread caller = Thread.currentThread();
+      ListenableFuture<Void> future = changeUpdateExector.submit(
+          requestScopePropagator.wrap(new Callable<Void>() {
+        @Override
+        public Void call() throws OrmException {
+          if (caller == Thread.currentThread()) {
+            insertChange(db);
+          } else {
+            ReviewDb db = schemaFactory.open();
+            try {
+              insertChange(db);
+            } finally {
+              db.close();
+            }
           }
-          if (isReviewer(footerLine)) {
-            reviewers.add(toAccountId(footerLine.getValue().trim()));
-          } else if (footerLine.matches(FooterKey.CC)) {
-            cc.add(toAccountId(footerLine.getValue().trim()));
+          synchronized (newProgress) {
+            newProgress.update(1);
           }
-        } catch (NoSuchAccountException e) {
-          continue;
+          return null;
         }
-      }
-      reviewers.remove(me);
-      cc.remove(me);
-      cc.removeAll(reviewers);
+      }));
+      return Futures.makeChecked(future, ORM_EXCEPTION);
+    }
 
-      db.changes().beginTransaction(change.getId());
-      try {
-        insertAncestors(ps.getId(), commit);
-        db.patchSets().insert(Collections.singleton(ps));
-        db.changes().insert(Collections.singleton(change));
-        ChangeUtil.updateTrackingIds(db, change, trackingFooters, footerLines);
-        approvalsUtil.addReviewers(change, ps, info, reviewers);
-        db.commit();
-      } finally {
-        db.rollback();
+    private void insertChange(ReviewDb db) throws OrmException {
+      final Account.Id me = currentUser.getAccountId();
+      final List<FooterLine> footerLines = commit.getFooterLines();
+      final MailRecipients recipients = new MailRecipients();
+      if (magicBranch != null) {
+        recipients.add(magicBranch.getMailRecipients());
       }
+      recipients.add(getRecipientsFromFooters(accountResolver, ps, footerLines));
+      recipients.remove(me);
+
+      changeInserter.insertChange(db, change, ps, commit, labelTypes,
+          footerLines, info, recipients.getReviewers());
 
       created = true;
-      replication.fire(project.getNameKey(), ps.getRefName());
-      hooks.doPatchsetCreatedHook(change, ps, db);
-      newProgress.update(1);
+
       workQueue.getDefaultQueue()
           .submit(requestScopePropagator.wrap(new Runnable() {
         @Override
@@ -1331,8 +1483,8 @@
                 createChangeSenderFactory.create(change);
             cm.setFrom(me);
             cm.setPatchSet(ps, info);
-            cm.addReviewers(reviewers);
-            cm.addExtraCC(cc);
+            cm.addReviewers(recipients.getReviewers());
+            cm.addExtraCC(recipients.getCcOnly());
             cm.send();
           } catch (Exception e) {
             log.error("Cannot send email for new change " + change.getId(), e);
@@ -1347,18 +1499,10 @@
     }
   }
 
-  private static boolean isReviewer(final FooterLine candidateFooterLine) {
-    return candidateFooterLine.matches(FooterKey.SIGNED_OFF_BY)
-        || candidateFooterLine.matches(FooterKey.ACKED_BY)
-        || candidateFooterLine.matches(REVIEWED_BY)
-        || candidateFooterLine.matches(TESTED_BY);
-  }
-
   private void preparePatchSetsForReplace() {
     try {
       readChangesForReplace();
       readPatchSetsForReplace();
-
       for (ReplaceRequest req : replaceByChange.values()) {
         if (req.inputCommand.getResult() == NOT_ATTEMPTED) {
           req.validate(false);
@@ -1386,10 +1530,10 @@
       }
     }
 
-    if (newChange != null && newChange.getResult() != NOT_ATTEMPTED) {
+    if (magicBranch != null && magicBranch.cmd.getResult() != NOT_ATTEMPTED) {
       // Cancel creations tied to refs/for/ or refs/drafts/ command.
       for (ReplaceRequest req : replaceByChange.values()) {
-        if (req.inputCommand == newChange && req.cmd != null) {
+        if (req.inputCommand == magicBranch.cmd && req.cmd != null) {
           req.cmd.setResult(Result.REJECTED_OTHER_REASON, "aborted");
         }
       }
@@ -1469,6 +1613,7 @@
       }
 
       rp.getRevWalk().parseBody(newCommit);
+
       if (!validCommit(changeCtl.getRefControl(), inputCommand, newCommit)) {
         return false;
       }
@@ -1494,6 +1639,13 @@
         try {
           final RevCommit prior = rp.getRevWalk().parseCommit(commitId);
 
+          // Don't allow the same commit to appear twice on the same change
+          //
+          if (newCommit == prior) {
+            reject(inputCommand, "commit already exists");
+            return false;
+          }
+
           // Don't allow a change to directly depend upon itself. This is a
           // very common error due to users making a new commit rather than
           // amending when trying to address review comments.
@@ -1503,13 +1655,6 @@
             return false;
           }
 
-          // Don't allow the same commit to appear twice on the same change
-          //
-          if (newCommit == prior) {
-            reject(inputCommand, "commit already exists");
-            return false;
-          }
-
           // Don't allow the same tree if the commit message is unmodified
           // or no parents were updated (rebase), else warn that only part
           // of the commit was modified.
@@ -1550,12 +1695,13 @@
         }
       }
 
-      change.nextPatchSetId();
-      newPatchSet = new PatchSet(change.currPatchSetId());
+      PatchSet.Id id =
+          ChangeUtil.nextPatchSetId(allRefs, change.currentPatchSetId());
+      newPatchSet = new PatchSet(id);
       newPatchSet.setCreatedOn(new Timestamp(System.currentTimeMillis()));
       newPatchSet.setUploader(currentUser.getAccountId());
       newPatchSet.setRevision(toRevId(newCommit));
-      if (newChange != null && MagicBranch.isDraft(newChange.getRefName())) {
+      if (magicBranch != null && magicBranch.isDraft()) {
         newPatchSet.setDraft(true);
       }
       info = patchSetInfoFactory.get(newCommit, newPatchSet.getId());
@@ -1566,52 +1712,55 @@
       return true;
     }
 
-    PatchSet.Id insertPatchSet() throws IOException, OrmException {
+    CheckedFuture<PatchSet.Id, OrmException> insertPatchSet()
+        throws IOException {
       rp.getRevWalk().parseBody(newCommit);
-      warnMalformedMessage(newCommit);
 
-      final Account.Id me = currentUser.getAccountId();
-      final Set<Account.Id> reviewers = new HashSet<Account.Id>(reviewerId);
-      final Set<Account.Id> cc = new HashSet<Account.Id>(ccId);
-      final List<FooterLine> footerLines = newCommit.getFooterLines();
-      for (final FooterLine footerLine : footerLines) {
-        try {
-          if (isReviewer(footerLine)) {
-            reviewers.add(toAccountId(footerLine.getValue().trim()));
-          } else if (footerLine.matches(FooterKey.CC)) {
-            cc.add(toAccountId(footerLine.getValue().trim()));
+      final Thread caller = Thread.currentThread();
+      ListenableFuture<PatchSet.Id> future = changeUpdateExector.submit(
+          requestScopePropagator.wrap(new Callable<PatchSet.Id>() {
+        @Override
+        public PatchSet.Id call() throws OrmException {
+          try {
+            if (caller == Thread.currentThread()) {
+              return insertPatchSet(db);
+            } else {
+              ReviewDb db = schemaFactory.open();
+              try {
+                return insertPatchSet(db);
+              } finally {
+                db.close();
+              }
+            }
+          } finally {
+            synchronized (replaceProgress) {
+              replaceProgress.update(1);
+            }
           }
-        } catch (NoSuchAccountException e) {
-          continue;
         }
-      }
-      reviewers.remove(me);
-      cc.remove(me);
-      cc.removeAll(reviewers);
+      }));
+      return Futures.makeChecked(future, ORM_EXCEPTION);
+    }
 
-      final Set<Account.Id> oldReviewers = new HashSet<Account.Id>();
-      final Set<Account.Id> oldCC = new HashSet<Account.Id>();
+    PatchSet.Id insertPatchSet(ReviewDb db) throws OrmException {
+      final Account.Id me = currentUser.getAccountId();
+      final List<FooterLine> footerLines = newCommit.getFooterLines();
+      final MailRecipients recipients = new MailRecipients();
+      if (magicBranch != null) {
+        recipients.add(magicBranch.getMailRecipients());
+      }
+      recipients.add(getRecipientsFromFooters(accountResolver, newPatchSet, footerLines));
+      recipients.remove(me);
 
       db.changes().beginTransaction(change.getId());
       try {
-        change =
-          db.changes().atomicUpdate(change.getId(), new AtomicUpdate<Change>() {
-            @Override
-            public Change update(Change change) {
-              if (change.getStatus().isClosed()) {
-                return null;
-              }
-
-              change.updateNumberOfPatchSets(newPatchSet.getPatchSetId());
-              return change;
-            }
-          });
-        if (change == null) {
+        change = db.changes().get(change.getId());
+        if (change == null || change.getStatus().isClosed()) {
           reject(inputCommand, "change is closed");
           return null;
         }
 
-        insertAncestors(newPatchSet.getId(), newCommit);
+        ChangeUtil.insertAncestors(db, newPatchSet.getId(), newCommit);
         db.patchSets().insert(Collections.singleton(newPatchSet));
 
         if (checkMergedInto) {
@@ -1619,22 +1768,13 @@
           mergedIntoRef = mergedInto != null ? mergedInto.getName() : null;
         }
 
-        List<PatchSetApproval> patchSetApprovals = approvalsUtil.copyVetosToLatestPatchSet(change);
-
-        final Set<Account.Id> haveApprovals = new HashSet<Account.Id>();
-        oldReviewers.clear();
-        oldCC.clear();
-
-        for (PatchSetApproval a : patchSetApprovals) {
-          haveApprovals.add(a.getAccountId());
-          if (a.getValue() != 0) {
-            oldReviewers.add(a.getAccountId());
-          } else {
-            oldCC.add(a.getAccountId());
-          }
-        }
-
-        approvalsUtil.addReviewers(change, newPatchSet, info, reviewers, haveApprovals);
+        List<PatchSetApproval> patchSetApprovals =
+            approvalsUtil.copyVetosToPatchSet(db, labelTypes, newPatchSet.getId());
+        final MailRecipients oldRecipients =
+            getRecipientsFromApprovals(patchSetApprovals);
+        approvalsUtil.addReviewers(db, labelTypes, change, newPatchSet, info,
+            recipients.getReviewers(), oldRecipients.getAll());
+        recipients.add(oldRecipients);
 
         msg =
             new ChangeMessage(new ChangeMessage.Key(change.getId(), ChangeUtil
@@ -1660,8 +1800,8 @@
                     return change;
                   }
 
-                  if (destTopicName != null) {
-                    change.setTopic(destTopicName);
+                  if (magicBranch != null && magicBranch.topic != null) {
+                    change.setTopic(magicBranch.topic);
                   }
                   if (change.getStatus() == Change.Status.DRAFT && newPatchSet.isDraft()) {
                     // Leave in draft status.
@@ -1696,9 +1836,9 @@
       if (cmd.getResult() == NOT_ATTEMPTED) {
         cmd.execute(rp);
       }
-      replication.fire(project.getNameKey(), newPatchSet.getRefName());
+      gitRefUpdated.fire(project.getNameKey(), newPatchSet.getRefName(),
+          ObjectId.zeroId(), newCommit);
       hooks.doPatchsetCreatedHook(change, newPatchSet, db);
-      replaceProgress.update(1);
       if (mergedIntoRef != null) {
         hooks.doChangeMergedHook(
             change, currentUser.getAccount(), newPatchSet, db);
@@ -1713,10 +1853,8 @@
             cm.setFrom(me);
             cm.setPatchSet(newPatchSet, info);
             cm.setChangeMessage(msg);
-            cm.addReviewers(reviewers);
-            cm.addExtraCC(cc);
-            cm.addReviewers(oldReviewers);
-            cm.addExtraCC(oldCC);
+            cm.addReviewers(recipients.getReviewers());
+            cm.addExtraCC(recipients.getCcOnly());
             cm.send();
           } catch (Exception e) {
             log.error("Cannot send email for new patch set " + newPatchSet.getId(), e);
@@ -1803,7 +1941,7 @@
         && ctl.canForgeCommitter()
         && ctl.canForgeGerritServerIdentity()
         && ctl.canUploadMerges()
-        && !project.isUseSignedOffBy()
+        && !projectControl.getProjectState().isUseSignedOffBy()
         && Iterables.isEmpty(rejectCommits)
         && !GitRepositoryManager.REF_CONFIG.equals(ctl.getRefName())
         && !(MagicBranch.isMagicBranch(cmd.getRefName())
@@ -1817,7 +1955,7 @@
     try {
       Set<ObjectId> existing = Sets.newHashSet();
       walk.markStart(walk.parseCommit(cmd.getNewId()));
-      markHeadsAsUninteresting(walk, existing);
+      markHeadsAsUninteresting(walk, existing, cmd.getRefName());
 
       RevCommit c;
       while ((c = walk.next()) != null) {
@@ -1835,222 +1973,27 @@
 
   private boolean validCommit(final RefControl ctl, final ReceiveCommand cmd,
       final RevCommit c) throws MissingObjectException, IOException {
-    rp.getRevWalk().parseBody(c);
-    final PersonIdent committer = c.getCommitterIdent();
-    final PersonIdent author = c.getAuthorIdent();
 
-    // Require permission to upload merges.
-    if (c.getParentCount() > 1 && !ctl.canUploadMerges()) {
-      reject(cmd, "you are not allowed to upload merges");
+    if (validCommits.contains(c)) {
+      return true;
+    }
+
+    CommitReceivedEvent receiveEvent =
+        new CommitReceivedEvent(cmd, project, ctl.getRefName(), c, currentUser);
+    CommitValidators commitValidators =
+        commitValidatorsFactory.create(ctl, sshInfo, repo);
+
+    try {
+      messages.addAll(commitValidators.validateForReceiveCommits(receiveEvent));
+    } catch (CommitValidationException e) {
+      messages.addAll(e.getMessages());
+      reject(cmd, e.getMessage());
       return false;
     }
-
-    // Don't allow the user to amend a merge created by Gerrit Code Review.
-    // This seems to happen all too often, due to users not paying any
-    // attention to what they are doing.
-    //
-    if (c.getParentCount() > 1
-        && author.getName().equals(gerritIdent.getName())
-        && author.getEmailAddress().equals(gerritIdent.getEmailAddress())
-        && !ctl.canForgeGerritServerIdentity()) {
-      reject(cmd, "do not amend merges not made by you");
-      return false;
-    }
-
-    // Require that author matches the uploader.
-    //
-    if (!currentUser.getEmailAddresses().contains(author.getEmailAddress())
-        && !ctl.canForgeAuthor()) {
-      sendInvalidEmailError(c, "author", author);
-      reject(cmd, "invalid author");
-      return false;
-    }
-
-    // Require that committer matches the uploader.
-    //
-    if (!currentUser.getEmailAddresses().contains(committer.getEmailAddress())
-        && !ctl.canForgeCommitter()) {
-      sendInvalidEmailError(c, "committer", committer);
-      reject(cmd, "invalid committer");
-      return false;
-    }
-
-    if (project.isUseSignedOffBy()) {
-      // If the project wants Signed-off-by / Acked-by lines, verify we
-      // have them for the blamable parties involved on this change.
-      //
-      boolean sboAuthor = false, sboCommitter = false, sboMe = false;
-      for (final FooterLine footer : c.getFooterLines()) {
-        if (footer.matches(FooterKey.SIGNED_OFF_BY)) {
-          final String e = footer.getEmailAddress();
-          if (e != null) {
-            sboAuthor |= author.getEmailAddress().equals(e);
-            sboCommitter |= committer.getEmailAddress().equals(e);
-            sboMe |= currentUser.getEmailAddresses().contains(e);
-          }
-        }
-      }
-      if (!sboAuthor && !sboCommitter && !sboMe && !ctl.canForgeCommitter()) {
-        reject(cmd, "not Signed-off-by author/committer/uploader");
-        return false;
-      }
-    }
-
-    final List<String> idList = c.getFooterLines(CHANGE_ID);
-    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 {
-        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(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;
-      }
-    }
-
+    validCommits.add(c);
     return true;
   }
 
-  private String getFixedCommitMsgWithChangeId(String errMsg, RevCommit c) {
-    // We handle 3 cases:
-    // 1. No change id in the commit message at all.
-    // 2. change id last in the commit message but missing empty line to create the footer.
-    // 3. there is a change-id somewhere in the commit message, but we ignore it.
-    final String changeId = "Change-Id:";
-    StringBuilder sb = new StringBuilder();
-    sb.append("ERROR: ").append(errMsg);
-    sb.append("\n");
-    sb.append("Suggestion for commit message:\n");
-
-    if (c.getFullMessage().indexOf(changeId)==-1) {
-      sb.append(c.getFullMessage());
-      sb.append("\n");
-      sb.append(changeId).append(" I").append(c.name());
-    } else {
-      String lines[] = c.getFullMessage().trim().split("\n");
-      String lastLine = lines.length > 0 ? lines[lines.length - 1] : "";
-
-      if (lastLine.indexOf(changeId)==0) {
-        for (int i = 0; i < lines.length - 1; i++) {
-          sb.append(lines[i]);
-          sb.append("\n");
-        }
-
-        sb.append("\n");
-        sb.append(lastLine);
-      } else {
-        sb.append(c.getFullMessage());
-        sb.append("\n");
-        sb.append(changeId).append(" I").append(c.name());
-        sb.append("\nHint: A potential Change-Id was found, but it was not in the footer of the commit message.");
-      }
-    }
-
-    return sb.toString();
-  }
-
-  private void sendInvalidEmailError(RevCommit c, String type, PersonIdent who) {
-    StringBuilder sb = new StringBuilder();
-    sb.append("\n");
-    sb.append("ERROR:  In commit " + c.name() + "\n");
-    sb.append("ERROR:  " + type + " email address " + who.getEmailAddress() + "\n");
-    sb.append("ERROR:  does not match your user account.\n");
-    sb.append("ERROR:\n");
-    if (currentUser.getEmailAddresses().isEmpty()) {
-      sb.append("ERROR:  You have not registered any email addresses.\n");
-    } else {
-      sb.append("ERROR:  The following addresses are currently registered:\n");
-      for (String address : currentUser.getEmailAddresses()) {
-        sb.append("ERROR:    " + address + "\n");
-      }
-    }
-    sb.append("ERROR:\n");
-    if (canonicalWebUrl != null) {
-      sb.append("ERROR:  To register an email address, please visit:\n");
-      sb.append("ERROR:  " + canonicalWebUrl + "#" + PageLinks.SETTINGS_CONTACT + "\n");
-    }
-    sb.append("\n");
-    addMessage(sb.toString());
-  }
-
-  private void warnMalformedMessage(RevCommit c) {
-    ObjectReader reader = rp.getRevWalk().getObjectReader();
-    if (65 < c.getShortMessage().length()) {
-      AbbreviatedObjectId id;
-      try {
-        id = reader.abbreviate(c);
-      } catch (IOException err) {
-        id = c.abbreviate(6);
-      }
-      addMessage("(W) " + id.name() //
-          + ": commit subject >65 characters; use shorter first paragraph");
-    }
-
-    int longLineCnt = 0, nonEmptyCnt = 0;
-    for (String line : c.getFullMessage().split("\n")) {
-      if (!line.trim().isEmpty()) {
-        nonEmptyCnt++;
-      }
-      if (70 < line.length()) {
-        longLineCnt++;
-      }
-    }
-
-    if (0 < longLineCnt && 33 < longLineCnt * 100 / nonEmptyCnt) {
-      AbbreviatedObjectId id;
-      try {
-        id = reader.abbreviate(c);
-      } catch (IOException err) {
-        id = c.abbreviate(6);
-      }
-      addMessage("(W) " + id.name() //
-          + ": commit message lines >70 characters; manually wrap lines");
-    }
-  }
-
   private void autoCloseChanges(final ReceiveCommand cmd) {
     final RevWalk rw = rp.getRevWalk();
     try {
@@ -2091,7 +2034,9 @@
       }
 
       for (final ReplaceRequest req : toClose) {
-        final PatchSet.Id psi = req.validate(true) ? req.insertPatchSet() : null;
+        final PatchSet.Id psi = req.validate(true)
+            ? req.insertPatchSet().checkedGet()
+            : null;
         if (psi != null) {
           closeChange(req.inputCommand, psi, req.newCommit);
           closeProgress.update(1);
@@ -2131,10 +2076,10 @@
     }
 
     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.
+        change.getStatus() == Change.Status.ABANDONED ||
+        !change.getDest().get().equals(refName)) {
+      // If it's already merged or the commit is not aimed for
+      // this change's destination, don't make further updates.
       //
       return null;
     }
@@ -2221,7 +2166,7 @@
       @Override
       public void run() {
         try {
-          final MergedSender cm = mergedSenderFactory.create(result.change);
+          final MergedSender cm = mergedSenderFactory.create(result.changeCtl);
           cm.setFrom(currentUser.getAccountId());
           cm.setPatchSet(result.newPatchSet, result.info);
           cm.send();
@@ -2238,20 +2183,6 @@
     }));
   }
 
-  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(toRevId(src.getParent(p)));
-      toInsert.add(a);
-    }
-    db.patchSetAncestors().insert(toInsert);
-  }
-
   private static RevId toRevId(final RevCommit src) {
     return new RevId(src.getId().name());
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsAdvertiseRefsHook.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsAdvertiseRefsHook.java
index ec8c080..0eb9b61 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsAdvertiseRefsHook.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveCommitsAdvertiseRefsHook.java
@@ -14,12 +14,14 @@
 
 package com.google.gerrit.server.git;
 
+import com.google.common.collect.Maps;
+import com.google.gerrit.server.util.MagicBranch;
+
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.transport.AdvertiseRefsHook;
 import org.eclipse.jgit.transport.BaseReceivePack;
 import org.eclipse.jgit.transport.UploadPack;
 
-import java.util.HashMap;
 import java.util.Map;
 
 /** Exposes only the non refs/changes/ reference names. */
@@ -36,12 +38,19 @@
     if (oldRefs == null) {
       oldRefs = rp.getRepository().getAllRefs();
     }
-    HashMap<String, Ref> r = new HashMap<String, Ref>();
+    Map<String, Ref> r = Maps.newHashMapWithExpectedSize(oldRefs.size());
     for (Map.Entry<String, Ref> e : oldRefs.entrySet()) {
-      if (!e.getKey().startsWith("refs/changes/")) {
-        r.put(e.getKey(), e.getValue());
+      String name = e.getKey();
+      if (!skip(name)) {
+        r.put(name, e.getValue());
       }
     }
     rp.setAdvertisedRefs(r, rp.getAdvertisedObjects());
   }
+
+  private static boolean skip(String name) {
+    return name.startsWith("refs/changes/")
+        || name.startsWith(GitRepositoryManager.REFS_CACHE_AUTOMERGE)
+        || MagicBranch.isMagicBranch(name);
+  }
 }
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
index 063db2d..1cbd227 100644
--- 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
@@ -14,15 +14,20 @@
 
 package com.google.gerrit.server.git;
 
+import com.google.common.util.concurrent.ListeningExecutorService;
+import com.google.common.util.concurrent.MoreExecutors;
+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.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;
 
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.ThreadPoolExecutor;
+import java.util.concurrent.TimeUnit;
+
 /** Module providing the {@link ReceiveCommitsExecutor}. */
 public class ReceiveCommitsExecutorModule extends AbstractModule {
   @Override
@@ -32,10 +37,31 @@
   @Provides
   @Singleton
   @ReceiveCommitsExecutor
-  public Executor getReceiveCommitsExecutor(@GerritServerConfig Config config,
+  public WorkQueue.Executor createReceiveCommitsExecutor(
+      @GerritServerConfig Config config,
       WorkQueue queues) {
     int poolSize = config.getInt("receive", null, "threadPoolSize",
         Runtime.getRuntime().availableProcessors());
     return queues.createQueue(poolSize, "ReceiveCommits");
   }
+
+  @Provides
+  @Singleton
+  @ChangeUpdateExecutor
+  public ListeningExecutorService createChangeUpdateExecutor(@GerritServerConfig Config config) {
+    int poolSize = config.getInt("receive", null, "changeUpdateThreads", 1);
+    if (poolSize <= 1) {
+      return MoreExecutors.sameThreadExecutor();
+    }
+    return MoreExecutors.listeningDecorator(
+        MoreExecutors.getExitingExecutorService(
+          new ThreadPoolExecutor(1, poolSize,
+              10, TimeUnit.MINUTES,
+              new ArrayBlockingQueue<Runnable>(poolSize),
+              new ThreadFactoryBuilder()
+                .setNameFormat("ChangeUpdate-%d")
+                .setDaemon(true)
+                .build(),
+              new ThreadPoolExecutor.CallerRunsPolicy())));
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveConfig.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveConfig.java
new file mode 100644
index 0000000..d1a3e40
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/ReceiveConfig.java
@@ -0,0 +1,37 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git;
+
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.lib.Config;
+
+@Singleton
+class ReceiveConfig {
+  final boolean checkMagicRefs;
+  final boolean checkReferencedObjectsAreReachable;
+
+  @Inject
+  ReceiveConfig(@GerritServerConfig Config config) {
+    checkMagicRefs = config.getBoolean(
+        "receive", null, "checkMagicRefs",
+        true);
+    checkReferencedObjectsAreReachable = config.getBoolean(
+        "receive", null, "checkReferencedObjectsAreReachable",
+        true);
+  }
+}
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 7b669b2..71dbf87 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,8 +14,8 @@
 
 package com.google.gerrit.server.git;
 
+import com.google.gerrit.common.data.LabelType;
 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;
 
@@ -51,9 +51,8 @@
     sb.append("Change-Id: ").append(changeKey.get()).append("\n");
   }
 
-  void appendApproval(ApprovalCategory category,
-      short value, Account user) {
-    sb.append(category.getLabelName());
+  void appendApproval(LabelType type, short value, Account user) {
+    sb.append(type.getName());
     sb.append(value < 0 ? "-" : "+").append(Math.abs(value)).append(": ");
     appendUserData(user);
     sb.append("\n");
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmitStrategy.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmitStrategy.java
new file mode 100644
index 0000000..7c2ba86
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmitStrategy.java
@@ -0,0 +1,187 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.PatchSetApproval;
+import com.google.gerrit.reviewdb.client.Project.SubmitType;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.IdentifiedUser;
+
+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.revwalk.RevCommit;
+import org.eclipse.jgit.revwalk.RevFlag;
+import org.eclipse.jgit.revwalk.RevWalk;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * Base class that submit strategies must extend. A submit strategy for a
+ * certain {@link SubmitType} defines how the submitted commits should be
+ * merged.
+ */
+public abstract class SubmitStrategy {
+
+  private PersonIdent refLogIdent;
+
+  static class Arguments {
+    protected final IdentifiedUser.GenericFactory identifiedUserFactory;
+    protected final PersonIdent myIdent;
+    protected final ReviewDb db;
+
+    protected final Repository repo;
+    protected final RevWalk rw;
+    protected final ObjectInserter inserter;
+    protected final RevFlag canMergeFlag;
+    protected final Set<RevCommit> alreadyAccepted;
+    protected final Branch.NameKey destBranch;
+    protected final MergeUtil mergeUtil;
+    protected final MergeSorter mergeSorter;
+
+    Arguments(final IdentifiedUser.GenericFactory identifiedUserFactory,
+        final PersonIdent myIdent, final ReviewDb db, final Repository repo,
+        final RevWalk rw, final ObjectInserter inserter,
+        final RevFlag canMergeFlag, final Set<RevCommit> alreadyAccepted,
+        final Branch.NameKey destBranch, final MergeUtil mergeUtil) {
+      this.identifiedUserFactory = identifiedUserFactory;
+      this.myIdent = myIdent;
+      this.db = db;
+
+      this.repo = repo;
+      this.rw = rw;
+      this.inserter = inserter;
+      this.canMergeFlag = canMergeFlag;
+      this.alreadyAccepted = alreadyAccepted;
+      this.destBranch = destBranch;
+      this.mergeUtil = mergeUtil;
+      this.mergeSorter = new MergeSorter(rw, alreadyAccepted, canMergeFlag);
+    }
+  }
+
+  protected final Arguments args;
+
+  SubmitStrategy(final Arguments args) {
+    this.args = args;
+  }
+
+  /**
+   * Runs this submit strategy. If possible the provided commits will be merged
+   * with this submit strategy.
+   *
+   * @param mergeTip the mergeTip
+   * @param toMerge the list of submitted commits that should be merged using
+   *        this submit strategy
+   * @return the new mergeTip
+   * @throws MergeException
+   */
+  public final CodeReviewCommit run(final CodeReviewCommit mergeTip,
+      final List<CodeReviewCommit> toMerge) throws MergeException {
+    refLogIdent = null;
+    return _run(mergeTip, toMerge);
+  }
+
+  /**
+   * Runs this submit strategy. If possible the provided commits will be merged
+   * with this submit strategy.
+   *
+   * @param mergeTip the mergeTip
+   * @param toMerge the list of submitted commits that should be merged using
+   *        this submit strategy
+   * @return the new mergeTip
+   * @throws MergeException
+   */
+  protected abstract CodeReviewCommit _run(CodeReviewCommit mergeTip,
+      List<CodeReviewCommit> toMerge) throws MergeException;
+
+  /**
+   * Checks whether the given commit can be merged.
+   *
+   * Subclasses must ensure that invoking this method does neither modify the
+   * git repository nor the Gerrit database.
+   *
+   * @param mergeTip the mergeTip
+   * @param toMerge the commit for which it should be checked whether it can be
+   *        merged or not
+   * @return <code>true</code> if the given commit can be merged, otherwise
+   *         <code>false</code>
+   * @throws MergeException
+   */
+  public abstract boolean dryRun(CodeReviewCommit mergeTip,
+      CodeReviewCommit toMerge) throws MergeException;
+
+  /**
+   * Returns the PersonIdent that should be used for the ref log entries when
+   * updating the destination branch. The ref log identity may be set after the
+   * {@link #run(CodeReviewCommit, List)} method finished.
+   *
+   * Do only call this method after the {@link #run(CodeReviewCommit, List)}
+   * method has been invoked.
+   *
+   * @return the ref log identity, may be <code>null</code>
+   */
+  public final PersonIdent getRefLogIdent() {
+    return refLogIdent;
+  }
+
+  /**
+   * Returns all commits that have been newly created for the changes that are
+   * getting merged.
+   *
+   * By default this method is returning an empty map, but subclasses may
+   * overwrite this method to provide newly created commits.
+   *
+   * Do only call this method after the {@link #run(CodeReviewCommit, List)}
+   * method has been invoked.
+   *
+   * @return new commits created for changes that are getting merged
+   */
+  public Map<Change.Id, CodeReviewCommit> getNewCommits() {
+    return Collections.emptyMap();
+  }
+
+  /**
+   * Returns whether a merge that failed with
+   * {@link RefUpdate.Result#LOCK_FAILURE} should be retried.
+   *
+   * May be overwritten by subclasses.
+   *
+   * @return <code>true</code> if a merge that failed with
+   *         {@link RefUpdate.Result#LOCK_FAILURE} should be retried, otherwise
+   *         <code>false</code>
+   */
+  public boolean retryOnLockFailure() {
+    return true;
+  }
+
+  /**
+   * Sets the ref log identity if it wasn't set yet.
+   *
+   * @param submitApproval the approval that submitted the patch set
+   */
+  protected final void setRefLogIdent(final PatchSetApproval submitApproval) {
+    if (refLogIdent == null && submitApproval != null) {
+      refLogIdent =
+          args.identifiedUserFactory.create(submitApproval.getAccountId())
+              .newRefLogIdent();
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmitStrategyFactory.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmitStrategyFactory.java
new file mode 100644
index 0000000..8bf831c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmitStrategyFactory.java
@@ -0,0 +1,112 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git;
+
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Project.SubmitType;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.changedetail.RebaseChange;
+import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.patch.PatchSetInfoFactory;
+import com.google.gerrit.server.project.NoSuchProjectException;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+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.RevFlag;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Set;
+
+import javax.annotation.Nullable;
+
+/** Factory to create a {@link SubmitStrategy} for a {@link SubmitType}. */
+public class SubmitStrategyFactory {
+  private static final Logger log = LoggerFactory
+      .getLogger(SubmitStrategyFactory.class);
+
+  private final IdentifiedUser.GenericFactory identifiedUserFactory;
+  private final PersonIdent myIdent;
+  private final PatchSetInfoFactory patchSetInfoFactory;
+  private final GitReferenceUpdated gitRefUpdated;
+  private final RebaseChange rebaseChange;
+  private final ProjectCache projectCache;
+  private final MergeUtil.Factory mergeUtilFactory;
+
+  @Inject
+  SubmitStrategyFactory(
+      final IdentifiedUser.GenericFactory identifiedUserFactory,
+      @GerritPersonIdent final PersonIdent myIdent,
+      final PatchSetInfoFactory patchSetInfoFactory,
+      @CanonicalWebUrl @Nullable final Provider<String> urlProvider,
+      final GitReferenceUpdated gitRefUpdated, final RebaseChange rebaseChange,
+      final ProjectCache projectCache,
+      final MergeUtil.Factory mergeUtilFactory) {
+    this.identifiedUserFactory = identifiedUserFactory;
+    this.myIdent = myIdent;
+    this.patchSetInfoFactory = patchSetInfoFactory;
+    this.gitRefUpdated = gitRefUpdated;
+    this.rebaseChange = rebaseChange;
+    this.projectCache = projectCache;
+    this.mergeUtilFactory = mergeUtilFactory;
+  }
+
+  public SubmitStrategy create(final SubmitType submitType, final ReviewDb db,
+      final Repository repo, final RevWalk rw, final ObjectInserter inserter,
+      final RevFlag canMergeFlag, final Set<RevCommit> alreadyAccepted,
+      final Branch.NameKey destBranch)
+      throws MergeException, NoSuchProjectException {
+    ProjectState project = getProject(destBranch);
+    final SubmitStrategy.Arguments args =
+        new SubmitStrategy.Arguments(identifiedUserFactory, myIdent, db, repo,
+            rw, inserter, canMergeFlag, alreadyAccepted, destBranch,
+            mergeUtilFactory.create(project));
+    switch (submitType) {
+      case CHERRY_PICK:
+        return new CherryPick(args, patchSetInfoFactory, gitRefUpdated);
+      case FAST_FORWARD_ONLY:
+        return new FastForwardOnly(args);
+      case MERGE_ALWAYS:
+        return new MergeAlways(args);
+      case MERGE_IF_NECESSARY:
+        return new MergeIfNecessary(args);
+      case REBASE_IF_NECESSARY:
+        return new RebaseIfNecessary(args, rebaseChange);
+      default:
+        final String errorMsg = "No submit strategy for: " + submitType;
+        log.error(errorMsg);
+        throw new MergeException(errorMsg);
+    }
+  }
+
+  private ProjectState getProject(Branch.NameKey branch)
+      throws NoSuchProjectException {
+    final ProjectState p = projectCache.get(branch.getParentKey());
+    if (p == null) {
+      throw new NoSuchProjectException(branch.getParentKey());
+    }
+    return p;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
index ccb91a3..eeab8f3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/SubmoduleOp.java
@@ -85,7 +85,7 @@
   private final Map<Change.Id, CodeReviewCommit> commits;
   private final PersonIdent myIdent;
   private final GitRepositoryManager repoManager;
-  private final GitReferenceUpdated replication;
+  private final GitReferenceUpdated gitRefUpdated;
   private final SchemaFactory<ReviewDb> schemaFactory;
   private final Set<Branch.NameKey> updatedSubscribers;
 
@@ -97,7 +97,7 @@
       @Assisted Project destProject, @Assisted List<Change> submitted,
       @Assisted final Map<Change.Id, CodeReviewCommit> commits,
       @GerritPersonIdent final PersonIdent myIdent,
-      GitRepositoryManager repoManager, GitReferenceUpdated replication) {
+      GitRepositoryManager repoManager, GitReferenceUpdated gitRefUpdated) {
     this.destBranch = destBranch;
     this.mergeTip = mergeTip;
     this.rw = rw;
@@ -109,7 +109,7 @@
     this.commits = commits;
     this.myIdent = myIdent;
     this.repoManager = repoManager;
-    this.replication = replication;
+    this.gitRefUpdated = gitRefUpdated;
 
     updatedSubscribers = new HashSet<Branch.NameKey>();
   }
@@ -217,7 +217,8 @@
           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)) {
+                && (c.statusCode == CommitMergeStatus.CLEAN_MERGE
+                    || c.statusCode == CommitMergeStatus.CLEAN_PICK || c.statusCode == CommitMergeStatus.CLEAN_REBASE)) {
               msgbuf += "\n";
               msgbuf += c.getFullMessage();
             }
@@ -336,7 +337,7 @@
       switch (rfu.update()) {
         case NEW:
         case FAST_FORWARD:
-          replication.fire(subscriber.getParentKey(), rfu.getName());
+          gitRefUpdated.fire(subscriber.getParentKey(), rfu);
           // TODO since this is performed "in the background" no mail will be
           // sent to inform users about the updated branch
           break;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java
index e9c5536..7c3c0c7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/VersionedMetaData.java
@@ -260,7 +260,7 @@
         switch (result) {
           case NEW:
             revision = rw.parseCommit(ru.getNewObjectId());
-            update.replicate(ru.getName());
+            update.fireGitRefUpdatedEvent(ru);
             return revision;
           default:
             throw new IOException("Cannot update " + ru.getName() + " in "
@@ -293,7 +293,7 @@
           case NEW:
           case FAST_FORWARD:
             revision = rw.parseCommit(ru.getNewObjectId());
-            update.replicate(ru.getName());
+            update.fireGitRefUpdatedEvent(ru);
             return revision;
 
           default:
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 8d27c0e..5cd0def 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
@@ -22,7 +22,6 @@
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gwtorm.server.OrmException;
-
 import org.eclipse.jgit.lib.Constants;
 import org.eclipse.jgit.lib.Ref;
 import org.eclipse.jgit.lib.Repository;
@@ -44,16 +43,19 @@
       LoggerFactory.getLogger(VisibleRefFilter.class);
 
   private final TagCache tagCache;
+  private final ChangeCache changeCache;
   private final Repository db;
   private final Project.NameKey projectName;
   private final ProjectControl projectCtl;
   private final ReviewDb reviewDb;
   private final boolean showChanges;
 
-  public VisibleRefFilter(final TagCache tagCache, final Repository db,
+  public VisibleRefFilter(final TagCache tagCache, final ChangeCache changeCache,
+      final Repository db,
       final ProjectControl projectControl, final ReviewDb reviewDb,
       final boolean showChanges) {
     this.tagCache = tagCache;
+    this.changeCache = changeCache;
     this.db = db;
     this.projectName = projectControl.getProject().getNameKey();
     this.projectCtl = projectControl;
@@ -79,7 +81,7 @@
       } else if (PatchSet.isRef(ref.getName())) {
         // Reference to a patch set is visible if the change is visible.
         //
-        if (visibleChanges.contains(Change.Id.fromRef(ref.getName()))) {
+        if (showChanges && visibleChanges.contains(Change.Id.fromRef(ref.getName()))) {
           result.put(ref.getName(), ref);
         }
 
@@ -135,7 +137,7 @@
     final Project project = projectCtl.getProject();
     try {
       final Set<Change.Id> visibleChanges = new HashSet<Change.Id>();
-      for (Change change : reviewDb.changes().byProject(project.getNameKey())) {
+      for (Change change : changeCache.get(project.getNameKey())) {
         if (projectCtl.controlFor(change).isVisible(reviewDb)) {
           visibleChanges.add(change.getId());
         }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidationException.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidationException.java
new file mode 100644
index 0000000..45278f9
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidationException.java
@@ -0,0 +1,42 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git.validators;
+
+import java.util.Collections;
+import java.util.List;
+
+public class CommitValidationException extends Exception {
+  private static final long serialVersionUID = 1L;
+  private final List<CommitValidationMessage> messages;
+
+  public CommitValidationException(String reason, List<CommitValidationMessage> messages) {
+    super(reason);
+    this.messages = messages;
+  }
+
+  public CommitValidationException(String reason) {
+    super(reason);
+    this.messages = Collections.emptyList();
+  }
+
+  public CommitValidationException(String reason, Throwable why) {
+    super(reason, why);
+    this.messages = Collections.emptyList();
+  }
+
+  public List<CommitValidationMessage> getMessages() {
+    return messages;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidationListener.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidationListener.java
new file mode 100644
index 0000000..8f11243
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidationListener.java
@@ -0,0 +1,39 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.git.validators;
+
+import com.google.gerrit.extensions.annotations.ExtensionPoint;
+import com.google.gerrit.server.events.CommitReceivedEvent;
+
+import java.util.List;
+
+/**
+ * Listener to provide validation on received commits.
+ *
+ * Invoked by Gerrit when a new commit is received, has passed basic Gerrit
+ * validation and can be then subject to extra validation checks.
+ */
+@ExtensionPoint
+public interface CommitValidationListener {
+  /**
+   * Commit validation.
+   *
+   * @param received commit event details
+   * @return list of validation messages
+   * @throws CommitValidationException if validation fails
+   */
+  public List<CommitValidationMessage> onCommitReceived(CommitReceivedEvent receiveEvent)
+      throws CommitValidationException;
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidationMessage.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidationMessage.java
new file mode 100644
index 0000000..ab86317
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidationMessage.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.validators;
+
+public class CommitValidationMessage {
+  private final String message;
+  private final boolean isError;
+
+  public CommitValidationMessage(final String message, final boolean isError) {
+    this.message = message;
+    this.isError = isError;
+  }
+
+  public String getMessage() {
+    return message;
+  }
+
+  public boolean isError() {
+    return isError;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidators.java b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidators.java
new file mode 100644
index 0000000..8abe501
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/git/validators/CommitValidators.java
@@ -0,0 +1,580 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.validators;
+
+import com.google.gerrit.common.PageLinks;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.events.CommitReceivedEvent;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.git.ValidationError;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.server.project.RefControl;
+import com.google.gerrit.server.ssh.SshInfo;
+import com.google.gerrit.server.util.MagicBranch;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+import com.jcraft.jsch.HostKey;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.lib.PersonIdent;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.FooterKey;
+import org.eclipse.jgit.revwalk.FooterLine;
+import org.eclipse.jgit.revwalk.RevCommit;
+import org.eclipse.jgit.util.SystemReader;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.Collections;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.regex.Pattern;
+
+import javax.annotation.Nullable;
+
+public class CommitValidators {
+  private static final Logger log = LoggerFactory
+      .getLogger(CommitValidators.class);
+
+  private static final FooterKey CHANGE_ID = new FooterKey("Change-Id");
+
+  private static final Pattern NEW_PATCHSET = Pattern
+      .compile("^refs/changes/(?:[0-9][0-9])?(/[1-9][0-9]*){1,2}(?:/new)?$");
+
+  public interface Factory {
+    CommitValidators create(RefControl refControl, SshInfo sshInfo,
+        Repository repo);
+  }
+
+  private final PersonIdent gerritIdent;
+  private final RefControl refControl;
+  private final String canonicalWebUrl;
+  private final SshInfo sshInfo;
+  private final Repository repo;
+  private final DynamicSet<CommitValidationListener> commitValidationListeners;
+
+  @Inject
+  CommitValidators(@GerritPersonIdent final PersonIdent gerritIdent,
+      @CanonicalWebUrl @Nullable final String canonicalWebUrl,
+      final DynamicSet<CommitValidationListener> commitValidationListeners,
+      @Assisted final SshInfo sshInfo,
+      @Assisted final Repository repo, @Assisted final RefControl refControl) {
+    this.gerritIdent = gerritIdent;
+    this.refControl = refControl;
+    this.canonicalWebUrl = canonicalWebUrl;
+    this.sshInfo = sshInfo;
+    this.repo = repo;
+    this.commitValidationListeners = commitValidationListeners;
+  }
+
+  public List<CommitValidationMessage> validateForReceiveCommits(
+      CommitReceivedEvent receiveEvent) throws CommitValidationException {
+
+    List<CommitValidationListener> validators =
+        new LinkedList<CommitValidationListener>();
+
+    validators.add(new UploadMergesPermissionValidator(refControl));
+    validators.add(new AmendedGerritMergeCommitValidationListener(
+        refControl, gerritIdent));
+    validators.add(new AuthorUploaderValidator(refControl, canonicalWebUrl));
+    validators.add(new CommitterUploaderValidator(refControl, canonicalWebUrl));
+    validators.add(new SignedOffByValidator(refControl, canonicalWebUrl));
+    if (MagicBranch.isMagicBranch(receiveEvent.command.getRefName())
+        || NEW_PATCHSET.matcher(receiveEvent.command.getRefName()).matches()) {
+      validators.add(new ChangeIdValidator(refControl, canonicalWebUrl, sshInfo));
+    }
+    validators.add(new ConfigValidator(refControl, repo));
+    validators.add(new PluginCommitValidationListener(commitValidationListeners));
+
+    List<CommitValidationMessage> messages =
+        new LinkedList<CommitValidationMessage>();
+
+    try {
+      for (CommitValidationListener commitValidator : validators) {
+        messages.addAll(commitValidator.onCommitReceived(receiveEvent));
+      }
+    } catch (CommitValidationException e) {
+      // Keep the old messages (and their order) in case of an exception
+      messages.addAll(e.getMessages());
+      throw new CommitValidationException(e.getMessage(), messages);
+    }
+    return messages;
+  }
+
+  public List<CommitValidationMessage> validateForGerritCommits(
+      CommitReceivedEvent receiveEvent) throws CommitValidationException {
+
+    List<CommitValidationListener> validators =
+        new LinkedList<CommitValidationListener>();
+
+    validators.add(new UploadMergesPermissionValidator(refControl));
+    validators.add(new AmendedGerritMergeCommitValidationListener(
+        refControl, gerritIdent));
+    validators.add(new AuthorUploaderValidator(refControl, canonicalWebUrl));
+    validators.add(new SignedOffByValidator(refControl, canonicalWebUrl));
+    if (MagicBranch.isMagicBranch(receiveEvent.command.getRefName())
+        || NEW_PATCHSET.matcher(receiveEvent.command.getRefName()).matches()) {
+      validators.add(new ChangeIdValidator(refControl, canonicalWebUrl, sshInfo));
+    }
+    validators.add(new ConfigValidator(refControl, repo));
+    validators.add(new PluginCommitValidationListener(commitValidationListeners));
+
+    List<CommitValidationMessage> messages =
+        new LinkedList<CommitValidationMessage>();
+
+    try {
+      for (CommitValidationListener commitValidator : validators) {
+        messages.addAll(commitValidator.onCommitReceived(receiveEvent));
+      }
+    } catch (CommitValidationException e) {
+      // Keep the old messages (and their order) in case of an exception
+      messages.addAll(e.getMessages());
+      throw new CommitValidationException(e.getMessage(), messages);
+    }
+    return messages;
+  }
+
+  public static class ChangeIdValidator implements CommitValidationListener {
+    private final RefControl refControl;
+    private final String canonicalWebUrl;
+    private final SshInfo sshInfo;
+
+    public ChangeIdValidator(RefControl refControl, String canonicalWebUrl,
+        SshInfo sshInfo) {
+      this.refControl = refControl;
+      this.canonicalWebUrl = canonicalWebUrl;
+      this.sshInfo = sshInfo;
+    }
+
+    @Override
+    public List<CommitValidationMessage> onCommitReceived(
+        CommitReceivedEvent receiveEvent) throws CommitValidationException {
+
+      final ProjectControl projectControl = refControl.getProjectControl();
+      IdentifiedUser currentUser = (IdentifiedUser) refControl.getCurrentUser();
+      final List<String> idList = receiveEvent.commit.getFooterLines(CHANGE_ID);
+
+      List<CommitValidationMessage> messages =
+          new LinkedList<CommitValidationMessage>();
+
+      if (idList.isEmpty()) {
+        if (projectControl.getProjectState().isRequireChangeID()) {
+          String errMsg = "missing Change-Id in commit message footer";
+          messages.add(getFixedCommitMsgWithChangeId(errMsg, receiveEvent.commit,
+              currentUser, canonicalWebUrl, sshInfo));
+          throw new CommitValidationException(errMsg, messages);
+        }
+      } else if (idList.size() > 1) {
+        throw new CommitValidationException(
+            "multiple Change-Id lines in commit message footer", messages);
+      } 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 footer";
+          messages.add(getFixedCommitMsgWithChangeId(errMsg, receiveEvent.commit,
+              currentUser, canonicalWebUrl, sshInfo));
+          throw new CommitValidationException(errMsg, messages);
+        }
+      }
+      return Collections.<CommitValidationMessage>emptyList();
+    }
+  }
+
+  /**
+   * If this is the special project configuration branch, validate the config.
+   */
+  public static class ConfigValidator implements CommitValidationListener {
+    private final RefControl refControl;
+    private final Repository repo;
+
+    public ConfigValidator(RefControl refControl, Repository repo) {
+      this.refControl = refControl;
+      this.repo = repo;
+    }
+
+    @Override
+    public List<CommitValidationMessage> onCommitReceived(
+        CommitReceivedEvent receiveEvent) throws CommitValidationException {
+      IdentifiedUser currentUser = (IdentifiedUser) refControl.getCurrentUser();
+
+      if (GitRepositoryManager.REF_CONFIG.equals(refControl.getRefName())) {
+        List<CommitValidationMessage> messages =
+            new LinkedList<CommitValidationMessage>();
+
+        try {
+          ProjectConfig cfg =
+              new ProjectConfig(receiveEvent.project.getNameKey());
+          cfg.load(repo, receiveEvent.command.getNewId());
+          if (!cfg.getValidationErrors().isEmpty()) {
+            addError("Invalid project configuration:", messages);
+            for (ValidationError err : cfg.getValidationErrors()) {
+              addError("  " + err.getMessage(), messages);
+            }
+            throw new ConfigInvalidException("invalid project configuration");
+          }
+        } catch (Exception e) {
+          log.error("User " + currentUser.getUserName()
+              + " tried to push invalid project configuration "
+              + receiveEvent.command.getNewId().name() + " for "
+              + receiveEvent.project.getName(), e);
+          throw new CommitValidationException("invalid project configuration",
+              messages);
+        }
+      }
+      return Collections.<CommitValidationMessage>emptyList();
+    }
+  }
+
+  /** Require permission to upload merges. */
+  public static class UploadMergesPermissionValidator implements
+      CommitValidationListener {
+    private final RefControl refControl;
+
+    public UploadMergesPermissionValidator(RefControl refControl) {
+      this.refControl = refControl;
+    }
+
+    @Override
+    public List<CommitValidationMessage> onCommitReceived(
+        CommitReceivedEvent receiveEvent) throws CommitValidationException {
+      if (receiveEvent.commit.getParentCount() > 1
+          && !refControl.canUploadMerges()) {
+        throw new CommitValidationException("you are not allowed to upload merges");
+      }
+      return Collections.<CommitValidationMessage>emptyList();
+    }
+  }
+
+  /** Execute commit validation plug-ins */
+  public static class PluginCommitValidationListener implements
+      CommitValidationListener {
+    private final DynamicSet<CommitValidationListener> commitValidationListeners;
+
+    public PluginCommitValidationListener(
+        final DynamicSet<CommitValidationListener> commitValidationListeners) {
+      this.commitValidationListeners = commitValidationListeners;
+    }
+
+    @Override
+    public List<CommitValidationMessage> onCommitReceived(
+        CommitReceivedEvent receiveEvent) throws CommitValidationException {
+      List<CommitValidationMessage> messages =
+          new LinkedList<CommitValidationMessage>();
+
+      for (CommitValidationListener validator : commitValidationListeners) {
+        try {
+          messages.addAll(validator.onCommitReceived(receiveEvent));
+        } catch (CommitValidationException e) {
+          messages.addAll(e.getMessages());
+          throw new CommitValidationException(e.getMessage(), messages);
+        }
+      }
+      return messages;
+    }
+  }
+
+  public static class SignedOffByValidator implements CommitValidationListener {
+    private final RefControl refControl;
+
+    public SignedOffByValidator(RefControl refControl, String canonicalWebUrl) {
+      this.refControl = refControl;
+    }
+
+    @Override
+    public List<CommitValidationMessage> onCommitReceived(
+        CommitReceivedEvent receiveEvent) throws CommitValidationException {
+      IdentifiedUser currentUser = (IdentifiedUser) refControl.getCurrentUser();
+      final PersonIdent committer = receiveEvent.commit.getCommitterIdent();
+      final PersonIdent author = receiveEvent.commit.getAuthorIdent();
+      final ProjectControl projectControl = refControl.getProjectControl();
+
+      if (projectControl.getProjectState().isUseSignedOffBy()) {
+        boolean sboAuthor = false, sboCommitter = false, sboMe = false;
+        for (final FooterLine footer : receiveEvent.commit.getFooterLines()) {
+          if (footer.matches(FooterKey.SIGNED_OFF_BY)) {
+            final String e = footer.getEmailAddress();
+            if (e != null) {
+              sboAuthor |= author.getEmailAddress().equals(e);
+              sboCommitter |= committer.getEmailAddress().equals(e);
+              sboMe |= currentUser.getEmailAddresses().contains(e);
+            }
+          }
+        }
+        if (!sboAuthor && !sboCommitter && !sboMe
+            && !refControl.canForgeCommitter()) {
+          throw new CommitValidationException(
+              "not Signed-off-by author/committer/uploader in commit message footer");
+        }
+      }
+      return Collections.<CommitValidationMessage>emptyList();
+    }
+  }
+
+  /** Require that author matches the uploader. */
+  public static class AuthorUploaderValidator implements
+      CommitValidationListener {
+    private final RefControl refControl;
+    private final String canonicalWebUrl;
+
+    public AuthorUploaderValidator(RefControl refControl, String canonicalWebUrl) {
+      this.refControl = refControl;
+      this.canonicalWebUrl = canonicalWebUrl;
+    }
+
+    @Override
+    public List<CommitValidationMessage> onCommitReceived(
+        CommitReceivedEvent receiveEvent) throws CommitValidationException {
+      IdentifiedUser currentUser = (IdentifiedUser) refControl.getCurrentUser();
+      final PersonIdent author = receiveEvent.commit.getAuthorIdent();
+
+      if (!currentUser.getEmailAddresses().contains(author.getEmailAddress())
+          && !refControl.canForgeAuthor()) {
+        List<CommitValidationMessage> messages =
+            new LinkedList<CommitValidationMessage>();
+
+        messages.add(getInvalidEmailError(receiveEvent.commit, "author", author,
+            currentUser, canonicalWebUrl));
+        throw new CommitValidationException("invalid author", messages);
+      }
+      return Collections.<CommitValidationMessage>emptyList();
+    }
+  }
+
+  /** Require that committer matches the uploader. */
+  public static class CommitterUploaderValidator implements
+      CommitValidationListener {
+    private final RefControl refControl;
+    private final String canonicalWebUrl;
+
+    public CommitterUploaderValidator(RefControl refControl,
+        String canonicalWebUrl) {
+      this.refControl = refControl;
+      this.canonicalWebUrl = canonicalWebUrl;
+    }
+
+    @Override
+    public List<CommitValidationMessage> onCommitReceived(
+        CommitReceivedEvent receiveEvent) throws CommitValidationException {
+      IdentifiedUser currentUser = (IdentifiedUser) refControl.getCurrentUser();
+      final PersonIdent committer = receiveEvent.commit.getCommitterIdent();
+      if (!currentUser.getEmailAddresses()
+          .contains(committer.getEmailAddress())
+          && !refControl.canForgeCommitter()) {
+        List<CommitValidationMessage> messages =
+            new LinkedList<CommitValidationMessage>();
+        messages.add(getInvalidEmailError(receiveEvent.commit, "committer", committer,
+            currentUser, canonicalWebUrl));
+        throw new CommitValidationException("invalid committer", messages);
+      }
+      return Collections.<CommitValidationMessage>emptyList();
+    }
+  }
+
+  /**
+   * Don't allow the user to amend a merge created by Gerrit Code Review. This
+   * seems to happen all too often, due to users not paying any attention to
+   * what they are doing.
+   */
+  public static class AmendedGerritMergeCommitValidationListener implements
+      CommitValidationListener {
+    private final PersonIdent gerritIdent;
+    private final RefControl refControl;
+
+    public AmendedGerritMergeCommitValidationListener(
+        final RefControl refControl, final PersonIdent gerritIdent) {
+      this.refControl = refControl;
+      this.gerritIdent = gerritIdent;
+    }
+
+    @Override
+    public List<CommitValidationMessage> onCommitReceived(
+        CommitReceivedEvent receiveEvent) throws CommitValidationException {
+      final PersonIdent author = receiveEvent.commit.getAuthorIdent();
+
+      if (receiveEvent.commit.getParentCount() > 1
+          && author.getName().equals(gerritIdent.getName())
+          && author.getEmailAddress().equals(gerritIdent.getEmailAddress())
+          && !refControl.canForgeGerritServerIdentity()) {
+        throw new CommitValidationException("do not amend merges not made by you");
+      }
+      return Collections.<CommitValidationMessage>emptyList();
+    }
+  }
+
+  private static CommitValidationMessage getInvalidEmailError(RevCommit c, String type,
+      PersonIdent who, IdentifiedUser currentUser, String canonicalWebUrl) {
+    StringBuilder sb = new StringBuilder();
+    sb.append("\n");
+    sb.append("ERROR:  In commit " + c.name() + "\n");
+    sb.append("ERROR:  " + type + " email address " + who.getEmailAddress()
+        + "\n");
+    sb.append("ERROR:  does not match your user account.\n");
+    sb.append("ERROR:\n");
+    if (currentUser.getEmailAddresses().isEmpty()) {
+      sb.append("ERROR:  You have not registered any email addresses.\n");
+    } else {
+      sb.append("ERROR:  The following addresses are currently registered:\n");
+      for (String address : currentUser.getEmailAddresses()) {
+        sb.append("ERROR:    " + address + "\n");
+      }
+    }
+    sb.append("ERROR:\n");
+    if (canonicalWebUrl != null) {
+      sb.append("ERROR:  To register an email address, please visit:\n");
+      sb.append("ERROR:  " + canonicalWebUrl + "#" + PageLinks.SETTINGS_CONTACT
+          + "\n");
+    }
+    sb.append("\n");
+    return new CommitValidationMessage(sb.toString(), false);
+  }
+
+  /**
+   * We handle 3 cases:
+   * 1. No change id in the commit message at all.
+   * 2. Change id last in the commit message but missing empty line to create the footer.
+   * 3. There is a change-id somewhere in the commit message, but we ignore it.
+   *
+   * @return The fixed up commit message
+   */
+  private static CommitValidationMessage getFixedCommitMsgWithChangeId(final String errMsg,
+      final RevCommit c, final IdentifiedUser currentUser,
+      String canonicalWebUrl, final SshInfo sshInfo) {
+    final String changeId = "Change-Id:";
+    StringBuilder sb = new StringBuilder();
+    sb.append("ERROR: ").append(errMsg);
+    sb.append('\n');
+    sb.append("Suggestion for commit message:\n");
+
+    if (c.getFullMessage().indexOf(changeId) == -1) {
+      sb.append(c.getFullMessage());
+      sb.append('\n');
+      sb.append(changeId).append(" I").append(c.name());
+    } else {
+      String lines[] = c.getFullMessage().trim().split("\n");
+      String lastLine = lines.length > 0 ? lines[lines.length - 1] : "";
+
+      if (lastLine.indexOf(changeId) == 0) {
+        for (int i = 0; i < lines.length - 1; i++) {
+          sb.append(lines[i]);
+          sb.append('\n');
+        }
+
+        sb.append('\n');
+        sb.append(lastLine);
+      } else {
+        sb.append(c.getFullMessage());
+        sb.append('\n');
+        sb.append(changeId).append(" I").append(c.name());
+        sb.append('\n');
+        sb.append("Hint: A potential Change-Id was found, but it was not in the ");
+        sb.append("footer (last paragraph) of the commit message.");
+      }
+    }
+    sb.append('\n');
+    sb.append('\n');
+    sb.append("Hint: To automatically insert Change-Id, install the hook:\n");
+    sb.append(getCommitMessageHookInstallationHint(currentUser,
+        canonicalWebUrl, sshInfo)).append('\n');
+    sb.append('\n');
+
+    return new CommitValidationMessage(sb.toString(), false);
+  }
+
+  private static String getCommitMessageHookInstallationHint(
+      final IdentifiedUser currentUser, String canonicalWebUrl,
+      final SshInfo sshInfo) {
+    final List<HostKey> hostKeys = sshInfo.getHostKeys();
+
+    // If there are no SSH keys, the commit-msg hook must be installed via
+    // HTTP(S)
+    if (hostKeys.isEmpty()) {
+      String p = ".git/hooks/commit-msg";
+      return String.format(
+          "  curl -o %s %s/tools/hooks/commit-msg ; chmod +x %s", p,
+          getGerritUrl(canonicalWebUrl), p);
+    }
+
+    // SSH keys exist, so the hook can be installed with scp.
+    String sshHost;
+    int sshPort;
+    String host = hostKeys.get(0).getHost();
+    int c = host.lastIndexOf(':');
+    if (0 <= c) {
+      if (host.startsWith("*:")) {
+        sshHost = getGerritHost(canonicalWebUrl);
+      } else {
+        sshHost = host.substring(0, c);
+      }
+      sshPort = Integer.parseInt(host.substring(c + 1));
+    } else {
+      sshHost = host;
+      sshPort = 22;
+    }
+
+    return String.format("  scp -p -P %d %s@%s:hooks/commit-msg .git/hooks/",
+        sshPort, currentUser.getUserName(), sshHost);
+  }
+
+  /**
+   * Get the Gerrit URL.
+   *
+   * @return the canonical URL (with any trailing slash removed) if it is
+   *         configured, otherwise fall back to "http://hostname" where hostname
+   *         is the value returned by {@link #getGerritHost()}.
+   */
+  private static String getGerritUrl(String canonicalWebUrl) {
+    if (canonicalWebUrl != null) {
+      if (canonicalWebUrl.endsWith("/")) {
+        return canonicalWebUrl.substring(0, canonicalWebUrl.lastIndexOf("/"));
+      }
+      return canonicalWebUrl;
+    } else {
+      return "http://" + getGerritHost(canonicalWebUrl);
+    }
+  }
+
+  /**
+   * Get the Gerrit hostname.
+   *
+   * @return the hostname from the canonical URL if it is configured, otherwise
+   *         whatever the OS says the hostname is.
+   */
+  private static String getGerritHost(String canonicalWebUrl) {
+    String host;
+    if (canonicalWebUrl != null) {
+      try {
+        host = new URL(canonicalWebUrl).getHost();
+      } catch (MalformedURLException e) {
+        host = SystemReader.getInstance().getHostname();
+      }
+    } else {
+      host = SystemReader.getInstance().getHostname();
+    }
+    return host;
+  }
+
+  private static void addError(String error,
+      List<CommitValidationMessage> messages) {
+    messages.add(new CommitValidationMessage(error, true));
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/AddIncludedGroups.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/AddIncludedGroups.java
new file mode 100644
index 0000000..aa3f30e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/AddIncludedGroups.java
@@ -0,0 +1,174 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.group;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.gerrit.common.data.GroupDescription;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.DefaultInput;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuid;
+import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuidAudit;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.GroupControl;
+import com.google.gerrit.server.account.GroupIncludeCache;
+import com.google.gerrit.server.group.AddIncludedGroups.Input;
+import com.google.gerrit.server.group.GroupJson.GroupInfo;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.util.List;
+import java.util.Map;
+
+public class AddIncludedGroups implements RestModifyView<GroupResource, Input> {
+  static class Input {
+    @DefaultInput
+    String _oneGroup;
+
+    List<String> groups;
+
+    static Input init(Input in) {
+      if (in == null) {
+        in = new Input();
+      }
+      if (in.groups == null) {
+        in.groups = Lists.newArrayListWithCapacity(1);
+      }
+      if (!Strings.isNullOrEmpty(in._oneGroup)) {
+        in.groups.add(in._oneGroup);
+      }
+      return in;
+    }
+  }
+
+  private final Provider<GroupsCollection> groupsCollection;
+  private final GroupIncludeCache groupIncludeCache;
+  private final ReviewDb db;
+  private final GroupJson json;
+
+  @Inject
+  public AddIncludedGroups(Provider<GroupsCollection> groupsCollection,
+      GroupIncludeCache groupIncludeCache,
+      ReviewDb db, GroupJson json) {
+    this.groupsCollection = groupsCollection;
+    this.groupIncludeCache = groupIncludeCache;
+    this.db = db;
+    this.json = json;
+  }
+
+  @Override
+  public List<GroupInfo> apply(GroupResource resource, Input input)
+      throws MethodNotAllowedException, AuthException, BadRequestException,
+      UnprocessableEntityException, OrmException {
+    AccountGroup group = resource.toAccountGroup();
+    if (group == null) {
+      throw new MethodNotAllowedException();
+    }
+    input = Input.init(input);
+
+    GroupControl control = resource.getControl();
+    Map<AccountGroup.UUID, AccountGroupIncludeByUuid> newIncludedGroups = Maps.newHashMap();
+    List<AccountGroupIncludeByUuidAudit> newIncludedGroupsAudits = Lists.newLinkedList();
+    List<GroupInfo> result = Lists.newLinkedList();
+    Account.Id me = ((IdentifiedUser) control.getCurrentUser()).getAccountId();
+
+    for (String includedGroup : input.groups) {
+      GroupDescription.Basic d = groupsCollection.get().parse(includedGroup);
+      if (!control.canAddGroup(d.getGroupUUID())) {
+        throw new AuthException(String.format("Cannot add group: %s",
+            d.getName()));
+      }
+
+      if (!newIncludedGroups.containsKey(d.getGroupUUID())) {
+        AccountGroupIncludeByUuid.Key agiKey =
+            new AccountGroupIncludeByUuid.Key(group.getId(),
+                d.getGroupUUID());
+        AccountGroupIncludeByUuid agi = db.accountGroupIncludesByUuid().get(agiKey);
+        if (agi == null) {
+          agi = new AccountGroupIncludeByUuid(agiKey);
+          newIncludedGroups.put(d.getGroupUUID(), agi);
+          newIncludedGroupsAudits.add(new AccountGroupIncludeByUuidAudit(agi, me));
+        }
+      }
+      result.add(json.format(d));
+    }
+
+    if (!newIncludedGroups.isEmpty()) {
+      db.accountGroupIncludesByUuidAudit().insert(newIncludedGroupsAudits);
+      db.accountGroupIncludesByUuid().insert(newIncludedGroups.values());
+      for (AccountGroupIncludeByUuid agi : newIncludedGroups.values()) {
+        groupIncludeCache.evictMemberIn(agi.getIncludeUUID());
+      }
+      groupIncludeCache.evictMembersOf(group.getGroupUUID());
+    }
+
+    return result;
+  }
+
+  static class PutIncludedGroup implements RestModifyView<GroupResource, PutIncludedGroup.Input> {
+    static class Input {
+    }
+
+    private final Provider<AddIncludedGroups> put;
+    private final String id;
+
+    PutIncludedGroup(Provider<AddIncludedGroups> put, String id) {
+      this.put = put;
+      this.id = id;
+    }
+
+    @Override
+    public GroupInfo apply(GroupResource resource, Input input)
+        throws MethodNotAllowedException, AuthException, BadRequestException,
+        UnprocessableEntityException, OrmException {
+      AddIncludedGroups.Input in = new AddIncludedGroups.Input();
+      in.groups = ImmutableList.of(id);
+      List<GroupInfo> list = put.get().apply(resource, in);
+      if (list.size() == 1) {
+        return list.get(0);
+      }
+      throw new IllegalStateException();
+    }
+  }
+
+  static class UpdateIncludedGroup implements RestModifyView<IncludedGroupResource, PutIncludedGroup.Input> {
+    static class Input {
+    }
+
+    private final Provider<GetIncludedGroup> get;
+
+    @Inject
+    UpdateIncludedGroup(Provider<GetIncludedGroup> get) {
+      this.get = get;
+    }
+
+    @Override
+    public Object apply(IncludedGroupResource resource,
+        PutIncludedGroup.Input input) throws MethodNotAllowedException, OrmException {
+      // Do nothing, the group is already included.
+      return get.get().apply(resource);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/AddMembers.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/AddMembers.java
new file mode 100644
index 0000000..7f32a4d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/AddMembers.java
@@ -0,0 +1,221 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.group;
+
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.DefaultInput;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupMember;
+import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
+import com.google.gerrit.reviewdb.client.AuthType;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountCache;
+import com.google.gerrit.server.account.AccountException;
+import com.google.gerrit.server.account.AccountInfo;
+import com.google.gerrit.server.account.AccountManager;
+import com.google.gerrit.server.account.AccountResolver;
+import com.google.gerrit.server.account.AccountsCollection;
+import com.google.gerrit.server.account.AuthRequest;
+import com.google.gerrit.server.account.GroupControl;
+import com.google.gerrit.server.config.AuthConfig;
+import com.google.gerrit.server.group.AddMembers.Input;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.util.List;
+import java.util.Map;
+
+class AddMembers implements RestModifyView<GroupResource, Input> {
+  static class Input {
+    @DefaultInput
+    String _oneMember;
+
+    List<String> members;
+
+    static Input init(Input in) {
+      if (in == null) {
+        in = new Input();
+      }
+      if (in.members == null) {
+        in.members = Lists.newArrayListWithCapacity(1);
+      }
+      if (!Strings.isNullOrEmpty(in._oneMember)) {
+        in.members.add(in._oneMember);
+      }
+      return in;
+    }
+  }
+
+  private final AccountManager accountManager;
+  private final AuthType authType;
+  private final Provider<AccountsCollection> accounts;
+  private final AccountResolver accountResolver;
+  private final AccountCache accountCache;
+  private final ReviewDb db;
+
+  @Inject
+  AddMembers(AccountManager accountManager,
+      AuthConfig authConfig,
+      Provider<AccountsCollection> accounts,
+      AccountResolver accountResolver,
+      AccountCache accountCache,
+      ReviewDb db) {
+    this.accountManager = accountManager;
+    this.authType = authConfig.getAuthType();
+    this.accounts = accounts;
+    this.accountResolver = accountResolver;
+    this.accountCache = accountCache;
+    this.db = db;
+  }
+
+  @Override
+  public List<AccountInfo> apply(GroupResource resource, Input input)
+      throws AuthException, MethodNotAllowedException,
+      UnprocessableEntityException, OrmException {
+    AccountGroup internalGroup = resource.toAccountGroup();
+    if (internalGroup == null) {
+      throw new MethodNotAllowedException();
+    }
+    input = Input.init(input);
+
+    GroupControl control = resource.getControl();
+    Map<Account.Id, AccountGroupMember> newAccountGroupMembers = Maps.newHashMap();
+    List<AccountGroupMemberAudit> newAccountGroupMemberAudits = Lists.newLinkedList();
+    List<AccountInfo> result = Lists.newLinkedList();
+    Account.Id me = ((IdentifiedUser) control.getCurrentUser()).getAccountId();
+
+    for (String nameOrEmail : input.members) {
+      Account a = findAccount(nameOrEmail);
+      if (!a.isActive()) {
+        throw new UnprocessableEntityException(String.format(
+            "Account Inactive: %s", nameOrEmail));
+      }
+
+      if (!control.canAddMember(a.getId())) {
+        throw new AuthException("Cannot add member: " + a.getFullName());
+      }
+
+      if (!newAccountGroupMembers.containsKey(a.getId())) {
+        AccountGroupMember.Key key =
+            new AccountGroupMember.Key(a.getId(), internalGroup.getId());
+        AccountGroupMember m = db.accountGroupMembers().get(key);
+        if (m == null) {
+          m = new AccountGroupMember(key);
+          newAccountGroupMembers.put(m.getAccountId(), m);
+          newAccountGroupMemberAudits.add(new AccountGroupMemberAudit(m, me));
+        }
+      }
+      result.add(AccountInfo.parse(a, true));
+    }
+
+    db.accountGroupMembersAudit().insert(newAccountGroupMemberAudits);
+    db.accountGroupMembers().insert(newAccountGroupMembers.values());
+    for (AccountGroupMember m : newAccountGroupMembers.values()) {
+      accountCache.evict(m.getAccountId());
+    }
+
+    return result;
+  }
+
+  private Account findAccount(String nameOrEmail) throws AuthException,
+      UnprocessableEntityException, OrmException {
+    try {
+      return accounts.get().parse(nameOrEmail).getAccount();
+    } catch (UnprocessableEntityException e) {
+      // might be because the account does not exist or because the account is
+      // not visible
+      switch (authType) {
+        case HTTP_LDAP:
+        case CLIENT_SSL_CERT_LDAP:
+        case LDAP:
+          if (accountResolver.find(nameOrEmail) == null) {
+            // account does not exist, try to create it
+            return createAccountByLdap(nameOrEmail);
+          }
+          break;
+        default:
+      }
+      throw e;
+    }
+  }
+
+  private Account createAccountByLdap(String user) {
+    if (!user.matches(Account.USER_NAME_PATTERN)) {
+      return null;
+    }
+
+    try {
+      AuthRequest req = AuthRequest.forUser(user);
+      req.setSkipAuthentication(true);
+      return accountCache.get(accountManager.authenticate(req).getAccountId())
+          .getAccount();
+    } catch (AccountException e) {
+      return null;
+    }
+  }
+
+  static class PutMember implements RestModifyView<GroupResource, PutMember.Input> {
+    static class Input {
+    }
+
+    private final Provider<AddMembers> put;
+    private final String id;
+
+    PutMember(Provider<AddMembers> put, String id) {
+      this.put = put;
+      this.id = id;
+    }
+
+    @Override
+    public Object apply(GroupResource resource, PutMember.Input input)
+        throws AuthException, MethodNotAllowedException,
+        UnprocessableEntityException, OrmException {
+      AddMembers.Input in = new AddMembers.Input();
+      in._oneMember = id;
+      List<AccountInfo> list = put.get().apply(resource, in);
+      if (list.size() == 1) {
+        return list.get(0);
+      }
+      throw new IllegalStateException();
+    }
+  }
+
+  static class UpdateMember implements RestModifyView<MemberResource, PutMember.Input> {
+    static class Input {
+    }
+
+    private final Provider<GetMember> get;
+
+    @Inject
+    UpdateMember(Provider<GetMember> get) {
+      this.get = get;
+    }
+
+    @Override
+    public Object apply(MemberResource resource, PutMember.Input input) {
+      // Do nothing, the user is already a member.
+      return get.get().apply(resource);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/CreateGroup.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/CreateGroup.java
new file mode 100644
index 0000000..6914cf2
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/CreateGroup.java
@@ -0,0 +1,116 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.group;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Strings;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.common.data.GroupDescription;
+import com.google.gerrit.common.data.GroupDescriptions;
+import com.google.gerrit.common.errors.NameAlreadyUsedException;
+import com.google.gerrit.common.errors.PermissionDeniedException;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.extensions.restapi.Url;
+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.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.group.CreateGroup.Input;
+import com.google.gerrit.server.group.GroupJson.GroupInfo;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.assistedinject.Assisted;
+
+import org.eclipse.jgit.lib.Config;
+
+import java.util.Collections;
+
+@RequiresCapability(GlobalCapability.CREATE_GROUP)
+class CreateGroup implements RestModifyView<TopLevelResource, Input> {
+  static class Input {
+    String name;
+    String description;
+    Boolean visibleToAll;
+    String ownerId;
+  }
+
+  static interface Factory {
+    CreateGroup create(@Assisted String name);
+  }
+
+  private final Provider<IdentifiedUser> self;
+  private final GroupsCollection groups;
+  private final PerformCreateGroup.Factory op;
+  private final GroupJson json;
+  private final boolean defaultVisibleToAll;
+  private final String name;
+
+  @Inject
+  CreateGroup(Provider<IdentifiedUser> self, GroupsCollection groups,
+      PerformCreateGroup.Factory performCreateGroupFactory, GroupJson json,
+      @GerritServerConfig Config cfg, @Assisted String name) {
+    this.self = self;
+    this.groups = groups;
+    this.op = performCreateGroupFactory;
+    this.json = json;
+    this.defaultVisibleToAll = cfg.getBoolean("groups", "newGroupsVisibleToAll", false);
+    this.name = name;
+  }
+
+  @Override
+  public GroupInfo apply(TopLevelResource resource, Input input)
+      throws AuthException, BadRequestException, UnprocessableEntityException,
+      NameAlreadyUsedException, OrmException {
+    if (input == null) {
+      input = new Input();
+    }
+    if (input.name != null && !name.equals(input.name)) {
+      throw new BadRequestException("name must match URL");
+    }
+
+    AccountGroup.Id ownerId = owner(input);
+    AccountGroup group;
+    try {
+      group = op.create().createGroup(
+          name,
+          Strings.emptyToNull(input.description),
+          Objects.firstNonNull(input.visibleToAll, defaultVisibleToAll),
+          ownerId,
+          ownerId == null
+            ? Collections.singleton(self.get().getAccountId())
+            : Collections.<Account.Id> emptySet(),
+          null);
+    } catch (PermissionDeniedException e) {
+      throw new AuthException(e.getMessage());
+    }
+    return json.format(GroupDescriptions.forAccountGroup(group));
+  }
+
+  private AccountGroup.Id owner(Input input)
+      throws UnprocessableEntityException {
+    if (input.ownerId != null) {
+      GroupDescription.Basic d = groups.parseInternal(Url.decode(input.ownerId));
+      return GroupDescriptions.toAccountGroup(d).getId();
+    }
+    return null;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteIncludedGroups.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteIncludedGroups.java
new file mode 100644
index 0000000..15f9325
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteIncludedGroups.java
@@ -0,0 +1,153 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.group;
+
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.gerrit.common.data.GroupDescription;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuid;
+import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuidAudit;
+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.GroupControl;
+import com.google.gerrit.server.account.GroupIncludeCache;
+import com.google.gerrit.server.group.AddIncludedGroups.Input;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.util.List;
+import java.util.Map;
+
+public class DeleteIncludedGroups implements RestModifyView<GroupResource, Input> {
+  private final Provider<GroupsCollection> groupsCollection;
+  private final GroupIncludeCache groupIncludeCache;
+  private final ReviewDb db;
+  private final Provider<CurrentUser> self;
+
+  @Inject
+  DeleteIncludedGroups(Provider<GroupsCollection> groupsCollection,
+      GroupIncludeCache groupIncludeCache, ReviewDb db,
+      Provider<CurrentUser> self) {
+    this.groupsCollection = groupsCollection;
+    this.groupIncludeCache = groupIncludeCache;
+    this.db = db;
+    this.self = self;
+  }
+
+  @Override
+  public Object apply(GroupResource resource, Input input)
+      throws MethodNotAllowedException, AuthException, BadRequestException,
+      UnprocessableEntityException, OrmException {
+    AccountGroup internalGroup = resource.toAccountGroup();
+    if (internalGroup == null) {
+      throw new MethodNotAllowedException();
+    }
+    input = Input.init(input);
+
+    final GroupControl control = resource.getControl();
+    final Map<AccountGroup.UUID, AccountGroupIncludeByUuid> includedGroups = getIncludedGroups(internalGroup.getId());
+    final List<AccountGroupIncludeByUuid> toRemove = Lists.newLinkedList();
+
+    for (final String includedGroup : input.groups) {
+      GroupDescription.Basic d = groupsCollection.get().parse(includedGroup);
+      if (!control.canRemoveGroup(d.getGroupUUID())) {
+        throw new AuthException(String.format("Cannot delete group: %s",
+            d.getName()));
+      }
+
+      AccountGroupIncludeByUuid g = includedGroups.remove(d.getGroupUUID());
+      if (g != null) {
+        toRemove.add(g);
+      }
+    }
+
+    if (!toRemove.isEmpty()) {
+      writeAudits(toRemove);
+      db.accountGroupIncludesByUuid().delete(toRemove);
+      for (final AccountGroupIncludeByUuid g : toRemove) {
+        groupIncludeCache.evictMemberIn(g.getIncludeUUID());
+      }
+      groupIncludeCache.evictMembersOf(internalGroup.getGroupUUID());
+    }
+
+    return Response.none();
+  }
+
+  private Map<AccountGroup.UUID, AccountGroupIncludeByUuid> getIncludedGroups(
+      final AccountGroup.Id groupId) throws OrmException {
+    final Map<AccountGroup.UUID, AccountGroupIncludeByUuid> groups =
+        Maps.newHashMap();
+    for (final AccountGroupIncludeByUuid g : db.accountGroupIncludesByUuid().byGroup(groupId)) {
+      groups.put(g.getIncludeUUID(), g);
+    }
+    return groups;
+  }
+
+  private void writeAudits(final List<AccountGroupIncludeByUuid> toBeRemoved)
+      throws OrmException {
+    final Account.Id me = ((IdentifiedUser) self.get()).getAccountId();
+    final List<AccountGroupIncludeByUuidAudit> auditUpdates = Lists.newLinkedList();
+    for (final AccountGroupIncludeByUuid g : toBeRemoved) {
+      AccountGroupIncludeByUuidAudit audit = null;
+      for (AccountGroupIncludeByUuidAudit a : db
+          .accountGroupIncludesByUuidAudit().byGroupInclude(g.getGroupId(),
+              g.getIncludeUUID())) {
+        if (a.isActive()) {
+          audit = a;
+          break;
+        }
+      }
+
+      if (audit != null) {
+        audit.removed(me);
+        auditUpdates.add(audit);
+      }
+    }
+    db.accountGroupIncludesByUuidAudit().update(auditUpdates);
+  }
+
+  static class DeleteIncludedGroup implements
+      RestModifyView<IncludedGroupResource, DeleteIncludedGroup.Input> {
+    static class Input {
+    }
+
+    private final Provider<DeleteIncludedGroups> delete;
+
+    @Inject
+    DeleteIncludedGroup(final Provider<DeleteIncludedGroups> delete) {
+      this.delete = delete;
+    }
+
+    @Override
+    public Object apply(IncludedGroupResource resource, Input input)
+        throws MethodNotAllowedException, AuthException, BadRequestException,
+        UnprocessableEntityException, OrmException {
+      AddIncludedGroups.Input in = new AddIncludedGroups.Input();
+      in.groups = ImmutableList.of(resource.getMember().get());
+      return delete.get().apply(resource, in);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteMembers.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteMembers.java
new file mode 100644
index 0000000..07276ca
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/DeleteMembers.java
@@ -0,0 +1,151 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.group;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupMember;
+import com.google.gerrit.reviewdb.client.AccountGroupMemberAudit;
+import com.google.gerrit.reviewdb.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.AccountsCollection;
+import com.google.gerrit.server.account.GroupControl;
+import com.google.gerrit.server.group.AddMembers.Input;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.util.List;
+import java.util.Map;
+
+public class DeleteMembers implements RestModifyView<GroupResource, Input> {
+  private final Provider<AccountsCollection> accounts;
+  private final AccountCache accountCache;
+  private final ReviewDb db;
+  private final Provider<CurrentUser> self;
+
+  @Inject
+  DeleteMembers(Provider<AccountsCollection> accounts,
+      AccountCache accountCache, ReviewDb db, Provider<CurrentUser> self) {
+    this.accounts = accounts;
+    this.accountCache = accountCache;
+    this.db = db;
+    this.self = self;
+  }
+
+  @Override
+  public Object apply(GroupResource resource, Input input)
+      throws AuthException, MethodNotAllowedException,
+      UnprocessableEntityException, OrmException {
+    AccountGroup internalGroup = resource.toAccountGroup();
+    if (internalGroup == null) {
+      throw new MethodNotAllowedException();
+    }
+    input = Input.init(input);
+
+    final GroupControl control = resource.getControl();
+    final Map<Account.Id, AccountGroupMember> members = getMembers(internalGroup.getId());
+    final List<AccountGroupMember> toRemove = Lists.newLinkedList();
+
+    for (final String nameOrEmail : input.members) {
+      Account a = accounts.get().parse(nameOrEmail).getAccount();
+
+      if (!control.canRemoveMember(a.getId())) {
+        throw new AuthException("Cannot delete member: " + a.getFullName());
+      }
+
+      final AccountGroupMember m = members.remove(a.getId());
+      if (m != null) {
+        toRemove.add(m);
+      }
+    }
+
+    writeAudits(toRemove);
+    db.accountGroupMembers().delete(toRemove);
+    for (final AccountGroupMember m : toRemove) {
+      accountCache.evict(m.getAccountId());
+    }
+
+    return Response.none();
+  }
+
+  private void writeAudits(final List<AccountGroupMember> toBeRemoved)
+      throws OrmException {
+    final Account.Id me = ((IdentifiedUser) self.get()).getAccountId();
+    final List<AccountGroupMemberAudit> auditUpdates = Lists.newLinkedList();
+    final List<AccountGroupMemberAudit> auditInserts = Lists.newLinkedList();
+    for (final AccountGroupMember m : toBeRemoved) {
+      AccountGroupMemberAudit audit = null;
+      for (AccountGroupMemberAudit a : db.accountGroupMembersAudit()
+          .byGroupAccount(m.getAccountGroupId(), m.getAccountId())) {
+        if (a.isActive()) {
+          audit = a;
+          break;
+        }
+      }
+
+      if (audit != null) {
+        audit.removed(me);
+        auditUpdates.add(audit);
+      } else {
+        audit = new AccountGroupMemberAudit(m, me);
+        audit.removedLegacy();
+        auditInserts.add(audit);
+      }
+    }
+    db.accountGroupMembersAudit().update(auditUpdates);
+    db.accountGroupMembersAudit().insert(auditInserts);
+  }
+
+  private Map<Account.Id, AccountGroupMember> getMembers(
+      final AccountGroup.Id groupId) throws OrmException {
+    final Map<Account.Id, AccountGroupMember> members = Maps.newHashMap();
+    for (final AccountGroupMember m : db.accountGroupMembers().byGroup(groupId)) {
+      members.put(m.getAccountId(), m);
+    }
+    return members;
+  }
+
+  static class DeleteMember implements RestModifyView<MemberResource, DeleteMember.Input> {
+    static class Input {
+    }
+
+    private final Provider<DeleteMembers> delete;
+
+    @Inject
+    DeleteMember(final Provider<DeleteMembers> delete) {
+      this.delete = delete;
+    }
+
+    @Override
+    public Object apply(MemberResource resource, Input input)
+        throws AuthException, MethodNotAllowedException,
+        UnprocessableEntityException, OrmException, NoSuchGroupException {
+      AddMembers.Input in = new AddMembers.Input();
+      in._oneMember = resource.getMember().getAccountId().toString();
+      return delete.get().apply(resource, in);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GetDescription.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetDescription.java
new file mode 100644
index 0000000..4674a21
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetDescription.java
@@ -0,0 +1,31 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.group;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+
+class GetDescription implements RestReadView<GroupResource> {
+  @Override
+  public String apply(GroupResource resource) throws MethodNotAllowedException {
+    AccountGroup group = resource.toAccountGroup();
+    if (group == null) {
+      throw new MethodNotAllowedException();
+    }
+    return Strings.nullToEmpty(group.getDescription());
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GetDetail.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetDetail.java
new file mode 100644
index 0000000..f1d2e15
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetDetail.java
@@ -0,0 +1,36 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.group;
+
+import com.google.gerrit.common.groups.ListGroupsOption;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.group.GroupJson.GroupInfo;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+
+public class GetDetail implements RestReadView<GroupResource> {
+  private final GroupJson json;
+
+  @Inject
+  GetDetail(GroupJson json) {
+    this.json = json.addOption(ListGroupsOption.MEMBERS)
+        .addOption(ListGroupsOption.INCLUDES);
+  }
+
+  @Override
+  public GroupInfo apply(GroupResource rsrc) throws OrmException {
+    return json.format(rsrc);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GetGroup.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetGroup.java
new file mode 100644
index 0000000..09180dd
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetGroup.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.group;
+
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.group.GroupJson.GroupInfo;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+
+class GetGroup implements RestReadView<GroupResource> {
+  private final GroupJson json;
+
+  @Inject
+  GetGroup(GroupJson json) {
+    this.json = json;
+  }
+
+  @Override
+  public GroupInfo apply(GroupResource resource) throws OrmException {
+    return json.format(resource.getGroup());
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GetIncludedGroup.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetIncludedGroup.java
new file mode 100644
index 0000000..32f20c0
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetIncludedGroup.java
@@ -0,0 +1,34 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.group;
+
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.group.GroupJson.GroupInfo;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+
+public class GetIncludedGroup implements RestReadView<IncludedGroupResource>  {
+  private final GroupJson json;
+
+  @Inject
+  GetIncludedGroup(GroupJson json) {
+    this.json = json;
+  }
+
+  @Override
+  public GroupInfo apply(IncludedGroupResource rsrc) throws OrmException {
+    return json.format(rsrc.getMemberDescription());
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GetMember.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetMember.java
new file mode 100644
index 0000000..5651001
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetMember.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.group;
+
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.account.AccountInfo;
+
+public class GetMember implements RestReadView<MemberResource> {
+  @Override
+  public AccountInfo apply(MemberResource resource) {
+    return AccountInfo.parse(resource.getMember().getAccount(), true);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GetName.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetName.java
new file mode 100644
index 0000000..c6da7c4
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetName.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.group;
+
+import com.google.gerrit.extensions.restapi.RestReadView;
+
+public class GetName implements RestReadView<GroupResource> {
+
+  @Override
+  public String apply(GroupResource resource) {
+    return resource.getName();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GetOptions.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetOptions.java
new file mode 100644
index 0000000..3fbfc70
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetOptions.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.group;
+
+import com.google.gerrit.extensions.restapi.RestReadView;
+
+public class GetOptions implements RestReadView<GroupResource> {
+
+  @Override
+  public GroupOptionsInfo apply(GroupResource resource) {
+    return new GroupOptionsInfo(resource.getGroup());
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GetOwner.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetOwner.java
new file mode 100644
index 0000000..0113a164
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GetOwner.java
@@ -0,0 +1,52 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.group;
+
+import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.account.GroupControl;
+import com.google.gerrit.server.group.GroupJson.GroupInfo;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+
+public class GetOwner implements RestReadView<GroupResource> {
+
+  private final GroupControl.Factory controlFactory;
+  private final GroupJson json;
+
+  @Inject
+  GetOwner(GroupControl.Factory controlFactory, GroupJson json) {
+    this.controlFactory = controlFactory;
+    this.json = json;
+  }
+
+  @Override
+  public GroupInfo apply(GroupResource resource)
+      throws MethodNotAllowedException, ResourceNotFoundException, OrmException {
+    AccountGroup group = resource.toAccountGroup();
+    if (group == null) {
+      throw new MethodNotAllowedException();
+    }
+    try {
+      GroupControl c = controlFactory.validateFor(group.getOwnerGroupUUID());
+      return json.format(c.getGroup());
+    } catch (NoSuchGroupException e) {
+      throw new ResourceNotFoundException();
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupJson.java
new file mode 100644
index 0000000..a74e6ef
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupJson.java
@@ -0,0 +1,146 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.group;
+
+import static com.google.gerrit.common.groups.ListGroupsOption.INCLUDES;
+import static com.google.gerrit.common.groups.ListGroupsOption.MEMBERS;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.common.data.GroupDescription;
+import com.google.gerrit.common.data.GroupDescriptions;
+import com.google.gerrit.common.groups.ListGroupsOption;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.extensions.restapi.Url;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.account.AccountInfo;
+import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.account.GroupControl;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.util.Collection;
+import java.util.EnumSet;
+import java.util.List;
+
+public class GroupJson {
+  private final GroupBackend groupBackend;
+  private final GroupControl.Factory groupControlFactory;
+  private final Provider<ListMembers> listMembers;
+  private final Provider<ListIncludedGroups> listIncludes;
+  private EnumSet<ListGroupsOption> options;
+
+  @Inject
+  GroupJson(GroupBackend groupBackend,
+      GroupControl.Factory groupControlFactory,
+      Provider<ListMembers> listMembers,
+      Provider<ListIncludedGroups> listIncludes) {
+    this.groupBackend = groupBackend;
+    this.groupControlFactory = groupControlFactory;
+    this.listMembers = listMembers;
+    this.listIncludes = listIncludes;
+
+    options = EnumSet.noneOf(ListGroupsOption.class);
+  }
+
+  public GroupJson addOption(ListGroupsOption o) {
+    options.add(o);
+    return this;
+  }
+
+  public GroupJson addOptions(Collection<ListGroupsOption> o) {
+    options.addAll(o);
+    return this;
+  }
+
+  public GroupInfo format(GroupResource rsrc) throws OrmException {
+    GroupInfo info = init(rsrc.getGroup());
+    initMembersAndIncludes(rsrc, info);
+    return info;
+  }
+
+  public GroupInfo format(GroupDescription.Basic group) throws OrmException {
+    GroupInfo info = init(group);
+    if (options.contains(MEMBERS) || options.contains(INCLUDES)) {
+      GroupResource rsrc =
+          new GroupResource(groupControlFactory.controlFor(group));
+      initMembersAndIncludes(rsrc, info);
+    }
+    return info;
+  }
+
+  private GroupInfo init(GroupDescription.Basic group) {
+    GroupInfo info = new GroupInfo();
+    info.id = Url.encode(group.getGroupUUID().get());
+    info.name = Strings.emptyToNull(group.getName());
+    info.url = Strings.emptyToNull(group.getUrl());
+    info.options = new GroupOptionsInfo(group);
+
+    AccountGroup g = GroupDescriptions.toAccountGroup(group);
+    if (g != null) {
+      info.description = Strings.emptyToNull(g.getDescription());
+      info.groupId = g.getId().get();
+      if (g.getOwnerGroupUUID() != null) {
+        info.ownerId = Url.encode(g.getOwnerGroupUUID().get());
+        GroupDescription.Basic o = groupBackend.get(g.getOwnerGroupUUID());
+        if (o != null) {
+          info.owner = o.getName();
+        }
+      }
+    }
+
+    return info;
+  }
+
+  private GroupInfo initMembersAndIncludes(GroupResource rsrc, GroupInfo info)
+      throws OrmException {
+    if (rsrc.toAccountGroup() == null) {
+      return info;
+    }
+    try {
+      if (options.contains(MEMBERS)) {
+        info.members = listMembers.get().apply(rsrc);
+      }
+
+      if (options.contains(INCLUDES)) {
+        info.includes = listIncludes.get().apply(rsrc);
+      }
+      return info;
+    } catch (MethodNotAllowedException e) {
+      // should never happen, this exception is only thrown if we would try to
+      // list members/includes of an external group, but in case of an external
+      // group we return before
+      throw new IllegalStateException(e);
+    }
+  }
+
+  public static class GroupInfo {
+    final String kind = "gerritcodereview#group";
+    public String id;
+    public String name;
+    public String url;
+    public GroupOptionsInfo options;
+
+    // These fields are only supplied for internal groups.
+    public String description;
+    public Integer groupId;
+    public String owner;
+    public String ownerId;
+
+    // These fields are only supplied for internal groups, but only if requested
+    public List<AccountInfo> members;
+    public List<GroupInfo> includes;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupOptionsInfo.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupOptionsInfo.java
new file mode 100644
index 0000000..6be92d1
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupOptionsInfo.java
@@ -0,0 +1,32 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.group;
+
+import com.google.gerrit.common.data.GroupDescription;
+import com.google.gerrit.common.data.GroupDescriptions;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+
+public class GroupOptionsInfo {
+  public Boolean visibleToAll;
+
+  public GroupOptionsInfo(GroupDescription.Basic group) {
+    AccountGroup ag = GroupDescriptions.toAccountGroup(group);
+    visibleToAll = ag != null && ag.isVisibleToAll() ? true : null;
+  }
+
+  public GroupOptionsInfo(AccountGroup group) {
+    visibleToAll = group.isVisibleToAll() ? true : null;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupResource.java
new file mode 100644
index 0000000..32513a7
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupResource.java
@@ -0,0 +1,58 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.group;
+
+import com.google.gerrit.common.data.GroupDescription;
+import com.google.gerrit.common.data.GroupDescriptions;
+import com.google.gerrit.extensions.restapi.RestResource;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.account.GroupControl;
+import com.google.inject.TypeLiteral;
+
+public class GroupResource implements RestResource {
+  public static final TypeLiteral<RestView<GroupResource>> GROUP_KIND =
+      new TypeLiteral<RestView<GroupResource>>() {};
+
+  private final GroupControl control;
+
+  GroupResource(GroupControl control) {
+    this.control = control;
+  }
+
+  GroupResource(GroupResource rsrc) {
+    this.control = rsrc.getControl();
+  }
+
+  public GroupDescription.Basic getGroup() {
+    return control.getGroup();
+  }
+
+  public String getName() {
+    return getGroup().getName();
+  }
+
+  public AccountGroup.UUID getGroupUUID() {
+    return getGroup().getGroupUUID();
+  }
+
+  public AccountGroup toAccountGroup() {
+    return GroupDescriptions.toAccountGroup(getGroup());
+  }
+
+  public GroupControl getControl() {
+    return control;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupsCollection.java
new file mode 100644
index 0000000..918a530
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/GroupsCollection.java
@@ -0,0 +1,180 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.group;
+
+import com.google.gerrit.common.data.GroupDescription;
+import com.google.gerrit.common.data.GroupDescriptions;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.AcceptsCreate;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestCollection;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.server.AnonymousUser;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.account.GroupBackends;
+import com.google.gerrit.server.account.GroupControl;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class GroupsCollection implements
+    RestCollection<TopLevelResource, GroupResource>,
+    AcceptsCreate<TopLevelResource> {
+  private final DynamicMap<RestView<GroupResource>> views;
+  private final Provider<ListGroups> list;
+  private final CreateGroup.Factory createGroup;
+  private final GroupControl.Factory groupControlFactory;
+  private final GroupBackend groupBackend;
+  private final Provider<CurrentUser> self;
+
+  @Inject
+  GroupsCollection(final DynamicMap<RestView<GroupResource>> views,
+      final Provider<ListGroups> list,
+      final CreateGroup.Factory createGroup,
+      final GroupControl.Factory groupControlFactory,
+      final GroupBackend groupBackend,
+      final Provider<CurrentUser> self) {
+    this.views = views;
+    this.list = list;
+    this.createGroup = createGroup;
+    this.groupControlFactory = groupControlFactory;
+    this.groupBackend = groupBackend;
+    this.self = self;
+  }
+
+  @Override
+  public RestView<TopLevelResource> list() throws ResourceNotFoundException,
+      AuthException {
+    final CurrentUser user = self.get();
+    if (user instanceof AnonymousUser) {
+      throw new AuthException("Authentication required");
+    } else if(!(user instanceof IdentifiedUser)) {
+      throw new ResourceNotFoundException();
+    }
+
+    return list.get();
+  }
+
+  @Override
+  public GroupResource parse(TopLevelResource parent, IdString id)
+      throws AuthException, ResourceNotFoundException {
+    final CurrentUser user = self.get();
+    if (user instanceof AnonymousUser) {
+      throw new AuthException("Authentication required");
+    } else if(!(user instanceof IdentifiedUser)) {
+      throw new ResourceNotFoundException(id);
+    }
+
+    GroupDescription.Basic group = _parse(id.get());
+    if (group == null) {
+      throw new ResourceNotFoundException(id.get());
+    }
+    GroupControl ctl = groupControlFactory.controlFor(group);
+    if (!ctl.isVisible()) {
+      throw new ResourceNotFoundException(id);
+    }
+    return new GroupResource(ctl);
+  }
+
+  /**
+   * Parses a group ID from a request body and returns the group.
+   *
+   * @param id ID of the group, can be a group UUID, a group name or a legacy
+   *        group ID
+   * @return the group
+   * @throws UnprocessableEntityException thrown if the group ID cannot be
+   *         resolved or if the group is not visible to the calling user
+   */
+  public GroupDescription.Basic parse(String id)
+      throws UnprocessableEntityException {
+    GroupDescription.Basic group = _parse(id);
+    if (group == null || !groupControlFactory.controlFor(group).isVisible()) {
+      throw new UnprocessableEntityException(String.format(
+          "Group Not Found: %s", id));
+    }
+    return group;
+  }
+
+  /**
+   * Parses a group ID from a request body and returns the group if it is a
+   * Gerrit internal group.
+   *
+   * @param id ID of the group, can be a group UUID, a group name or a legacy
+   *        group ID
+   * @return the group
+   * @throws UnprocessableEntityException thrown if the group ID cannot be
+   *         resolved, if the group is not visible to the calling user or if
+   *         it's an external group
+   */
+  public GroupDescription.Basic parseInternal(String id)
+      throws UnprocessableEntityException {
+    GroupDescription.Basic group = parse(id);
+    if (GroupDescriptions.toAccountGroup(group) == null) {
+      throw new UnprocessableEntityException(String.format(
+          "External Group Not Allowed: %s", id));
+    }
+    return group;
+  }
+
+  private GroupDescription.Basic _parse(String id) {
+    AccountGroup.UUID uuid = new AccountGroup.UUID(id);
+    if (groupBackend.handles(uuid)) {
+      GroupDescription.Basic d = groupBackend.get(uuid);
+      if (d != null) {
+        return d;
+      }
+    }
+
+    // Might be a legacy AccountGroup.Id.
+    if (id.matches("^[1-9][0-9]*$")) {
+      try {
+        AccountGroup.Id legacyId = AccountGroup.Id.parse(id);
+        return groupControlFactory.controlFor(legacyId).getGroup();
+      } catch (IllegalArgumentException invalidId) {
+      } catch (NoSuchGroupException e) {
+      }
+    }
+
+    // Might be a group name, be nice and accept unique names.
+    GroupReference ref = GroupBackends.findExactSuggestion(groupBackend, id);
+    if (ref != null) {
+      GroupDescription.Basic d = groupBackend.get(ref.getUUID());
+      if (d != null) {
+        return d;
+      }
+    }
+
+    return null;
+  }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public CreateGroup create(TopLevelResource root, IdString name) {
+    return createGroup.create(name.get());
+  }
+
+  @Override
+  public DynamicMap<RestView<GroupResource>> views() {
+    return views;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/IncludedGroupResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/IncludedGroupResource.java
new file mode 100644
index 0000000..89b99a1
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/IncludedGroupResource.java
@@ -0,0 +1,40 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.group;
+
+import com.google.gerrit.common.data.GroupDescription;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.inject.TypeLiteral;
+
+public class IncludedGroupResource extends GroupResource {
+  public static final TypeLiteral<RestView<IncludedGroupResource>> INCLUDED_GROUP_KIND =
+      new TypeLiteral<RestView<IncludedGroupResource>>() {};
+
+  private final GroupDescription.Basic member;
+
+  IncludedGroupResource(GroupResource group, GroupDescription.Basic member) {
+    super(group);
+    this.member = member;
+  }
+
+  public AccountGroup.UUID getMember() {
+    return getMemberDescription().getGroupUUID();
+  }
+
+  public GroupDescription.Basic getMemberDescription() {
+    return member;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/IncludedGroupsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/IncludedGroupsCollection.java
new file mode 100644
index 0000000..a352cac
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/IncludedGroupsCollection.java
@@ -0,0 +1,97 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.group;
+
+import com.google.gerrit.common.data.GroupDescription;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.AcceptsCreate;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ChildCollection;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuid;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.group.AddIncludedGroups.PutIncludedGroup;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class IncludedGroupsCollection implements
+    ChildCollection<GroupResource, IncludedGroupResource>,
+    AcceptsCreate<GroupResource> {
+  private final DynamicMap<RestView<IncludedGroupResource>> views;
+  private final Provider<ListIncludedGroups> list;
+  private final Provider<GroupsCollection> groupsCollection;
+  private final Provider<ReviewDb> dbProvider;
+  private final Provider<AddIncludedGroups> put;
+
+  @Inject
+  IncludedGroupsCollection(DynamicMap<RestView<IncludedGroupResource>> views,
+      Provider<ListIncludedGroups> list,
+      Provider<GroupsCollection> groupsCollection,
+      Provider<ReviewDb> dbProvider, Provider<AddIncludedGroups> put) {
+    this.views = views;
+    this.list = list;
+    this.groupsCollection = groupsCollection;
+    this.dbProvider = dbProvider;
+    this.put = put;
+  }
+
+  @Override
+  public RestView<GroupResource> list() {
+    return list.get();
+  }
+
+  @Override
+  public IncludedGroupResource parse(GroupResource resource, IdString id)
+      throws MethodNotAllowedException, AuthException,
+      ResourceNotFoundException, OrmException {
+    AccountGroup parent = resource.toAccountGroup();
+    if (parent == null) {
+      throw new MethodNotAllowedException();
+    }
+
+    GroupDescription.Basic member =
+        groupsCollection.get().parse(TopLevelResource.INSTANCE, id).getGroup();
+    if (isMember(parent, member)
+        && resource.getControl().canSeeGroup(member.getGroupUUID())) {
+      return new IncludedGroupResource(resource, member);
+    }
+    throw new ResourceNotFoundException(id);
+  }
+
+  private boolean isMember(AccountGroup parent, GroupDescription.Basic member)
+      throws OrmException {
+    return dbProvider.get().accountGroupIncludesByUuid().get(
+        new AccountGroupIncludeByUuid.Key(
+            parent.getId(),
+            member.getGroupUUID())) != null;
+  }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public PutIncludedGroup create(GroupResource group, IdString id) {
+    return new PutIncludedGroup(put, id.get());
+  }
+
+  @Override
+  public DynamicMap<RestView<IncludedGroupResource>> views() {
+    return views;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/ListGroups.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListGroups.java
new file mode 100644
index 0000000..03ec067
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListGroups.java
@@ -0,0 +1,234 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.group;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.gerrit.common.data.GroupDescriptions;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.common.groups.ListGroupsOption;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.extensions.restapi.Url;
+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.OutputFormat;
+import com.google.gerrit.server.account.AccountResource;
+import com.google.gerrit.server.account.GetGroups;
+import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.account.GroupComparator;
+import com.google.gerrit.server.account.GroupControl;
+import com.google.gerrit.server.group.GroupJson.GroupInfo;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gson.reflect.TypeToken;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.kohsuke.args4j.Option;
+
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.Set;
+
+/** List groups visible to the calling user. */
+public class ListGroups implements RestReadView<TopLevelResource> {
+
+  protected final GroupCache groupCache;
+
+  private final GroupControl.Factory groupControlFactory;
+  private final GroupControl.GenericFactory genericGroupControlFactory;
+  private final Provider<IdentifiedUser> identifiedUser;
+  private final IdentifiedUser.GenericFactory userFactory;
+  private final Provider<GetGroups> accountGetGroups;
+  private final GroupJson json;
+  private EnumSet<ListGroupsOption> options;
+
+  @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;
+
+  @Option(name = "--owned", usage = "to list only groups that are owned by the specified user"
+      + " or by the calling user if no user was specifed")
+  private boolean owned;
+
+  private Set<AccountGroup.UUID> groupsToInspect = Sets.newHashSet();
+
+  @Option(name = "-q", usage = "group to inspect")
+  void addGroup(final AccountGroup.UUID id) {
+    groupsToInspect.add(id);
+  }
+
+  @Option(name = "-m", metaVar = "MATCH", usage = "match group substring")
+  private String matchSubstring;
+
+  @Option(name = "-o", multiValued = true, usage = "Output options per group")
+  public void addOption(ListGroupsOption o) {
+    options.add(o);
+  }
+
+  @Option(name = "-O", usage = "Output option flags, in hex")
+  void setOptionFlagsHex(String hex) {
+    options.addAll(ListGroupsOption.fromBits(Integer.parseInt(hex, 16)));
+  }
+
+  @Inject
+  protected ListGroups(final GroupCache groupCache,
+      final GroupControl.Factory groupControlFactory,
+      final GroupControl.GenericFactory genericGroupControlFactory,
+      final Provider<IdentifiedUser> identifiedUser,
+      final IdentifiedUser.GenericFactory userFactory,
+      final Provider<GetGroups> accountGetGroups, GroupJson json) {
+    this.groupCache = groupCache;
+    this.groupControlFactory = groupControlFactory;
+    this.genericGroupControlFactory = genericGroupControlFactory;
+    this.identifiedUser = identifiedUser;
+    this.userFactory = userFactory;
+    this.accountGetGroups = accountGetGroups;
+    this.json = json;
+    this.options = EnumSet.noneOf(ListGroupsOption.class);
+  }
+
+  public Account.Id getUser() {
+    return user;
+  }
+
+  public List<ProjectControl> getProjects() {
+    return projects;
+  }
+
+  @Override
+  public Object apply(TopLevelResource resource) throws AuthException,
+      BadRequestException, ResourceConflictException, Exception {
+    final Map<String, GroupInfo> output = Maps.newTreeMap();
+    for (GroupInfo info : get()) {
+      output.put(Objects.firstNonNull(
+          info.name,
+          "Group " + Url.decode(info.id)), info);
+      info.name = null;
+    }
+    return OutputFormat.JSON.newGson().toJsonTree(output,
+        new TypeToken<Map<String, GroupInfo>>() {}.getType());
+  }
+
+  public List<GroupInfo> get() throws OrmException {
+    List<GroupInfo> groupInfos;
+    if (user != null) {
+      if (owned) {
+        groupInfos = getGroupsOwnedBy(userFactory.create(user));
+      } else {
+        groupInfos = accountGetGroups.get().apply(
+            new AccountResource(userFactory.create(user)));
+      }
+    } else {
+      if (owned) {
+        groupInfos = getGroupsOwnedBy(identifiedUser.get());
+      } else {
+        List<AccountGroup> groupList;
+        if (!projects.isEmpty()) {
+          Map<AccountGroup.UUID, AccountGroup> groups = Maps.newHashMap();
+          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) {
+                groups.put(group.getGroupUUID(), group);
+              }
+            }
+          }
+          groupList = filterGroups(groups.values());
+        } else {
+          groupList = filterGroups(groupCache.all());
+        }
+        groupInfos = Lists.newArrayListWithCapacity(groupList.size());
+        for (AccountGroup group : groupList) {
+          groupInfos.add(json.addOptions(options).format(
+              GroupDescriptions.forAccountGroup(group)));
+        }
+      }
+    }
+    return groupInfos;
+  }
+
+  private List<GroupInfo> getGroupsOwnedBy(IdentifiedUser user)
+      throws OrmException {
+    List<GroupInfo> groups = Lists.newArrayList();
+    for (AccountGroup g : filterGroups(groupCache.all())) {
+      GroupControl ctl = groupControlFactory.controlFor(g);
+      try {
+        if (genericGroupControlFactory.controlFor(user, g.getGroupUUID())
+            .isOwner()) {
+          groups.add(json.addOptions(options).format(ctl.getGroup()));
+        }
+      } catch (NoSuchGroupException e) {
+        continue;
+      }
+    }
+    return groups;
+  }
+
+  private List<AccountGroup> filterGroups(final Iterable<AccountGroup> groups) {
+    final List<AccountGroup> filteredGroups = Lists.newArrayList();
+    final boolean isAdmin =
+        identifiedUser.get().getCapabilities().canAdministrateServer();
+    for (final AccountGroup group : groups) {
+      if (!Strings.isNullOrEmpty(matchSubstring)) {
+        if (!group.getName().toLowerCase(Locale.US)
+            .contains(matchSubstring.toLowerCase(Locale.US))) {
+          continue;
+        }
+      }
+      if (!isAdmin) {
+        final GroupControl c = groupControlFactory.controlFor(group);
+        if (!c.isVisible()) {
+          continue;
+        }
+      }
+      if ((visibleToAll && !group.isVisibleToAll())
+          || (groupType != null && !groupType.equals(group.getType()))) {
+        continue;
+      }
+      if (!groupsToInspect.isEmpty()
+          && !groupsToInspect.contains(group.getGroupUUID())) {
+        continue;
+      }
+      filteredGroups.add(group);
+    }
+    Collections.sort(filteredGroups, new GroupComparator());
+    return filteredGroups;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/ListIncludedGroups.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListIncludedGroups.java
new file mode 100644
index 0000000..3691e80
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListIncludedGroups.java
@@ -0,0 +1,88 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.group;
+
+import static com.google.common.base.Strings.nullToEmpty;
+
+import com.google.common.collect.Lists;
+import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuid;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.account.GroupControl;
+import com.google.gerrit.server.group.GroupJson.GroupInfo;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.slf4j.Logger;
+
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+
+public class ListIncludedGroups implements RestReadView<GroupResource> {
+  private static final Logger log = org.slf4j.LoggerFactory.getLogger(ListIncludedGroups.class);
+
+  private final GroupControl.Factory controlFactory;
+  private final Provider<ReviewDb> dbProvider;
+  private final GroupJson json;
+
+  @Inject
+  ListIncludedGroups(GroupControl.Factory controlFactory,
+      Provider<ReviewDb> dbProvider, GroupJson json) {
+    this.controlFactory = controlFactory;
+    this.dbProvider = dbProvider;
+    this.json = json;
+  }
+
+  @Override
+  public List<GroupInfo> apply(GroupResource rsrc)
+      throws MethodNotAllowedException, OrmException {
+    if (rsrc.toAccountGroup() == null) {
+      throw new MethodNotAllowedException();
+    }
+
+    boolean ownerOfParent = rsrc.getControl().isOwner();
+    List<GroupInfo> included = Lists.newArrayList();
+    for (AccountGroupIncludeByUuid u : dbProvider.get()
+        .accountGroupIncludesByUuid()
+        .byGroup(rsrc.toAccountGroup().getId())) {
+      try {
+        GroupControl i = controlFactory.controlFor(u.getIncludeUUID());
+        if (ownerOfParent || i.isVisible()) {
+          included.add(json.format(i.getGroup()));
+        }
+      } catch (NoSuchGroupException notFound) {
+        log.warn(String.format("Group %s no longer available, included into ",
+            u.getIncludeUUID(),
+            rsrc.getGroup().getName()));
+        continue;
+      }
+    }
+    Collections.sort(included, new Comparator<GroupInfo>() {
+      @Override
+      public int compare(GroupInfo a, GroupInfo b) {
+        int cmp = nullToEmpty(a.name).compareTo(nullToEmpty(b.name));
+        if (cmp != 0) {
+          return cmp;
+        }
+        return nullToEmpty(a.id).compareTo(nullToEmpty(b.id));
+      }
+    });
+    return included;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/ListMembers.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListMembers.java
new file mode 100644
index 0000000..d32c632
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/ListMembers.java
@@ -0,0 +1,121 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.group;
+
+import com.google.common.collect.ComparisonChain;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Ordering;
+import com.google.gerrit.common.data.GroupDetail;
+import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuid;
+import com.google.gerrit.reviewdb.client.AccountGroupMember;
+import com.google.gerrit.server.account.AccountInfo;
+import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.account.GroupDetailFactory;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+
+import org.kohsuke.args4j.Option;
+
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+
+public class ListMembers implements RestReadView<GroupResource> {
+  private final GroupCache groupCache;
+  private final GroupDetailFactory.Factory groupDetailFactory;
+  private final AccountInfo.Loader accountLoader;
+
+  @Option(name = "--recursive", usage = "to resolve included groups recursively")
+  private boolean recursive;
+
+  @Inject
+  ListMembers(GroupCache groupCache,
+      GroupDetailFactory.Factory groupDetailFactory,
+      AccountInfo.Loader.Factory accountLoaderFactory) {
+    this.groupCache = groupCache;
+    this.groupDetailFactory = groupDetailFactory;
+    this.accountLoader = accountLoaderFactory.create(true);
+  }
+
+  @Override
+  public List<AccountInfo> apply(final GroupResource resource)
+      throws MethodNotAllowedException, OrmException {
+    if (resource.toAccountGroup() == null) {
+      throw new MethodNotAllowedException();
+    }
+    final Map<Account.Id, AccountInfo> members =
+        getMembers(resource.getGroupUUID(), new HashSet<AccountGroup.UUID>());
+    final List<AccountInfo> memberInfos = Lists.newArrayList(members.values());
+    Collections.sort(memberInfos, new Comparator<AccountInfo>() {
+      @Override
+      public int compare(AccountInfo a, AccountInfo b) {
+        return ComparisonChain.start()
+            .compare(a.name, b.name, Ordering.natural().nullsFirst())
+            .compare(a.email, b.email, Ordering.natural().nullsFirst())
+            .compare(a._account_id, b._account_id, Ordering.natural().nullsFirst()).result();
+      }
+    });
+    return memberInfos;
+  }
+
+  private Map<Account.Id, AccountInfo> getMembers(
+      final AccountGroup.UUID groupUUID,
+      final HashSet<AccountGroup.UUID> seenGroups) throws OrmException {
+    seenGroups.add(groupUUID);
+
+    final Map<Account.Id, AccountInfo> members = Maps.newHashMap();
+    final AccountGroup group = groupCache.get(groupUUID);
+    if (group == null) {
+      // the included group is an external group and can't be resolved
+      return Collections.emptyMap();
+    }
+
+    final GroupDetail groupDetail;
+    try {
+      groupDetail = groupDetailFactory.create(group.getId()).call();
+    } catch (NoSuchGroupException e) {
+      // the included group is not visible
+      return Collections.emptyMap();
+    }
+
+    if (groupDetail.members != null) {
+      for (final AccountGroupMember m : groupDetail.members) {
+        if (!members.containsKey(m.getAccountId())) {
+          members.put(m.getAccountId(), accountLoader.get(m.getAccountId()));
+        }
+      }
+    }
+
+    if (recursive) {
+      if (groupDetail.includes != null) {
+        for (final AccountGroupIncludeByUuid includedGroup : groupDetail.includes) {
+          if (!seenGroups.contains(includedGroup.getIncludeUUID())) {
+            members.putAll(getMembers(includedGroup.getIncludeUUID(), seenGroups));
+          }
+        }
+      }
+    }
+    accountLoader.fill();
+    return members;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/MemberResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/MemberResource.java
new file mode 100644
index 0000000..52a37a8
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/MemberResource.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.group;
+
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.inject.TypeLiteral;
+
+public class MemberResource extends GroupResource {
+  public static final TypeLiteral<RestView<MemberResource>> MEMBER_KIND =
+      new TypeLiteral<RestView<MemberResource>>() {};
+
+  private final IdentifiedUser user;
+
+  public MemberResource(GroupResource group, IdentifiedUser user) {
+    super(group);
+    this.user = user;
+  }
+
+  public IdentifiedUser getMember() {
+    return user;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/MembersCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/MembersCollection.java
new file mode 100644
index 0000000..efed115
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/MembersCollection.java
@@ -0,0 +1,91 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.group;
+
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.AcceptsCreate;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.ChildCollection;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.reviewdb.client.AccountGroupMember;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.AccountsCollection;
+import com.google.gerrit.server.group.AddMembers.PutMember;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class MembersCollection implements
+    ChildCollection<GroupResource, MemberResource>,
+    AcceptsCreate<GroupResource> {
+  private final DynamicMap<RestView<MemberResource>> views;
+  private final Provider<ListMembers> list;
+  private final Provider<AccountsCollection> accounts;
+  private final Provider<ReviewDb> db;
+  private final Provider<AddMembers> put;
+
+  @Inject
+  MembersCollection(DynamicMap<RestView<MemberResource>> views,
+      Provider<ListMembers> list,
+      Provider<AccountsCollection> accounts,
+      Provider<ReviewDb> db,
+      Provider<AddMembers> put) {
+    this.views = views;
+    this.list = list;
+    this.accounts = accounts;
+    this.db = db;
+    this.put = put;
+  }
+
+  @Override
+  public RestView<GroupResource> list() throws ResourceNotFoundException,
+      AuthException {
+    return list.get();
+  }
+
+  @Override
+  public MemberResource parse(GroupResource parent, IdString id)
+      throws MethodNotAllowedException, AuthException,
+      ResourceNotFoundException, OrmException {
+    if (parent.toAccountGroup() == null) {
+      throw new MethodNotAllowedException();
+    }
+
+    IdentifiedUser user = accounts.get().parse(TopLevelResource.INSTANCE, id).getUser();
+    AccountGroupMember.Key key =
+        new AccountGroupMember.Key(user.getAccountId(), parent.toAccountGroup().getId());
+    if (db.get().accountGroupMembers().get(key) != null
+        && parent.getControl().canSeeMember(user.getAccountId())) {
+      return new MemberResource(parent, user);
+    }
+    throw new ResourceNotFoundException(id);
+  }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public PutMember create(GroupResource group, IdString id) {
+    return new PutMember(put, id.get());
+  }
+
+  @Override
+  public DynamicMap<RestView<MemberResource>> views() {
+    return views;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/Module.java
new file mode 100644
index 0000000..97338f1
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/Module.java
@@ -0,0 +1,69 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.group;
+
+import static com.google.gerrit.server.group.GroupResource.GROUP_KIND;
+import static com.google.gerrit.server.group.IncludedGroupResource.INCLUDED_GROUP_KIND;
+import static com.google.gerrit.server.group.MemberResource.MEMBER_KIND;
+
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.RestApiModule;
+import com.google.gerrit.server.group.AddIncludedGroups.UpdateIncludedGroup;
+import com.google.gerrit.server.group.AddMembers.UpdateMember;
+import com.google.gerrit.server.group.DeleteIncludedGroups.DeleteIncludedGroup;
+import com.google.gerrit.server.group.DeleteMembers.DeleteMember;
+import com.google.inject.assistedinject.FactoryModuleBuilder;
+
+public class Module extends RestApiModule {
+  @Override
+  protected void configure() {
+    bind(GroupsCollection.class);
+
+    DynamicMap.mapOf(binder(), GROUP_KIND);
+    DynamicMap.mapOf(binder(), MEMBER_KIND);
+    DynamicMap.mapOf(binder(), INCLUDED_GROUP_KIND);
+
+    get(GROUP_KIND).to(GetGroup.class);
+    put(GROUP_KIND).to(PutGroup.class);
+    get(GROUP_KIND, "detail").to(GetDetail.class);
+    post(GROUP_KIND, "members").to(AddMembers.class);
+    post(GROUP_KIND, "members.add").to(AddMembers.class);
+    post(GROUP_KIND, "members.delete").to(DeleteMembers.class);
+    post(GROUP_KIND, "groups").to(AddIncludedGroups.class);
+    post(GROUP_KIND, "groups.add").to(AddIncludedGroups.class);
+    post(GROUP_KIND, "groups.delete").to(DeleteIncludedGroups.class);
+    get(GROUP_KIND, "description").to(GetDescription.class);
+    put(GROUP_KIND, "description").to(PutDescription.class);
+    delete(GROUP_KIND, "description").to(PutDescription.class);
+    get(GROUP_KIND, "name").to(GetName.class);
+    put(GROUP_KIND, "name").to(PutName.class);
+    get(GROUP_KIND, "owner").to(GetOwner.class);
+    put(GROUP_KIND, "owner").to(PutOwner.class);
+    get(GROUP_KIND, "options").to(GetOptions.class);
+    put(GROUP_KIND, "options").to(PutOptions.class);
+
+    child(GROUP_KIND, "members").to(MembersCollection.class);
+    get(MEMBER_KIND).to(GetMember.class);
+    put(MEMBER_KIND).to(UpdateMember.class);
+    delete(MEMBER_KIND).to(DeleteMember.class);
+
+    child(GROUP_KIND, "groups").to(IncludedGroupsCollection.class);
+    get(INCLUDED_GROUP_KIND).to(GetIncludedGroup.class);
+    put(INCLUDED_GROUP_KIND).to(UpdateIncludedGroup.class);
+    delete(INCLUDED_GROUP_KIND).to(DeleteIncludedGroup.class);
+
+    install(new FactoryModuleBuilder().build(CreateGroup.Factory.class));
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/PutDescription.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/PutDescription.java
new file mode 100644
index 0000000..dcc0a76
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/PutDescription.java
@@ -0,0 +1,77 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.group;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.DefaultInput;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.group.PutDescription.Input;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+
+import java.util.Collections;
+
+public class PutDescription implements RestModifyView<GroupResource, Input> {
+  static class Input {
+    @DefaultInput
+    String description;
+  }
+
+  private final GroupCache groupCache;
+  private final ReviewDb db;
+
+  @Inject
+  PutDescription(GroupCache groupCache, ReviewDb db) {
+    this.groupCache = groupCache;
+    this.db = db;
+  }
+
+  @Override
+  public Object apply(GroupResource resource, Input input)
+      throws MethodNotAllowedException, AuthException, NoSuchGroupException,
+      ResourceNotFoundException, OrmException {
+    if (input == null) {
+      input = new Input(); // Delete would set description to null.
+    }
+
+    if (resource.toAccountGroup() == null) {
+      throw new MethodNotAllowedException();
+    } else if (!resource.getControl().isOwner()) {
+      throw new AuthException("Not group owner");
+    }
+
+    AccountGroup group = db.accountGroups().get(
+        resource.toAccountGroup().getId());
+    if (group == null) {
+      throw new ResourceNotFoundException();
+    }
+
+    group.setDescription(Strings.emptyToNull(input.description));
+    db.accountGroups().update(Collections.singleton(group));
+    groupCache.evict(group);
+
+    return Strings.isNullOrEmpty(input.description)
+        ? Response.none()
+        : input.description;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/PutGroup.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/PutGroup.java
new file mode 100644
index 0000000..9cbed6c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/PutGroup.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.group;
+
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.server.group.CreateGroup.Input;
+
+public class PutGroup implements RestModifyView<GroupResource, Input> {
+  @Override
+  public Object apply(GroupResource resource, Input input)
+      throws ResourceConflictException {
+    throw new ResourceConflictException("Group already exists");
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/PutName.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/PutName.java
new file mode 100644
index 0000000..e18337a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/PutName.java
@@ -0,0 +1,74 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.group;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.common.errors.InvalidNameException;
+import com.google.gerrit.common.errors.NameAlreadyUsedException;
+import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.DefaultInput;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.server.account.PerformRenameGroup;
+import com.google.gerrit.server.group.PutName.Input;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+
+public class PutName implements RestModifyView<GroupResource, Input> {
+  static class Input {
+    @DefaultInput
+    String name;
+  }
+
+  private final PerformRenameGroup.Factory performRenameGroupFactory;
+
+  @Inject
+  PutName(PerformRenameGroup.Factory performRenameGroupFactory) {
+    this.performRenameGroupFactory = performRenameGroupFactory;
+  }
+
+  @Override
+  public String apply(GroupResource resource, Input input)
+      throws MethodNotAllowedException, AuthException, BadRequestException,
+      ResourceNotFoundException, ResourceConflictException, OrmException {
+    if (resource.toAccountGroup() == null) {
+      throw new MethodNotAllowedException();
+    } else if (!resource.getControl().isOwner()) {
+      throw new AuthException("Not group owner");
+    } else if (input == null || Strings.isNullOrEmpty(input.name)) {
+      throw new BadRequestException("name is required");
+    }
+
+    final String newName = input.name.trim();
+    if (resource.toAccountGroup().getName().equals(newName)) {
+      return newName;
+    }
+
+    try {
+      return performRenameGroupFactory.create().renameGroup(
+          resource.toAccountGroup().getId(), newName).group.getName();
+    } catch (NoSuchGroupException e) {
+      throw new ResourceNotFoundException();
+    } catch (InvalidNameException e) {
+      throw new BadRequestException(e.getMessage());
+    } catch (NameAlreadyUsedException e) {
+      throw new ResourceConflictException(e.getMessage());
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/PutOptions.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/PutOptions.java
new file mode 100644
index 0000000..74060de
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/PutOptions.java
@@ -0,0 +1,74 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.group;
+
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.group.PutOptions.Input;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+
+import java.util.Collections;
+
+public class PutOptions implements RestModifyView<GroupResource, Input> {
+  static class Input {
+    Boolean visibleToAll;
+  }
+
+  private final GroupCache groupCache;
+  private final ReviewDb db;
+
+  @Inject
+  PutOptions(GroupCache groupCache, ReviewDb db) {
+    this.groupCache = groupCache;
+    this.db = db;
+  }
+
+  @Override
+  public GroupOptionsInfo apply(GroupResource resource, Input input)
+      throws MethodNotAllowedException, AuthException, BadRequestException,
+      ResourceNotFoundException, OrmException {
+    if (resource.toAccountGroup() == null) {
+      throw new MethodNotAllowedException();
+    } else if (!resource.getControl().isOwner()) {
+      throw new AuthException("Not group owner");
+    }
+
+    if (input == null) {
+      throw new BadRequestException("options are required");
+    }
+    if (input.visibleToAll == null) {
+      input.visibleToAll = false;
+    }
+
+    AccountGroup group = db.accountGroups().get(
+        resource.toAccountGroup().getId());
+    if (group == null) {
+      throw new ResourceNotFoundException();
+    }
+
+    group.setVisibleToAll(input.visibleToAll);
+    db.accountGroups().update(Collections.singleton(group));
+    groupCache.evict(group);
+
+    return new GroupOptionsInfo(group);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/group/PutOwner.java b/gerrit-server/src/main/java/com/google/gerrit/server/group/PutOwner.java
new file mode 100644
index 0000000..ca6c06f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/group/PutOwner.java
@@ -0,0 +1,86 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.group;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.common.data.GroupDescription;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.DefaultInput;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.group.GroupJson.GroupInfo;
+import com.google.gerrit.server.group.PutOwner.Input;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.util.Collections;
+
+public class PutOwner implements RestModifyView<GroupResource, Input> {
+  static class Input {
+    @DefaultInput
+    String owner;
+  }
+
+  private final Provider<GroupsCollection> groupsCollection;
+  private final GroupCache groupCache;
+  private final ReviewDb db;
+  private final GroupJson json;
+
+  @Inject
+  PutOwner(Provider<GroupsCollection> groupsCollection, GroupCache groupCache,
+      ReviewDb db, GroupJson json) {
+    this.groupsCollection = groupsCollection;
+    this.groupCache = groupCache;
+    this.db = db;
+    this.json = json;
+  }
+
+  @Override
+  public GroupInfo apply(GroupResource resource, Input input)
+      throws ResourceNotFoundException, MethodNotAllowedException,
+      AuthException, BadRequestException, UnprocessableEntityException,
+      OrmException {
+    AccountGroup group = resource.toAccountGroup();
+    if (group == null) {
+      throw new MethodNotAllowedException();
+    } else if (!resource.getControl().isOwner()) {
+      throw new AuthException("Not group owner");
+    }
+
+    if (input == null || Strings.isNullOrEmpty(input.owner)) {
+      throw new BadRequestException("owner is required");
+    }
+
+    group = db.accountGroups().get(group.getId());
+    if (group == null) {
+      throw new ResourceNotFoundException();
+    }
+
+    GroupDescription.Basic owner = groupsCollection.get().parse(input.owner);
+    if (!group.getOwnerGroupUUID().equals(owner.getGroupUUID())) {
+      group.setOwnerGroupUUID(owner.getGroupUUID());
+      db.accountGroups().update(Collections.singleton(group));
+      groupCache.evict(group);
+    }
+    return json.format(owner);
+  }
+}
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 0eb3dfe..8387f51 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,9 +14,9 @@
 
 package com.google.gerrit.server.mail;
 
-import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
-import com.google.gerrit.server.config.AnonymousCowardName;
+import com.google.gerrit.reviewdb.client.Change;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -28,9 +28,8 @@
   }
 
   @Inject
-  public AbandonedSender(EmailArguments ea,
-      @AnonymousCowardName String anonymousCowardName, @Assisted Change c) {
-    super(ea, anonymousCowardName, c, "abandon");
+  public AbandonedSender(EmailArguments ea, @Assisted Change c) {
+    super(ea, c, "abandon");
   }
 
   @Override
@@ -39,7 +38,8 @@
 
     ccAllApprovals();
     bccStarredBy();
-    bccWatches(NotifyType.ALL_COMMENTS);
+    includeWatchers(NotifyType.ABANDONED_CHANGES);
+    includeWatchers(NotifyType.ALL_COMMENTS);
   }
 
   @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 f7ace27..c181a9c 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,9 +14,8 @@
 
 package com.google.gerrit.server.mail;
 
+import com.google.gerrit.common.errors.EmailException;
 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;
 
@@ -27,10 +26,8 @@
   }
 
   @Inject
-  public AddReviewerSender(EmailArguments ea,
-      @AnonymousCowardName String anonymousCowardName, SshInfo sshInfo,
-      @Assisted Change c) {
-    super(ea, anonymousCowardName, sshInfo, c);
+  public AddReviewerSender(EmailArguments ea, @Assisted Change c) {
+    super(ea, c);
   }
 
   @Override
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 6c33949..885f1aa 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,15 +14,9 @@
 
 package com.google.gerrit.server.mail;
 
-import com.google.common.collect.Lists;
-import com.google.common.collect.Sets;
-import com.google.gerrit.common.data.GroupDescriptions;
-import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
-import com.google.gerrit.reviewdb.client.AccountGroupInclude;
-import com.google.gerrit.reviewdb.client.AccountGroupMember;
-import com.google.gerrit.reviewdb.client.AccountProjectWatch;
 import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
@@ -30,35 +24,35 @@
 import com.google.gerrit.reviewdb.client.PatchSet;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.client.PatchSetInfo;
-import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.StarredChange;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.git.NotifyConfig;
+import com.google.gerrit.server.mail.ProjectWatch.Watchers;
 import com.google.gerrit.server.patch.PatchList;
 import com.google.gerrit.server.patch.PatchListEntry;
 import com.google.gerrit.server.patch.PatchListNotAvailableException;
 import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
 import com.google.gerrit.server.project.ProjectState;
-import com.google.gerrit.server.query.Predicate;
-import com.google.gerrit.server.query.QueryParseException;
 import com.google.gerrit.server.query.change.ChangeData;
-import com.google.gerrit.server.query.change.ChangeQueryBuilder;
-import com.google.gerrit.server.query.change.SingleGroupUser;
 import com.google.gwtorm.server.OrmException;
 
+import org.eclipse.jgit.diff.DiffFormatter;
+import org.eclipse.jgit.internal.JGitText;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.util.RawParseUtils;
+import org.eclipse.jgit.util.TemporaryBuffer;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
+import java.io.IOException;
 import java.text.MessageFormat;
 import java.util.Collections;
 import java.util.Date;
 import java.util.HashSet;
-import java.util.Queue;
 import java.util.Set;
 import java.util.TreeSet;
 
 /** Sends an email to one or more interested parties. */
-public abstract class ChangeEmail extends OutgoingEmail {
+public abstract class ChangeEmail extends NotificationEmail {
   private static final Logger log = LoggerFactory.getLogger(ChangeEmail.class);
 
   protected final Change change;
@@ -71,11 +65,10 @@
   protected Set<Account.Id> authors;
   protected boolean emailOnlyAuthors;
 
-  protected ChangeEmail(EmailArguments ea, final String anonymousCowardName,
-      final Change c, final String mc) {
-    super(ea, anonymousCowardName, mc);
+  protected ChangeEmail(EmailArguments ea, Change c, String mc) {
+    super(ea, mc, c.getProject(), c.getDest());
     change = c;
-    changeData = change != null ? new ChangeData(change) : null;
+    changeData = new ChangeData(change);
     emailOnlyAuthors = false;
   }
 
@@ -121,11 +114,16 @@
       }
     } catch (OrmException e) {
     }
+    formatFooter();
   }
 
   /** Format the message body by calling {@link #appendText(String)}. */
   protected abstract void formatChange() throws EmailException;
 
+  /** Format the message footer by calling {@link #appendText(String)}. */
+  protected void formatFooter() throws EmailException {
+  }
+
   /** Setup the message headers and envelope (TO, CC, BCC). */
   protected void init() throws EmailException {
     if (args.projectCache != null) {
@@ -158,23 +156,10 @@
     }
     setChangeSubjectHeader();
     setHeader("X-Gerrit-Change-Id", "" + change.getKey().get());
-    setListIdHeader();
     setChangeUrlHeader();
     setCommitIdHeader();
   }
 
-  private void setListIdHeader() throws EmailException {
-    // Set a reasonable list id so that filters can be used to sort messages
-    setVHeader("List-Id", "<$email.listId.replace('@', '.')>");
-    if (getSettingsUrl() != null) {
-      setVHeader("List-Unsubscribe", "<$email.settingsUrl>");
-    }
-  }
-
-  public String getListId() throws EmailException {
-    return velocify("gerrit-$projectName.replace('/', '-')@$email.gerritHost");
-  }
-
   private void setChangeUrlHeader() {
     final String u = getChangeUrl();
     if (u != null) {
@@ -196,7 +181,7 @@
 
   /** Get a link to the change; null if the server doesn't know its own address. */
   public String getChangeUrl() {
-    if (change != null && getGerritUrl() != null) {
+    if (getGerritUrl() != null) {
       final StringBuilder r = new StringBuilder();
       r.append(getGerritUrl());
       r.append(change.getChangeId());
@@ -316,164 +301,10 @@
     }
   }
 
-  /** BCC users and groups that want notification of events. */
-  protected void bccWatches(NotifyType type) {
-    try {
-      Watchers matching = getWatches(type);
-      for (Account.Id user : matching.accounts) {
-        add(RecipientType.BCC, user);
-      }
-      for (Address addr : matching.emails) {
-        add(RecipientType.BCC, addr);
-      }
-    } catch (OrmException err) {
-      // Just don't CC everyone. Better to send a partial message to those
-      // we already have queued up then to fail deliver entirely to people
-      // who have a lower interest in the change.
-      log.warn("Cannot BCC watchers for " + type, err);
-    }
-  }
-
-  /** Returns all watches that are relevant */
-  protected final Watchers getWatches(NotifyType type) throws OrmException {
-    Watchers matching = new Watchers();
-    if (changeData == null) {
-      return matching;
-    }
-
-    Set<Account.Id> projectWatchers = new HashSet<Account.Id>();
-
-    for (AccountProjectWatch w : args.db.get().accountProjectWatches()
-        .byProject(change.getProject())) {
-      projectWatchers.add(w.getAccountId());
-      if (w.isNotify(type)) {
-        add(matching, w);
-      }
-    }
-
-    for (AccountProjectWatch w : args.db.get().accountProjectWatches()
-        .byProject(args.allProjectsName)) {
-      if (!projectWatchers.contains(w.getAccountId()) && w.isNotify(type)) {
-        add(matching, w);
-      }
-    }
-
-    ProjectState state = projectState;
-    while (state != null) {
-      for (NotifyConfig nc : state.getConfig().getNotifyConfigs()) {
-        if (nc.isNotify(type)) {
-          try {
-            add(matching, nc, state.getProject().getNameKey());
-          } catch (QueryParseException e) {
-            log.warn(String.format(
-                "Project %s has invalid notify %s filter \"%s\": %s",
-                state.getProject().getName(), nc.getName(),
-                nc.getFilter(), e.getMessage()));
-          }
-        }
-      }
-      state = state.getParentState();
-    }
-
-    return matching;
-  }
-
-  protected static class Watchers {
-    protected final Set<Account.Id> accounts = Sets.newHashSet();
-    protected final Set<Address> emails = Sets.newHashSet();
-  }
-
-  @SuppressWarnings("unchecked")
-  private void add(Watchers matching, NotifyConfig nc, Project.NameKey project)
-      throws OrmException, QueryParseException {
-    for (GroupReference ref : nc.getGroups()) {
-      AccountGroup group =
-          GroupDescriptions.toAccountGroup(args.groupBackend.get(ref.getUUID()));
-      if (group == null) {
-        log.warn(String.format(
-            "Project %s has invalid group %s in notify section %s",
-            project.get(), ref.getName(), nc.getName()));
-        continue;
-      }
-
-      if (group.getType() != AccountGroup.Type.INTERNAL) {
-        log.warn(String.format(
-            "Project %s cannot use group %s of type %s in notify section %s",
-            project.get(), ref.getName(), group.getType(), nc.getName()));
-        continue;
-      }
-
-      ChangeQueryBuilder qb = args.queryBuilder.create(new SingleGroupUser(
-          args.capabilityControlFactory,
-          ref.getUUID()));
-      qb.setAllowFile(true);
-      Predicate<ChangeData> p = qb.is_visible();
-      if (nc.getFilter() != null) {
-        p = Predicate.and(qb.parse(nc.getFilter()), p);
-        p = args.queryRewriter.get().rewrite(p);
-      }
-      if (p.match(changeData)) {
-        recursivelyAddAllAccounts(matching, group);
-      }
-    }
-
-    if (!nc.getAddresses().isEmpty()) {
-      if (nc.getFilter() != null) {
-        ChangeQueryBuilder qb = args.queryBuilder.create(args.anonymousUser);
-        qb.setAllowFile(true);
-        Predicate<ChangeData> p = qb.parse(nc.getFilter());
-        p = args.queryRewriter.get().rewrite(p);
-        if (p.match(changeData)) {
-          matching.emails.addAll(nc.getAddresses());
-        }
-      } else {
-        matching.emails.addAll(nc.getAddresses());
-      }
-    }
-  }
-
-  private void recursivelyAddAllAccounts(Watchers matching, AccountGroup group)
-      throws OrmException {
-    Set<AccountGroup.Id> seen = Sets.newHashSet();
-    Queue<AccountGroup.Id> scan = Lists.newLinkedList();
-    scan.add(group.getId());
-    seen.add(group.getId());
-    while (!scan.isEmpty()) {
-      AccountGroup.Id next = scan.remove();
-      for (AccountGroupMember m : args.db.get().accountGroupMembers()
-          .byGroup(next)) {
-        matching.accounts.add(m.getAccountId());
-      }
-      for (AccountGroupInclude m : args.db.get().accountGroupIncludes()
-          .byGroup(next)) {
-        if (seen.add(m.getIncludeId())) {
-          scan.add(m.getIncludeId());
-        }
-      }
-    }
-  }
-
-  @SuppressWarnings("unchecked")
-  private void add(Watchers matching, AccountProjectWatch w)
-      throws OrmException {
-    IdentifiedUser user =
-        args.identifiedUserFactory.create(args.db, w.getAccountId());
-    ChangeQueryBuilder qb = args.queryBuilder.create(user);
-    Predicate<ChangeData> p = qb.is_visible();
-    if (w.getFilter() != null) {
-      try {
-        qb.setAllowFile(true);
-        p = Predicate.and(qb.parse(w.getFilter()), p);
-        p = args.queryRewriter.get().rewrite(p);
-        if (p.match(changeData)) {
-          matching.accounts.add(w.getAccountId());
-        }
-      } catch (QueryParseException e) {
-        // Ignore broken filter expressions.
-      }
-    } else if (p.match(changeData)) {
-      matching.accounts.add(w.getAccountId());
-    }
+  @Override
+  protected final Watchers getWatchers(NotifyType type) throws OrmException {
+    ProjectWatch watch = new ProjectWatch(args, project, projectState, changeData);
+    return watch.getWatchers(type);
   }
 
   /** Any user who has published comments on this change. */
@@ -509,7 +340,6 @@
 
   protected boolean isVisibleTo(final Account.Id to) throws OrmException {
     return projectState == null
-        || change == null
         || projectState.controlFor(args.identifiedUserFactory.create(to))
             .controlFor(change).isVisible(args.db.get());
   }
@@ -539,11 +369,54 @@
     velocityContext.put("change", change);
     velocityContext.put("changeId", change.getKey());
     velocityContext.put("coverLetter", getCoverLetter());
-    velocityContext.put("branch", change.getDest());
     velocityContext.put("fromName", getNameFor(fromId));
-    velocityContext.put("projectName", //
-        projectState != null ? projectState.getProject().getName() : null);
     velocityContext.put("patchSet", patchSet);
     velocityContext.put("patchSetInfo", patchSetInfo);
   }
+
+  public boolean getIncludeDiff() {
+    return args.settings.includeDiff;
+  }
+
+  /** Show patch set as unified difference. */
+  public String getUnifiedDiff() {
+    PatchList patchList;
+    try {
+      patchList = getPatchList();
+      if (patchList.getOldId() == null) {
+        // Octopus merges are not well supported for diff output by Gerrit.
+        // Currently these always have a null oldId in the PatchList.
+        return "[Octopus merge; cannot be formatted as a diff.]\n";
+      }
+    } catch (PatchListNotAvailableException e) {
+      log.error("Cannot format patch", e);
+      return "";
+    }
+
+    TemporaryBuffer.Heap buf =
+        new TemporaryBuffer.Heap(args.settings.maximumDiffSize);
+    DiffFormatter fmt = new DiffFormatter(buf);
+    Repository git;
+    try {
+      git = args.server.openRepository(change.getProject());
+    } catch (IOException e) {
+      log.error("Cannot open repository to format patch", e);
+      return "";
+    }
+    try {
+      fmt.setRepository(git);
+      fmt.setDetectRenames(true);
+      fmt.format(patchList.getOldId(), patchList.getNewId());
+      return RawParseUtils.decode(buf.toByteArray());
+    } catch (IOException e) {
+      if (JGitText.get().inMemoryBufferLimitExceeded.equals(e.getMessage())) {
+        return "";
+      }
+      log.error("Cannot format patch", e);
+      return "";
+    } finally {
+      fmt.release();
+      git.close();
+    }
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommentSender.java
index e7cc1ff..b16ab2a 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,11 +14,12 @@
 
 package com.google.gerrit.server.mail;
 
+import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
-import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
-import com.google.gerrit.server.config.AnonymousCowardName;
+import com.google.gerrit.server.change.PostReview.NotifyHandling;
 import com.google.gerrit.server.patch.PatchFile;
 import com.google.gerrit.server.patch.PatchList;
 import com.google.gerrit.server.patch.PatchListNotAvailableException;
@@ -37,15 +38,18 @@
 /** Send comments, after the author of them hit used Publish Comments in the UI. */
 public class CommentSender extends ReplyToChangeSender {
   public static interface Factory {
-    public CommentSender create(Change change);
+    public CommentSender create(NotifyHandling notify, Change change);
   }
 
+  private final NotifyHandling notify;
   private List<PatchLineComment> inlineComments = Collections.emptyList();
 
   @Inject
   public CommentSender(EmailArguments ea,
-      @AnonymousCowardName String anonymousCowardName, @Assisted Change c) {
-    super(ea, anonymousCowardName, c, "comment");
+      @Assisted NotifyHandling notify,
+      @Assisted Change c) {
+    super(ea, c, "comment");
+    this.notify = notify;
   }
 
   public void setPatchLineComments(final List<PatchLineComment> plc) {
@@ -67,9 +71,13 @@
   protected void init() throws EmailException {
     super.init();
 
-    ccAllApprovals();
-    bccStarredBy();
-    bccWatches(NotifyType.ALL_COMMENTS);
+    if (notify.compareTo(NotifyHandling.OWNER_REVIEWERS) >= 0) {
+      ccAllApprovals();
+    }
+    if (notify.compareTo(NotifyHandling.ALL) >= 0) {
+      bccStarredBy();
+      includeWatchers(NotifyType.ALL_COMMENTS);
+    }
   }
 
   @Override
@@ -77,6 +85,15 @@
     appendText(velocifyFile("Comment.vm"));
   }
 
+  @Override
+  public void formatFooter() throws EmailException {
+    appendText(velocifyFile("CommentFooter.vm"));
+  }
+
+  public boolean hasInlineComments() {
+    return !inlineComments.isEmpty();
+  }
+
   public String getInlineComments() {
     return getInlineComments(1);
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommitMessageEditedSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommitMessageEditedSender.java
new file mode 100644
index 0000000..e388f16
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/CommitMessageEditedSender.java
@@ -0,0 +1,36 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.mail;
+
+import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.inject.Inject;
+import com.google.inject.assistedinject.Assisted;
+
+public class CommitMessageEditedSender extends ReplacePatchSetSender {
+  public static interface Factory {
+    CommitMessageEditedSender create(Change change);
+  }
+
+  @Inject
+  public CommitMessageEditedSender(EmailArguments ea, @Assisted Change c) {
+    super(ea, c);
+  }
+
+  @Override
+  protected void formatChange() throws EmailException {
+    appendText(velocifyFile("CommitMessageEdited.vm"));
+  }
+}
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 9b82c71..3c9f39c 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,11 +14,12 @@
 
 package com.google.gerrit.server.mail;
 
+import com.google.common.collect.Iterables;
+import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.server.config.AnonymousCowardName;
-import com.google.gerrit.server.ssh.SshInfo;
+import com.google.gerrit.server.mail.ProjectWatch.Watchers;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
@@ -36,10 +37,8 @@
   }
 
   @Inject
-  public CreateChangeSender(EmailArguments ea,
-      @AnonymousCowardName String anonymousCowardName, SshInfo sshInfo,
-      @Assisted Change c) {
-    super(ea, anonymousCowardName, sshInfo, c);
+  public CreateChangeSender(EmailArguments ea, @Assisted Change c) {
+    super(ea, c);
   }
 
   @Override
@@ -47,31 +46,33 @@
     super.init();
 
     try {
-      // BCC anyone who has interest in this project's changes
-      // Try to mark interested owners with a TO and not a BCC line.
-      //
-      Watchers matching = getWatches(NotifyType.NEW_CHANGES);
-      for (Account.Id user : matching.accounts) {
+      // Try to mark interested owners with TO and CC or BCC line.
+      Watchers matching = getWatchers(NotifyType.NEW_CHANGES);
+      for (Account.Id user : Iterables.concat(
+          matching.to.accounts,
+          matching.cc.accounts,
+          matching.bcc.accounts)) {
         if (isOwnerOfProjectOrBranch(user)) {
           add(RecipientType.TO, user);
-        } else {
-          add(RecipientType.BCC, user);
         }
       }
-      for (Address addr : matching.emails) {
-        add(RecipientType.BCC, addr);
-      }
+
+      // Add everyone else. Owners added above will not be duplicated.
+      add(RecipientType.TO, matching.to);
+      add(RecipientType.CC, matching.cc);
+      add(RecipientType.BCC, matching.bcc);
     } catch (OrmException err) {
       // Just don't CC everyone. Better to send a partial message to those
       // we already have queued up then to fail deliver entirely to people
       // who have a lower interest in the change.
-      log.warn("Cannot BCC watchers for new change", err);
+      log.warn("Cannot notify watchers for new change", err);
     }
+
+    includeWatchers(NotifyType.NEW_PATCHSETS);
   }
 
   private boolean isOwnerOfProjectOrBranch(Account.Id user) {
     return projectState != null
-        && change != null
         && projectState.controlFor(args.identifiedUserFactory.create(user))
           .controlForRef(change.getDest())
           .isOwner();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java
index e6eda82..3e50283 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailArguments.java
@@ -21,7 +21,9 @@
 import com.google.gerrit.server.account.AccountCache;
 import com.google.gerrit.server.account.CapabilityControl;
 import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.account.GroupIncludeCache;
 import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.patch.PatchListCache;
@@ -29,17 +31,21 @@
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.query.change.ChangeQueryBuilder;
 import com.google.gerrit.server.query.change.ChangeQueryRewriter;
+import com.google.gerrit.server.ssh.SshAdvertisedAddresses;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
 import org.apache.velocity.runtime.RuntimeInstance;
 
+import java.util.List;
+
 import javax.annotation.Nullable;
 
 class EmailArguments {
   final GitRepositoryManager server;
   final ProjectCache projectCache;
   final GroupBackend groupBackend;
+  final GroupIncludeCache groupIncludes;
   final AccountCache accountCache;
   final PatchListCache patchListCache;
   final FromAddressGenerator fromAddressGenerator;
@@ -48,8 +54,10 @@
   final IdentifiedUser.GenericFactory identifiedUserFactory;
   final CapabilityControl.Factory capabilityControlFactory;
   final AnonymousUser anonymousUser;
+  final String anonymousCowardName;
   final Provider<String> urlProvider;
   final AllProjectsName allProjectsName;
+  final List<String> sshAddresses;
 
   final ChangeQueryBuilder.Factory queryBuilder;
   final Provider<ChangeQueryRewriter> queryRewriter;
@@ -59,21 +67,25 @@
 
   @Inject
   EmailArguments(GitRepositoryManager server, ProjectCache projectCache,
-      GroupBackend groupBackend, AccountCache accountCache,
+      GroupBackend groupBackend, GroupIncludeCache groupIncludes,
+      AccountCache accountCache,
       PatchListCache patchListCache, FromAddressGenerator fromAddressGenerator,
       EmailSender emailSender, PatchSetInfoFactory patchSetInfoFactory,
       GenericFactory identifiedUserFactory,
       CapabilityControl.Factory capabilityControlFactory,
       AnonymousUser anonymousUser,
+      @AnonymousCowardName String anonymousCowardName,
       @CanonicalWebUrl @Nullable Provider<String> urlProvider,
       AllProjectsName allProjectsName,
       ChangeQueryBuilder.Factory queryBuilder,
       Provider<ChangeQueryRewriter> queryRewriter, Provider<ReviewDb> db,
       RuntimeInstance velocityRuntime,
-      EmailSettings settings) {
+      EmailSettings settings,
+      @SshAdvertisedAddresses List<String> sshAddresses) {
     this.server = server;
     this.projectCache = projectCache;
     this.groupBackend = groupBackend;
+    this.groupIncludes = groupIncludes;
     this.accountCache = accountCache;
     this.patchListCache = patchListCache;
     this.fromAddressGenerator = fromAddressGenerator;
@@ -82,6 +94,7 @@
     this.identifiedUserFactory = identifiedUserFactory;
     this.capabilityControlFactory = capabilityControlFactory;
     this.anonymousUser = anonymousUser;
+    this.anonymousCowardName = anonymousCowardName;
     this.urlProvider = urlProvider;
     this.allProjectsName = allProjectsName;
     this.queryBuilder = queryBuilder;
@@ -89,5 +102,6 @@
     this.db = db;
     this.velocityRuntime = velocityRuntime;
     this.settings = settings;
+    this.sshAddresses = sshAddresses;
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailException.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailException.java
deleted file mode 100644
index a33cb63..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailException.java
+++ /dev/null
@@ -1,27 +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.mail;
-
-public class EmailException extends Exception {
-  private static final long serialVersionUID = 1L;
-
-  public EmailException(String msg) {
-    super(msg);
-  }
-
-  public EmailException(String msg, Throwable why) {
-    super(msg, why);
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailModule.java
new file mode 100644
index 0000000..f50f538
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailModule.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.mail;
+
+import com.google.gerrit.server.config.FactoryModule;
+
+public class EmailModule extends FactoryModule {
+  @Override
+  protected void configure() {
+    factory(AbandonedSender.Factory.class);
+    factory(CommentSender.Factory.class);
+    factory(RevertedSender.Factory.class);
+    factory(RestoredSender.Factory.class);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailSender.java
index 9c4f4c4..a7a1028 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/EmailSender.java
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.server.mail;
 
+import com.google.gerrit.common.errors.EmailException;
+
 import java.util.Collection;
 import java.util.Map;
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailUtil.java
new file mode 100644
index 0000000..f22c6e4
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/MailUtil.java
@@ -0,0 +1,131 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.common.errors.NoSuchAccountException;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.PatchSet;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+import com.google.gerrit.server.account.AccountResolver;
+import com.google.gwtorm.server.OrmException;
+
+import org.eclipse.jgit.revwalk.FooterKey;
+import org.eclipse.jgit.revwalk.FooterLine;
+
+import java.util.Collections;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+public class MailUtil {
+  private static final FooterKey REVIEWED_BY = new FooterKey("Reviewed-by");
+  private static final FooterKey TESTED_BY = new FooterKey("Tested-by");
+
+  public static MailRecipients getRecipientsFromFooters(
+      final AccountResolver accountResolver, final PatchSet ps,
+      final List<FooterLine> footerLines) throws OrmException {
+    final MailRecipients recipients = new MailRecipients();
+    if (!ps.isDraft()) {
+      for (final FooterLine footerLine : footerLines) {
+        try {
+          if (isReviewer(footerLine)) {
+            recipients.reviewers.add(toAccountId(accountResolver, footerLine
+                .getValue().trim()));
+          } else if (footerLine.matches(FooterKey.CC)) {
+            recipients.cc.add(toAccountId(accountResolver, footerLine
+                .getValue().trim()));
+          }
+        } catch (NoSuchAccountException e) {
+          continue;
+        }
+      }
+    }
+    return recipients;
+  }
+
+  public static MailRecipients getRecipientsFromApprovals(
+      final List<PatchSetApproval> approvals) {
+    final MailRecipients recipients = new MailRecipients();
+    for (PatchSetApproval a : approvals) {
+      if (a.getValue() != 0) {
+        recipients.reviewers.add(a.getAccountId());
+      } else {
+        recipients.cc.add(a.getAccountId());
+      }
+    }
+    return recipients;
+  }
+
+  private static Account.Id toAccountId(final AccountResolver accountResolver,
+      final String nameOrEmail) throws OrmException, NoSuchAccountException {
+    final Account a = accountResolver.findByNameOrEmail(nameOrEmail);
+    if (a == null) {
+      throw new NoSuchAccountException("\"" + nameOrEmail
+          + "\" is not registered");
+    }
+    return a.getId();
+  }
+
+  private static boolean isReviewer(final FooterLine candidateFooterLine) {
+    return candidateFooterLine.matches(FooterKey.SIGNED_OFF_BY)
+        || candidateFooterLine.matches(FooterKey.ACKED_BY)
+        || candidateFooterLine.matches(REVIEWED_BY)
+        || candidateFooterLine.matches(TESTED_BY);
+  }
+
+  public static class MailRecipients {
+    private final Set<Account.Id> reviewers;
+    private final Set<Account.Id> cc;
+
+    public MailRecipients() {
+      this.reviewers = new HashSet<Account.Id>();
+      this.cc = new HashSet<Account.Id>();
+    }
+
+    public MailRecipients(final Set<Account.Id> reviewers,
+        final Set<Account.Id> cc) {
+      this.reviewers = new HashSet<Account.Id>(reviewers);
+      this.cc = new HashSet<Account.Id>(cc);
+    }
+
+    public void add(final MailRecipients recipients) {
+      reviewers.addAll(recipients.reviewers);
+      cc.addAll(recipients.cc);
+    }
+
+    public void remove(final Account.Id toRemove) {
+      reviewers.remove(toRemove);
+      cc.remove(toRemove);
+    }
+
+    public Set<Account.Id> getReviewers() {
+      return Collections.unmodifiableSet(reviewers);
+    }
+
+    public Set<Account.Id> getCcOnly() {
+      final Set<Account.Id> cc = new HashSet<Account.Id>(this.cc);
+      cc.removeAll(reviewers);
+      return Collections.unmodifiableSet(cc);
+    }
+
+    public Set<Account.Id> getAll() {
+      final Set<Account.Id> all =
+          new HashSet<Account.Id>(reviewers.size() + cc.size());
+      all.addAll(reviewers);
+      all.addAll(cc);
+      return Collections.unmodifiableSet(all);
+    }
+  }
+}
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 b695fb4..3a5f7eb 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,8 +14,8 @@
 
 package com.google.gerrit.server.mail;
 
+import com.google.gerrit.common.errors.EmailException;
 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;
 
@@ -26,9 +26,8 @@
   }
 
   @Inject
-  public MergeFailSender(EmailArguments ea,
-      @AnonymousCowardName String anonymousCowardName, @Assisted Change c) {
-    super(ea, anonymousCowardName, c, "merge-failed");
+  public MergeFailSender(EmailArguments ea, @Assisted Change c) {
+    super(ea, 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 70b2d7f..37d800d 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
@@ -14,36 +14,32 @@
 
 package com.google.gerrit.server.mail;
 
-import com.google.gerrit.common.data.ApprovalType;
-import com.google.gerrit.common.data.ApprovalTypes;
+import com.google.common.collect.HashBasedTable;
+import com.google.common.collect.Table;
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.common.data.LabelTypes;
+import com.google.gerrit.common.data.LabelValue;
+import com.google.gerrit.common.errors.EmailException;
 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.PatchSetApproval;
 import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
-import com.google.gerrit.server.config.AnonymousCowardName;
+import com.google.gerrit.reviewdb.client.PatchSetApproval;
+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 java.util.HashMap;
-import java.util.Map;
-
 /** Send notice about a change successfully merged. */
 public class MergedSender extends ReplyToChangeSender {
   public static interface Factory {
-    public MergedSender create(Change change);
+    public MergedSender create(ChangeControl change);
   }
 
-  private final ApprovalTypes approvalTypes;
+  private final LabelTypes labelTypes;
 
   @Inject
-  public MergedSender(EmailArguments ea,
-      @AnonymousCowardName String anonymousCowardName, ApprovalTypes at,
-      @Assisted Change c) {
-    super(ea, anonymousCowardName, c, "merged");
-    approvalTypes = at;
+  public MergedSender(EmailArguments ea, @Assisted ChangeControl c) {
+    super(ea, c.getChange(), "merged");
+    labelTypes = c.getLabelTypes();
   }
 
   @Override
@@ -52,8 +48,8 @@
 
     ccAllApprovals();
     bccStarredBy();
-    bccWatches(NotifyType.ALL_COMMENTS);
-    bccWatches(NotifyType.SUBMITTED_CHANGES);
+    includeWatchers(NotifyType.ALL_COMMENTS);
+    includeWatchers(NotifyType.SUBMITTED_CHANGES);
   }
 
   @Override
@@ -63,18 +59,18 @@
 
   public String getApprovals() {
     try {
-      final Map<Account.Id, Map<ApprovalCategory.Id, PatchSetApproval>> pos =
-          new HashMap<Account.Id, Map<ApprovalCategory.Id, PatchSetApproval>>();
-
-      final Map<Account.Id, Map<ApprovalCategory.Id, PatchSetApproval>> neg =
-          new HashMap<Account.Id, Map<ApprovalCategory.Id, PatchSetApproval>>();
-
+      Table<Account.Id, String, PatchSetApproval> pos = HashBasedTable.create();
+      Table<Account.Id, String, PatchSetApproval> neg = HashBasedTable.create();
       for (PatchSetApproval ca : args.db.get().patchSetApprovals()
           .byPatchSet(patchSet.getId())) {
+        LabelType lt = labelTypes.byLabel(ca.getLabelId());
+        if (lt == null) {
+          continue;
+        }
         if (ca.getValue() > 0) {
-          insert(pos, ca);
+          pos.put(ca.getAccountId(), lt.getName(), ca);
         } else if (ca.getValue() < 0) {
-          insert(neg, ca);
+          neg.put(ca.getAccountId(), lt.getName(), ca);
         }
       }
 
@@ -85,22 +81,20 @@
     return "";
   }
 
-  private String format(final String type,
-      final Map<Account.Id, Map<ApprovalCategory.Id, PatchSetApproval>> list) {
+  private String format(String type,
+      Table<Account.Id, String, PatchSetApproval> approvals) {
     StringBuilder txt = new StringBuilder();
-    if (list.isEmpty()) {
+    if (approvals.isEmpty()) {
       return "";
     }
-    txt.append(type + ":\n");
-    for (final Map.Entry<Account.Id, Map<ApprovalCategory.Id, PatchSetApproval>> ent : list
-        .entrySet()) {
-      final Map<ApprovalCategory.Id, PatchSetApproval> l = ent.getValue();
+    txt.append(type).append(":\n");
+    for (Account.Id id : approvals.rowKeySet()) {
       txt.append("  ");
-      txt.append(getNameFor(ent.getKey()));
+      txt.append(getNameFor(id));
       txt.append(": ");
       boolean first = true;
-      for (ApprovalType at : approvalTypes.getApprovalTypes()) {
-        final PatchSetApproval ca = l.get(at.getCategory().getId());
+      for (LabelType lt : labelTypes.getLabelTypes()) {
+        PatchSetApproval ca = approvals.get(id, lt.getName());
         if (ca == null) {
           continue;
         }
@@ -111,32 +105,18 @@
           txt.append("; ");
         }
 
-        final ApprovalCategoryValue v = at.getValue(ca);
+        LabelValue v = lt.getValue(ca);
         if (v != null) {
-          txt.append(v.getName());
+          txt.append(v.getText());
         } else {
-          txt.append(at.getCategory().getName());
-          txt.append("=");
-          if (ca.getValue() > 0) {
-            txt.append("+");
-          }
-          txt.append("" + ca.getValue());
+          txt.append(lt.getName());
+          txt.append('=');
+          txt.append(LabelValue.formatValue(ca.getValue()));
         }
       }
-      txt.append("\n");
+      txt.append('\n');
     }
-    txt.append("\n");
+    txt.append('\n');
     return txt.toString();
   }
-
-  private void insert(
-      final Map<Account.Id, Map<ApprovalCategory.Id, PatchSetApproval>> list,
-      final PatchSetApproval ca) {
-    Map<ApprovalCategory.Id, PatchSetApproval> m = list.get(ca.getAccountId());
-    if (m == null) {
-      m = new HashMap<ApprovalCategory.Id, PatchSetApproval>();
-      list.put(ca.getAccountId(), m);
-    }
-    m.put(ca.getCategoryId(), ca);
-  }
 }
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 82c1405..9ff0dbd 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,23 +14,10 @@
 
 package com.google.gerrit.server.mail;
 
+import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.server.patch.PatchList;
-import com.google.gerrit.server.patch.PatchListNotAvailableException;
-import com.google.gerrit.server.ssh.SshInfo;
 
-import com.jcraft.jsch.HostKey;
-
-import org.eclipse.jgit.diff.DiffFormatter;
-import org.eclipse.jgit.internal.JGitText;
-import org.eclipse.jgit.lib.Repository;
-import org.eclipse.jgit.util.RawParseUtils;
-import org.eclipse.jgit.util.TemporaryBuffer;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashSet;
@@ -39,17 +26,11 @@
 
 /** Sends an email alerting a user to a new change for them to review. */
 public abstract class NewChangeSender extends ChangeEmail {
-  private static final Logger log =
-      LoggerFactory.getLogger(NewChangeSender.class);
-
-  private final SshInfo sshInfo;
   private final Set<Account.Id> reviewers = new HashSet<Account.Id>();
   private final Set<Account.Id> extraCC = new HashSet<Account.Id>();
 
-  protected NewChangeSender(EmailArguments ea, String anonymousCowardName,
-      SshInfo sshInfo, Change c) {
-    super(ea, anonymousCowardName, c, "newchange");
-    this.sshInfo = sshInfo;
+  protected NewChangeSender(EmailArguments ea, Change c) {
+    super(ea, c, "newchange");
   }
 
   public void addReviewers(final Collection<Account.Id> cc) {
@@ -86,63 +67,4 @@
     }
     return names;
   }
-
-  public String getSshHost() {
-    final List<HostKey> hostKeys = sshInfo.getHostKeys();
-    if (hostKeys.isEmpty()) {
-      return null;
-    }
-
-    final String host = hostKeys.get(0).getHost();
-    if (host.startsWith("*:")) {
-      return getGerritHost() + host.substring(1);
-    }
-    return host;
-  }
-
-  public boolean getIncludeDiff() {
-    return args.settings.includeDiff;
-  }
-
-  /** Show patch set as unified difference.  */
-  public String getUnifiedDiff() {
-    PatchList patchList;
-    try {
-      patchList = getPatchList();
-      if (patchList.getOldId() == null) {
-        // Octopus merges are not well supported for diff output by Gerrit.
-        // Currently these always have a null oldId in the PatchList.
-        return "";
-      }
-    } catch (PatchListNotAvailableException e) {
-      log.error("Cannot format patch", e);
-      return "";
-    }
-
-    TemporaryBuffer.Heap buf =
-        new TemporaryBuffer.Heap(args.settings.maximumDiffSize);
-    DiffFormatter fmt = new DiffFormatter(buf);
-    Repository git;
-    try {
-      git = args.server.openRepository(change.getProject());
-    } catch (IOException e) {
-      log.error("Cannot open repository to format patch", e);
-      return "";
-    }
-    try {
-      fmt.setRepository(git);
-      fmt.setDetectRenames(true);
-      fmt.format(patchList.getOldId(), patchList.getNewId());
-      return RawParseUtils.decode(buf.toByteArray());
-    } catch (IOException e) {
-      if (JGitText.get().inMemoryBufferLimitExceeded.equals(e.getMessage())) {
-        return "";
-      }
-      log.error("Cannot format patch", e);
-      return "";
-    } finally {
-      fmt.release();
-      git.close();
-    }
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/NotificationEmail.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/NotificationEmail.java
new file mode 100644
index 0000000..f0c43d3
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/NotificationEmail.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.server.mail;
+
+import com.google.common.collect.Iterables;
+import com.google.gerrit.common.errors.EmailException;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.mail.ProjectWatch.Watchers;
+import com.google.gwtorm.server.OrmException;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Common class for notifications that are related to a project and branch
+ */
+public abstract class NotificationEmail extends OutgoingEmail {
+  private static final Logger log =
+      LoggerFactory.getLogger(NotificationEmail.class);
+
+  protected Project.NameKey project;
+  protected Branch.NameKey branch;
+
+  protected NotificationEmail(EmailArguments ea,
+      String mc, Project.NameKey project, Branch.NameKey branch) {
+    super(ea, mc);
+
+    this.project = project;
+    this.branch = branch;
+  }
+
+  @Override
+  protected void init() throws EmailException {
+    super.init();
+    setListIdHeader();
+  }
+
+  private void setListIdHeader() throws EmailException {
+    // Set a reasonable list id so that filters can be used to sort messages
+    setVHeader("List-Id", "<$email.listId.replace('@', '.')>");
+    if (getSettingsUrl() != null) {
+      setVHeader("List-Unsubscribe", "<$email.settingsUrl>");
+    }
+  }
+
+  public String getListId() throws EmailException {
+    return velocify("gerrit-$projectName.replace('/', '-')@$email.gerritHost");
+  }
+
+  /** Include users and groups that want notification of events. */
+  protected void includeWatchers(NotifyType type) {
+    try {
+      Watchers matching = getWatchers(type);
+      add(RecipientType.TO, matching.to);
+      add(RecipientType.CC, matching.cc);
+      add(RecipientType.BCC, matching.bcc);
+    } catch (OrmException err) {
+      // Just don't CC everyone. Better to send a partial message to those
+      // we already have queued up then to fail deliver entirely to people
+      // who have a lower interest in the change.
+      log.warn("Cannot BCC watchers for " + type, err);
+    }
+  }
+
+  /** Returns all watchers that are relevant */
+  protected abstract Watchers getWatchers(NotifyType type) throws OrmException;
+
+  /** Add users or email addresses to the TO, CC, or BCC list. */
+  protected void add(RecipientType type, Watchers.List list) {
+    for (Account.Id user : list.accounts) {
+      add(type, user);
+    }
+    for (Address addr : list.emails) {
+      add(type, addr);
+    }
+  }
+
+  public String getSshHost() {
+    String host = Iterables.getFirst(args.sshAddresses, null);
+    if (host == null) {
+      return null;
+    }
+    if (host.startsWith("*:")) {
+      return getGerritHost() + host.substring(1);
+    }
+    return host;
+  }
+
+  @Override
+  protected void setupVelocityContext() {
+    super.setupVelocityContext();
+    velocityContext.put("projectName", project.get());
+    velocityContext.put("branch", branch);
+  }
+}
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 caa441d..997bc03 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
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.mail;
 
 import com.google.common.collect.Sets;
+import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.UserIdentity;
 import com.google.gerrit.server.account.AccountState;
@@ -59,14 +60,11 @@
   protected VelocityContext velocityContext;
 
   protected final EmailArguments args;
-  private final String anonymousCowardName;
   protected Account.Id fromId;
 
 
-  protected OutgoingEmail(EmailArguments ea, final String anonymousCowardName,
-      final String mc) {
+  protected OutgoingEmail(EmailArguments ea, String mc) {
     args = ea;
-    this.anonymousCowardName = anonymousCowardName;
     messageClass = mc;
     headers = new LinkedHashMap<String, EmailHeader>();
   }
@@ -235,7 +233,7 @@
   /** Lookup a human readable name for an account, usually the "full name". */
   protected String getNameFor(final Account.Id accountId) {
     if (accountId == null) {
-      return anonymousCowardName;
+      return args.anonymousCowardName;
     }
 
     final Account userAccount = args.accountCache.get(accountId).getAccount();
@@ -244,7 +242,7 @@
       name = userAccount.getPreferredEmail();
     }
     if (name == null) {
-      name = anonymousCowardName + " #" + accountId;
+      name = args.anonymousCowardName + " #" + accountId;
     }
     return name;
   }
@@ -263,7 +261,7 @@
       return email;
 
     } else /* (name == null && email == null) */{
-      return anonymousCowardName + " #" + accountId;
+      return args.anonymousCowardName + " #" + accountId;
     }
   }
 
@@ -332,6 +330,8 @@
             case CC:
               ((EmailHeader.AddressList) headers.get(HDR_CC)).add(addr);
               break;
+            case BCC:
+              break;
           }
         }
       } else {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/ProjectWatch.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ProjectWatch.java
new file mode 100644
index 0000000..84304b8
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/ProjectWatch.java
@@ -0,0 +1,216 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.common.base.Strings;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.gerrit.common.data.GroupDescription;
+import com.google.gerrit.common.data.GroupDescriptions;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.reviewdb.client.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.Project;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.git.NotifyConfig;
+import com.google.gerrit.server.project.ProjectState;
+import com.google.gerrit.server.query.Predicate;
+import com.google.gerrit.server.query.QueryParseException;
+import com.google.gerrit.server.query.change.ChangeData;
+import com.google.gerrit.server.query.change.ChangeQueryBuilder;
+import com.google.gerrit.server.query.change.SingleGroupUser;
+import com.google.gwtorm.server.OrmException;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+public class ProjectWatch {
+  private static final Logger log = LoggerFactory.getLogger(ProjectWatch.class);
+
+  protected final EmailArguments args;
+  protected final ProjectState projectState;
+  protected final Project.NameKey project;
+  protected final ChangeData changeData;
+
+  public ProjectWatch(EmailArguments args, Project.NameKey project,
+    ProjectState projectState, ChangeData changeData) {
+    this.args = args;
+    this.project = project;
+    this.projectState = projectState;
+    this.changeData = changeData;
+  }
+
+  /** Returns all watchers that are relevant */
+  public final Watchers getWatchers(NotifyType type) throws OrmException {
+    Watchers matching = new Watchers();
+    Set<Account.Id> projectWatchers = new HashSet<Account.Id>();
+
+    for (AccountProjectWatch w : args.db.get().accountProjectWatches()
+        .byProject(project)) {
+      if (w.isNotify(type)) {
+        projectWatchers.add(w.getAccountId());
+        add(matching, w);
+      }
+    }
+
+    for (AccountProjectWatch w : args.db.get().accountProjectWatches()
+        .byProject(args.allProjectsName)) {
+      if (!projectWatchers.contains(w.getAccountId()) && w.isNotify(type)) {
+        add(matching, w);
+      }
+    }
+
+    for (ProjectState state : projectState.tree()) {
+      for (NotifyConfig nc : state.getConfig().getNotifyConfigs()) {
+        if (nc.isNotify(type)) {
+          try {
+            add(matching, nc, state.getProject().getNameKey());
+          } catch (QueryParseException e) {
+            log.warn(String.format(
+                "Project %s has invalid notify %s filter \"%s\": %s",
+                state.getProject().getName(), nc.getName(),
+                nc.getFilter(), e.getMessage()));
+          }
+        }
+      }
+    }
+
+    return matching;
+  }
+
+  public static class Watchers {
+    static class List {
+      protected final Set<Account.Id> accounts = Sets.newHashSet();
+      protected final Set<Address> emails = Sets.newHashSet();
+    }
+    protected final List to = new List();
+    protected final List cc = new List();
+    protected final List bcc = new List();
+
+    List list(NotifyConfig.Header header) {
+      switch (header) {
+        case TO:
+          return to;
+        case CC:
+          return cc;
+        default:
+        case BCC:
+          return bcc;
+      }
+    }
+  }
+
+  private void add(Watchers matching, NotifyConfig nc, Project.NameKey project)
+      throws OrmException, QueryParseException {
+    for (GroupReference ref : nc.getGroups()) {
+      CurrentUser user = new SingleGroupUser(args.capabilityControlFactory,
+          ref.getUUID());
+      if (filterMatch(user, nc.getFilter())) {
+        deliverToMembers(matching.list(nc.getHeader()), ref.getUUID());
+      }
+    }
+
+    if (!nc.getAddresses().isEmpty()) {
+      if (filterMatch(null, nc.getFilter())) {
+        matching.list(nc.getHeader()).emails.addAll(nc.getAddresses());
+      }
+    }
+  }
+
+  private void deliverToMembers(
+      Watchers.List matching,
+      AccountGroup.UUID startUUID) throws OrmException {
+    ReviewDb db = args.db.get();
+    Set<AccountGroup.UUID> seen = Sets.newHashSet();
+    List<AccountGroup.UUID> q = Lists.newArrayList();
+
+    seen.add(startUUID);
+    q.add(startUUID);
+
+    while (!q.isEmpty()) {
+      AccountGroup.UUID uuid = q.remove(q.size() - 1);
+      GroupDescription.Basic group = args.groupBackend.get(uuid);
+      if (!Strings.isNullOrEmpty(group.getEmailAddress())) {
+        // If the group has an email address, do not expand membership.
+        matching.emails.add(new Address(group.getEmailAddress()));
+        continue;
+      }
+
+      AccountGroup ig = GroupDescriptions.toAccountGroup(group);
+      if (ig == null) {
+        // Non-internal groups cannot be expanded by the server.
+        continue;
+      }
+
+      for (AccountGroupMember m : db.accountGroupMembers().byGroup(ig.getId())) {
+        matching.accounts.add(m.getAccountId());
+      }
+      for (AccountGroup.UUID m : args.groupIncludes.membersOf(uuid)) {
+        if (seen.add(m)) {
+          q.add(m);
+        }
+      }
+    }
+  }
+
+  private void add(Watchers matching, AccountProjectWatch w)
+      throws OrmException {
+    IdentifiedUser user =
+        args.identifiedUserFactory.create(args.db, w.getAccountId());
+
+    try {
+      if (filterMatch(user, w.getFilter())) {
+        matching.bcc.accounts.add(w.getAccountId());
+      }
+    } catch (QueryParseException e) {
+      // Ignore broken filter expressions.
+    }
+  }
+
+  @SuppressWarnings("unchecked")
+  private boolean filterMatch(CurrentUser user, String filter)
+      throws OrmException, QueryParseException {
+    ChangeQueryBuilder qb;
+    Predicate<ChangeData> p = null;
+
+    if (user == null) {
+      qb = args.queryBuilder.create(args.anonymousUser);
+    } else {
+      qb = args.queryBuilder.create(user);
+      p = qb.is_visible();
+    }
+
+    if (filter != null) {
+      qb.setAllowFile(true);
+      Predicate<ChangeData> filterPredicate = qb.parse(filter);
+      if (p == null) {
+        p = filterPredicate;
+      } else {
+        p = Predicate.and(filterPredicate, p);
+      }
+      p = args.queryRewriter.get().rewrite(p);
+    }
+    return p == null ? true : p.match(changeData);
+  }
+}
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
index 8fc8238..f9a85b9 100644
--- 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
@@ -14,9 +14,8 @@
 
 package com.google.gerrit.server.mail;
 
+import com.google.gerrit.common.errors.EmailException;
 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;
 
@@ -27,10 +26,8 @@
   }
 
   @Inject
-  public RebasedPatchSetSender(EmailArguments ea,
-      @AnonymousCowardName String anonymousCowardName, SshInfo si,
-      @Assisted Change c) {
-    super(ea, anonymousCowardName, si, c);
+  public RebasedPatchSetSender(EmailArguments ea, @Assisted Change c) {
+    super(ea, c);
   }
 
   @Override
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 17fe9c6..eb32700 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,8 +14,8 @@
 
 package com.google.gerrit.server.mail;
 
+import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -32,10 +32,9 @@
   @Inject
   public RegisterNewEmailSender(EmailArguments ea,
       EmailTokenVerifier etv,
-      @AnonymousCowardName String anonymousCowardName,
       IdentifiedUser callingUser,
       @Assisted final String address) {
-    super(ea, anonymousCowardName, "registernewemail");
+    super(ea, "registernewemail");
     tokenVerifier = etv;
     user = callingUser;
     addr = address;
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 9705b8a..d80fb50 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,15 +14,13 @@
 
 package com.google.gerrit.server.mail;
 
+import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
 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;
 
-import com.jcraft.jsch.HostKey;
-
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.HashSet;
@@ -37,14 +35,10 @@
 
   private final Set<Account.Id> reviewers = new HashSet<Account.Id>();
   private final Set<Account.Id> extraCC = new HashSet<Account.Id>();
-  private final SshInfo sshInfo;
 
   @Inject
-  public ReplacePatchSetSender(EmailArguments ea,
-      @AnonymousCowardName String anonymousCowardName, SshInfo si,
-      @Assisted Change c) {
-    super(ea, anonymousCowardName, c, "newpatchset");
-    sshInfo = si;
+  public ReplacePatchSetSender(EmailArguments ea, @Assisted Change c) {
+    super(ea, c, "newpatchset");
   }
 
   public void addReviewers(final Collection<Account.Id> cc) {
@@ -68,6 +62,7 @@
     add(RecipientType.CC, extraCC);
     rcptToAuthors(RecipientType.CC);
     bccStarredBy();
+    includeWatchers(NotifyType.NEW_PATCHSETS);
   }
 
   @Override
@@ -85,17 +80,4 @@
     }
     return names;
   }
-
-  public String getSshHost() {
-    final List<HostKey> hostKeys = sshInfo.getHostKeys();
-    if (hostKeys.isEmpty()) {
-      return null;
-    }
-
-    final String host = hostKeys.get(0).getHost();
-    if (host.startsWith("*:")) {
-      return getGerritHost() + host.substring(1);
-    }
-    return host;
-  }
 }
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 2a77d39..42ac917 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,6 +14,7 @@
 
 package com.google.gerrit.server.mail;
 
+import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.reviewdb.client.Change;
 
 /** Alert a user to a reply to a change, usually commentary made during review. */
@@ -22,9 +23,8 @@
     public T create(Change change);
   }
 
-  protected ReplyToChangeSender(EmailArguments ea, String anonymousCowardName,
-      Change c, String mc) {
-    super(ea, anonymousCowardName, c, mc);
+  protected ReplyToChangeSender(EmailArguments ea, Change c, String mc) {
+    super(ea, c, mc);
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RestoredSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RestoredSender.java
index 946c29f..7f464f7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RestoredSender.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RestoredSender.java
@@ -14,9 +14,9 @@
 
 package com.google.gerrit.server.mail;
 
-import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
-import com.google.gerrit.server.config.AnonymousCowardName;
+import com.google.gerrit.reviewdb.client.Change;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -28,9 +28,8 @@
   }
 
   @Inject
-  public RestoredSender(EmailArguments ea,
-      @AnonymousCowardName String anonymousCowardName, @Assisted Change c) {
-    super(ea, anonymousCowardName, c, "restore");
+  public RestoredSender(EmailArguments ea, @Assisted Change c) {
+    super(ea, c, "restore");
   }
 
   @Override
@@ -39,7 +38,7 @@
 
     ccAllApprovals();
     bccStarredBy();
-    bccWatches(NotifyType.ALL_COMMENTS);
+    includeWatchers(NotifyType.ALL_COMMENTS);
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/RevertedSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/RevertedSender.java
index 033bd56..d1389fb 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,9 +14,9 @@
 
 package com.google.gerrit.server.mail;
 
-import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.reviewdb.client.AccountProjectWatch.NotifyType;
-import com.google.gerrit.server.config.AnonymousCowardName;
+import com.google.gerrit.reviewdb.client.Change;
 import com.google.inject.Inject;
 import com.google.inject.assistedinject.Assisted;
 
@@ -27,9 +27,8 @@
   }
 
   @Inject
-  public RevertedSender(EmailArguments ea,
-      @AnonymousCowardName String anonymousCowardName, @Assisted Change c) {
-    super(ea, anonymousCowardName, c, "revert");
+  public RevertedSender(EmailArguments ea, @Assisted Change c) {
+    super(ea, c, "revert");
   }
 
   @Override
@@ -38,7 +37,7 @@
 
     ccAllApprovals();
     bccStarredBy();
-    bccWatches(NotifyType.ALL_COMMENTS);
+    includeWatchers(NotifyType.ALL_COMMENTS);
   }
 
   @Override
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/mail/SmtpEmailSender.java b/gerrit-server/src/main/java/com/google/gerrit/server/mail/SmtpEmailSender.java
index ce45ffe..72df560 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
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.mail;
 
 import com.google.gerrit.common.Version;
+import com.google.gerrit.common.errors.EmailException;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.inject.AbstractModule;
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
deleted file mode 100644
index df93852..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/AddReviewer.java
+++ /dev/null
@@ -1,256 +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.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/IntraLineDiffKey.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineDiffKey.java
index 62ed5e4..5b37b92 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
@@ -82,6 +82,10 @@
     return bId;
   }
 
+  public boolean isIgnoreWhitespace() {
+    return ignoreWhitespace;
+  }
+
   Project.NameKey getProject() {
     return projectKey;
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineLoader.java
index 5b65920..b95994f 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineLoader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineLoader.java
@@ -29,10 +29,7 @@
 
 import java.util.ArrayList;
 import java.util.List;
-import java.util.concurrent.ArrayBlockingQueue;
-import java.util.concurrent.BlockingQueue;
 import java.util.concurrent.TimeUnit;
-import java.util.concurrent.atomic.AtomicInteger;
 import java.util.regex.Pattern;
 
 class IntraLineLoader extends CacheLoader<IntraLineDiffKey, IntraLineDiff> {
@@ -44,16 +41,12 @@
   private static final Pattern CONTROL_BLOCK_START_RE = Pattern
       .compile("[{:][ \\t]*$");
 
-  private final BlockingQueue<Worker> workerPool;
+  private final IntraLineWorkerPool workerPool;
   private final long timeoutMillis;
 
   @Inject
-  IntraLineLoader(final @GerritServerConfig Config cfg) {
-    final int workers =
-        cfg.getInt("cache", PatchListCacheImpl.INTRA_NAME, "maxIdleWorkers",
-            Runtime.getRuntime().availableProcessors() * 3 / 2);
-    workerPool = new ArrayBlockingQueue<Worker>(workers, true /* fair */);
-
+  IntraLineLoader(IntraLineWorkerPool pool, @GerritServerConfig Config cfg) {
+    workerPool = pool;
     timeoutMillis =
         ConfigUtil.getTimeUnit(cfg, "cache", PatchListCacheImpl.INTRA_NAME,
             "timeout", TimeUnit.MILLISECONDS.convert(5, TimeUnit.SECONDS),
@@ -62,26 +55,17 @@
 
   @Override
   public IntraLineDiff load(IntraLineDiffKey key) throws Exception {
-    Worker w = workerPool.poll();
-    if (w == null) {
-      w = new Worker();
-    }
+    IntraLineWorkerPool.Worker w = workerPool.acquire();
+    IntraLineWorkerPool.Worker.Result r = w.computeWithTimeout(key, timeoutMillis);
 
-    Worker.Result r = w.computeWithTimeout(key, timeoutMillis);
-
-    if (r == Worker.Result.TIMEOUT) {
+    if (r == IntraLineWorkerPool.Worker.Result.TIMEOUT) {
       // Don't keep this thread. We have to murder it unsafely, which
       // means its unable to be reused in the future. Return back a
       // null result, indicating the cache cannot load this key.
       //
       return new IntraLineDiff(IntraLineDiff.Status.TIMEOUT);
     }
-
-    if (!workerPool.offer(w)) {
-      // If the idle worker pool is full, terminate this thread.
-      //
-      w.end();
-    }
+    workerPool.release(w);
 
     if (r.error != null) {
       // If there was an error computing the result, carry it
@@ -93,127 +77,7 @@
     return r.diff;
   }
 
-  private static class Worker {
-    private static final AtomicInteger count = new AtomicInteger(1);
-
-    private final ArrayBlockingQueue<Input> input;
-    private final ArrayBlockingQueue<Result> result;
-    private final Thread thread;
-
-    Worker() {
-      input = new ArrayBlockingQueue<Input>(1);
-      result = new ArrayBlockingQueue<Result>(1);
-
-      thread = new Thread(new Runnable() {
-        public void run() {
-          workerLoop();
-        }
-      });
-      thread.setName("IntraLineDiff-" + count.getAndIncrement());
-      thread.setDaemon(true);
-      thread.start();
-    }
-
-    Result computeWithTimeout(IntraLineDiffKey key, long timeoutMillis)
-        throws Exception {
-      if (!input.offer(new Input(key))) {
-        log.error("Cannot enqueue task to thread " + thread.getName());
-        return Result.TIMEOUT;
-      }
-
-      Result r = result.poll(timeoutMillis, TimeUnit.MILLISECONDS);
-      if (r != null) {
-        return r;
-      } else {
-        log.warn(timeoutMillis + " ms timeout reached for IntraLineDiff"
-            + " in project " + key.getProject().get() //
-            + " on commit " + key.getCommit().name() //
-            + " for path " + key.getPath() //
-            + " comparing " + key.getBlobA().name() //
-            + ".." + key.getBlobB().name() //
-            + ".  Killing " + thread.getName());
-        forcefullyKillThreadInAnUglyWay();
-        return Result.TIMEOUT;
-      }
-    }
-
-    @SuppressWarnings("deprecation")
-    private void forcefullyKillThreadInAnUglyWay() {
-      try {
-        thread.stop();
-      } catch (Throwable error) {
-        // Ignore any reason the thread won't stop.
-        log.error("Cannot stop runaway thread " + thread.getName(), error);
-      }
-    }
-
-    void end() {
-      if (!input.offer(Input.END_THREAD)) {
-        log.error("Cannot gracefully stop thread " + thread.getName());
-      }
-    }
-
-    private void workerLoop() {
-      try {
-        for (;;) {
-          Input in;
-          try {
-            in = input.take();
-          } catch (InterruptedException e) {
-            log.error("Unexpected interrupt on " + thread.getName());
-            continue;
-          }
-
-          if (in == Input.END_THREAD) {
-            return;
-          }
-
-          Result r;
-          try {
-            r = new Result(IntraLineLoader.compute(in.key));
-          } catch (Exception error) {
-            r = new Result(error);
-          }
-
-          if (!result.offer(r)) {
-            log.error("Cannot return result from " + thread.getName());
-          }
-        }
-      } catch (ThreadDeath iHaveBeenShot) {
-        // Handle thread death by gracefully returning to the caller,
-        // allowing the thread to be destroyed.
-      }
-    }
-
-    private static class Input {
-      static final Input END_THREAD = new Input(null);
-
-      final IntraLineDiffKey key;
-
-      Input(IntraLineDiffKey key) {
-        this.key = key;
-      }
-    }
-
-    static class Result {
-      static final Result TIMEOUT = new Result((IntraLineDiff) null);
-
-      final IntraLineDiff diff;
-      final Exception error;
-
-      Result(IntraLineDiff diff) {
-        this.diff = diff;
-        this.error = null;
-      }
-
-      Result(Exception error) {
-        this.diff = null;
-        this.error = error;
-      }
-    }
-  }
-
-  private static IntraLineDiff compute(IntraLineDiffKey key) throws Exception {
+  static IntraLineDiff compute(IntraLineDiffKey key) throws Exception {
     List<Edit> edits = new ArrayList<Edit>(key.getEdits());
     Text aContent = key.getTextA();
     Text bContent = key.getTextB();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineWorkerPool.java b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineWorkerPool.java
new file mode 100644
index 0000000..5c6338fe
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/patch/IntraLineWorkerPool.java
@@ -0,0 +1,182 @@
+// 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 static com.google.gerrit.server.patch.IntraLineLoader.log;
+
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.inject.AbstractModule;
+import com.google.inject.Inject;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.lib.Config;
+
+import java.util.concurrent.ArrayBlockingQueue;
+import java.util.concurrent.BlockingQueue;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+
+@Singleton
+public class IntraLineWorkerPool {
+  public static class Module extends AbstractModule {
+    @Override
+    protected void configure() {
+      bind(IntraLineWorkerPool.class);
+    }
+  }
+
+  private final BlockingQueue<Worker> workerPool;
+
+  @Inject
+  public IntraLineWorkerPool(@GerritServerConfig Config cfg) {
+    int workers = cfg.getInt(
+        "cache", PatchListCacheImpl.INTRA_NAME, "maxIdleWorkers",
+        Runtime.getRuntime().availableProcessors() * 3 / 2);
+    workerPool = new ArrayBlockingQueue<Worker>(workers, true /* fair */);
+  }
+
+  Worker acquire() {
+    Worker w = workerPool.poll();
+    if (w == null) {
+      // If no worker is immediately available, start a new one.
+      // Maximum parallelism is controlled by the web server.
+      w = new Worker();
+      w.start();
+    }
+    return w;
+  }
+
+  void release(Worker w) {
+    if (!workerPool.offer(w)) {
+      // If the idle worker pool is full, terminate the worker.
+      w.shutdownGracefully();
+    }
+  }
+
+  static class Worker extends Thread {
+    private static final AtomicInteger count = new AtomicInteger(1);
+
+    private final ArrayBlockingQueue<Input> input;
+    private final ArrayBlockingQueue<Result> result;
+
+    Worker() {
+      input = new ArrayBlockingQueue<Input>(1);
+      result = new ArrayBlockingQueue<Result>(1);
+
+      setName("IntraLineDiff-" + count.getAndIncrement());
+      setDaemon(true);
+    }
+
+    Result computeWithTimeout(IntraLineDiffKey key, long timeoutMillis)
+        throws Exception {
+      if (!input.offer(new Input(key))) {
+        log.error("Cannot enqueue task to thread " + getName());
+        return Result.TIMEOUT;
+      }
+
+      Result r = result.poll(timeoutMillis, TimeUnit.MILLISECONDS);
+      if (r != null) {
+        return r;
+      } else {
+        log.warn(timeoutMillis + " ms timeout reached for IntraLineDiff"
+            + " in project " + key.getProject().get()
+            + " on commit " + key.getCommit().name()
+            + " for path " + key.getPath()
+            + " comparing " + key.getBlobA().name()
+            + ".." + key.getBlobB().name()
+            + ".  Killing " + getName());
+        forcefullyKillThreadInAnUglyWay();
+        return Result.TIMEOUT;
+      }
+    }
+
+    @SuppressWarnings("deprecation")
+    private void forcefullyKillThreadInAnUglyWay() {
+      try {
+        stop();
+      } catch (Throwable error) {
+        // Ignore any reason the thread won't stop.
+        log.error("Cannot stop runaway thread " + getName(), error);
+      }
+    }
+
+    private void shutdownGracefully() {
+      if (!input.offer(Input.END_THREAD)) {
+        log.error("Cannot gracefully stop thread " + getName());
+      }
+    }
+
+    @Override
+    public void run() {
+      try {
+        for (;;) {
+          Input in;
+          try {
+            in = input.take();
+          } catch (InterruptedException e) {
+            log.error("Unexpected interrupt on " + getName());
+            continue;
+          }
+
+          if (in == Input.END_THREAD) {
+            return;
+          }
+
+          Result r;
+          try {
+            r = new Result(IntraLineLoader.compute(in.key));
+          } catch (Exception error) {
+            r = new Result(error);
+          }
+
+          if (!result.offer(r)) {
+            log.error("Cannot return result from " + getName());
+          }
+        }
+      } catch (ThreadDeath iHaveBeenShot) {
+        // Handle thread death by gracefully returning to the caller,
+        // allowing the thread to be destroyed.
+      }
+    }
+
+    private static class Input {
+      static final Input END_THREAD = new Input(null);
+
+      final IntraLineDiffKey key;
+
+      Input(IntraLineDiffKey key) {
+        this.key = key;
+      }
+    }
+
+    static class Result {
+      static final Result TIMEOUT = new Result((IntraLineDiff) null);
+
+      final IntraLineDiff diff;
+      final Exception error;
+
+      Result(IntraLineDiff diff) {
+        this.diff = diff;
+        this.error = null;
+      }
+
+      Result(Exception error) {
+        this.diff = null;
+        this.error = error;
+      }
+    }
+  }
+}
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
deleted file mode 100644
index fdabcaa..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/patch/PublishComments.java
+++ /dev/null
@@ -1,380 +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.patch;
-
-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.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.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.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;
-
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-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;
-import java.util.concurrent.Callable;
-
-public class PublishComments implements Callable<VoidResult> {
-  private static final Logger log =
-      LoggerFactory.getLogger(PublishComments.class);
-
-  public interface Factory {
-    PublishComments create(PatchSet.Id patchSetId, String messageText,
-        Set<ApprovalCategoryValue.Id> approvals, boolean forceMessage);
-  }
-
-  private final SchemaFactory<ReviewDb> schemaFactory;
-  private final ReviewDb db;
-  private final IdentifiedUser user;
-  private final ApprovalTypes types;
-  private final CommentSender.Factory commentSenderFactory;
-  private final PatchSetInfoFactory patchSetInfoFactory;
-  private final ChangeControl.Factory changeControlFactory;
-  private final FunctionState.Factory functionStateFactory;
-  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;
-  private ChangeMessage message;
-  private List<PatchLineComment> drafts;
-
-  @Inject
-  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 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 boolean forceMessage) {
-    this.schemaFactory = sf;
-    this.db = db;
-    this.user = user;
-    this.types = approvalTypes;
-    this.patchSetInfoFactory = patchSetInfoFactory;
-    this.commentSenderFactory = commentSenderFactory;
-    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,
-      InvalidChangeOperationException, OrmException {
-    final Change.Id changeId = patchSetId.getParentKey();
-    final ChangeControl ctl = changeControlFactory.validateFor(changeId);
-    change = ctl.getChange();
-    patchSet = db.patchSets().get(patchSetId);
-    if (patchSet == null) {
-      throw new NoSuchChangeException(changeId);
-    }
-    drafts = drafts();
-
-    db.changes().beginTransaction(changeId);
-    try {
-      publishDrafts();
-
-      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();
-    }
-
-    email();
-    fireHook();
-    return VoidResult.INSTANCE;
-  }
-
-  private void publishDrafts() throws OrmException {
-    for (final PatchLineComment c : drafts) {
-      c.setStatus(PatchLineComment.Status.PUBLISHED);
-      c.updated();
-    }
-    db.patchComments().update(drafts);
-  }
-
-  private void publishApprovals(ChangeControl ctl)
-      throws InvalidChangeOperationException, OrmException {
-    ChangeUtil.updated(change);
-
-    final Set<ApprovalCategory.Id> dirty = new HashSet<ApprovalCategory.Id>();
-    final List<PatchSetApproval> ins = new ArrayList<PatchSetApproval>();
-    final List<PatchSetApproval> upd = new ArrayList<PatchSetApproval>();
-    final Collection<PatchSetApproval> all =
-        db.patchSetApprovals().byPatchSet(patchSetId).toList();
-    final Map<ApprovalCategory.Id, PatchSetApproval> mine = mine(all);
-
-    // Ensure any new approvals are stored properly.
-    //
-    for (final ApprovalCategoryValue.Id want : approvals) {
-      PatchSetApproval a = mine.get(want.getParentKey());
-      if (a == null) {
-        a = new PatchSetApproval(new PatchSetApproval.Key(//
-            patchSetId, user.getAccountId(), want.getParentKey()), want.get());
-        a.cache(change);
-        ins.add(a);
-        all.add(a);
-        mine.put(a.getCategoryId(), a);
-        dirty.add(a.getCategoryId());
-      }
-    }
-
-    // Normalize all of the items the user is changing.
-    //
-    final FunctionState functionState =
-        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);
-      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.
-        //
-        a.setGranted();
-        dirty.add(a.getCategoryId());
-      }
-      if (!ins.contains(a)) {
-        upd.add(a);
-      }
-    }
-
-    // Format a message explaining the actions taken.
-    //
-    final StringBuilder msgbuf = new StringBuilder();
-    for (final ApprovalType at : types.getApprovalTypes()) {
-      if (dirty.contains(at.getCategory().getId())) {
-        final PatchSetApproval a = mine.get(at.getCategory().getId());
-        if (a.getValue() == 0 && ins.contains(a)) {
-          // Don't say "no score" for an initial entry.
-          continue;
-        }
-
-        final ApprovalCategoryValue val = at.getValue(a);
-        if (msgbuf.length() > 0) {
-          msgbuf.append("; ");
-        }
-        if (val != null && val.getName() != null && !val.getName().isEmpty()) {
-          msgbuf.append(val.getName());
-        } else {
-          msgbuf.append(at.getCategory().getName());
-          msgbuf.append(" ");
-          if (a.getValue() > 0) msgbuf.append('+');
-          msgbuf.append(a.getValue());
-        }
-      }
-    }
-
-    // Update dashboards for everyone else.
-    //
-    for (PatchSetApproval a : all) {
-      if (!user.getAccountId().equals(a.getAccountId())) {
-        a.cache(change);
-        upd.add(a);
-      }
-    }
-
-    db.patchSetApprovals().update(upd);
-    db.patchSetApprovals().insert(ins);
-
-    summarizeInlineComments(msgbuf);
-    message(msgbuf.toString());
-  }
-
-  private void publishMessageOnly() throws OrmException {
-    StringBuilder msgbuf = new StringBuilder();
-    summarizeInlineComments(msgbuf);
-    message(msgbuf.toString());
-  }
-
-  private void message(String actions) throws OrmException {
-    if ((actions == null || actions.isEmpty())
-        && (messageText == null || messageText.isEmpty())) {
-      // They had nothing to say?
-      //
-      return;
-    }
-
-    final StringBuilder msgbuf = new StringBuilder();
-    msgbuf.append("Patch Set " + patchSetId.get() + ":");
-    if (actions != null && !actions.isEmpty()) {
-      msgbuf.append(" ");
-      msgbuf.append(actions);
-    }
-    msgbuf.append("\n\n");
-    msgbuf.append(messageText != null ? messageText : "");
-
-    message = new ChangeMessage(new ChangeMessage.Key(change.getId(),//
-        ChangeUtil.messageUUID(db)), user.getAccountId(), patchSetId);
-    message.setMessage(msgbuf.toString());
-    db.changeMessages().insert(Collections.singleton(message));
-  }
-
-  private Map<ApprovalCategory.Id, PatchSetApproval> mine(
-      Collection<PatchSetApproval> all) {
-    Map<ApprovalCategory.Id, PatchSetApproval> r =
-        new HashMap<ApprovalCategory.Id, PatchSetApproval>();
-    for (PatchSetApproval a : all) {
-      if (user.getAccountId().equals(a.getAccountId())) {
-        r.put(a.getCategoryId(), a);
-      }
-    }
-    return r;
-  }
-
-  private void touchChange() {
-    try {
-      ChangeUtil.touch(change, db);
-    } catch (OrmException e) {
-    }
-  }
-
-  private List<PatchLineComment> drafts() throws OrmException {
-    return db.patchComments().draftByPatchSetAuthor(patchSetId, user.getAccountId()).toList();
-  }
-
-  private void email() {
-    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() 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, db);
-  }
-
-  private void summarizeInlineComments(StringBuilder in) {
-    if (!drafts.isEmpty()) {
-      if (in.length() != 0) {
-        in.append("\n\n");
-      }
-      if (drafts.size() == 1) {
-        in.append("(1 inline comment)");
-      } else {
-        in.append("(" + drafts.size() + " inline comments)");
-      }
-    }
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/AutoRegisterModules.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/AutoRegisterModules.java
index e8af060..6062ae9 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/AutoRegisterModules.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/AutoRegisterModules.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.plugins;
 
+import static com.google.gerrit.server.plugins.AutoRegisterUtil.calculateBindAnnotation;
 import static com.google.gerrit.server.plugins.PluginGuiceEnvironment.is;
 
 import com.google.common.collect.LinkedListMultimap;
@@ -26,7 +27,6 @@
 import com.google.inject.Module;
 import com.google.inject.Scopes;
 import com.google.inject.TypeLiteral;
-import com.google.inject.internal.UniqueAnnotations;
 
 import org.eclipse.jgit.util.IO;
 import org.objectweb.asm.AnnotationVisitor;
@@ -116,16 +116,7 @@
           @SuppressWarnings("unchecked")
           Class<Object> impl = (Class<Object>) e.getValue();
 
-          Annotation n = impl.getAnnotation(Export.class);
-          if (n == null) {
-            n = impl.getAnnotation(javax.inject.Named.class);
-          }
-          if (n == null) {
-            n = impl.getAnnotation(com.google.inject.name.Named.class);
-          }
-          if (n == null) {
-            n = UniqueAnnotations.create();
-          }
+          Annotation n = calculateBindAnnotation(impl);
           bind(type).annotatedWith(n).to(impl);
         }
       }
@@ -245,9 +236,16 @@
 
       if (rawType.getAnnotation(ExtensionPoint.class) != null) {
         TypeLiteral<?> tl = TypeLiteral.get(type);
-        if (env.hasDynamicSet(tl)) {
+        if (env.hasDynamicItem(tl)) {
           sysSingletons.add(clazz);
           sysListen.put(tl, clazz);
+          httpGen.listen(tl, clazz);
+          sshGen.listen(tl, clazz);
+        } else if (env.hasDynamicSet(tl)) {
+          sysSingletons.add(clazz);
+          sysListen.put(tl, clazz);
+          httpGen.listen(tl, clazz);
+          sshGen.listen(tl, clazz);
         } else if (env.hasDynamicMap(tl)) {
           if (clazz.getAnnotation(Export.class) == null) {
             throw new InvalidPluginException(String.format(
@@ -256,6 +254,8 @@
           }
           sysSingletons.add(clazz);
           sysListen.put(tl, clazz);
+          httpGen.listen(tl, clazz);
+          sshGen.listen(tl, clazz);
         } else {
           throw new InvalidPluginException(String.format(
               "Cannot register %s, server does not accept %s",
@@ -299,7 +299,7 @@
     return data;
   }
 
-  private static class ClassData implements ClassVisitor {
+  private static class ClassData extends ClassVisitor {
     private static final String EXPORT = Type.getType(Export.class).getDescriptor();
     private static final String LISTEN = Type.getType(Listen.class).getDescriptor();
 
@@ -308,6 +308,10 @@
     String exportedAsName;
     boolean listen;
 
+    ClassData() {
+      super(Opcodes.ASM4);
+    }
+
     boolean isConcrete() {
       return (access & Opcodes.ACC_ABSTRACT) == 0
           && (access & Opcodes.ACC_INTERFACE) == 0;
@@ -370,8 +374,12 @@
     }
   }
 
-  private static abstract class AbstractAnnotationVisitor implements
+  private static abstract class AbstractAnnotationVisitor extends
       AnnotationVisitor {
+    AbstractAnnotationVisitor() {
+      super(Opcodes.ASM4);
+    }
+
     @Override
     public AnnotationVisitor visitAnnotation(String arg0, String arg1) {
       return null;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/AutoRegisterUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/AutoRegisterUtil.java
new file mode 100644
index 0000000..3256d7f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/AutoRegisterUtil.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.plugins;
+
+import com.google.gerrit.extensions.annotations.Export;
+import com.google.inject.internal.UniqueAnnotations;
+
+import java.lang.annotation.Annotation;
+
+public final class AutoRegisterUtil {
+
+  public static Annotation calculateBindAnnotation(Class<Object> impl) {
+    Annotation n = impl.getAnnotation(Export.class);
+    if (n == null) {
+      n = impl.getAnnotation(javax.inject.Named.class);
+    }
+    if (n == null) {
+      n = impl.getAnnotation(com.google.inject.name.Named.class);
+    }
+    if (n == null) {
+      n = UniqueAnnotations.create();
+    }
+    return n;
+  }
+
+  private AutoRegisterUtil() {
+    // hide default constructor
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CleanupHandle.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CleanupHandle.java
index 7018a3b..b04f337 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CleanupHandle.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CleanupHandle.java
@@ -16,19 +16,14 @@
 
 import java.io.File;
 import java.io.IOException;
-import java.lang.ref.ReferenceQueue;
-import java.lang.ref.WeakReference;
 import java.util.jar.JarFile;
 
-class CleanupHandle extends WeakReference<ClassLoader> {
+class CleanupHandle {
   private final File tmpFile;
   private final JarFile jarFile;
 
   CleanupHandle(File tmpFile,
-      JarFile jarFile,
-      ClassLoader ref,
-      ReferenceQueue<ClassLoader> queue) {
-    super(ref, queue);
+      JarFile jarFile) {
     this.tmpFile = tmpFile;
     this.jarFile = jarFile;
   }
@@ -39,7 +34,9 @@
     } catch (IOException err) {
     }
     if (!tmpFile.delete() && tmpFile.exists()) {
-      PluginLoader.log.warn("Cannot delete " + tmpFile.getAbsolutePath());
+      PluginLoader.log.warn("Cannot delete " + tmpFile.getAbsolutePath()
+          + ", retrying to delete it on termination of the virtual machine");
+      tmpFile.deleteOnExit();
     } else {
       PluginLoader.log.info("Cleaned plugin " + tmpFile.getName());
     }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CopyConfigModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CopyConfigModule.java
index f34826d..67ee7b4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CopyConfigModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/CopyConfigModule.java
@@ -15,6 +15,9 @@
 package com.google.gerrit.server.plugins;
 
 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.AnonymousCowardName;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePath;
 import com.google.gerrit.server.config.SitePaths;
@@ -27,6 +30,7 @@
 import com.google.inject.Singleton;
 
 import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.PersonIdent;
 
 import java.io.File;
 
@@ -93,6 +97,25 @@
   }
 
   @Inject
+  @AnonymousCowardName
+  private String anonymousCowardName;
+
+  @Provides
+  @AnonymousCowardName
+  String getAnonymousCowardName() {
+    return anonymousCowardName;
+  }
+
+  @Inject
+  private GerritPersonIdentProvider serverIdentProvider;
+
+  @Provides
+  @GerritPersonIdent
+  PersonIdent getServerIdent() {
+    return serverIdentProvider.get();
+  }
+
+  @Inject
   CopyConfigModule() {
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/DisablePlugin.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/DisablePlugin.java
new file mode 100644
index 0000000..cae5ce6
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/DisablePlugin.java
@@ -0,0 +1,42 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.plugins;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.server.plugins.DisablePlugin.Input;
+import com.google.inject.Inject;
+
+@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+class DisablePlugin implements RestModifyView<PluginResource, Input> {
+  static class Input {
+  }
+
+  private final PluginLoader loader;
+
+  @Inject
+  DisablePlugin(PluginLoader loader) {
+    this.loader = loader;
+  }
+
+  @Override
+  public Object apply(PluginResource resource, Input input) {
+    String name = resource.getName();
+    loader.disablePlugins(ImmutableSet.of(name));
+    return new ListPlugins.PluginInfo(loader.get(name));
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/EnablePlugin.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/EnablePlugin.java
new file mode 100644
index 0000000..f33d814
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/EnablePlugin.java
@@ -0,0 +1,56 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.plugins;
+
+import com.google.common.collect.ImmutableSet;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.server.plugins.EnablePlugin.Input;
+import com.google.inject.Inject;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+class EnablePlugin implements RestModifyView<PluginResource, Input> {
+  static class Input {
+  }
+
+  private final PluginLoader loader;
+
+  @Inject
+  EnablePlugin(PluginLoader loader) {
+    this.loader = loader;
+  }
+
+  @Override
+  public Object apply(PluginResource resource, Input input)
+      throws ResourceConflictException {
+    String name = resource.getName();
+    try {
+      loader.enablePlugins(ImmutableSet.of(name));
+    } catch (PluginInstallException e) {
+      StringWriter buf = new StringWriter();
+      buf.write(String.format("cannot enable %s\n", name));
+      PrintWriter pw = new PrintWriter(buf);
+      e.printStackTrace(pw);
+      pw.flush();
+      throw new ResourceConflictException(buf.toString());
+    }
+    return new ListPlugins.PluginInfo(loader.get(name));
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/GetStatus.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/GetStatus.java
new file mode 100644
index 0000000..2207d34
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/GetStatus.java
@@ -0,0 +1,24 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.plugins;
+
+import com.google.gerrit.extensions.restapi.RestReadView;
+
+class GetStatus implements RestReadView<PluginResource> {
+  @Override
+  public Object apply(PluginResource resource) {
+    return new ListPlugins.PluginInfo(resource.getPlugin());
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/InstallPlugin.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/InstallPlugin.java
new file mode 100644
index 0000000..e2cac6e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/InstallPlugin.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.server.plugins;
+
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.DefaultInput;
+import com.google.gerrit.extensions.restapi.PutInput;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.server.plugins.InstallPlugin.Input;
+import com.google.inject.Inject;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.PrintWriter;
+import java.io.StringWriter;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.util.zip.ZipException;
+
+@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+class InstallPlugin implements RestModifyView<TopLevelResource, Input> {
+  static class Input {
+    @DefaultInput
+    String url;
+    PutInput raw;
+  }
+
+  private final PluginLoader loader;
+  private final String name;
+  private final boolean created;
+
+  InstallPlugin(PluginLoader loader, String name, boolean created) {
+    this.loader = loader;
+    this.name = name;
+    this.created = created;
+  }
+
+  @Override
+  public Response<ListPlugins.PluginInfo> apply(TopLevelResource resource,
+      Input input) throws BadRequestException, IOException {
+    try {
+      InputStream in;
+      if (input.raw != null) {
+        in = input.raw.getInputStream();
+      } else {
+        try {
+          in = new URL(input.url).openStream();
+        } catch (MalformedURLException e) {
+          throw new BadRequestException(e.getMessage());
+        } catch (IOException e) {
+          throw new BadRequestException(e.getMessage());
+        }
+      }
+      try {
+        loader.installPluginFromStream(name, in);
+      } finally {
+        in.close();
+      }
+    } catch (PluginInstallException e) {
+      StringWriter buf = new StringWriter();
+      buf.write(String.format("cannot install %s", name));
+      if (e.getCause() instanceof ZipException) {
+        buf.write(": ");
+        buf.write(e.getCause().getMessage());
+      } else {
+        buf.write(":\n");
+        PrintWriter pw = new PrintWriter(buf);
+        e.printStackTrace(pw);
+        pw.flush();
+      }
+      throw new BadRequestException(buf.toString());
+    }
+
+    ListPlugins.PluginInfo info = new ListPlugins.PluginInfo(loader.get(name));
+    return created ? Response.created(info) : Response.ok(info);
+  }
+
+  @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+  static class Overwrite implements RestModifyView<PluginResource, Input> {
+    private final PluginLoader loader;
+
+    @Inject
+    Overwrite(PluginLoader loader) {
+      this.loader = loader;
+    }
+
+    @Override
+    public Response<ListPlugins.PluginInfo> apply(PluginResource resource,
+        Input input) throws BadRequestException, IOException {
+      return new InstallPlugin(loader, resource.getName(), false)
+        .apply(TopLevelResource.INSTANCE, input);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ListPlugins.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ListPlugins.java
index 3f7bc97..cb65e8c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ListPlugins.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ListPlugins.java
@@ -17,7 +17,16 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.server.OutputFormat;
+import com.google.gson.JsonElement;
 import com.google.gson.reflect.TypeToken;
 import com.google.inject.Inject;
 
@@ -34,10 +43,12 @@
 import java.util.Map;
 
 /** List the installed plugins. */
-public class ListPlugins {
+@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+public class ListPlugins implements RestReadView<TopLevelResource> {
   private final PluginLoader pluginLoader;
 
-  @Option(name = "--format", metaVar = "FMT", usage = "Output display format")
+  @Deprecated
+  @Option(name = "--format", usage = "(deprecated) output format")
   private OutputFormat format = OutputFormat.TEXT;
 
   @Option(name = "--all", aliases = {"-a"}, usage = "List all plugins, including disabled plugins")
@@ -57,19 +68,26 @@
     return this;
   }
 
-  public void display(OutputStream out) {
-    final PrintWriter stdout;
-    try {
-      stdout =
-          new PrintWriter(new BufferedWriter(new OutputStreamWriter(out,
-              "UTF-8")));
-    } catch (UnsupportedEncodingException e) {
-      // Our encoding is required by the specifications for the runtime.
-      throw new RuntimeException("JVM lacks UTF-8 encoding", e);
+  @Override
+  public Object apply(TopLevelResource resource) throws AuthException,
+      BadRequestException, ResourceConflictException, Exception {
+    format = OutputFormat.JSON;
+    return display(null);
+  }
+
+  public JsonElement display(OutputStream displayOutputStream)
+      throws UnsupportedEncodingException {
+    PrintWriter stdout = null;
+    if (displayOutputStream != null) {
+      try {
+        stdout = new PrintWriter(new BufferedWriter(
+            new OutputStreamWriter(displayOutputStream, "UTF-8")));
+      } catch (UnsupportedEncodingException e) {
+        throw new RuntimeException("JVM lacks UTF-8 encoding", e);
+      }
     }
 
     Map<String, PluginInfo> output = Maps.newTreeMap();
-
     List<Plugin> plugins = Lists.newArrayList(pluginLoader.getPlugins(all));
     Collections.sort(plugins, new Comparator<Plugin>() {
       @Override
@@ -80,15 +98,11 @@
 
     if (!format.isJson()) {
       stdout.format("%-30s %-10s %-8s\n", "Name", "Version", "Status");
-      stdout
-          .print("-------------------------------------------------------------------------------\n");
+      stdout.print("-------------------------------------------------------------------------------\n");
     }
 
     for (Plugin p : plugins) {
-      PluginInfo info = new PluginInfo();
-      info.version = p.getVersion();
-      info.disabled = p.isDisabled() ? true : null;
-
+      PluginInfo info = new PluginInfo(p);
       if (format.isJson()) {
         output.put(p.getName(), info);
       } else {
@@ -98,19 +112,29 @@
       }
     }
 
-    if (format.isJson()) {
+    if (stdout == null) {
+      return OutputFormat.JSON.newGson().toJsonTree(
+          output,
+          new TypeToken<Map<String, Object>>() {}.getType());
+    } else if (format.isJson()) {
       format.newGson().toJson(output,
           new TypeToken<Map<String, PluginInfo>>() {}.getType(), stdout);
       stdout.print('\n');
     }
     stdout.flush();
+    return null;
   }
 
-  private static class PluginInfo {
+  static class PluginInfo {
+    final String kind = "gerritcodereview#plugin";
+    String id;
     String version;
-    // disabled is only read via reflection when building the json output.  We
-    // do not want to show a compiler error that it isn't used.
-    @SuppressWarnings("unused")
     Boolean disabled;
+
+    PluginInfo(Plugin p) {
+      id = Url.encode(p.getName());
+      version = p.getVersion();
+      disabled = p.isDisabled() ? true : null;
+    }
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ModuleGenerator.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ModuleGenerator.java
index 92e3b1d..2011e9d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ModuleGenerator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ModuleGenerator.java
@@ -16,11 +16,14 @@
 
 import com.google.gerrit.extensions.annotations.Export;
 import com.google.inject.Module;
+import com.google.inject.TypeLiteral;
 
 public interface ModuleGenerator {
   void setPluginName(String name);
 
   void export(Export export, Class<?> type) throws InvalidPluginException;
 
+  void listen(TypeLiteral<?> tl, Class<?> clazz);
+
   Module create() throws InvalidPluginException;
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java
index 91ffbba..8e7192e 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/Plugin.java
@@ -20,8 +20,9 @@
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.extensions.registration.RegistrationHandle;
 import com.google.gerrit.extensions.registration.ReloadableRegistrationHandle;
-import com.google.gerrit.extensions.systemstatus.ServerInformation;
 import com.google.gerrit.lifecycle.LifecycleManager;
+import com.google.gerrit.server.PluginUser;
+import com.google.gerrit.server.util.RequestContext;
 import com.google.inject.AbstractModule;
 import com.google.inject.Guice;
 import com.google.inject.Injector;
@@ -29,7 +30,7 @@
 import com.google.inject.Provider;
 import com.google.inject.ProvisionException;
 
-import org.eclipse.jgit.storage.file.FileSnapshot;
+import org.eclipse.jgit.internal.storage.file.FileSnapshot;
 
 import java.io.File;
 import java.util.Collections;
@@ -42,7 +43,7 @@
 
 public class Plugin {
   public static enum ApiType {
-    EXTENSION, PLUGIN;
+    EXTENSION, PLUGIN, JS;
   }
 
   /** Unique key that changes whenever a plugin reloads. */
@@ -75,6 +76,8 @@
       return ApiType.EXTENSION;
     } else if (ApiType.PLUGIN.name().equalsIgnoreCase(v)) {
       return ApiType.PLUGIN;
+    } else if (ApiType.JS.name().equalsIgnoreCase(v)) {
+      return ApiType.JS;
     } else {
       throw new InvalidPluginException("Invalid Gerrit-ApiType: " + v);
     }
@@ -82,6 +85,7 @@
 
   private final CacheKey cacheKey;
   private final String name;
+  private final PluginUser pluginUser;
   private final File srcJar;
   private final FileSnapshot snapshot;
   private final JarFile jarFile;
@@ -101,6 +105,7 @@
   private List<ReloadableRegistrationHandle<?>> reloadableHandles;
 
   public Plugin(String name,
+      PluginUser pluginUser,
       File srcJar,
       FileSnapshot snapshot,
       JarFile jarFile,
@@ -112,6 +117,7 @@
       @Nullable Class<? extends Module> sshModule,
       @Nullable Class<? extends Module> httpModule) {
     this.cacheKey = new CacheKey(name);
+    this.pluginUser = pluginUser;
     this.name = name;
     this.srcJar = srcJar;
     this.snapshot = snapshot;
@@ -130,6 +136,10 @@
     return srcJar;
   }
 
+  PluginUser getPluginUser() {
+    return pluginUser;
+  }
+
   public CacheKey getCacheKey() {
     return cacheKey;
   }
@@ -171,7 +181,16 @@
     return disabled;
   }
 
-  public void start(PluginGuiceEnvironment env) throws Exception {
+  void start(PluginGuiceEnvironment env) throws Exception {
+    RequestContext oldContext = env.enter(this);
+    try {
+      startPlugin(env);
+    } finally {
+      env.exit(oldContext);
+    }
+  }
+
+  private void startPlugin(PluginGuiceEnvironment env) throws Exception {
     Injector root = newRootInjector(env);
     manager = new LifecycleManager();
 
@@ -228,20 +247,13 @@
 
   private Injector newRootInjector(final PluginGuiceEnvironment env) {
     List<Module> modules = Lists.newArrayListWithCapacity(4);
-    modules.add(env.getSysModule());
     if (apiType == ApiType.PLUGIN) {
       modules.add(env.getSysModule());
-    } else {
-      modules.add(new AbstractModule() {
-        @Override
-        protected void configure() {
-          bind(ServerInformation.class).toInstance(env.getServerInformation());
-        }
-      });
     }
     modules.add(new AbstractModule() {
       @Override
       protected void configure() {
+        bind(PluginUser.class).toInstance(pluginUser);
         bind(String.class)
           .annotatedWith(PluginName.class)
           .toInstance(name);
@@ -271,9 +283,14 @@
     return Guice.createInjector(modules);
   }
 
-  public void stop() {
+  void stop(PluginGuiceEnvironment env) {
     if (manager != null) {
-      manager.stop();
+      RequestContext oldContext = env.enter(this);
+      try {
+        manager.stop();
+      } finally {
+        env.exit(oldContext);
+      }
       manager = null;
       sysInjector = null;
       sshInjector = null;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java
index 18460ff..387ffa4 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginGuiceEnvironment.java
@@ -13,6 +13,8 @@
 // limitations under the License.
 
 package com.google.gerrit.server.plugins;
+
+import static com.google.gerrit.extensions.registration.PrivateInternals_DynamicTypes.dynamicItemsOf;
 import static com.google.gerrit.extensions.registration.PrivateInternals_DynamicTypes.dynamicMapsOf;
 import static com.google.gerrit.extensions.registration.PrivateInternals_DynamicTypes.dynamicSetsOf;
 
@@ -22,6 +24,7 @@
 import com.google.common.collect.Maps;
 import com.google.common.collect.Sets;
 import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.extensions.registration.DynamicItem;
 import com.google.gerrit.extensions.registration.DynamicMap;
 import com.google.gerrit.extensions.registration.DynamicSet;
 import com.google.gerrit.extensions.registration.PrivateInternals_DynamicMapImpl;
@@ -29,6 +32,9 @@
 import com.google.gerrit.extensions.registration.RegistrationHandle;
 import com.google.gerrit.extensions.registration.ReloadableRegistrationHandle;
 import com.google.gerrit.extensions.systemstatus.ServerInformation;
+import com.google.gerrit.server.util.PluginRequestContext;
+import com.google.gerrit.server.util.RequestContext;
+import com.google.gerrit.server.util.ThreadLocalRequestContext;
 import com.google.inject.AbstractModule;
 import com.google.inject.Binding;
 import com.google.inject.Guice;
@@ -62,6 +68,7 @@
 public class PluginGuiceEnvironment {
   private final Injector sysInjector;
   private final ServerInformation srvInfo;
+  private final ThreadLocalRequestContext local;
   private final CopyConfigModule copyConfigModule;
   private final Set<Key<?>> copyConfigKeys;
   private final List<StartPluginListener> onStart;
@@ -74,6 +81,8 @@
   private Provider<ModuleGenerator> sshGen;
   private Provider<ModuleGenerator> httpGen;
 
+  private Map<TypeLiteral<?>, DynamicItem<?>> sysItems;
+
   private Map<TypeLiteral<?>, DynamicSet<?>> sysSets;
   private Map<TypeLiteral<?>, DynamicSet<?>> sshSets;
   private Map<TypeLiteral<?>, DynamicSet<?>> httpSets;
@@ -85,10 +94,12 @@
   @Inject
   PluginGuiceEnvironment(
       Injector sysInjector,
+      ThreadLocalRequestContext local,
       ServerInformation srvInfo,
       CopyConfigModule ccm) {
     this.sysInjector = sysInjector;
     this.srvInfo = srvInfo;
+    this.local = local;
     this.copyConfigModule = ccm;
     this.copyConfigKeys = Guice.createInjector(ccm).getAllBindings().keySet();
 
@@ -98,6 +109,7 @@
     onReload = new CopyOnWriteArrayList<ReloadPluginListener>();
     onReload.addAll(listeners(sysInjector, ReloadPluginListener.class));
 
+    sysItems = dynamicItemsOf(sysInjector);
     sysSets = dynamicSetsOf(sysInjector);
     sysMaps = dynamicMapsOf(sysInjector);
   }
@@ -106,6 +118,10 @@
     return srvInfo;
   }
 
+  boolean hasDynamicItem(TypeLiteral<?> type) {
+    return sysItems.containsKey(type);
+  }
+
   boolean hasDynamicSet(TypeLiteral<?> type) {
     return sysSets.containsKey(type)
         || (sshSets != null && sshSets.containsKey(type))
@@ -177,18 +193,42 @@
     return httpGen.get();
   }
 
+  RequestContext enter(Plugin plugin) {
+    return local.setContext(new PluginRequestContext(plugin.getPluginUser()));
+  }
+
+  void exit(RequestContext old) {
+    local.setContext(old);
+  }
+
   void onStartPlugin(Plugin plugin) {
     for (StartPluginListener l : onStart) {
       l.onStartPlugin(plugin);
     }
 
-    attachSet(sysSets, plugin.getSysInjector(), plugin);
-    attachSet(sshSets, plugin.getSshInjector(), plugin);
-    attachSet(httpSets, plugin.getHttpInjector(), plugin);
+    RequestContext oldContext = enter(plugin);
+    try {
+      attachItem(sysItems, plugin.getSysInjector(), plugin);
 
-    attachMap(sysMaps, plugin.getSysInjector(), plugin);
-    attachMap(sshMaps, plugin.getSshInjector(), plugin);
-    attachMap(httpMaps, plugin.getHttpInjector(), plugin);
+      attachSet(sysSets, plugin.getSysInjector(), plugin);
+      attachSet(sshSets, plugin.getSshInjector(), plugin);
+      attachSet(httpSets, plugin.getHttpInjector(), plugin);
+
+      attachMap(sysMaps, plugin.getSysInjector(), plugin);
+      attachMap(sshMaps, plugin.getSshInjector(), plugin);
+      attachMap(httpMaps, plugin.getHttpInjector(), plugin);
+    } finally {
+      exit(oldContext);
+    }
+  }
+
+  private void attachItem(Map<TypeLiteral<?>, DynamicItem<?>> items,
+      @Nullable Injector src,
+      Plugin plugin) {
+    for (RegistrationHandle h : PrivateInternals_DynamicTypes
+        .attachItems(src, items, plugin.getName())) {
+      plugin.add(h);
+    }
   }
 
   private void attachSet(Map<TypeLiteral<?>, DynamicSet<?>> sets,
@@ -223,13 +263,20 @@
       old.put(h.getKey().getTypeLiteral(), h);
     }
 
-    reattachMap(old, sysMaps, newPlugin.getSysInjector(), newPlugin);
-    reattachMap(old, sshMaps, newPlugin.getSshInjector(), newPlugin);
-    reattachMap(old, httpMaps, newPlugin.getHttpInjector(), newPlugin);
+    RequestContext oldContext = enter(newPlugin);
+    try {
+      reattachMap(old, sysMaps, newPlugin.getSysInjector(), newPlugin);
+      reattachMap(old, sshMaps, newPlugin.getSshInjector(), newPlugin);
+      reattachMap(old, httpMaps, newPlugin.getHttpInjector(), newPlugin);
 
-    reattachSet(old, sysSets, newPlugin.getSysInjector(), newPlugin);
-    reattachSet(old, sshSets, newPlugin.getSshInjector(), newPlugin);
-    reattachSet(old, httpSets, newPlugin.getHttpInjector(), newPlugin);
+      reattachSet(old, sysSets, newPlugin.getSysInjector(), newPlugin);
+      reattachSet(old, sshSets, newPlugin.getSshInjector(), newPlugin);
+      reattachSet(old, httpSets, newPlugin.getHttpInjector(), newPlugin);
+
+      reattachItem(old, sysItems, newPlugin.getSysInjector(), newPlugin);
+    } finally {
+      exit(oldContext);
+    }
   }
 
   private void reattachMap(
@@ -350,6 +397,41 @@
       }
     }
   }
+  private void reattachItem(
+      ListMultimap<TypeLiteral<?>, ReloadableRegistrationHandle<?>> oldHandles,
+      Map<TypeLiteral<?>, DynamicItem<?>> items,
+      @Nullable Injector src,
+      Plugin newPlugin) {
+    if (src == null || items == null || items.isEmpty()) {
+      return;
+    }
+
+    for (Map.Entry<TypeLiteral<?>, DynamicItem<?>> e : items.entrySet()) {
+      @SuppressWarnings("unchecked")
+      TypeLiteral<Object> type = (TypeLiteral<Object>) e.getKey();
+
+      @SuppressWarnings("unchecked")
+      DynamicItem<Object> item = (DynamicItem<Object>) e.getValue();
+
+      Iterator<ReloadableRegistrationHandle<?>> oi =
+          oldHandles.get(type).iterator();
+
+      for (Binding<?> binding : bindings(src, type)) {
+        @SuppressWarnings("unchecked")
+        Binding<Object> b = (Binding<Object>) binding;
+        if (oi.hasNext()) {
+          @SuppressWarnings("unchecked")
+          ReloadableRegistrationHandle<Object> h =
+            (ReloadableRegistrationHandle<Object>) oi.next();
+          oi.remove();
+          replace(newPlugin, h, b);
+        } else {
+          newPlugin.add(item.set(b.getKey(), b.getProvider(),
+              newPlugin.getName()));
+        }
+      }
+    }
+  }
 
   private static <T> void replace(Plugin newPlugin,
       ReloadableRegistrationHandle<T> h, Binding<T> b) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
index d1c2aa4..035592c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginLoader.java
@@ -18,10 +18,13 @@
 import com.google.common.base.Strings;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
+import com.google.common.collect.Queues;
 import com.google.common.collect.Sets;
 import com.google.gerrit.extensions.annotations.PluginName;
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.extensions.systemstatus.ServerInformation;
+import com.google.gerrit.extensions.webui.JavaScriptPlugin;
+import com.google.gerrit.server.PluginUser;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
@@ -30,8 +33,8 @@
 import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
+import org.eclipse.jgit.internal.storage.file.FileSnapshot;
 import org.eclipse.jgit.lib.Config;
-import org.eclipse.jgit.storage.file.FileSnapshot;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -41,7 +44,6 @@
 import java.io.FileOutputStream;
 import java.io.IOException;
 import java.io.InputStream;
-import java.lang.ref.ReferenceQueue;
 import java.net.URL;
 import java.net.URLClassLoader;
 import java.text.SimpleDateFormat;
@@ -49,8 +51,10 @@
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Date;
+import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.Queue;
 import java.util.Set;
 import java.util.concurrent.ConcurrentMap;
 import java.util.concurrent.TimeUnit;
@@ -60,6 +64,7 @@
 
 @Singleton
 public class PluginLoader implements LifecycleListener {
+  static final String PLUGIN_TMP_PREFIX = "plugin_";
   static final Logger log = LoggerFactory.getLogger(PluginLoader.class);
 
   private final File pluginsDir;
@@ -67,11 +72,12 @@
   private final File tmpDir;
   private final PluginGuiceEnvironment env;
   private final ServerInformationImpl srvInfoImpl;
+  private final PluginUser.Factory pluginUserFactory;
   private final ConcurrentMap<String, Plugin> running;
   private final ConcurrentMap<String, Plugin> disabled;
   private final Map<String, FileSnapshot> broken;
-  private final ReferenceQueue<ClassLoader> cleanupQueue;
-  private final ConcurrentMap<CleanupHandle, Boolean> cleanupHandles;
+  private final Map<Plugin, CleanupHandle> cleanupHandles;
+  private final Queue<Plugin> toCleanup;
   private final Provider<PluginCleanerTask> cleaner;
   private final PluginScannerThread scanner;
 
@@ -79,6 +85,7 @@
   public PluginLoader(SitePaths sitePaths,
       PluginGuiceEnvironment pe,
       ServerInformationImpl sii,
+      PluginUser.Factory puf,
       Provider<PluginCleanerTask> pct,
       @GerritServerConfig Config cfg) {
     pluginsDir = sitePaths.plugins_dir;
@@ -86,10 +93,11 @@
     tmpDir = sitePaths.tmp_dir;
     env = pe;
     srvInfoImpl = sii;
+    pluginUserFactory = puf;
     running = Maps.newConcurrentMap();
     disabled = Maps.newConcurrentMap();
     broken = Maps.newHashMap();
-    cleanupQueue = new ReferenceQueue<ClassLoader>();
+    toCleanup = Queues.newArrayDeque();
     cleanupHandles = Maps.newConcurrentMap();
     cleaner = pct;
 
@@ -103,6 +111,14 @@
     }
   }
 
+  public Plugin get(String name) {
+    Plugin p = running.get(name);
+    if (p != null) {
+      return p;
+    }
+    return disabled.get(name);
+  }
+
   public Iterable<Plugin> getPlugins(boolean all) {
     if (!all) {
       return running.values();
@@ -178,6 +194,15 @@
     }
   }
 
+  synchronized private void unloadPlugin(Plugin plugin) {
+    String name = plugin.getName();
+    log.info(String.format("Unloading plugin %s", name));
+    plugin.stop(env);
+    running.remove(name);
+    disabled.remove(name);
+    toCleanup.add(plugin);
+  }
+
   public void disablePlugins(Set<String> names) {
     synchronized (this) {
       for (String name : names) {
@@ -190,8 +215,7 @@
         File off = new File(pluginsDir, active.getName() + ".jar.disabled");
         active.getSrcJar().renameTo(off);
 
-        active.stop();
-        running.remove(name);
+        unloadPlugin(active);
         try {
           FileSnapshot snapshot = FileSnapshot.save(off);
           Plugin offPlugin = loadPlugin(name, off, snapshot);
@@ -244,12 +268,12 @@
     srvInfoImpl.state = ServerInformation.State.SHUTDOWN;
     synchronized (this) {
       for (Plugin p : running.values()) {
-        p.stop();
+        unloadPlugin(p);
       }
       running.clear();
       disabled.clear();
       broken.clear();
-      if (cleanupHandles.size() > running.size()) {
+      if (!toCleanup.isEmpty()) {
         System.gc();
         processPendingCleanups();
       }
@@ -296,6 +320,10 @@
     dropRemovedDisabledPlugins(jars);
 
     for (File jar : jars) {
+      if (jar.getName().endsWith(".disabled")) {
+        continue;
+      }
+
       String name = nameOf(jar);
       FileSnapshot brokenTime = broken.get(name);
       if (brokenTime != null && !brokenTime.isModified(jar)) {
@@ -333,15 +361,14 @@
           && oldPlugin.canReload()
           && newPlugin.canReload();
       if (!reload && oldPlugin != null) {
-        oldPlugin.stop();
-        running.remove(name);
+        unloadPlugin(oldPlugin);
       }
       if (!newPlugin.isDisabled()) {
         newPlugin.start(env);
       }
       if (reload) {
         env.onReloadPlugin(oldPlugin, newPlugin);
-        oldPlugin.stop();
+        unloadPlugin(oldPlugin);
       } else if (!newPlugin.isDisabled()) {
         env.onStartPlugin(newPlugin);
       }
@@ -366,8 +393,7 @@
       }
     }
     for (String name : unload){
-      log.info(String.format("Unloading plugin %s", name));
-      running.remove(name).stop();
+      unloadPlugin(running.get(name));
     }
   }
 
@@ -384,16 +410,19 @@
   }
 
   synchronized int processPendingCleanups() {
-    CleanupHandle h;
-    while ((h = (CleanupHandle) cleanupQueue.poll()) != null) {
-      h.cleanup();
-      cleanupHandles.remove(h);
+    Iterator<Plugin> iterator = toCleanup.iterator();
+    while (iterator.hasNext()) {
+      Plugin plugin = iterator.next();
+      iterator.remove();
+
+      CleanupHandle cleanupHandle = cleanupHandles.remove(plugin);
+      cleanupHandle.cleanup();
     }
-    return Math.max(0, cleanupHandles.size() - running.size());
+    return toCleanup.size();
   }
 
   private void cleanInBackground() {
-    int cnt = Math.max(0, cleanupHandles.size() - running.size());
+    int cnt = toCleanup.size();
     if (0 < cnt) {
       cleaner.get().clean(cnt);
     }
@@ -437,19 +466,17 @@
       URL[] urls = {tmp.toURI().toURL()};
       ClassLoader parentLoader = parentFor(type);
       ClassLoader pluginLoader = new URLClassLoader(urls, parentLoader);
-      cleanupHandles.put(
-          new CleanupHandle(tmp, jarFile, pluginLoader, cleanupQueue),
-          Boolean.TRUE);
-
       Class<? extends Module> sysModule = load(sysName, pluginLoader);
       Class<? extends Module> sshModule = load(sshName, pluginLoader);
       Class<? extends Module> httpModule = load(httpName, pluginLoader);
-      keep = true;
-      return new Plugin(name,
+      Plugin plugin = new Plugin(name, pluginUserFactory.create(name),
           srcJar, snapshot,
           jarFile, manifest,
           new File(dataDir, name), type, pluginLoader,
           sysModule, sshModule, httpModule);
+      cleanupHandles.put(plugin, new CleanupHandle(tmp, jarFile));
+      keep = true;
+      return plugin;
     } finally {
       if (!keep) {
         jarFile.close();
@@ -464,6 +491,8 @@
         return PluginName.class.getClassLoader();
       case PLUGIN:
         return PluginLoader.class.getClassLoader();
+      case JS:
+        return JavaScriptPlugin.class.getClassLoader();
       default:
         throw new InvalidPluginException("Unsupported ApiType " + type);
     }
@@ -471,7 +500,7 @@
 
   private static String tempNameFor(String name) {
     SimpleDateFormat fmt = new SimpleDateFormat("yyMMdd_HHmm");
-    return "plugin_" + name + "_" + fmt.format(new Date()) + "_";
+    return PLUGIN_TMP_PREFIX + name + "_" + fmt.format(new Date()) + "_";
   }
 
   private Class<? extends Module> load(String name, ClassLoader pluginLoader)
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginModule.java
index ab7dc3c..4cdafb3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginModule.java
@@ -14,20 +14,37 @@
 
 package com.google.gerrit.server.plugins;
 
+import static com.google.gerrit.server.plugins.PluginResource.PLUGIN_KIND;
+
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.RestApiModule;
 import com.google.gerrit.extensions.systemstatus.ServerInformation;
 import com.google.gerrit.lifecycle.LifecycleModule;
 
-public class PluginModule extends LifecycleModule {
+public class PluginModule extends RestApiModule {
   @Override
   protected void configure() {
     bind(ServerInformationImpl.class);
     bind(ServerInformation.class).to(ServerInformationImpl.class);
 
     bind(PluginCleanerTask.class);
+    bind(PluginsCollection.class);
     bind(PluginGuiceEnvironment.class);
     bind(PluginLoader.class);
-
     bind(CopyConfigModule.class);
-    listener().to(PluginLoader.class);
+    install(new LifecycleModule() {
+      @Override
+      protected void configure() {
+        listener().to(PluginLoader.class);
+      }
+    });
+
+    DynamicMap.mapOf(binder(), PLUGIN_KIND);
+    put(PLUGIN_KIND).to(InstallPlugin.Overwrite.class);
+    delete(PLUGIN_KIND).to(DisablePlugin.class);
+    get(PLUGIN_KIND, "status").to(GetStatus.class);
+    post(PLUGIN_KIND, "disable").to(DisablePlugin.class);
+    post(PLUGIN_KIND, "enable").to(EnablePlugin.class);
+    post(PLUGIN_KIND, "reload").to(ReloadPlugin.class);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginResource.java
new file mode 100644
index 0000000..9572271
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginResource.java
@@ -0,0 +1,45 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.plugins;
+
+import com.google.gerrit.extensions.restapi.RestResource;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.inject.TypeLiteral;
+
+public class PluginResource implements RestResource {
+  public static final TypeLiteral<RestView<PluginResource>> PLUGIN_KIND =
+      new TypeLiteral<RestView<PluginResource>>() {};
+
+  private final Plugin plugin;
+  private final String name;
+
+  PluginResource(Plugin plugin) {
+    this.plugin = plugin;
+    this.name = plugin.getName();
+  }
+
+  PluginResource(String name) {
+    this.plugin = null;
+    this.name = name;
+  }
+
+  public String getName() {
+    return name;
+  }
+
+  public Plugin getPlugin() {
+    return plugin;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginsCollection.java
new file mode 100644
index 0000000..6d30b42
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/PluginsCollection.java
@@ -0,0 +1,69 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.plugins;
+
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.AcceptsCreate;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestCollection;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class PluginsCollection implements
+    RestCollection<TopLevelResource, PluginResource>,
+    AcceptsCreate<TopLevelResource> {
+
+  private final DynamicMap<RestView<PluginResource>> views;
+  private final PluginLoader loader;
+  private final Provider<ListPlugins> list;
+
+  @Inject
+  PluginsCollection(DynamicMap<RestView<PluginResource>> views,
+      PluginLoader loader, Provider<ListPlugins> list) {
+    this.views = views;
+    this.loader = loader;
+    this.list = list;
+  }
+
+  @Override
+  public RestView<TopLevelResource> list() throws ResourceNotFoundException {
+    return list.get();
+  }
+
+  @Override
+  public PluginResource parse(TopLevelResource parent, IdString id)
+      throws ResourceNotFoundException {
+    Plugin p = loader.get(id.get());
+    if (p == null) {
+      throw new ResourceNotFoundException(id);
+    }
+    return new PluginResource(p);
+  }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public InstallPlugin create(TopLevelResource parent, IdString id)
+      throws ResourceNotFoundException {
+    return new InstallPlugin(loader, id.get(), true /* created */);
+  }
+
+  @Override
+  public DynamicMap<RestView<PluginResource>> views() {
+    return views;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ReloadPlugin.java b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ReloadPlugin.java
new file mode 100644
index 0000000..9f9ef2db
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/plugins/ReloadPlugin.java
@@ -0,0 +1,57 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.plugins;
+
+import com.google.common.collect.ImmutableList;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.server.plugins.ReloadPlugin.Input;
+import com.google.inject.Inject;
+
+import java.io.PrintWriter;
+import java.io.StringWriter;
+
+@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+class ReloadPlugin implements RestModifyView<PluginResource, Input> {
+  static class Input {
+  }
+
+  private final PluginLoader loader;
+
+  @Inject
+  ReloadPlugin(PluginLoader loader) {
+    this.loader = loader;
+  }
+
+  @Override
+  public Object apply(PluginResource resource, Input input) throws ResourceConflictException {
+    String name = resource.getName();
+    try {
+      loader.reload(ImmutableList.of(name));
+    } catch (InvalidPluginException e) {
+      throw new ResourceConflictException(e.getMessage());
+    } catch (PluginInstallException e) {
+      StringWriter buf = new StringWriter();
+      buf.write(String.format("cannot reload %s\n", name));
+      PrintWriter pw = new PrintWriter(buf);
+      e.printStackTrace(pw);
+      pw.flush();
+      throw new ResourceConflictException(buf.toString());
+    }
+    return new ListPlugins.PluginInfo(loader.get(name));
+  }
+}
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 db4b021..6397b96 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,16 +14,16 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.common.data.PermissionRange;
 import com.google.gerrit.common.data.SubmitRecord;
+import com.google.gerrit.common.data.SubmitTypeRecord;
 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.query.change.ChangeData;
@@ -32,23 +32,19 @@
 import com.google.inject.Provider;
 import com.google.inject.util.Providers;
 
-import com.googlecode.prolog_cafe.compiler.CompileException;
 import com.googlecode.prolog_cafe.lang.IntegerTerm;
 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.SymbolTerm;
 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;
 
 import javax.annotation.Nullable;
 
@@ -195,12 +191,14 @@
 
   /** 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);
+    return (isOwner() || getRefControl().canPublishDrafts())
+        && 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);
+    return (isOwner() || getRefControl().canDeleteDrafts())
+        && isVisible(db);
   }
 
   /** Can this user rebase this change? */
@@ -215,6 +213,11 @@
         && getRefControl().canUpload(); // as long as you can upload too
   }
 
+  /** All available label types for this project. */
+  public LabelTypes getLabelTypes() {
+    return getProjectControl().getLabelTypes();
+  }
+
   /** All value ranges of any allowed label permission. */
   public List<PermissionRange> getLabelRanges() {
     return getRefControl().getLabelRanges();
@@ -266,25 +269,30 @@
 
   /** @return true if the user is allowed to remove this reviewer. */
   public boolean canRemoveReviewer(PatchSetApproval approval) {
+    return canRemoveReviewer(approval.getAccountId(), approval.getValue());
+  }
+
+  public boolean canRemoveReviewer(Account.Id reviewer, int value) {
     if (getChange().getStatus().isOpen()) {
       // A user can always remove themselves.
       //
       if (getCurrentUser() instanceof IdentifiedUser) {
         final IdentifiedUser i = (IdentifiedUser) getCurrentUser();
-        if (i.getAccountId().equals(approval.getAccountId())) {
+        if (i.getAccountId().equals(reviewer)) {
           return true; // can remove self
         }
       }
 
       // The change owner may remove any zero or positive score.
       //
-      if (isOwner() && 0 <= approval.getValue()) {
+      if (isOwner() && 0 <= value) {
         return true;
       }
 
-      // The branch owner, project owner, site admin can remove anyone.
-      //
-      if (getRefControl().isOwner() // branch owner
+      // Users with the remove reviewer permission, the branch owner, project
+      // owner and site admin can remove anyone
+      if (getRefControl().canRemoveReviewer() // has removal permissions
+          || getRefControl().isOwner() // branch owner
           || getProjectControl().isOwner() // project owner
           || getCurrentUser().getCapabilities().canAdministrateServer()) {
         return true;
@@ -294,16 +302,35 @@
     return false;
   }
 
+  /** Can this user edit the topic name? */
+  public boolean canEditTopicName() {
+    if (change.getStatus().isOpen()) {
+      return isOwner() // owner (aka creator) of the change can edit topic
+          || getRefControl().isOwner() // branch owner can edit topic
+          || getProjectControl().isOwner() // project owner can edit topic
+          || getCurrentUser().getCapabilities().canAdministrateServer() // site administers are god
+          || getRefControl().canEditTopicName() // user can edit topic on a specific ref
+      ;
+    } else {
+      return getRefControl().canForceEditTopicName();
+    }
+  }
+
   public List<SubmitRecord> getSubmitRecords(ReviewDb db, PatchSet patchSet) {
-    return canSubmit(db, patchSet, null, false, true);
+    return canSubmit(db, patchSet, null, false, true, false);
+  }
+
+  public boolean canSubmit() {
+    return getRefControl().canSubmit();
   }
 
   public List<SubmitRecord> canSubmit(ReviewDb db, PatchSet patchSet) {
-    return canSubmit(db, patchSet, null, false, false);
+    return canSubmit(db, patchSet, null, false, false, false);
   }
 
   public List<SubmitRecord> canSubmit(ReviewDb db, PatchSet patchSet,
-      @Nullable ChangeData cd, boolean fastEvalLabels, boolean allowClosed) {
+      @Nullable ChangeData cd, boolean fastEvalLabels, boolean allowClosed,
+      boolean allowDraft) {
     if (!allowClosed && change.getStatus().isClosed()) {
       SubmitRecord rec = new SubmitRecord();
       rec.status = SubmitRecord.Status.CLOSED;
@@ -314,122 +341,23 @@
       return ruleError("Patch set " + patchSet.getPatchSetId() + " is not current");
     }
 
-    try {
-      if (change.getStatus() == Change.Status.DRAFT) {
-        if (!isDraftVisible(db, cd)) {
-          return ruleError("Patch set " + patchSet.getPatchSetId() + " not found");
-        } else {
-          return ruleError("Cannot submit draft changes");
-        }
-      }
-      if (patchSet.isDraft()) {
-        if (!isDraftVisible(db, cd)) {
-          return ruleError("Patch set " + patchSet.getPatchSetId() + " not found");
-        } else {
-          return ruleError("Cannot submit draft patch sets");
-        }
-      }
-    } catch (OrmException err) {
-      return logRuleError("Cannot read patch set " + patchSet.getId(), err);
+    if ((change.getStatus() == Change.Status.DRAFT || patchSet.isDraft())
+        && !allowDraft) {
+      return cannotSubmitDraft(db, patchSet, cd);
     }
 
-    List<Term> results = new ArrayList<Term>();
-    Term submitRule;
-    ProjectState projectState = getProjectControl().getProjectState();
-    PrologEnvironment env;
-
+    List<Term> results;
+    SubmitRuleEvaluator evaluator;
     try {
-      env = projectState.newPrologEnvironment();
-    } catch (CompileException err) {
-      return logRuleError("Cannot consult rules.pl for "
-          + getProject().getName(), err);
-    }
-
-    try {
-      env.set(StoredValues.REVIEW_DB, db);
-      env.set(StoredValues.CHANGE, change);
-      env.set(StoredValues.CHANGE_DATA, cd);
-      env.set(StoredValues.PATCH_SET, patchSet);
-      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());
-      }
-
-      if (fastEvalLabels) {
-        env.once("gerrit", "assume_range_from_label");
-      }
-
-      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 {
-            if (fastEvalLabels) {
-              env.once("gerrit", "assume_range_from_label");
-            }
-
-            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();
+      evaluator = new SubmitRuleEvaluator(db, patchSet,
+          getProjectControl(),
+          this, change, cd,
+          fastEvalLabels,
+          "locate_submit_rule", "can_submit",
+          "locate_submit_filter", "filter_submit_results");
+      results = evaluator.evaluate();
+    } catch (RuleEvalException e) {
+      return logRuleError(e.getMessage(), e);
     }
 
     if (results.isEmpty()) {
@@ -437,12 +365,28 @@
       // 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.");
+      log.error("Submit rule '" + evaluator.getSubmitRule() + "' for change "
+          + change.getId() + " of " + getProject().getName()
+          + " has no solution.");
       return ruleError("Project submit rule has no solution");
     }
 
-    return resultsToSubmitRecord(submitRule, results);
+    return resultsToSubmitRecord(evaluator.getSubmitRule(), results);
+  }
+
+  private List<SubmitRecord> cannotSubmitDraft(ReviewDb db, PatchSet patchSet,
+      ChangeData cd) {
+    try {
+      if (!isDraftVisible(db, cd)) {
+        return ruleError("Patch set " + patchSet.getPatchSetId() + " not found");
+      } else if (patchSet.isDraft()) {
+        return ruleError("Cannot submit draft patch sets");
+      } else {
+        return ruleError("Cannot submit draft changes");
+      }
+    } catch (OrmException err) {
+      return logRuleError("Cannot read patch set " + patchSet.getId(), err);
+    }
   }
 
   /**
@@ -527,6 +471,64 @@
     return out;
   }
 
+  public SubmitTypeRecord getSubmitTypeRecord(ReviewDb db, PatchSet patchSet) {
+    return getSubmitTypeRecord(db, patchSet, null);
+  }
+
+  public SubmitTypeRecord getSubmitTypeRecord(ReviewDb db, PatchSet patchSet,
+      @Nullable ChangeData cd) {
+    try {
+      if (change.getStatus() == Change.Status.DRAFT && !isDraftVisible(db, cd)) {
+        return typeRuleError("Patch set " + patchSet.getPatchSetId()
+            + " not found");
+      }
+      if (patchSet.isDraft() && !isDraftVisible(db, cd)) {
+        return typeRuleError("Patch set " + patchSet.getPatchSetId()
+            + " not found");
+      }
+    } catch (OrmException err) {
+      return logTypeRuleError("Cannot read patch set " + patchSet.getId(),
+          err);
+    }
+
+    List<Term> results;
+    SubmitRuleEvaluator evaluator;
+    try {
+      evaluator = new SubmitRuleEvaluator(db, patchSet,
+          getProjectControl(), this, change, cd,
+          false,
+          "locate_submit_type", "get_submit_type",
+          "locate_submit_type_filter", "filter_submit_type_results");
+      results = evaluator.evaluate();
+    } catch (RuleEvalException e) {
+      return logTypeRuleError(e.getMessage(), e);
+    }
+
+    if (results.isEmpty()) {
+      // Should never occur for a well written rule
+      log.error("Submit rule '" + evaluator.getSubmitRule() + "' for change "
+          + change.getId() + " of " + getProject().getName()
+          + " has no solution.");
+      return typeRuleError("Project submit rule has no solution");
+    }
+
+    Term typeTerm = results.get(0);
+    if (!typeTerm.isSymbol()) {
+      log.error("Submit rule '" + evaluator.getSubmitRule() + "' for change "
+          + change.getId() + " of " + getProject().getName()
+          + " did not return a symbol.");
+      return typeRuleError("Project submit rule has invalid solution");
+    }
+
+    String typeName = ((SymbolTerm)typeTerm).name();
+    try {
+      return SubmitTypeRecord.OK(
+          Project.SubmitType.valueOf(typeName.toUpperCase()));
+    } catch (IllegalArgumentException e) {
+      return logInvalidType(evaluator.getSubmitRule(), typeName);
+    }
+  }
+
   private List<SubmitRecord> logInvalidResult(Term rule, Term record) {
     return logRuleError("Submit rule " + rule + " for change " + change.getId()
         + " of " + getProject().getName() + " output invalid result: " + record);
@@ -549,6 +551,29 @@
     return Collections.singletonList(rec);
   }
 
+  private SubmitTypeRecord logInvalidType(Term rule, String record) {
+    return logTypeRuleError("Submit type rule " + rule + " for change "
+        + change.getId() + " of " + getProject().getName()
+        + " output invalid result: " + record);
+  }
+
+  private SubmitTypeRecord logTypeRuleError(String err, Exception e) {
+    log.error(err, e);
+    return typeRuleError("Error evaluating project type rules, check server log");
+  }
+
+  private SubmitTypeRecord logTypeRuleError(String err) {
+    log.error(err);
+    return typeRuleError("Error evaluating project type rules, check server log");
+  }
+
+  private SubmitTypeRecord typeRuleError(String err) {
+    SubmitTypeRecord rec = new SubmitTypeRecord();
+    rec.status = SubmitTypeRecord.Status.RULE_ERROR;
+    rec.errorMessage = err;
+    return rec;
+  }
+
   private void appliedBy(SubmitRecord.Label label, Term status) {
     if (status.isStructure() && status.arity() == 1) {
       Term who = status.arg(0);
@@ -560,7 +585,7 @@
 
   private boolean isDraftVisible(ReviewDb db, ChangeData cd)
       throws OrmException {
-    return isOwner() || isReviewer(db, cd);
+    return isOwner() || isReviewer(db, cd) || getRefControl().canViewDrafts();
   }
 
   private static boolean isUser(Term who) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
index 3dbd7b7..5494146 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProject.java
@@ -1,4 +1,4 @@
-// Copyright (C) 2011 The Android Open Source Project
+// Copyright (C) 2013 The Android Open Source Project
 //
 // Licensed under the Apache License, Version 2.0 (the "License");
 // you may not use this file except in compliance with the License.
@@ -10,289 +10,115 @@
 // distributed under the License is distributed on an "AS IS" BASIS,
 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 // See the License for the specific language governing permissions and
-// limitations under the License
+// limitations under the License.
 
 package com.google.gerrit.server.project;
 
-import com.google.gerrit.common.data.AccessSection;
-import com.google.gerrit.common.data.GroupDescription;
-import com.google.gerrit.common.data.GroupReference;
-import com.google.gerrit.common.data.Permission;
-import com.google.gerrit.common.data.PermissionRule;
+import com.google.common.base.Objects;
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.common.errors.ProjectCreationFailedException;
-import com.google.gerrit.extensions.events.NewProjectCreatedListener;
-import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
 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.GroupBackend;
-import com.google.gerrit.server.config.ProjectOwnerGroups;
-import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
-import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.MetaDataUpdate;
-import com.google.gerrit.server.git.ProjectConfig;
-import com.google.gerrit.server.git.RepositoryCaseMismatchException;
+import com.google.gerrit.reviewdb.client.Project.InheritableBoolean;
+import com.google.gerrit.reviewdb.client.Project.SubmitType;
+import com.google.gerrit.server.group.GroupsCollection;
+import com.google.gerrit.server.project.CreateProject.Input;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 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.Collections;
 import java.util.List;
-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);
+@RequiresCapability(GlobalCapability.CREATE_PROJECT)
+class CreateProject implements RestModifyView<TopLevelResource, Input> {
+  static class Input {
+    String name;
+    String parent;
+    String description;
+    boolean permissionsOnly;
+    boolean createEmptyCommit;
+    SubmitType submitType;
+    List<String> branches;
+    List<String> owners;
+    InheritableBoolean useContributorAgreements;
+    InheritableBoolean useSignedOffBy;
+    InheritableBoolean useContentMerge;
+    InheritableBoolean requireChangeId;
   }
 
-  private final Set<AccountGroup.UUID> projectOwnerGroups;
-  private final IdentifiedUser currentUser;
-  private final GitRepositoryManager repoManager;
-  private final GitReferenceUpdated referenceUpdated;
-  private final DynamicSet<NewProjectCreatedListener> createdListener;
-  private final PersonIdent serverIdent;
-  private CreateProjectArgs createProjectArgs;
-  private ProjectCache projectCache;
-  private GroupBackend groupBackend;
-  private MetaDataUpdate.User metaDataUpdateFactory;
+  static interface Factory {
+    CreateProject create(String name);
+  }
+
+  private final PerformCreateProject.Factory createProjectFactory;
+  private final Provider<ProjectsCollection> projectsCollection;
+  private final Provider<GroupsCollection> groupsCollection;
+  private final ProjectJson json;
+  private final String name;
 
   @Inject
-  CreateProject(@ProjectOwnerGroups Set<AccountGroup.UUID> pOwnerGroups,
-      IdentifiedUser identifiedUser, GitRepositoryManager gitRepoManager,
-      GitReferenceUpdated referenceUpdated,
-      DynamicSet<NewProjectCreatedListener> createdListener,
-      ReviewDb db,
-      @GerritPersonIdent PersonIdent personIdent, GroupBackend groupBackend,
-      MetaDataUpdate.User metaDataUpdateFactory,
-      @Assisted CreateProjectArgs createPArgs, ProjectCache pCache) {
-    this.projectOwnerGroups = pOwnerGroups;
-    this.currentUser = identifiedUser;
-    this.repoManager = gitRepoManager;
-    this.referenceUpdated = referenceUpdated;
-    this.createdListener = createdListener;
-    this.serverIdent = personIdent;
-    this.createProjectArgs = createPArgs;
-    this.projectCache = pCache;
-    this.groupBackend = groupBackend;
-    this.metaDataUpdateFactory = metaDataUpdateFactory;
+  CreateProject(PerformCreateProject.Factory performCreateProjectFactory,
+      Provider<ProjectsCollection> projectsCollection,
+      Provider<GroupsCollection> groupsCollection, ProjectJson json,
+      @Assisted String name) {
+    this.createProjectFactory = performCreateProjectFactory;
+    this.projectsCollection = projectsCollection;
+    this.groupsCollection = groupsCollection;
+    this.json = json;
+    this.name = name;
   }
 
-  public void createProject() throws ProjectCreationFailedException {
-    validateParameters();
-    final Project.NameKey nameKey = createProjectArgs.getProject();
-    try {
-      final String head =
-          createProjectArgs.permissionsOnly ? GitRepositoryManager.REF_CONFIG
-              : createProjectArgs.branch.get(0);
-      final Repository repo = repoManager.createRepository(nameKey);
-      try {
-        NewProjectCreatedListener.Event event = new NewProjectCreatedListener.Event() {
-          @Override
-          public String getProjectName() {
-            return nameKey.get();
-          }
-
-          @Override
-          public String getHeadName() {
-            return head;
-          }
-        };
-        for (NewProjectCreatedListener l : createdListener) {
-          l.onNewProjectCreated(event);
-        }
-
-        final RefUpdate u = repo.updateRef(Constants.HEAD);
-        u.disableRefLog();
-        u.link(head);
-
-        createProjectConfig();
-
-        if (!createProjectArgs.permissionsOnly
-            && createProjectArgs.createEmptyCommit) {
-          createEmptyCommits(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 (IOException ioErr) {
-        final String msg = "Cannot create " + nameKey;
-        log.error(msg, err);
-        throw new ProjectCreationFailedException(msg, ioErr);
-      }
-    } catch (Exception e) {
-      final String msg = "Cannot create " + nameKey;
-      log.error(msg, e);
-      throw new ProjectCreationFailedException(msg, e);
+  @Override
+  public Object apply(TopLevelResource resource, Input input)
+      throws BadRequestException, UnprocessableEntityException,
+      ProjectCreationFailedException {
+    if (input == null) {
+      input = new Input();
     }
-  }
-
-  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) {
-          GroupDescription.Basic g = groupBackend.get(ownerId);
-          if (g != null) {
-            GroupReference group = config.resolve(GroupReference.forGroup(g));
-            all.getPermission(Permission.OWNER, true).add(
-                new PermissionRule(group));
-          }
-        }
-      }
-
-      md.setMessage("Created project\n");
-      config.commit(md);
-    } finally {
-      md.close();
-    }
-    projectCache.onCreateProject(createProjectArgs.getProject());
-    repoManager.setProjectDescription(createProjectArgs.getProject(),
-        createProjectArgs.projectDescription);
-    referenceUpdated.fire(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 (input.name != null && !name.equals(input.name)) {
+      throw new BadRequestException("name must match URL");
     }
 
-    if (createProjectArgs.getProjectName().endsWith(Constants.DOT_GIT_EXT)) {
-      createProjectArgs.setProjectName(createProjectArgs.getProjectName()
-          .substring(
-              0,
-              createProjectArgs.getProjectName().length()
-                  - Constants.DOT_GIT_EXT.length()));
+    final CreateProjectArgs args = new CreateProjectArgs();
+    args.setProjectName(name);
+    if (!Strings.isNullOrEmpty(input.parent)) {
+      args.newParent = projectsCollection.get().parse(input.parent).getControl();
     }
-
-    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);
-    }
-
-    List<String> transformedBranches = new ArrayList<String>();
-    if (createProjectArgs.branch == null ||
-        createProjectArgs.branch.isEmpty()) {
-      createProjectArgs.branch = Collections.singletonList(Constants.MASTER);
-    }
-    for (String branch : createProjectArgs.branch) {
-      while (branch.startsWith("/")) {
-        branch = branch.substring(1);
+    args.createEmptyCommit = input.createEmptyCommit;
+    args.permissionsOnly = input.permissionsOnly;
+    args.projectDescription = Strings.emptyToNull(input.description);
+    args.submitType =
+        Objects.firstNonNull(input.submitType, SubmitType.MERGE_IF_NECESSARY);
+    args.branch = input.branches;
+    if (input.owners != null) {
+      List<AccountGroup.UUID> ownerIds =
+          Lists.newArrayListWithCapacity(input.owners.size());
+      for (String owner : input.owners) {
+        ownerIds.add(groupsCollection.get().parse(owner).getGroupUUID());
       }
-      if (!branch.startsWith(Constants.R_HEADS)) {
-        branch = Constants.R_HEADS + branch;
-      }
-      if (!Repository.isValidRefName(branch)) {
-        throw new ProjectCreationFailedException(String.format(
-            "Branch \"%s\" is not a valid name.", branch));
-      }
-      if (!transformedBranches.contains(branch)) {
-        transformedBranches.add(branch);
-      }
+      args.ownerIds = ownerIds;
     }
-    createProjectArgs.branch = transformedBranches;
-  }
+    args.contributorAgreements =
+        Objects.firstNonNull(input.useContributorAgreements,
+            InheritableBoolean.INHERIT);
+    args.signedOffBy =
+        Objects.firstNonNull(input.useSignedOffBy, InheritableBoolean.INHERIT);
+    args.contentMerge =
+        input.submitType == SubmitType.FAST_FORWARD_ONLY
+            ? InheritableBoolean.FALSE : Objects.firstNonNull(
+                input.useContentMerge, InheritableBoolean.INHERIT);
+    args.changeIdRequired =
+        Objects.firstNonNull(input.requireChangeId, InheritableBoolean.INHERIT);
 
-  private void createEmptyCommits(final Repository repo,
-      final Project.NameKey project, final List<String> refs)
-      throws IOException {
-    ObjectInserter oi = repo.newObjectInserter();
-    try {
-      CommitBuilder cb = new CommitBuilder();
-      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();
-
-      for (String ref : refs) {
-        RefUpdate ru = repo.updateRef(ref);
-        ru.setNewObjectId(id);
-        final Result result = ru.update();
-        switch (result) {
-          case NEW:
-            referenceUpdated.fire(project, ref);
-            break;
-          default: {
-            throw new IOException(String.format(
-              "Failed to create ref \"%s\": %s", ref, result.name()));
-          }
-        }
-      }
-    } catch (IOException e) {
-      log.error(
-          "Cannot create empty commit for "
-              + createProjectArgs.getProjectName(), e);
-      throw e;
-    } finally {
-      oi.release();
-    }
+    Project p = createProjectFactory.create(args).createProject();
+    return Response.created(json.format(p));
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProjectArgs.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProjectArgs.java
index 2dee4f4..7bbd2e7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProjectArgs.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/CreateProjectArgs.java
@@ -16,6 +16,7 @@
 
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.Project.InheritableBoolean;
 import com.google.gerrit.reviewdb.client.Project.SubmitType;
 
 import java.util.List;
@@ -27,15 +28,20 @@
   public ProjectControl newParent;
   public String projectDescription;
   public SubmitType submitType;
-  public boolean contributorAgreements;
-  public boolean signedOffBy;
+  public InheritableBoolean contributorAgreements;
+  public InheritableBoolean signedOffBy;
   public boolean permissionsOnly;
   public List<String> branch;
-  public boolean contentMerge;
-  public boolean changeIdRequired;
+  public InheritableBoolean contentMerge;
+  public InheritableBoolean changeIdRequired;
   public boolean createEmptyCommit;
 
   public CreateProjectArgs() {
+    contributorAgreements = InheritableBoolean.INHERIT;
+    signedOffBy = InheritableBoolean.INHERIT;
+    contentMerge = InheritableBoolean.INHERIT;
+    changeIdRequired = InheritableBoolean.INHERIT;
+    submitType = SubmitType.MERGE_IF_NECESSARY;
   }
 
   public Project.NameKey getProject() {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DashboardResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DashboardResource.java
new file mode 100644
index 0000000..0e2db48
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DashboardResource.java
@@ -0,0 +1,68 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import com.google.gerrit.extensions.restapi.RestResource;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.inject.TypeLiteral;
+
+import org.eclipse.jgit.lib.Config;
+
+public class DashboardResource implements RestResource {
+  public static final TypeLiteral<RestView<DashboardResource>> DASHBOARD_KIND =
+      new TypeLiteral<RestView<DashboardResource>>() {};
+
+  static DashboardResource projectDefault(ProjectControl ctl) {
+    return new DashboardResource(ctl, null, null, null, true);
+  }
+
+  private final ProjectControl control;
+  private final String refName;
+  private final String pathName;
+  private final Config config;
+  private final boolean projectDefault;
+
+  DashboardResource(ProjectControl control,
+      String refName,
+      String pathName,
+      Config config,
+      boolean projectDefault) {
+    this.control = control;
+    this.refName = refName;
+    this.pathName = pathName;
+    this.config = config;
+    this.projectDefault = projectDefault;
+  }
+
+  public ProjectControl getControl() {
+    return control;
+  }
+
+  public String getRefName() {
+    return refName;
+  }
+
+  public String getPathName() {
+    return pathName;
+  }
+
+  public Config getConfig() {
+    return config;
+  }
+
+  public boolean isProjectDefault() {
+    return projectDefault;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DashboardsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DashboardsCollection.java
new file mode 100644
index 0000000..a50e71c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DashboardsCollection.java
@@ -0,0 +1,233 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.git.GitRepositoryManager.REFS_DASHBOARDS;
+
+import com.google.common.base.Joiner;
+import com.google.common.base.Objects;
+import com.google.common.base.Splitter;
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.AcceptsCreate;
+import com.google.gerrit.extensions.restapi.ChildCollection;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestApiException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.extensions.restapi.Url;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.UrlEncoded;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gson.annotations.SerializedName;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.errors.AmbiguousObjectException;
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.IncorrectObjectTypeException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.BlobBasedConfig;
+import org.eclipse.jgit.lib.Config;
+import org.eclipse.jgit.lib.ObjectId;
+import org.eclipse.jgit.lib.Repository;
+
+import java.io.IOException;
+import java.io.UnsupportedEncodingException;
+import java.util.List;
+
+class DashboardsCollection implements
+    ChildCollection<ProjectResource, DashboardResource>,
+    AcceptsCreate<ProjectResource>{
+  private final GitRepositoryManager gitManager;
+  private final DynamicMap<RestView<DashboardResource>> views;
+  private final Provider<ListDashboards> list;
+  private final Provider<SetDefaultDashboard.CreateDefault> createDefault;
+
+  @Inject
+  DashboardsCollection(GitRepositoryManager gitManager,
+      DynamicMap<RestView<DashboardResource>> views,
+      Provider<ListDashboards> list,
+      Provider<SetDefaultDashboard.CreateDefault> createDefault) {
+    this.gitManager = gitManager;
+    this.views = views;
+    this.list = list;
+    this.createDefault = createDefault;
+  }
+
+  @Override
+  public RestView<ProjectResource> list() throws ResourceNotFoundException {
+    return list.get();
+  }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public RestModifyView<ProjectResource, ?> create(ProjectResource parent,
+      IdString id) throws RestApiException {
+    if (id.equals("default")) {
+      return createDefault.get();
+    }
+    throw new ResourceNotFoundException(id);
+  }
+
+  @Override
+  public DashboardResource parse(ProjectResource parent, IdString id)
+      throws ResourceNotFoundException, IOException, ConfigInvalidException {
+    ProjectControl myCtl = parent.getControl();
+    if (id.equals("default")) {
+      return DashboardResource.projectDefault(myCtl);
+    }
+
+    List<String> parts = Lists.newArrayList(
+        Splitter.on(':').limit(2).split(id.get()));
+    if (parts.size() != 2) {
+      throw new ResourceNotFoundException(id);
+    }
+
+    CurrentUser user = myCtl.getCurrentUser();
+    String ref = parts.get(0);
+    String path = parts.get(1);
+    for (ProjectState ps : myCtl.getProjectState().tree()) {
+      try {
+        return parse(ps.controlFor(user), ref, path, myCtl);
+      } catch (AmbiguousObjectException e) {
+        throw new ResourceNotFoundException(id);
+      } catch (IncorrectObjectTypeException e) {
+        throw new ResourceNotFoundException(id);
+      } catch (ConfigInvalidException e) {
+        throw new ResourceNotFoundException(id);
+      } catch (ResourceNotFoundException e) {
+        continue;
+      }
+    }
+    throw new ResourceNotFoundException(id);
+  }
+
+  private DashboardResource parse(ProjectControl ctl, String ref, String path,
+      ProjectControl myCtl)
+      throws ResourceNotFoundException, IOException, AmbiguousObjectException,
+          IncorrectObjectTypeException, ConfigInvalidException {
+    String id = ref + ":" + path;
+    if (!ref.startsWith(REFS_DASHBOARDS)) {
+      ref = REFS_DASHBOARDS + ref;
+    }
+    if (!Repository.isValidRefName(ref)
+        || !ctl.controlForRef(ref).canRead()) {
+      throw new ResourceNotFoundException(id);
+    }
+
+    Repository git;
+    try {
+      git = gitManager.openRepository(ctl.getProject().getNameKey());
+    } catch (RepositoryNotFoundException e) {
+      throw new ResourceNotFoundException(id);
+    }
+    try {
+      ObjectId objId = git.resolve(ref + ":" + path);
+      if (objId == null) {
+        throw new ResourceNotFoundException(id);
+      }
+      BlobBasedConfig cfg = new BlobBasedConfig(null, git, objId);
+      return new DashboardResource(myCtl, ref, path, cfg, false);
+    } finally {
+      git.close();
+    }
+  }
+
+  @Override
+  public DynamicMap<RestView<DashboardResource>> views() {
+    return views;
+  }
+
+  static DashboardInfo parse(Project definingProject, String refName,
+      String path, Config config, String project, boolean setDefault)
+      throws UnsupportedEncodingException {
+    DashboardInfo info = new DashboardInfo(refName, path);
+    info.project = project;
+    info.definingProject = definingProject.getName();
+    info.title = replace(project, config.getString("dashboard", null, "title"));
+    info.description = replace(project, config.getString("dashboard", null, "description"));
+    info.foreach = config.getString("dashboard", null, "foreach");
+
+    if (setDefault) {
+      String id = refName + ":" + path;
+      info.isDefault = id.equals(defaultOf(definingProject)) ? true : null;
+    }
+
+    UrlEncoded u = new UrlEncoded("/dashboard/");
+    u.put("title", Objects.firstNonNull(info.title, info.path));
+    if (info.foreach != null) {
+      u.put("foreach", replace(project, info.foreach));
+    }
+    for (String name : config.getSubsections("section")) {
+      Section s = new Section();
+      s.name = name;
+      s.query = config.getString("section", name, "query");
+      u.put(s.name, replace(project, s.query));
+      info.sections.add(s);
+    }
+    info.url = u.toString().replace("%3A", ":");
+
+    return info;
+  }
+
+  private static String replace(String project, String query) {
+    return query.replace("${project}", project);
+  }
+
+  private static String defaultOf(Project proj) {
+    final String defaultId = Objects.firstNonNull(
+        proj.getLocalDefaultDashboard(),
+        Strings.nullToEmpty(proj.getDefaultDashboard()));
+    if (defaultId.startsWith(REFS_DASHBOARDS)) {
+      return defaultId.substring(REFS_DASHBOARDS.length());
+    } else {
+      return defaultId;
+    }
+  }
+
+  static class DashboardInfo {
+    final String kind = "gerritcodereview#dashboard";
+    String id;
+    String project;
+    String definingProject;
+    String ref;
+    String path;
+    String description;
+    String foreach;
+    String url;
+
+    @SerializedName("default")
+    Boolean isDefault;
+
+    String title;
+    List<Section> sections = Lists.newArrayList();
+
+    DashboardInfo(String ref, String name)
+        throws UnsupportedEncodingException {
+      this.ref = ref;
+      this.path = name;
+      this.id = Joiner.on(':').join(Url.encode(ref), Url.encode(path));
+    }
+  }
+
+  static class Section {
+    String name;
+    String query;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteDashboard.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteDashboard.java
new file mode 100644
index 0000000..da1e46b
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/DeleteDashboard.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.project;
+
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.server.project.DeleteDashboard.Input;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+class DeleteDashboard implements RestModifyView<DashboardResource, Input> {
+  static class Input {
+    String commitMessage;
+  }
+
+  private final Provider<SetDefaultDashboard> defaultSetter;
+
+  @Inject
+  DeleteDashboard(Provider<SetDefaultDashboard> defaultSetter) {
+    this.defaultSetter = defaultSetter;
+  }
+
+  @Override
+  public Object apply(DashboardResource resource, Input input)
+      throws AuthException, BadRequestException, ResourceConflictException,
+      Exception {
+    if (resource.isProjectDefault()) {
+      SetDashboard.Input in = new SetDashboard.Input();
+      in.commitMessage = input != null ? input.commitMessage : null;
+      return defaultSetter.get().apply(resource, in);
+    }
+
+    // TODO: Implement delete of dashboards by API.
+    throw new MethodNotAllowedException();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GarbageCollect.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GarbageCollect.java
new file mode 100644
index 0000000..d4d923e
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GarbageCollect.java
@@ -0,0 +1,93 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.common.base.Charsets;
+import com.google.gerrit.common.data.GarbageCollectionResult;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.StreamingResponse;
+import com.google.gerrit.server.git.GarbageCollection;
+import com.google.gerrit.server.project.GarbageCollect.Input;
+import com.google.inject.Inject;
+
+import java.io.IOException;
+import java.io.OutputStream;
+import java.io.OutputStreamWriter;
+import java.io.PrintWriter;
+import java.util.Collections;
+
+@RequiresCapability(GlobalCapability.RUN_GC)
+public class GarbageCollect implements RestModifyView<ProjectResource, Input> {
+  public static class Input {
+  }
+
+  private GarbageCollection.Factory garbageCollectionFactory;
+
+  @Inject
+  GarbageCollect(GarbageCollection.Factory garbageCollectionFactory) {
+    this.garbageCollectionFactory = garbageCollectionFactory;
+  }
+
+  @Override
+  public StreamingResponse apply(final ProjectResource rsrc, Input input) {
+    return new StreamingResponse() {
+      @Override
+      public String getContentType() {
+        return "text/plain;charset=UTF-8";
+      }
+
+      @Override
+      public void stream(OutputStream out) throws IOException {
+        PrintWriter writer = new PrintWriter(
+            new OutputStreamWriter(out, Charsets.UTF_8)) {
+          @Override
+          public void println() {
+            write('\n');
+          }
+        };
+        try {
+          GarbageCollectionResult result = garbageCollectionFactory.create().run(
+              Collections.singletonList(rsrc.getNameKey()), writer);
+          if (result.hasErrors()) {
+            for (GarbageCollectionResult.Error e : result.getErrors()) {
+              String msg;
+              switch (e.getType()) {
+                case REPOSITORY_NOT_FOUND:
+                  msg = "error: project \"" + e.getProjectName() + "\" not found";
+                  break;
+                case GC_ALREADY_SCHEDULED:
+                  msg = "error: garbage collection for project \""
+                      + e.getProjectName() + "\" was already scheduled";
+                  break;
+                case GC_FAILED:
+                  msg = "error: garbage collection for project \"" + e.getProjectName()
+                      + "\" failed";
+                  break;
+                default:
+                  msg = "error: garbage collection for project \"" + e.getProjectName()
+                      + "\" failed: " + e.getType();
+              }
+              writer.println(msg);
+            }
+          }
+        } finally {
+          writer.flush();
+        }
+      }
+    };
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetDashboard.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetDashboard.java
new file mode 100644
index 0000000..51d6191
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetDashboard.java
@@ -0,0 +1,104 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import static com.google.gerrit.server.git.GitRepositoryManager.REFS_DASHBOARDS;
+
+import com.google.common.base.Splitter;
+import com.google.common.base.Strings;
+import com.google.common.collect.Lists;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.extensions.restapi.Url;
+import com.google.gerrit.server.project.DashboardsCollection.DashboardInfo;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.kohsuke.args4j.Option;
+
+import java.io.IOException;
+import java.util.List;
+
+class GetDashboard implements RestReadView<DashboardResource> {
+  private final DashboardsCollection dashboards;
+
+  @Option(name = "--inherited", usage = "include inherited dashboards")
+  private boolean inherited;
+
+  @Inject
+  GetDashboard(DashboardsCollection dashboards) {
+    this.dashboards = dashboards;
+  }
+
+  @Override
+  public DashboardInfo apply(DashboardResource resource)
+      throws ResourceNotFoundException, IOException, ConfigInvalidException {
+    if (inherited && !resource.isProjectDefault()) {
+      // inherited flag can only be used with default.
+      throw new ResourceNotFoundException("inherited");
+    }
+
+    String project = resource.getControl().getProject().getName();
+    if (resource.isProjectDefault()) {
+      // The default is not resolved to a definition yet.
+      resource = defaultOf(resource.getControl());
+    }
+
+    return DashboardsCollection.parse(
+        resource.getControl().getProject(),
+        resource.getRefName().substring(REFS_DASHBOARDS.length()),
+        resource.getPathName(),
+        resource.getConfig(),
+        project,
+        true);
+  }
+
+  private DashboardResource defaultOf(ProjectControl ctl)
+      throws ResourceNotFoundException, IOException, ConfigInvalidException {
+    String id = ctl.getProject().getLocalDefaultDashboard();
+    if (Strings.isNullOrEmpty(id)) {
+      id = ctl.getProject().getDefaultDashboard();
+    }
+    if ("default".equals(id)) {
+      throw new ResourceNotFoundException();
+    } else if (!Strings.isNullOrEmpty(id)) {
+      return parse(ctl, id);
+    } else if (!inherited) {
+      throw new ResourceNotFoundException();
+    }
+
+    for (ProjectState ps : ctl.getProjectState().tree()) {
+      id = ps.getProject().getDefaultDashboard();
+      if ("default".equals(id)) {
+        throw new ResourceNotFoundException();
+      } else if (!Strings.isNullOrEmpty(id)) {
+        ctl = ps.controlFor(ctl.getCurrentUser());
+        return parse(ctl, id);
+      }
+    }
+    throw new ResourceNotFoundException();
+  }
+
+  private DashboardResource parse(ProjectControl ctl, String id)
+      throws ResourceNotFoundException, IOException, ConfigInvalidException {
+    List<String> p = Lists.newArrayList(Splitter.on(':').limit(2).split(id));
+    String ref = Url.encode(p.get(0));
+    String path = Url.encode(p.get(1));
+    return dashboards.parse(
+        new ProjectResource(ctl),
+        IdString.fromUrl(ref + ':' + path));
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetDescription.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetDescription.java
new file mode 100644
index 0000000..d8fabab
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetDescription.java
@@ -0,0 +1,27 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import com.google.common.base.Strings;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.client.Project;
+
+class GetDescription implements RestReadView<ProjectResource> {
+  @Override
+  public Object apply(ProjectResource resource) {
+    Project project = resource.getControl().getProject();
+    return Strings.nullToEmpty(project.getDescription());
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetHead.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetHead.java
new file mode 100644
index 0000000..ea39f73
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetHead.java
@@ -0,0 +1,69 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.auth.AuthException;
+import com.google.gerrit.server.git.GitRepositoryManager;
+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 java.io.IOException;
+
+public class GetHead implements RestReadView<ProjectResource> {
+
+  private GitRepositoryManager repoManager;
+
+  @Inject
+  GetHead(GitRepositoryManager repoManager) {
+    this.repoManager = repoManager;
+  }
+
+  @Override
+  public String apply(ProjectResource rsrc) throws AuthException,
+      ResourceNotFoundException, IOException {
+    Repository repo = null;
+    try {
+      repo = repoManager.openRepository(rsrc.getNameKey());
+      Ref head = repo.getRef(Constants.HEAD);
+      if (head == null) {
+        throw new ResourceNotFoundException(Constants.HEAD);
+      } else if (head.isSymbolic()) {
+        String n = head.getTarget().getName();
+        if (rsrc.getControl().controlForRef(n).isVisible()) {
+          return n;
+        }
+        throw new AuthException();
+      } else if (head.getObjectId() != null) {
+        if (rsrc.getControl().isOwner()) {
+          return head.getObjectId().name();
+        }
+        throw new AuthException();
+      }
+      throw new ResourceNotFoundException(Constants.HEAD);
+    } catch (RepositoryNotFoundException e) {
+      throw new ResourceNotFoundException(rsrc.getName());
+    } finally {
+      if (repo != null) {
+        repo.close();
+      }
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetParent.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetParent.java
new file mode 100644
index 0000000..1cebd87
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetParent.java
@@ -0,0 +1,36 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.inject.Inject;
+
+class GetParent implements RestReadView<ProjectResource> {
+  private final AllProjectsName allProjectsName;
+
+  @Inject
+  GetParent(AllProjectsName allProjectsName) {
+    this.allProjectsName = allProjectsName;
+  }
+
+  @Override
+  public Object apply(ProjectResource resource) {
+    Project project = resource.getControl().getProject();
+    Project.NameKey parentName = project.getParent(allProjectsName);
+    return parentName != null ? parentName.get() : "";
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetProject.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetProject.java
new file mode 100644
index 0000000..a482278
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetProject.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.project;
+
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.inject.Inject;
+
+class GetProject implements RestReadView<ProjectResource> {
+
+  private final ProjectJson json;
+
+  @Inject
+  GetProject(ProjectJson json) {
+    this.json = json;
+  }
+
+  @Override
+  public Object apply(ProjectResource rsrc) {
+    return json.format(rsrc);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/GetStatistics.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetStatistics.java
new file mode 100644
index 0000000..548b85a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/GetStatistics.java
@@ -0,0 +1,62 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.api.GarbageCollectCommand;
+import org.eclipse.jgit.api.Git;
+import org.eclipse.jgit.api.errors.GitAPIException;
+import org.eclipse.jgit.api.errors.JGitInternalException;
+import org.eclipse.jgit.lib.Repository;
+
+import java.io.IOException;
+
+@RequiresCapability(GlobalCapability.RUN_GC)
+public class GetStatistics implements RestReadView<ProjectResource> {
+
+  private final GitRepositoryManager repoManager;
+
+  @Inject
+  GetStatistics(GitRepositoryManager repoManager) {
+    this.repoManager = repoManager;
+  }
+
+  @Override
+  public RepositoryStatistics apply(ProjectResource rsrc)
+      throws ResourceNotFoundException, ResourceConflictException {
+    try {
+      Repository repo = repoManager.openRepository(rsrc.getNameKey());
+      try {
+        GarbageCollectCommand gc = Git.wrap(repo).gc();
+        return new RepositoryStatistics(gc.getStatistics());
+      } catch (GitAPIException e) {
+        throw new ResourceConflictException(e.getMessage());
+      } catch (JGitInternalException e) {
+        throw new ResourceConflictException(e.getMessage());
+      } finally {
+        repo.close();
+      }
+    } catch (IOException e) {
+      throw new ResourceNotFoundException(rsrc.getName());
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListDashboards.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListDashboards.java
new file mode 100644
index 0000000..c063618
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListDashboards.java
@@ -0,0 +1,141 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.git.GitRepositoryManager.REFS_DASHBOARDS;
+
+import com.google.common.collect.Lists;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.project.DashboardsCollection.DashboardInfo;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.BlobBasedConfig;
+import org.eclipse.jgit.lib.FileMode;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.eclipse.jgit.revwalk.RevWalk;
+import org.eclipse.jgit.treewalk.TreeWalk;
+import org.kohsuke.args4j.Option;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.IOException;
+import java.util.List;
+
+class ListDashboards implements RestReadView<ProjectResource> {
+  private static final Logger log = LoggerFactory.getLogger(DashboardsCollection.class);
+
+  private final GitRepositoryManager gitManager;
+
+  @Option(name = "--inherited", usage = "include inherited dashboards")
+  private boolean inherited;
+
+  @Inject
+  ListDashboards(GitRepositoryManager gitManager) {
+    this.gitManager = gitManager;
+  }
+
+  @Override
+  public Object apply(ProjectResource resource)
+      throws ResourceNotFoundException, IOException {
+    ProjectControl ctl = resource.getControl();
+    String project = ctl.getProject().getName();
+    if (!inherited) {
+      return scan(resource.getControl(), project, true);
+    }
+
+    List<List<DashboardInfo>> all = Lists.newArrayList();
+    boolean setDefault = true;
+    for (ProjectState ps : ctl.getProjectState().tree()) {
+      ctl = ps.controlFor(ctl.getCurrentUser());
+      if (ctl.isVisible()) {
+        List<DashboardInfo> list = scan(ctl, project, setDefault);
+        for (DashboardInfo d : list) {
+          if (d.isDefault != null && Boolean.TRUE.equals(d.isDefault)) {
+            setDefault = false;
+          }
+        }
+        if (!list.isEmpty()) {
+          all.add(list);
+        }
+      }
+    }
+    return all;
+  }
+
+  private List<DashboardInfo> scan(ProjectControl ctl, String project,
+      boolean setDefault) throws ResourceNotFoundException, IOException {
+    Repository git;
+    try {
+      git = gitManager.openRepository(ctl.getProject().getNameKey());
+    } catch (RepositoryNotFoundException e) {
+      throw new ResourceNotFoundException();
+    }
+    try {
+      RevWalk rw = new RevWalk(git);
+      try {
+        List<DashboardInfo> all = Lists.newArrayList();
+        for (Ref ref : git.getRefDatabase().getRefs(REFS_DASHBOARDS).values()) {
+          if (ctl.controlForRef(ref.getName()).canRead()) {
+            all.addAll(scanDashboards(ctl.getProject(), git, rw, ref,
+                project, setDefault));
+          }
+        }
+        return all;
+      } finally {
+        rw.release();
+      }
+    } finally {
+      git.close();
+    }
+  }
+
+  private List<DashboardInfo> scanDashboards(Project definingProject,
+      Repository git, RevWalk rw, Ref ref, String project, boolean setDefault)
+      throws IOException {
+    List<DashboardInfo> list = Lists.newArrayList();
+    TreeWalk tw = new TreeWalk(rw.getObjectReader());
+    try {
+      tw.addTree(rw.parseTree(ref.getObjectId()));
+      tw.setRecursive(true);
+      while (tw.next()) {
+        if (tw.getFileMode(0) == FileMode.REGULAR_FILE) {
+          try {
+            list.add(DashboardsCollection.parse(
+                definingProject,
+                ref.getName().substring(REFS_DASHBOARDS.length()),
+                tw.getPathString(),
+                new BlobBasedConfig(null, git, tw.getObjectId(0)),
+                project,
+                setDefault));
+          } catch (ConfigInvalidException e) {
+            log.warn(String.format(
+                "Cannot parse dashboard %s:%s:%s: %s",
+                definingProject.getName(), ref.getName(), tw.getPathString(),
+                e.getMessage()));
+          }
+        }
+      }
+    } finally {
+      tw.release();
+    }
+    return list;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java
index e5a11ca..59b544a 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ListProjects.java
@@ -14,13 +14,25 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.common.base.Predicate;
+import com.google.common.base.Strings;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.common.errors.NoSuchGroupException;
+import com.google.gerrit.extensions.restapi.BinaryResult;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+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.CurrentUser;
 import com.google.gerrit.server.OutputFormat;
 import com.google.gerrit.server.StringUtil;
+import com.google.gerrit.server.account.GroupCache;
+import com.google.gerrit.server.account.GroupControl;
 import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.project.ProjectJson.ProjectInfo;
 import com.google.gerrit.server.util.TreeFormatter;
 import com.google.gson.reflect.TypeToken;
 import com.google.inject.Inject;
@@ -34,6 +46,7 @@
 import org.slf4j.LoggerFactory;
 
 import java.io.BufferedWriter;
+import java.io.ByteArrayOutputStream;
 import java.io.IOException;
 import java.io.OutputStream;
 import java.io.OutputStreamWriter;
@@ -42,6 +55,7 @@
 import java.util.Arrays;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Locale;
 import java.util.Map;
 import java.util.Set;
 import java.util.SortedSet;
@@ -49,7 +63,7 @@
 import java.util.TreeSet;
 
 /** List projects visible to the calling user. */
-public class ListProjects {
+public class ListProjects implements RestReadView<TopLevelResource> {
   private static final Logger log = LoggerFactory.getLogger(ListProjects.class);
 
   public static enum FilterType {
@@ -86,41 +100,82 @@
 
   private final CurrentUser currentUser;
   private final ProjectCache projectCache;
+  private final GroupCache groupCache;
+  private final GroupControl.Factory groupControlFactory;
   private final GitRepositoryManager repoManager;
   private final ProjectNode.Factory projectNodeFactory;
 
-  @Option(name = "--format", metaVar = "FMT", usage = "Output display format")
+  @Deprecated
+  @Option(name = "--format", usage = "(deprecated) output format")
   private OutputFormat format = OutputFormat.TEXT;
 
   @Option(name = "--show-branch", aliases = {"-b"}, multiValued = true,
       usage = "displays the sha of each project in the specified branch")
-  private List<String> showBranch;
+  public void addShowBranch(String branch) {
+    showBranch.add(branch);
+  }
 
   @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;
+  public void setShowTree(boolean showTree) {
+    this.showTree = showTree;
+  }
 
   @Option(name = "--type", usage = "type of project")
-  private FilterType type = FilterType.CODE;
+  public void setFilterType(FilterType type) {
+    this.type = type;
+  }
 
   @Option(name = "--description", aliases = {"-d"}, usage = "include description of project in list")
-  private boolean showDescription;
+  public void setShowDescription(boolean showDescription) {
+    this.showDescription = showDescription;
+  }
 
   @Option(name = "--all", usage = "display all projects that are accessible by the calling user")
-  private boolean all;
+  public void setAll(boolean all) {
+    this.all = all;
+  }
 
   @Option(name = "--limit", aliases = {"-n"}, metaVar = "CNT", usage = "maximum number of projects to list")
-  private int limit;
+  public void setLimit(int limit) {
+    this.limit = limit;
+  }
 
+  @Option(name = "-p", metaVar = "PREFIX", usage = "match project prefix")
+  public void setMatchPrefix(String matchPrefix) {
+    this.matchPrefix = matchPrefix;
+  }
+
+  @Option(name = "-m", metaVar = "MATCH", usage = "match project substring")
+  public void setMatchSubstring(String matchSubstring) {
+    this.matchSubstring = matchSubstring;
+  }
+
+  @Option(name = "--has-acl-for", metaVar = "GROUP", usage =
+      "displays only projects on which access rights for this group are directly assigned")
+  public void setGroupUuid(AccountGroup.UUID groupUuid) {
+    this.groupUuid = groupUuid;
+  }
+
+  private final List<String> showBranch = Lists.newArrayList();
+  private boolean showTree;
+  private FilterType type = FilterType.CODE;
+  private boolean showDescription;
+  private boolean all;
+  private int limit;
   private String matchPrefix;
+  private String matchSubstring;
+  private AccountGroup.UUID groupUuid;
 
   @Inject
   protected ListProjects(CurrentUser currentUser, ProjectCache projectCache,
-      GitRepositoryManager repoManager,
-      ProjectNode.Factory projectNodeFactory) {
+      GroupCache groupCache, GroupControl.Factory groupControlFactory,
+      GitRepositoryManager repoManager, ProjectNode.Factory projectNodeFactory) {
     this.currentUser = currentUser;
     this.projectCache = projectCache;
+    this.groupCache = groupCache;
+    this.groupControlFactory = groupControlFactory;
     this.repoManager = repoManager;
     this.projectNodeFactory = projectNodeFactory;
   }
@@ -142,22 +197,36 @@
   }
 
   public ListProjects setFormat(OutputFormat fmt) {
-    this.format = fmt;
+    format = fmt;
     return this;
   }
 
-  public ListProjects setMatchPrefix(String prefix) {
-    this.matchPrefix = prefix;
-    return this;
+  @Override
+  public Object apply(TopLevelResource resource) {
+    if (format == OutputFormat.TEXT) {
+      ByteArrayOutputStream buf = new ByteArrayOutputStream();
+      display(buf);
+      return BinaryResult.create(buf.toByteArray())
+          .setContentType("text/plain")
+          .setCharacterEncoding("UTF-8");
+    }
+    return apply();
   }
 
-  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);
+  public Map<String, ProjectInfo> apply() {
+    format = OutputFormat.JSON;
+    return display(null);
+  }
+
+  public Map<String, ProjectInfo> display(OutputStream displayOutputStream) {
+    PrintWriter stdout = null;
+    if (displayOutputStream != null) {
+      try {
+        stdout = new PrintWriter(new BufferedWriter(
+            new OutputStreamWriter(displayOutputStream, "UTF-8")));
+      } catch (UnsupportedEncodingException e) {
+        throw new RuntimeException("JVM lacks UTF-8 encoding", e);
+      }
     }
 
     int found = 0;
@@ -175,16 +244,33 @@
           //
           continue;
         }
+
+        final ProjectControl pctl = e.controlFor(currentUser);
+        if (groupUuid != null) {
+          try {
+            if (!groupControlFactory.controlFor(groupUuid).isVisible()) {
+              break;
+            }
+          } catch (NoSuchGroupException ex) {
+            break;
+          }
+          if (!pctl.getLocalGroups().contains(
+              GroupReference.forGroup(groupCache.get(groupUuid)))) {
+            continue;
+          }
+        }
+
         ProjectInfo info = new ProjectInfo();
         if (type == FilterType.PARENT_CANDIDATES) {
-          ProjectState parentState = e.getParentState();
+          ProjectState parentState = Iterables.getFirst(e.parents(), null);
           if (parentState != null
               && !output.keySet().contains(parentState.getProject().getName())
               && !rejected.contains(parentState.getProject().getName())) {
             ProjectControl parentCtrl = parentState.controlFor(currentUser);
             if (parentCtrl.isVisible() || parentCtrl.isOwner()) {
               info.name = parentState.getProject().getName();
-              info.description = parentState.getProject().getDescription();
+              info.description = Strings.emptyToNull(
+                  parentState.getProject().getDescription());
             } else {
               rejected.add(parentState.getProject().getName());
               continue;
@@ -194,7 +280,6 @@
           }
 
         } else {
-          final ProjectControl pctl = e.controlFor(currentUser);
           final boolean isVisible = pctl.isVisible() || (all && pctl.isOwner());
           if (showTree && !format.isJson()) {
             treeMap.put(projectName,
@@ -210,7 +295,7 @@
 
           info.name = projectName.get();
           if (showTree && format.isJson()) {
-            ProjectState parent = e.getParentState();
+            ProjectState parent = Iterables.getFirst(e.parents(), null);
             if (parent != null) {
               ProjectControl parentCtrl = parent.controlFor(currentUser);
               if (parentCtrl.isVisible() || parentCtrl.isOwner()) {
@@ -224,12 +309,12 @@
               }
             }
           }
-          if (showDescription && !e.getProject().getDescription().isEmpty()) {
-            info.description = e.getProject().getDescription();
+          if (showDescription) {
+            info.description = Strings.emptyToNull(e.getProject().getDescription());
           }
 
           try {
-            if (showBranch != null) {
+            if (!showBranch.isEmpty()) {
               Repository git = repoManager.openRepository(projectName);
               try {
                 if (!type.matches(git)) {
@@ -277,12 +362,12 @@
           break;
         }
 
-        if (format.isJson()) {
+        if (stdout == null || format.isJson()) {
           output.put(info.name, info);
           continue;
         }
 
-        if (showBranch != null) {
+        if (!showBranch.isEmpty()) {
           for (String name : showBranch) {
             String ref = info.branches != null ? info.branches.get(name) : null;
             if (ref == null) {
@@ -302,21 +387,38 @@
         stdout.print('\n');
       }
 
-      if (format.isJson()) {
+      for (ProjectInfo info : output.values()) {
+        info.finish();
+        info.name = null;
+      }
+      if (stdout == null) {
+        return output;
+      } else if (format.isJson()) {
         format.newGson().toJson(
             output, new TypeToken<Map<String, ProjectInfo>>() {}.getType(), stdout);
         stdout.print('\n');
       } else if (showTree && treeMap.size() > 0) {
         printProjectTree(stdout, treeMap);
       }
+      return null;
     } finally {
-      stdout.flush();
+      if (stdout != null) {
+        stdout.flush();
+      }
     }
   }
 
-  private Iterable<NameKey> scan() {
+  private Iterable<Project.NameKey> scan() {
     if (matchPrefix != null) {
       return projectCache.byName(matchPrefix);
+    } else if (matchSubstring != null) {
+      return Iterables.filter(projectCache.all(),
+          new Predicate<Project.NameKey>() {
+            public boolean apply(Project.NameKey in) {
+              return in.get().toLowerCase(Locale.US)
+                  .contains(matchSubstring.toLowerCase(Locale.US));
+            }
+          });
     } else {
       return projectCache.all();
     }
@@ -379,11 +481,4 @@
     }
     return false;
   }
-
-  private static class ProjectInfo {
-    transient String name;
-    String parent;
-    String description;
-    Map<String, String> branches;
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java
new file mode 100644
index 0000000..94e3162
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/Module.java
@@ -0,0 +1,55 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import static com.google.gerrit.server.project.DashboardResource.DASHBOARD_KIND;
+import static com.google.gerrit.server.project.ProjectResource.PROJECT_KIND;
+
+import com.google.gerrit.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.RestApiModule;
+import com.google.gerrit.server.project.CreateProject;
+import com.google.inject.assistedinject.FactoryModuleBuilder;
+
+public class Module extends RestApiModule {
+  @Override
+  protected void configure() {
+    bind(ProjectsCollection.class);
+    bind(DashboardsCollection.class);
+
+    DynamicMap.mapOf(binder(), PROJECT_KIND);
+    DynamicMap.mapOf(binder(), DASHBOARD_KIND);
+
+    put(PROJECT_KIND).to(PutProject.class);
+    get(PROJECT_KIND).to(GetProject.class);
+    get(PROJECT_KIND, "description").to(GetDescription.class);
+    put(PROJECT_KIND, "description").to(PutDescription.class);
+    delete(PROJECT_KIND, "description").to(PutDescription.class);
+
+    get(PROJECT_KIND, "parent").to(GetParent.class);
+    put(PROJECT_KIND, "parent").to(SetParent.class);
+
+    get(PROJECT_KIND, "HEAD").to(GetHead.class);
+    put(PROJECT_KIND, "HEAD").to(SetHead.class);
+
+    get(PROJECT_KIND, "statistics.git").to(GetStatistics.class);
+    post(PROJECT_KIND, "gc").to(GarbageCollect.class);
+
+    child(PROJECT_KIND, "dashboards").to(DashboardsCollection.class);
+    get(DASHBOARD_KIND).to(GetDashboard.class);
+    put(DASHBOARD_KIND).to(SetDashboard.class);
+    delete(DASHBOARD_KIND).to(DeleteDashboard.class);
+    install(new FactoryModuleBuilder().build(CreateProject.Factory.class));
+  }
+}
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 2ac494d..aa6fb16 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
@@ -20,11 +20,13 @@
 public class NoSuchProjectException extends Exception {
   private static final long serialVersionUID = 1L;
 
+  private static final String MESSAGE = "Project not found: ";
+
   public NoSuchProjectException(final Project.NameKey key) {
     this(key, null);
   }
 
   public NoSuchProjectException(final Project.NameKey key, final Throwable why) {
-    super(key.toString(), why);
+    super(MESSAGE + key.toString(), why);
   }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/PerformCreateProject.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/PerformCreateProject.java
new file mode 100644
index 0000000..d68725f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/PerformCreateProject.java
@@ -0,0 +1,293 @@
+// 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.ProjectUtil;
+import com.google.gerrit.common.data.AccessSection;
+import com.google.gerrit.common.data.GroupDescription;
+import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.common.data.Permission;
+import com.google.gerrit.common.data.PermissionRule;
+import com.google.gerrit.common.errors.ProjectCreationFailedException;
+import com.google.gerrit.extensions.events.NewProjectCreatedListener;
+import com.google.gerrit.extensions.registration.DynamicSet;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.GroupBackend;
+import com.google.gerrit.server.config.ProjectOwnerGroups;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.git.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.Collections;
+import java.util.List;
+import java.util.Set;
+
+
+/** Common class that holds the code to create projects */
+public class PerformCreateProject {
+  private static final Logger log = LoggerFactory
+      .getLogger(PerformCreateProject.class);
+
+  public interface Factory {
+    PerformCreateProject create(CreateProjectArgs createProjectArgs);
+  }
+
+  private final Set<AccountGroup.UUID> projectOwnerGroups;
+  private final IdentifiedUser currentUser;
+  private final GitRepositoryManager repoManager;
+  private final GitReferenceUpdated referenceUpdated;
+  private final DynamicSet<NewProjectCreatedListener> createdListener;
+  private final PersonIdent serverIdent;
+  private final CreateProjectArgs createProjectArgs;
+  private final ProjectCache projectCache;
+  private final GroupBackend groupBackend;
+  private final MetaDataUpdate.User metaDataUpdateFactory;
+
+  @Inject
+  PerformCreateProject(@ProjectOwnerGroups Set<AccountGroup.UUID> pOwnerGroups,
+      IdentifiedUser identifiedUser, GitRepositoryManager gitRepoManager,
+      GitReferenceUpdated referenceUpdated,
+      DynamicSet<NewProjectCreatedListener> createdListener,
+      @GerritPersonIdent PersonIdent personIdent, GroupBackend groupBackend,
+      MetaDataUpdate.User metaDataUpdateFactory,
+      @Assisted CreateProjectArgs createPArgs, ProjectCache pCache) {
+    this.projectOwnerGroups = pOwnerGroups;
+    this.currentUser = identifiedUser;
+    this.repoManager = gitRepoManager;
+    this.referenceUpdated = referenceUpdated;
+    this.createdListener = createdListener;
+    this.serverIdent = personIdent;
+    this.createProjectArgs = createPArgs;
+    this.projectCache = pCache;
+    this.groupBackend = groupBackend;
+    this.metaDataUpdateFactory = metaDataUpdateFactory;
+  }
+
+  public Project createProject() throws ProjectCreationFailedException {
+    validateParameters();
+    final Project.NameKey nameKey = createProjectArgs.getProject();
+    try {
+      final String head =
+          createProjectArgs.permissionsOnly ? GitRepositoryManager.REF_CONFIG
+              : createProjectArgs.branch.get(0);
+      final Repository repo = repoManager.createRepository(nameKey);
+      try {
+        NewProjectCreatedListener.Event event = new NewProjectCreatedListener.Event() {
+          @Override
+          public String getProjectName() {
+            return nameKey.get();
+          }
+
+          @Override
+          public String getHeadName() {
+            return head;
+          }
+        };
+        for (NewProjectCreatedListener l : createdListener) {
+          l.onNewProjectCreated(event);
+        }
+
+        final RefUpdate u = repo.updateRef(Constants.HEAD);
+        u.disableRefLog();
+        u.link(head);
+
+        createProjectConfig();
+
+        if (!createProjectArgs.permissionsOnly
+            && createProjectArgs.createEmptyCommit) {
+          createEmptyCommits(repo, nameKey, createProjectArgs.branch);
+        }
+
+        return projectCache.get(nameKey).getProject();
+      } 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");
+          }
+          throw err;
+        } finally {
+          repo.close();
+        }
+      } catch (IOException ioErr) {
+        final String msg = "Cannot create " + nameKey;
+        log.error(msg, err);
+        throw new ProjectCreationFailedException(msg, ioErr);
+      }
+    } 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) {
+          GroupDescription.Basic g = groupBackend.get(ownerId);
+          if (g != null) {
+            GroupReference group = config.resolve(GroupReference.forGroup(g));
+            all.getPermission(Permission.OWNER, true).add(
+                new PermissionRule(group));
+          }
+        }
+      }
+
+      md.setMessage("Created project\n");
+      config.commit(md);
+    } finally {
+      md.close();
+    }
+    projectCache.onCreateProject(createProjectArgs.getProject());
+    repoManager.setProjectDescription(createProjectArgs.getProject(),
+        createProjectArgs.projectDescription);
+  }
+
+  private void validateParameters() throws ProjectCreationFailedException {
+    if (createProjectArgs.getProjectName() == null
+        || createProjectArgs.getProjectName().isEmpty()) {
+      throw new ProjectCreationFailedException("Project name is required");
+    }
+
+    String nameWithoutSuffix = ProjectUtil.stripGitSuffix(createProjectArgs.getProjectName());
+    createProjectArgs.setProjectName(nameWithoutSuffix);
+
+    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);
+    }
+
+    List<String> transformedBranches = new ArrayList<String>();
+    if (createProjectArgs.branch == null ||
+        createProjectArgs.branch.isEmpty()) {
+      createProjectArgs.branch = Collections.singletonList(Constants.MASTER);
+    }
+    for (String branch : createProjectArgs.branch) {
+      while (branch.startsWith("/")) {
+        branch = branch.substring(1);
+      }
+      if (!branch.startsWith(Constants.R_HEADS)) {
+        branch = Constants.R_HEADS + branch;
+      }
+      if (!Repository.isValidRefName(branch)) {
+        throw new ProjectCreationFailedException(String.format(
+            "Branch \"%s\" is not a valid name.", branch));
+      }
+      if (!transformedBranches.contains(branch)) {
+        transformedBranches.add(branch);
+      }
+    }
+    createProjectArgs.branch = transformedBranches;
+  }
+
+  private void createEmptyCommits(final Repository repo,
+      final Project.NameKey project, final List<String> refs)
+      throws IOException {
+    ObjectInserter oi = repo.newObjectInserter();
+    try {
+      CommitBuilder cb = new CommitBuilder();
+      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();
+
+      for (String ref : refs) {
+        RefUpdate ru = repo.updateRef(ref);
+        ru.setNewObjectId(id);
+        final Result result = ru.update();
+        switch (result) {
+          case NEW:
+            referenceUpdated.fire(project, ru);
+            break;
+          default: {
+            throw new IOException(String.format(
+              "Failed to create ref \"%s\": %s", ref, 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/PermissionCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/PermissionCollection.java
index e416ed7..483ecaf 100644
--- 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
@@ -16,10 +16,13 @@
 
 import static com.google.gerrit.server.project.RefControl.isRE;
 
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.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.inject.Inject;
 import com.google.inject.Singleton;
 
@@ -73,7 +76,7 @@
       }
 
       boolean perUser = false;
-      List<AccessSection> sections = new ArrayList<AccessSection>();
+      Map<AccessSection, Project.NameKey> sectionToProject = Maps.newLinkedHashMap();
       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
@@ -93,9 +96,10 @@
         }
 
         if (matcher.match(ref, username)) {
-          sections.add(matcher.section);
+          sectionToProject.put(matcher.section, matcher.project);
         }
       }
+      List<AccessSection> sections = Lists.newArrayList(sectionToProject.keySet());
       sorter.sort(ref, sections);
 
       Set<SeenRule> seen = new HashSet<SeenRule>();
@@ -104,7 +108,9 @@
 
       HashMap<String, List<PermissionRule>> permissions =
           new HashMap<String, List<PermissionRule>>();
+      Map<PermissionRule, ProjectRef> ruleProps = Maps.newIdentityHashMap();
       for (AccessSection section : sections) {
+        Project.NameKey project = sectionToProject.get(section);
         for (Permission permission : section.getPermissions()) {
           boolean exclusivePermissionExists =
               exclusiveGroupPermissions.contains(permission.getName());
@@ -124,6 +130,7 @@
                 permissions.put(permission.getName(), r);
               }
               r.add(rule);
+              ruleProps.put(rule, new ProjectRef(project, section.getName()));
             }
           }
 
@@ -133,16 +140,20 @@
         }
       }
 
-      return new PermissionCollection(permissions, perUser ? username : null);
+      return new PermissionCollection(permissions, ruleProps,
+          perUser ? username : null);
     }
   }
 
   private final Map<String, List<PermissionRule>> rules;
+  private final Map<PermissionRule, ProjectRef> ruleProps;
   private final String username;
 
   private PermissionCollection(Map<String, List<PermissionRule>> rules,
+      Map<PermissionRule, ProjectRef> ruleProps,
       String username) {
     this.rules = rules;
+    this.ruleProps = ruleProps;
     this.username = username;
   }
 
@@ -167,6 +178,10 @@
     return r != null ? r : Collections.<PermissionRule> emptyList();
   }
 
+  ProjectRef getRuleProps(PermissionRule rule) {
+    return ruleProps.get(rule);
+  }
+
   /**
    * Obtain all declared permission rules that match the reference.
    *
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 7a111311..697b838 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,8 +14,11 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
 
+import java.util.Set;
+
 /** Cache of project information, including access rights. */
 public interface ProjectCache {
   /** @return the parent state for all projects on this server. */
@@ -42,6 +45,13 @@
   public abstract Iterable<Project.NameKey> all();
 
   /**
+   * @return estimated set of relevant groups extracted from hot project access
+   *         rules. If the cache is cold or too small for the entire project set
+   *         of the server, this set may be incomplete.
+   */
+  public abstract Set<AccountGroup.UUID> guessRelevantGroupUUIDs();
+
+  /**
    * Filter the set of registered project names by common prefix.
    *
    * @param prefix common prefix.
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
index f86562c..c96ebdf 100644
--- 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
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.common.util.concurrent.ThreadFactoryBuilder;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.inject.Inject;
@@ -23,9 +24,7 @@
 
 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
@@ -34,29 +33,24 @@
 
   @Inject
   public ProjectCacheClock(@GerritServerConfig Config serverConfig) {
-    this(TimeUnit.MILLISECONDS.convert(
-        ConfigUtil.getTimeUnit(serverConfig,
-            "cache", "projects", "checkFrequency",
-            5, TimeUnit.MINUTES), TimeUnit.MINUTES));
+    this(checkFrequency(serverConfig));
   }
 
   public ProjectCacheClock(long checkFrequencyMillis) {
-    if (10 < checkFrequencyMillis) {
+    if (checkFrequencyMillis == Long.MAX_VALUE) {
+      // Start with generation 1 (to avoid magic 0 below).
+      // Do not begin background thread, disabling the clock.
+      generation = 1;
+    } else 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);
+      ScheduledExecutorService executor = Executors.newScheduledThreadPool(
+          1,
+          new ThreadFactoryBuilder()
+            .setNameFormat("ProjectCacheClock-%d")
+            .setDaemon(true)
+            .setPriority(Thread.MIN_PRIORITY)
+            .build());
       executor.scheduleAtFixedRate(new Runnable() {
         @Override
         public void run() {
@@ -75,4 +69,16 @@
   long read() {
     return generation;
   }
+
+  private static long checkFrequency(Config serverConfig) {
+    String freq = serverConfig.getString("cache", "projects", "checkFrequency");
+    if (freq != null
+        && ("disabled".equalsIgnoreCase(freq) || "off".equalsIgnoreCase(freq))) {
+      return Long.MAX_VALUE;
+    }
+    return TimeUnit.MILLISECONDS.convert(
+        ConfigUtil.getTimeUnit(serverConfig,
+            "cache", "projects", "checkFrequency",
+            5, TimeUnit.MINUTES), TimeUnit.MINUTES);
+  }
 }
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 a7fdb4e..272c128 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
@@ -17,6 +17,7 @@
 import com.google.common.cache.CacheLoader;
 import com.google.common.cache.LoadingCache;
 import com.google.common.collect.Sets;
+import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.cache.CacheModule;
 import com.google.gerrit.server.config.AllProjectsName;
@@ -36,6 +37,7 @@
 import java.util.Collections;
 import java.util.Iterator;
 import java.util.NoSuchElementException;
+import java.util.Set;
 import java.util.SortedSet;
 import java.util.concurrent.ExecutionException;
 import java.util.concurrent.locks.Lock;
@@ -171,6 +173,18 @@
   }
 
   @Override
+  public Set<AccountGroup.UUID> guessRelevantGroupUUIDs() {
+    Set<AccountGroup.UUID> groups = Sets.newHashSet();
+    for (Project.NameKey n : all()) {
+      ProjectState p = byName.getIfPresent(n.get());
+      if (p != null) {
+        groups.addAll(p.getConfig().getAllGroupUUIDs());
+      }
+    }
+    return groups;
+  }
+
+  @Override
   public Iterable<Project.NameKey> byName(final String pfx) {
     final Iterable<Project.NameKey> src;
     try {
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 513f1b1..56d4be3 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
@@ -20,6 +20,7 @@
 import com.google.gerrit.common.data.Capable;
 import com.google.gerrit.common.data.ContributorAgreement;
 import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.common.data.Permission;
 import com.google.gerrit.common.data.PermissionRule;
 import com.google.gerrit.common.data.PermissionRule.Action;
@@ -120,10 +121,11 @@
   private final Collection<ContributorAgreement> contributorAgreements;
 
   private List<SectionMatcher> allSections;
+  private List<SectionMatcher> localSections;
+  private LabelTypes labelTypes;
   private Map<String, RefControl> refControls;
   private Boolean declaredOwner;
 
-
   @Inject
   ProjectControl(@GitUploadPackGroups Set<AccountGroup.UUID> uploadGroups,
       @GitReceivePackGroups Set<AccountGroup.UUID> receiveGroups,
@@ -180,6 +182,13 @@
     return state.getProject();
   }
 
+  public LabelTypes getLabelTypes() {
+    if (labelTypes == null) {
+      labelTypes = state.getLabelTypes();
+    }
+    return labelTypes;
+  }
+
   private boolean isHidden() {
     return getProject().getState().equals(Project.State.HIDDEN);
   }
@@ -231,16 +240,24 @@
       String pName = state.getProject().getName();
       return new Capable("Upload denied for project '" + pName + "'");
     }
-    Project project = state.getProject();
-    if (project.isUseContributorAgreements()) {
+    if (state.isUseContributorAgreements()) {
       return verifyActiveContributorAgreement();
     }
     return Capable.OK;
   }
 
   public Set<GroupReference> getAllGroups() {
+    return getGroups(access());
+  }
+
+  public Set<GroupReference> getLocalGroups() {
+    return getGroups(localAccess());
+  }
+
+  private static Set<GroupReference> getGroups(
+      final List<SectionMatcher> sectionMatcherList) {
     final Set<GroupReference> all = new HashSet<GroupReference>();
-    for (final SectionMatcher matcher : access()) {
+    for (final SectionMatcher matcher : sectionMatcherList) {
       final AccessSection section = matcher.section;
       for (final Permission permission : section.getPermissions()) {
         for (final PermissionRule rule : permission.getRules()) {
@@ -392,6 +409,13 @@
     return allSections;
   }
 
+  private List<SectionMatcher> localAccess() {
+    if (localSections == null) {
+      localSections = state.getLocalAccessSections();
+    }
+    return localSections;
+  }
+
   boolean match(PermissionRule rule) {
     return match(rule.getGroup().getUUID());
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectHierarchyIterator.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectHierarchyIterator.java
new file mode 100644
index 0000000..0724ce9
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectHierarchyIterator.java
@@ -0,0 +1,107 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.common.base.Joiner;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Sets;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.config.AllProjectsName;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Iterator;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.Set;
+
+/**
+ * Iterates from a project up through its parents to All-Projects.
+ * <p>
+ * If a cycle is detected the cycle is broken and All-Projects is visited.
+ */
+class ProjectHierarchyIterator implements Iterator<ProjectState> {
+  private static final Logger log = LoggerFactory.getLogger(ProjectHierarchyIterator.class);
+
+  private final ProjectCache cache;
+  private final AllProjectsName allProjectsName;
+  private final Set<Project.NameKey> seen;
+  private ProjectState next;
+
+  ProjectHierarchyIterator(ProjectCache c,
+      AllProjectsName all,
+      ProjectState firstResult) {
+    cache = c;
+    allProjectsName = all;
+
+    seen = Sets.newLinkedHashSet();
+    seen.add(firstResult.getProject().getNameKey());
+    next = firstResult;
+  }
+
+  @Override
+  public boolean hasNext() {
+    return next != null;
+  }
+
+  @Override
+  public ProjectState next() {
+    ProjectState n = next;
+    if (n == null) {
+      throw new NoSuchElementException();
+    }
+    next = computeNext(n);
+    return n;
+  }
+
+  private ProjectState computeNext(ProjectState n) {
+    Project.NameKey parentName = n.getProject().getParent();
+    if (parentName != null && visit(parentName)) {
+      ProjectState p = cache.get(parentName);
+      if (p != null) {
+        return p;
+      }
+    }
+
+    // Parent does not exist or was already visited.
+    // Fall back to visit All-Projects exactly once.
+    if (seen.add(allProjectsName)) {
+      return cache.get(allProjectsName);
+    }
+    return null;
+  }
+
+  private boolean visit(Project.NameKey parentName) {
+    if (seen.add(parentName)) {
+      return true;
+    }
+
+    List<String> order = Lists.newArrayListWithCapacity(seen.size() + 1);
+    for (Project.NameKey p : seen) {
+      order.add(p.get());
+    }
+    int idx = order.lastIndexOf(parentName.get());
+    order.add(parentName.get());
+    log.warn("Cycle detected in projects: "
+        + Joiner.on(" -> ").join(order.subList(idx, order.size())));
+    return false;
+  }
+
+  @Override
+  public void remove() {
+    throw new UnsupportedOperationException();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectJson.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectJson.java
new file mode 100644
index 0000000..8db5cbb
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectJson.java
@@ -0,0 +1,60 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.common.base.Strings;
+import com.google.gerrit.extensions.restapi.Url;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.inject.Inject;
+
+import java.util.Map;
+
+public class ProjectJson {
+
+  private final AllProjectsName allProjects;
+
+  @Inject
+  ProjectJson(AllProjectsName allProjects) {
+    this.allProjects = allProjects;
+  }
+
+  public ProjectInfo format(ProjectResource rsrc) {
+    return format(rsrc.getControl().getProject());
+  }
+
+  public ProjectInfo format(Project p) {
+    ProjectInfo info = new ProjectInfo();
+    info.name = p.getName();
+    Project.NameKey parentName = p.getParent(allProjects);
+    info.parent = parentName != null ? parentName.get() : null;
+    info.description = Strings.emptyToNull(p.getDescription());
+    info.finish();
+    return info;
+  }
+
+  public static class ProjectInfo {
+    public final String kind = "gerritcodereview#project";
+    public String id;
+    public String name;
+    public String parent;
+    public String description;
+    public Map<String, String> branches;
+
+    void finish() {
+      id = Url.encode(name);
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectRef.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectRef.java
new file mode 100644
index 0000000..0315fad
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectRef.java
@@ -0,0 +1,45 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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;
+
+class ProjectRef {
+
+  final Project.NameKey project;
+  final String ref;
+
+  ProjectRef(Project.NameKey project, String ref) {
+    this.project = project;
+    this.ref = ref;
+  }
+
+  @Override
+  public boolean equals(Object other) {
+    return other instanceof ProjectRef
+        && project.equals(((ProjectRef) other).project)
+        && ref.equals(((ProjectRef) other).ref);
+  }
+
+  @Override
+  public int hashCode() {
+    return project.hashCode() * 31 + ref.hashCode();
+  }
+
+  @Override
+  public String toString() {
+    return project + ", " + ref;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectResource.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectResource.java
new file mode 100644
index 0000000..aeca0e8
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectResource.java
@@ -0,0 +1,43 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import com.google.gerrit.extensions.restapi.RestResource;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.inject.TypeLiteral;
+
+public class ProjectResource implements RestResource {
+  public static final TypeLiteral<RestView<ProjectResource>> PROJECT_KIND =
+      new TypeLiteral<RestView<ProjectResource>>() {};
+
+  private final ProjectControl control;
+
+  ProjectResource(ProjectControl control) {
+    this.control = control;
+  }
+
+  public String getName() {
+    return control.getProject().getName();
+  }
+
+  public Project.NameKey getNameKey() {
+    return control.getProject().getNameKey();
+  }
+
+  public ProjectControl getControl() {
+    return control;
+  }
+}
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 e06c948..6150a3f 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,13 +14,20 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.common.base.Function;
+import com.google.common.base.Predicate;
+import com.google.common.collect.Iterables;
 import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.common.data.LabelTypes;
 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.InheritableBoolean;
 import com.google.gerrit.rules.PrologEnvironment;
 import com.google.gerrit.rules.RulesCache;
 import com.google.gerrit.server.CurrentUser;
@@ -39,11 +46,14 @@
 import org.eclipse.jgit.lib.Repository;
 
 import java.io.IOException;
+import java.io.InputStream;
 import java.util.ArrayList;
 import java.util.Collection;
 import java.util.Collections;
 import java.util.HashSet;
+import java.util.Iterator;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
 /** Cached information on a project. */
@@ -76,7 +86,7 @@
   private final CapabilityCollection capabilities;
 
   @Inject
-  protected ProjectState(
+  public ProjectState(
       final ProjectCache projectCache,
       final AllProjectsName allProjectsName,
       final ProjectControl.AssistedFactory projectControlFactory,
@@ -165,6 +175,20 @@
     return envFactory.create(pmc);
   }
 
+  /**
+   * Like {@link #newPrologEnvironment()} but instead of reading the rules.pl
+   * read the provided input stream.
+   *
+   * @param name a name of the input stream. Could be any name.
+   * @param in InputStream to read prolog rules from
+   * @throws CompileException
+   */
+  public PrologEnvironment newPrologEnvironment(String name, InputStream in)
+      throws CompileException {
+    PrologMachineCopy pmc = rulesCache.loadMachine(name, in);
+    return envFactory.create(pmc);
+  }
+
   public Project getProject() {
     return config.getProject();
   }
@@ -174,7 +198,7 @@
   }
 
   /** Get the sections that pertain only to this project. */
-  private List<SectionMatcher> getLocalAccessSections() {
+  List<SectionMatcher> getLocalAccessSections() {
     List<SectionMatcher> sm = localAccessSections;
     if (sm == null) {
       Collection<AccessSection> fromConfig = config.getAccessSections();
@@ -192,7 +216,8 @@
           section.setPermissions(copy);
         }
 
-        SectionMatcher matcher = SectionMatcher.wrap(section);
+        SectionMatcher matcher = SectionMatcher.wrap(getProject().getNameKey(),
+            section);
         if (matcher != null) {
           sm.add(matcher);
         }
@@ -212,23 +237,9 @@
       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 {
+    List<SectionMatcher> all = Lists.newArrayList();
+    for (ProjectState s : tree()) {
       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;
   }
@@ -240,16 +251,11 @@
    *         that has local owners are returned
    */
   public Set<AccountGroup.UUID> getOwners() {
-    Project.NameKey parentName = getProject().getParent();
-    if (!localOwners.isEmpty() || parentName == null || isAllProjects) {
-      return localOwners;
+    for (ProjectState p : tree()) {
+      if (!p.localOwners.isEmpty()) {
+        return p.localOwners;
+      }
     }
-
-    ProjectState parent = projectCache.get(parentName);
-    if (parent != null) {
-      return parent.getOwners();
-    }
-
     return Collections.emptySet();
   }
 
@@ -257,23 +263,13 @@
    * @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..
    */
-  boolean isOwner(GroupMembership groups) {
-    Set<Project.NameKey> seen = new HashSet<Project.NameKey>();
-    seen.add(getProject().getNameKey());
-
-    ProjectState s = this;
-    do {
-      if (groups.containsAnyOf(s.localOwners)) {
-        return true;
+  boolean isOwner(final GroupMembership groups) {
+    return Iterables.any(tree(), new Predicate<ProjectState>() {
+      @Override
+      public boolean apply(ProjectState in) {
+        return groups.containsAnyOf(in.localOwners);
       }
-
-      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) {
@@ -281,18 +277,104 @@
   }
 
   /**
-   * @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.
+   * @return an iterable that walks through this project and then the parents of
+   *         this project. Starts from this project and progresses up the
+   *         hierarchy to All-Projects.
    */
-  public ProjectState getParentState() {
-    if (isAllProjects) {
-      return null;
-    }
-    return projectCache.get(getProject().getParent(allProjectsName));
+  public Iterable<ProjectState> tree() {
+    return new Iterable<ProjectState>() {
+      @Override
+      public Iterator<ProjectState> iterator() {
+        return new ProjectHierarchyIterator(
+            projectCache, allProjectsName,
+            ProjectState.this);
+      }
+    };
+  }
+
+  /**
+   * @return an iterable that walks through the parents of this project. Starts
+   *         from the immediate parent of this project and progresses up the
+   *         hierarchy to All-Projects.
+   */
+  public Iterable<ProjectState> parents() {
+    return Iterables.skip(tree(), 1);
   }
 
   public boolean isAllProjects() {
     return isAllProjects;
   }
-}
\ No newline at end of file
+
+  public boolean isUseContributorAgreements() {
+    return getInheritableBoolean(new Function<Project, InheritableBoolean>() {
+      @Override
+      public InheritableBoolean apply(Project input) {
+        return input.getUseContributorAgreements();
+      }
+    });
+  }
+
+  public boolean isUseContentMerge() {
+    return getInheritableBoolean(new Function<Project, InheritableBoolean>() {
+      @Override
+      public InheritableBoolean apply(Project input) {
+        return input.getUseContentMerge();
+      }
+    });
+  }
+
+  public boolean isUseSignedOffBy() {
+    return getInheritableBoolean(new Function<Project, InheritableBoolean>() {
+      @Override
+      public InheritableBoolean apply(Project input) {
+        return input.getUseSignedOffBy();
+      }
+    });
+  }
+
+  public boolean isRequireChangeID() {
+    return getInheritableBoolean(new Function<Project, InheritableBoolean>() {
+      @Override
+      public InheritableBoolean apply(Project input) {
+        return input.getRequireChangeID();
+      }
+    });
+  }
+
+  public LabelTypes getLabelTypes() {
+    Map<String, LabelType> types = Maps.newLinkedHashMap();
+    List<ProjectState> projects = Lists.newArrayList(tree());
+    Collections.reverse(projects);
+    for (ProjectState s : projects) {
+      for (LabelType type : s.getConfig().getLabelSections().values()) {
+        String lower = type.getName().toLowerCase();
+        LabelType old = types.get(lower);
+        if (old == null || old.canOverride()) {
+          types.put(lower, type);
+        }
+      }
+    }
+    List<LabelType> all = Lists.newArrayListWithCapacity(types.size());
+    for (LabelType type : types.values()) {
+      if (!type.getValues().isEmpty()) {
+        all.add(type);
+      }
+    }
+    return new LabelTypes(Collections.unmodifiableList(all));
+  }
+
+  private boolean getInheritableBoolean(Function<Project, InheritableBoolean> func) {
+    for (ProjectState s : tree()) {
+      switch (func.apply(s.getProject())) {
+        case TRUE:
+          return true;
+        case FALSE:
+          return false;
+        case INHERIT:
+        default:
+          continue;
+      }
+    }
+    return false;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectsCollection.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectsCollection.java
new file mode 100644
index 0000000..4c00439
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/ProjectsCollection.java
@@ -0,0 +1,109 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.extensions.registration.DynamicMap;
+import com.google.gerrit.extensions.restapi.AcceptsCreate;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestCollection;
+import com.google.gerrit.extensions.restapi.RestView;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.OutputFormat;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+public class ProjectsCollection implements
+    RestCollection<TopLevelResource, ProjectResource>,
+    AcceptsCreate<TopLevelResource> {
+  private final DynamicMap<RestView<ProjectResource>> views;
+  private final Provider<ListProjects> list;
+  private final ProjectControl.GenericFactory controlFactory;
+  private final Provider<CurrentUser> user;
+  private final CreateProject.Factory createProjectFactory;
+
+  @Inject
+  ProjectsCollection(DynamicMap<RestView<ProjectResource>> views,
+      Provider<ListProjects> list,
+      ProjectControl.GenericFactory controlFactory,
+      CreateProject.Factory factory, Provider<CurrentUser> user) {
+    this.views = views;
+    this.list = list;
+    this.controlFactory = controlFactory;
+    this.user = user;
+    this.createProjectFactory = factory;
+  }
+
+  @Override
+  public RestView<TopLevelResource> list() {
+    return list.get().setFormat(OutputFormat.JSON);
+  }
+
+  @Override
+  public ProjectResource parse(TopLevelResource parent, IdString id)
+      throws ResourceNotFoundException {
+    ProjectResource rsrc = _parse(id.get());
+    if (rsrc == null) {
+      throw new ResourceNotFoundException(id);
+    }
+    return rsrc;
+  }
+
+  /**
+   * Parses a project ID from a request body and returns the project.
+   *
+   * @param id ID of the project, can be a project name
+   * @return the project
+   * @throws UnprocessableEntityException thrown if the project ID cannot be
+   *         resolved or if the project is not visible to the calling user
+   */
+  public ProjectResource parse(String id) throws UnprocessableEntityException {
+    ProjectResource rsrc = _parse(id);
+    if (rsrc == null) {
+      throw new UnprocessableEntityException(String.format(
+          "Project Not Found: %s", id));
+    }
+    return rsrc;
+  }
+
+  private ProjectResource _parse(String id) {
+    ProjectControl ctl;
+    try {
+      ctl = controlFactory.controlFor(
+          new Project.NameKey(id),
+          user.get());
+    } catch (NoSuchProjectException e) {
+      return null;
+    }
+    if (!ctl.isVisible() && !ctl.isOwner()) {
+      return null;
+    }
+    return new ProjectResource(ctl);
+  }
+
+  @Override
+  public DynamicMap<RestView<ProjectResource>> views() {
+    return views;
+  }
+
+  @SuppressWarnings("unchecked")
+  @Override
+  public CreateProject create(TopLevelResource parent, IdString name) {
+    return createProjectFactory.create(name.get());
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutDescription.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutDescription.java
new file mode 100644
index 0000000..f7dd3cb
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutDescription.java
@@ -0,0 +1,107 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+import com.google.common.base.Objects;
+import com.google.common.base.Strings;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.DefaultInput;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+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.MetaDataUpdate;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.project.PutDescription.Input;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+
+import java.io.IOException;
+
+class PutDescription implements RestModifyView<ProjectResource, Input> {
+  static class Input {
+    @DefaultInput
+    String description;
+    String commitMessage;
+  }
+
+  private final ProjectCache cache;
+  private final MetaDataUpdate.Server updateFactory;
+  private final GitRepositoryManager gitMgr;
+
+  @Inject
+  PutDescription(ProjectCache cache,
+      MetaDataUpdate.Server updateFactory,
+      GitRepositoryManager gitMgr) {
+    this.cache = cache;
+    this.updateFactory = updateFactory;
+    this.gitMgr = gitMgr;
+  }
+
+  @Override
+  public Object apply(ProjectResource resource, Input input)
+      throws AuthException, BadRequestException, ResourceConflictException,
+      ResourceNotFoundException, IOException {
+    if (input == null) {
+      input = new Input(); // Delete would set description to null.
+    }
+
+    ProjectControl ctl = resource.getControl();
+    IdentifiedUser user = (IdentifiedUser) ctl.getCurrentUser();
+    if (!ctl.isOwner()) {
+      throw new AuthException("not project owner");
+    }
+
+    try {
+      MetaDataUpdate md = updateFactory.create(resource.getNameKey());
+      try {
+        ProjectConfig config = ProjectConfig.read(md);
+        Project project = config.getProject();
+        project.setDescription(Strings.emptyToNull(input.description));
+
+        String msg = Objects.firstNonNull(
+          Strings.emptyToNull(input.commitMessage),
+          "Updated description.\n");
+        if (!msg.endsWith("\n")) {
+          msg += "\n";
+        }
+        md.setAuthor(user);
+        md.setMessage(msg);
+        config.commit(md);
+        cache.evict(ctl.getProject());
+        gitMgr.setProjectDescription(
+            resource.getNameKey(),
+            project.getDescription());
+
+        return Strings.isNullOrEmpty(project.getDescription())
+            ? Response.none()
+            : project.getDescription();
+      } finally {
+        md.close();
+      }
+    } catch (RepositoryNotFoundException notFound) {
+      throw new ResourceNotFoundException(resource.getName());
+    } catch (ConfigInvalidException e) {
+      throw new ResourceConflictException(String.format(
+          "invalid project.config: %s", e.getMessage()));
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/PutProject.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutProject.java
new file mode 100644
index 0000000..0a96cb5
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/PutProject.java
@@ -0,0 +1,28 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.server.project.CreateProject.Input;
+
+public class PutProject implements RestModifyView<ProjectResource, Input> {
+  @Override
+  public Object apply(ProjectResource resource, Input input)
+      throws ResourceConflictException {
+    throw new ResourceConflictException("Project \"" + resource.getName()
+        + "\" already exists");
+  }
+}
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 a6182d1..59b7670 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,12 +14,15 @@
 
 package com.google.gerrit.server.project;
 
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
 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.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
@@ -41,6 +44,7 @@
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 
 /** Manages access control for Git references (aka branches, tags). */
@@ -57,6 +61,7 @@
   private Boolean owner;
   private Boolean canForgeAuthor;
   private Boolean canForgeCommitter;
+  private Boolean isVisible;
 
   RefControl(ProjectControl projectControl, String ref,
       PermissionCollection relevant) {
@@ -102,8 +107,31 @@
 
   /** Can this user see this reference exists? */
   public boolean isVisible() {
-    return (getCurrentUser() instanceof InternalUser || canPerform(Permission.READ))
-        && canRead();
+    if (isVisible == null) {
+      isVisible =
+          (getCurrentUser() instanceof InternalUser || canPerform(Permission.READ))
+              && canRead();
+    }
+    return isVisible;
+  }
+
+  /**
+   * True if this reference is visible by all REGISTERED_USERS
+   */
+  public boolean isVisibleByRegisteredUsers() {
+    List<PermissionRule> access = relevant.getPermission(Permission.READ);
+    Set<ProjectRef> allows = Sets.newHashSet();
+    Set<ProjectRef> blocks = Sets.newHashSet();
+    for (PermissionRule rule : access) {
+      if (rule.isBlock()) {
+        blocks.add(relevant.getRuleProps(rule));
+      } else if (rule.getGroup().getUUID().equals(AccountGroup.ANONYMOUS_USERS)
+          || rule.getGroup().getUUID().equals(AccountGroup.REGISTERED_USERS)) {
+        allows.add(relevant.getRuleProps(rule));
+      }
+    }
+    blocks.removeAll(allows);
+    return blocks.isEmpty() && !allows.isEmpty();
   }
 
   /**
@@ -193,16 +221,7 @@
       // 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;
+    return canForcePerform(Permission.PUSH);
   }
 
   /**
@@ -218,7 +237,8 @@
     }
     boolean owner;
     switch (getCurrentUser().getAccessPath()) {
-      case WEB_UI:
+      case REST_API:
+      case JSON_RPC:
         owner = isOwner();
         break;
 
@@ -258,11 +278,10 @@
       // than if it doesn't have a PGP signature.
       //
       if (tag.getFullMessage().contains("-----BEGIN PGP SIGNATURE-----\n")) {
-        return owner || canPerform(Permission.PUSH_TAG);
+        return owner || canPerform(Permission.PUSH_SIGNED_TAG);
       } else {
         return owner || canPerform(Permission.PUSH_TAG);
       }
-
     } else {
       return false;
     }
@@ -285,7 +304,8 @@
     }
 
     switch (getCurrentUser().getAccessPath()) {
-      case WEB_UI:
+      case REST_API:
+      case JSON_RPC:
         return isOwner() || canPushWithForce();
 
       case GIT:
@@ -322,6 +342,36 @@
     return canPerform(Permission.ABANDON);
   }
 
+  /** @return true if this user can remove a reviewer for a change. */
+  public boolean canRemoveReviewer() {
+    return canPerform(Permission.REMOVE_REVIEWER);
+  }
+
+  /** @return true if this user can view draft changes. */
+  public boolean canViewDrafts() {
+    return canPerform(Permission.VIEW_DRAFTS);
+  }
+
+  /** @return true if this user can publish draft changes. */
+  public boolean canPublishDrafts() {
+    return canPerform(Permission.PUBLISH_DRAFTS);
+  }
+
+  /** @return true if this user can delete draft changes. */
+  public boolean canDeleteDrafts() {
+    return canPerform(Permission.DELETE_DRAFTS);
+  }
+
+  /** @return true if this user can edit topic names. */
+  public boolean canEditTopicName() {
+    return canPerform(Permission.EDIT_TOPIC_NAME);
+  }
+
+  /** @return true if this user can force edit topic names. */
+  public boolean canForceEditTopicName() {
+    return canForcePerform(Permission.EDIT_TOPIC_NAME);
+  }
+
   /** All value ranges of any allowed label permission. */
   public List<PermissionRange> getLabelRanges() {
     List<PermissionRange> r = new ArrayList<PermissionRange>();
@@ -351,39 +401,97 @@
     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) {
+  private static class AllowedRange {
+    private int allowMin = 0;
+    private int allowMax = 0;
+    private int blockMin = Integer.MIN_VALUE;
+    private int blockMax = Integer.MAX_VALUE;
+
+    void update(PermissionRule rule) {
       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());
+        allowMin = Math.min(allowMin, rule.getMin());
+        allowMax = Math.max(allowMax, rule.getMax());
       }
     }
-    if (blockMin > Integer.MIN_VALUE) {
-      min = Math.max(min, blockMin + 1);
+
+    int getAllowMin() {
+      return allowMin;
     }
-    if (blockMax < Integer.MAX_VALUE) {
-      max = Math.min(max, blockMax - 1);
+    int getAllowMax() {
+      return allowMax;
     }
+    int getBlockMin() {
+      // ALLOW wins over BLOCK on the same project
+      return Math.min(blockMin, allowMin - 1);
+    }
+    int getBlockMax() {
+      // ALLOW wins over BLOCK on the same project
+      return Math.max(blockMax, allowMax + 1);
+    }
+  }
+
+  private PermissionRange toRange(String permissionName,
+      List<PermissionRule> ruleList) {
+    Map<ProjectRef, AllowedRange> ranges = Maps.newHashMap();
+    for (PermissionRule rule : ruleList) {
+      ProjectRef p = relevant.getRuleProps(rule);
+      AllowedRange r = ranges.get(p);
+      if (r == null) {
+        r = new AllowedRange();
+        ranges.put(p, r);
+      }
+      r.update(rule);
+    }
+    int allowMin = 0;
+    int allowMax = 0;
+    int blockMin = Integer.MIN_VALUE;
+    int blockMax = Integer.MAX_VALUE;
+    for (AllowedRange r : ranges.values()) {
+      allowMin = Math.min(allowMin, r.getAllowMin());
+      allowMax = Math.max(allowMax, r.getAllowMax());
+      blockMin = Math.max(blockMin, r.getBlockMin());
+      blockMax = Math.min(blockMax, r.getBlockMax());
+    }
+
+    // BLOCK wins over ALLOW across projects
+    int min = Math.max(allowMin, blockMin + 1);
+    int max = Math.min(allowMax, 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);
+    Set<ProjectRef> allows = Sets.newHashSet();
+    Set<ProjectRef> blocks = Sets.newHashSet();
     for (PermissionRule rule : access) {
       if (rule.isBlock() && !rule.getForce()) {
-        return false;
+        blocks.add(relevant.getRuleProps(rule));
+      } else {
+        allows.add(relevant.getRuleProps(rule));
       }
     }
-    return !access.isEmpty();
+    blocks.removeAll(allows);
+    return blocks.isEmpty() && !allows.isEmpty();
+  }
+
+  /** True if the user has force this permission. Works only for non labels. */
+  private boolean canForcePerform(String permissionName) {
+    List<PermissionRule> access = access(permissionName);
+    Set<ProjectRef> allows = Sets.newHashSet();
+    Set<ProjectRef> blocks = Sets.newHashSet();
+    for (PermissionRule rule : access) {
+      if (rule.isBlock()) {
+        blocks.add(relevant.getRuleProps(rule));
+      } else if (rule.getForce()) {
+        allows.add(relevant.getRuleProps(rule));
+      }
+    }
+    blocks.removeAll(allows);
+    return blocks.isEmpty() && !allows.isEmpty();
   }
 
   /** Rules for the given permission, or the empty list. */
@@ -428,7 +536,12 @@
 
   public static String shortestExample(String pattern) {
     if (isRE(pattern)) {
-      return toRegExp(pattern).toAutomaton().getShortestExample(true);
+      // Since Brics will substitute dot [.] with \0 when generating
+      // shortest example, any usage of dot will fail in
+      // Repository.isValidRefName() if not combined with star [*].
+      // To get around this, we substitute the \0 with an arbitrary
+      // accepted character.
+      return toRegExp(pattern).toAutomaton().getShortestExample(true).replace('\0', '-');
     } else if (pattern.endsWith("/*")) {
       return pattern.substring(0, pattern.length() - 1) + '1';
     } else {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/RepositoryStatistics.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/RepositoryStatistics.java
new file mode 100644
index 0000000..d8294c0
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/RepositoryStatistics.java
@@ -0,0 +1,32 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.common.base.CaseFormat;
+
+import java.util.Map.Entry;
+import java.util.Properties;
+import java.util.TreeMap;
+
+class RepositoryStatistics extends TreeMap<String, Object> {
+  private static final long serialVersionUID = 1L;
+
+  public RepositoryStatistics(Properties p) {
+    for (Entry<Object, Object> e : p.entrySet()) {
+      put(CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE,
+          e.getKey().toString()), e.getValue());
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/RuleEvalException.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/RuleEvalException.java
new file mode 100644
index 0000000..9dae11c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/RuleEvalException.java
@@ -0,0 +1,26 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.project;
+
+@SuppressWarnings("serial")
+public class RuleEvalException extends Exception {
+  public RuleEvalException(String message) {
+    super(message);
+  }
+
+  RuleEvalException(String message, Throwable cause) {
+    super(message, cause);
+  }
+}
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
index 1c70b04..6f8af80 100644
--- 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
@@ -18,6 +18,7 @@
 
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.ParameterizedString;
+import com.google.gerrit.reviewdb.client.Project;
 
 import dk.brics.automaton.Automaton;
 
@@ -31,33 +32,37 @@
  * faster selection of which sections are relevant to any given input reference.
  */
 abstract class SectionMatcher {
-  static SectionMatcher wrap(AccessSection section) {
+  static SectionMatcher wrap(Project.NameKey project, AccessSection section) {
     String ref = section.getName();
     if (AccessSection.isValid(ref)) {
-      return wrap(ref, section);
+      return wrap(project, ref, section);
     } else {
       return null;
     }
   }
 
-  static SectionMatcher wrap(String pattern, AccessSection section) {
+  static SectionMatcher wrap(Project.NameKey project, String pattern,
+      AccessSection section) {
     if (pattern.contains("${")) {
-      return new ExpandParameters(pattern, section);
+      return new ExpandParameters(project, pattern, section);
 
     } else if (isRE(pattern)) {
-      return new Regexp(pattern, section);
+      return new Regexp(project, pattern, section);
 
     } else if (pattern.endsWith("/*")) {
-      return new Prefix(pattern.substring(0, pattern.length() - 1), section);
+      return new Prefix(project, pattern.substring(0, pattern.length() - 1),
+          section);
 
     } else {
-      return new Exact(pattern, section);
+      return new Exact(project, pattern, section);
     }
   }
 
+  final Project.NameKey project;
   final AccessSection section;
 
-  SectionMatcher(AccessSection section) {
+  SectionMatcher(Project.NameKey project, AccessSection section) {
+    this.project = project;
     this.section = section;
   }
 
@@ -66,8 +71,8 @@
   private static class Exact extends SectionMatcher {
     private final String expect;
 
-    Exact(String name, AccessSection section) {
-      super(section);
+    Exact(Project.NameKey project, String name, AccessSection section) {
+      super(project, section);
       expect = name;
     }
 
@@ -80,8 +85,8 @@
   private static class Prefix extends SectionMatcher {
     private final String prefix;
 
-    Prefix(String pfx, AccessSection section) {
-      super(section);
+    Prefix(Project.NameKey project, String pfx, AccessSection section) {
+      super(project, section);
       prefix = pfx;
     }
 
@@ -94,8 +99,8 @@
   private static class Regexp extends SectionMatcher {
     private final Pattern pattern;
 
-    Regexp(String re, AccessSection section) {
-      super(section);
+    Regexp(Project.NameKey project, String re, AccessSection section) {
+      super(project, section);
       pattern = Pattern.compile(re);
     }
 
@@ -109,8 +114,9 @@
     private final ParameterizedString template;
     private final String prefix;
 
-    ExpandParameters(String pattern, AccessSection section) {
-      super(section);
+    ExpandParameters(Project.NameKey project, String pattern,
+        AccessSection section) {
+      super(project, section);
       template = new ParameterizedString(pattern);
 
       if (isRE(pattern)) {
@@ -141,7 +147,7 @@
         u = username;
       }
 
-      SectionMatcher next = wrap(
+      SectionMatcher next = wrap(project,
           template.replace(Collections.singletonMap("username", u)),
           section);
       return next != null ? next.match(ref, username) : false;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDashboard.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDashboard.java
new file mode 100644
index 0000000..930da12
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDashboard.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.project;
+
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.DefaultInput;
+import com.google.gerrit.extensions.restapi.MethodNotAllowedException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.server.project.SetDashboard.Input;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+class SetDashboard implements RestModifyView<DashboardResource, Input> {
+  static class Input {
+    @DefaultInput
+    String id;
+    String commitMessage;
+  }
+
+  private final Provider<SetDefaultDashboard> defaultSetter;
+
+  @Inject
+  SetDashboard(Provider<SetDefaultDashboard> defaultSetter) {
+    this.defaultSetter = defaultSetter;
+  }
+
+  @Override
+  public Object apply(DashboardResource resource, Input input)
+      throws AuthException, BadRequestException, ResourceConflictException,
+      Exception {
+    if (resource.isProjectDefault()) {
+      return defaultSetter.get().apply(resource, input);
+    }
+
+    // TODO: Implement creation/update of dashboards by API.
+    throw new MethodNotAllowedException();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDefaultDashboard.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDefaultDashboard.java
new file mode 100644
index 0000000..3f70bc2
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetDefaultDashboard.java
@@ -0,0 +1,149 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.common.base.Objects;
+import com.google.common.base.Strings;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.Response;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.project.DashboardsCollection.DashboardInfo;
+import com.google.gerrit.server.project.SetDashboard.Input;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.kohsuke.args4j.Option;
+
+class SetDefaultDashboard implements RestModifyView<DashboardResource, Input> {
+  private final ProjectCache cache;
+  private final MetaDataUpdate.Server updateFactory;
+  private final DashboardsCollection dashboards;
+  private final Provider<GetDashboard> get;
+
+  @Option(name = "--inherited", usage = "set dashboard inherited by children")
+  private boolean inherited;
+
+  @Inject
+  SetDefaultDashboard(ProjectCache cache,
+      MetaDataUpdate.Server updateFactory,
+      DashboardsCollection dashboards,
+      Provider<GetDashboard> get) {
+    this.cache = cache;
+    this.updateFactory = updateFactory;
+    this.dashboards = dashboards;
+    this.get = get;
+  }
+
+  @Override
+  public Object apply(DashboardResource resource, Input input)
+      throws AuthException, BadRequestException, ResourceConflictException,
+      Exception {
+    if (input == null) {
+      input = new Input(); // Delete would set input to null.
+    }
+    input.id = Strings.emptyToNull(input.id);
+
+    ProjectControl ctl = resource.getControl();
+    IdentifiedUser user = (IdentifiedUser) ctl.getCurrentUser();
+    if (!ctl.isOwner()) {
+      throw new AuthException("not project owner");
+    }
+
+    DashboardResource target = null;
+    if (input.id != null) {
+      try {
+        target = dashboards.parse(
+            new ProjectResource(ctl),
+            IdString.fromUrl(input.id));
+      } catch (ResourceNotFoundException e) {
+        throw new BadRequestException("dashboard " + input.id + " not found");
+      }
+    }
+
+    try {
+      MetaDataUpdate md = updateFactory.create(ctl.getProject().getNameKey());
+      try {
+        ProjectConfig config = ProjectConfig.read(md);
+        Project project = config.getProject();
+        if (inherited) {
+          project.setDefaultDashboard(input.id);
+        } else {
+          project.setLocalDefaultDashboard(input.id);
+        }
+
+        String msg = Objects.firstNonNull(
+          Strings.emptyToNull(input.commitMessage),
+          input.id == null
+            ? "Removed default dashboard.\n"
+            : String.format("Changed default dashboard to %s.\n", input.id));
+        if (!msg.endsWith("\n")) {
+          msg += "\n";
+        }
+        md.setAuthor(user);
+        md.setMessage(msg);
+        config.commit(md);
+        cache.evict(ctl.getProject());
+
+        if (target != null) {
+          DashboardInfo info = get.get().apply(target);
+          info.isDefault = true;
+          return info;
+        }
+        return Response.none();
+      } finally {
+        md.close();
+      }
+    } catch (RepositoryNotFoundException notFound) {
+      throw new ResourceNotFoundException(ctl.getProject().getName());
+    } catch (ConfigInvalidException e) {
+      throw new ResourceConflictException(String.format(
+          "invalid project.config: %s", e.getMessage()));
+    }
+  }
+
+  static class CreateDefault implements
+      RestModifyView<ProjectResource, SetDashboard.Input> {
+    private final Provider<SetDefaultDashboard> setDefault;
+
+    @Option(name = "--inherited", usage = "set dashboard inherited by children")
+    private boolean inherited;
+
+    @Inject
+    CreateDefault(Provider<SetDefaultDashboard> setDefault) {
+      this.setDefault = setDefault;
+    }
+
+    @Override
+    public Object apply(ProjectResource resource, Input input)
+        throws AuthException, BadRequestException, ResourceConflictException,
+        Exception {
+      SetDefaultDashboard set = setDefault.get();
+      set.inherited = inherited;
+      return Response.created(set.apply(
+          DashboardResource.projectDefault(resource.getControl()),
+          input));
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetHead.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetHead.java
new file mode 100644
index 0000000..3e7a42f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetHead.java
@@ -0,0 +1,97 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.common.base.Strings;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.DefaultInput;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.UnprocessableEntityException;
+import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.auth.AuthException;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.project.SetHead.Input;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Constants;
+import org.eclipse.jgit.lib.RefUpdate;
+import org.eclipse.jgit.lib.Repository;
+
+import java.io.IOException;
+
+public class SetHead implements RestModifyView<ProjectResource, Input> {
+  static class Input {
+    @DefaultInput
+    String ref;
+  }
+
+  private final GitRepositoryManager repoManager;
+  private final IdentifiedUser identifiedUser;
+
+  @Inject
+  SetHead(GitRepositoryManager repoManager, IdentifiedUser identifiedUser) {
+    this.repoManager = repoManager;
+    this.identifiedUser = identifiedUser;
+  }
+
+  @Override
+  public String apply(ProjectResource rsrc, Input input) throws AuthException,
+      ResourceNotFoundException, BadRequestException,
+      UnprocessableEntityException, IOException {
+    if (!rsrc.getControl().isOwner()) {
+      throw new AuthException("restricted to project owner");
+    }
+    if (input == null || Strings.isNullOrEmpty(input.ref)) {
+      throw new BadRequestException("ref required");
+    }
+    String ref = input.ref;
+    if (!ref.startsWith(Constants.R_REFS)) {
+      ref = Constants.R_HEADS + ref;
+    }
+
+    Repository repo = null;
+    try {
+      repo = repoManager.openRepository(rsrc.getNameKey());
+      if (repo.getRef(ref) == null) {
+        throw new UnprocessableEntityException(String.format(
+            "Ref Not Found: %s", ref));
+      }
+
+      if (!repo.getRef(Constants.HEAD).getTarget().getName().equals(ref)) {
+        final RefUpdate u = repo.updateRef(Constants.HEAD, true);
+        u.setRefLogIdent(identifiedUser.newRefLogIdent());
+        RefUpdate.Result res = u.link(ref);
+        switch(res) {
+          case NO_CHANGE:
+          case RENAMED:
+          case FORCED:
+          case NEW:
+            break;
+          default:
+            throw new IOException("Setting HEAD failed with " + res);
+        }
+      }
+      return ref;
+    } catch (RepositoryNotFoundException e) {
+      throw new ResourceNotFoundException(rsrc.getName());
+    } finally {
+      if (repo != null) {
+        repo.close();
+      }
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SetParent.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetParent.java
new file mode 100644
index 0000000..c1fb5de
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SetParent.java
@@ -0,0 +1,98 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.common.base.Objects;
+import com.google.common.base.Strings;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.DefaultInput;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.IdentifiedUser;
+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.SetParent.Input;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.errors.ConfigInvalidException;
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+
+class SetParent implements RestModifyView<ProjectResource, Input> {
+  static class Input {
+    @DefaultInput
+    String parent;
+    String commitMessage;
+  }
+
+  private final ProjectCache cache;
+  private final MetaDataUpdate.Server updateFactory;
+  private final AllProjectsName allProjects;
+
+  @Inject
+  SetParent(ProjectCache cache,
+      MetaDataUpdate.Server updateFactory,
+      AllProjectsName allProjects) {
+    this.cache = cache;
+    this.updateFactory = updateFactory;
+    this.allProjects = allProjects;
+  }
+
+  @Override
+  public String apply(ProjectResource resource, Input input)
+      throws AuthException, BadRequestException, ResourceConflictException,
+      Exception {
+    ProjectControl ctl = resource.getControl();
+    IdentifiedUser user = (IdentifiedUser) ctl.getCurrentUser();
+    if (!user.getCapabilities().canAdministrateServer()) {
+      throw new AuthException("not administrator");
+    }
+
+    try {
+      MetaDataUpdate md = updateFactory.create(resource.getNameKey());
+      try {
+        ProjectConfig config = ProjectConfig.read(md);
+        Project project = config.getProject();
+        project.setParentName(Strings.emptyToNull(input.parent));
+
+        String msg = Strings.emptyToNull(input.commitMessage);
+        if (msg == null) {
+          msg = String.format(
+              "Changed parent to %s.\n",
+              Objects.firstNonNull(project.getParentName(), allProjects.get()));
+        } else if (!msg.endsWith("\n")) {
+          msg += "\n";
+        }
+        md.setAuthor(user);
+        md.setMessage(msg);
+        config.commit(md);
+        cache.evict(ctl.getProject());
+
+        Project.NameKey parentName = project.getParent(allProjects);
+        return parentName != null ? parentName.get() : "";
+      } finally {
+        md.close();
+      }
+    } catch (RepositoryNotFoundException notFound) {
+      throw new ResourceNotFoundException(resource.getName());
+    } catch (ConfigInvalidException e) {
+      throw new ResourceConflictException(String.format(
+          "invalid project.config: %s", e.getMessage()));
+    }
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java b/gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
new file mode 100644
index 0000000..349567c
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/project/SubmitRuleEvaluator.java
@@ -0,0 +1,249 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.common.collect.Lists;
+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.rules.PrologEnvironment;
+import com.google.gerrit.rules.StoredValues;
+import com.google.gerrit.server.query.change.ChangeData;
+
+import com.googlecode.prolog_cafe.compiler.CompileException;
+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.Term;
+import com.googlecode.prolog_cafe.lang.VariableTerm;
+
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.List;
+
+import javax.annotation.Nullable;
+
+/**
+ * Evaluates a submit-like Prolog rule found in the rules.pl file of the current
+ * project and filters the results through rules found in the parent projects,
+ * all the way up to All-Projects.
+ */
+public class SubmitRuleEvaluator {
+  private final ReviewDb db;
+  private final PatchSet patchSet;
+  private final ProjectControl projectControl;
+  private final ChangeControl changeControl;
+  private final Change change;
+  private final ChangeData cd;
+  private final boolean fastEvalLabels;
+  private final String userRuleLocatorName;
+  private final String userRuleWrapperName;
+  private final String filterRuleLocatorName;
+  private final String filterRuleWrapperName;
+  private final boolean skipFilters;
+  private final InputStream rulesInputStream;
+
+  private Term submitRule;
+  private String projectName;
+
+  /**
+   * @param userRuleLocatorName The name of the rule used to locate the
+   *        user-supplied rule.
+   * @param userRuleWrapperName The name of the wrapper rule used to evaluate
+   *        the user-supplied rule.
+   * @param filterRuleLocatorName The name of the rule used to locate the filter
+   *        rule.
+   * @param filterRuleWrapperName The name of the rule used to evaluate the
+   *        filter rule.
+   */
+  public SubmitRuleEvaluator(ReviewDb db, PatchSet patchSet,
+      ProjectControl projectControl,
+      ChangeControl changeControl, Change change, @Nullable ChangeData cd,
+      boolean fastEvalLabels,
+      String userRuleLocatorName, String userRuleWrapperName,
+      String filterRuleLocatorName, String filterRuleWrapperName) {
+    this(db, patchSet, projectControl, changeControl, change, cd,
+        fastEvalLabels, userRuleLocatorName, userRuleWrapperName,
+        filterRuleLocatorName, filterRuleWrapperName, false, null);
+  }
+
+  /**
+   * @param userRuleLocatorName The name of the rule used to locate the
+   *        user-supplied rule.
+   * @param userRuleWrapperName The name of the wrapper rule used to evaluate
+   *        the user-supplied rule.
+   * @param filterRuleLocatorName The name of the rule used to locate the filter
+   *        rule.
+   * @param filterRuleWrapperName The name of the rule used to evaluate the
+   *        filter rule.
+   * @param skipSubmitFilters if <code>true</code> submit filter will not be
+   *        applied
+   * @param rules when non-null the rules will be read from this input stream
+   *        instead of refs/meta/config:rules.pl file
+   */
+  public SubmitRuleEvaluator(ReviewDb db, PatchSet patchSet,
+      ProjectControl projectControl,
+      ChangeControl changeControl, Change change, @Nullable ChangeData cd,
+      boolean fastEvalLabels,
+      String userRuleLocatorName, String userRuleWrapperName,
+      String filterRuleLocatorName, String filterRuleWrapperName,
+      boolean skipSubmitFilters, InputStream rules) {
+    this.db = db;
+    this.patchSet = patchSet;
+    this.projectControl = projectControl;
+    this.changeControl = changeControl;
+    this.change = change;
+    this.cd = cd;
+    this.fastEvalLabels = fastEvalLabels;
+    this.userRuleLocatorName = userRuleLocatorName;
+    this.userRuleWrapperName = userRuleWrapperName;
+    this.filterRuleLocatorName = filterRuleLocatorName;
+    this.filterRuleWrapperName = filterRuleWrapperName;
+    this.skipFilters = skipSubmitFilters;
+    this.rulesInputStream = rules;
+  }
+
+  /**
+   * Evaluates the given rule and filters.
+   *
+   * Sets the {@link #submitRule} to the Term found by the
+   * {@link #userRuleLocatorName}. This can be used when reporting error(s) on
+   * unexpected return value of this method.
+   *
+   * @return List of {@link Term} objects returned from the evaluated rules.
+   * @throws RuleEvalException
+   */
+  public List<Term> evaluate() throws RuleEvalException {
+    PrologEnvironment env = getPrologEnvironment();
+    try {
+      submitRule = env.once("gerrit", userRuleLocatorName, new VariableTerm());
+      if (fastEvalLabels) {
+        env.once("gerrit", "assume_range_from_label");
+      }
+
+      List<Term> results = new ArrayList<Term>();
+      try {
+        for (Term[] template : env.all("gerrit", userRuleWrapperName,
+            submitRule, new VariableTerm())) {
+          results.add(template[1]);
+        }
+      } catch (PrologException err) {
+        throw new RuleEvalException("Exception calling " + submitRule
+            + " on change " + change.getId() + " of " + getProjectName(),
+            err);
+      } catch (RuntimeException err) {
+        throw new RuleEvalException("Exception calling " + submitRule
+            + " on change " + change.getId() + " of " + getProjectName(),
+            err);
+      }
+
+      Term resultsTerm = toListTerm(results);
+      if (!skipFilters) {
+        resultsTerm = runSubmitFilters(resultsTerm, env);
+      }
+      if (resultsTerm.isList()) {
+        List<Term> r = Lists.newArrayList();
+        for (Term t = resultsTerm; t.isList();) {
+          ListTerm l = (ListTerm) t;
+          r.add(l.car().dereference());
+          t = l.cdr().dereference();
+        }
+        return r;
+      }
+      return Collections.emptyList();
+    } finally {
+      env.close();
+    }
+  }
+
+  private PrologEnvironment getPrologEnvironment() throws RuleEvalException {
+    ProjectState projectState = projectControl.getProjectState();
+    PrologEnvironment env;
+    try {
+      if (rulesInputStream == null) {
+        env = projectState.newPrologEnvironment();
+      } else {
+        env = projectState.newPrologEnvironment("stdin", rulesInputStream);
+      }
+    } catch (CompileException err) {
+      throw new RuleEvalException("Cannot consult rules.pl for "
+          + getProjectName(), err);
+    }
+    env.set(StoredValues.REVIEW_DB, db);
+    env.set(StoredValues.CHANGE, change);
+    env.set(StoredValues.CHANGE_DATA, cd);
+    env.set(StoredValues.PATCH_SET, patchSet);
+    env.set(StoredValues.CHANGE_CONTROL, changeControl);
+    return env;
+  }
+
+  private Term runSubmitFilters(Term results, PrologEnvironment env) throws RuleEvalException {
+    ProjectState projectState = projectControl.getProjectState();
+    PrologEnvironment childEnv = env;
+    for (ProjectState parentState : projectState.parents()) {
+      PrologEnvironment parentEnv;
+      try {
+        parentEnv = parentState.newPrologEnvironment();
+      } catch (CompileException err) {
+        throw new RuleEvalException("Cannot consult rules.pl for "
+            + parentState.getProject().getName(), err);
+      }
+
+      parentEnv.copyStoredValues(childEnv);
+      Term filterRule =
+          parentEnv.once("gerrit", filterRuleLocatorName, new VariableTerm());
+      try {
+        if (fastEvalLabels) {
+          env.once("gerrit", "assume_range_from_label");
+        }
+
+        Term[] template =
+            parentEnv.once("gerrit", filterRuleWrapperName, filterRule,
+                results, new VariableTerm());
+        results = template[2];
+      } catch (PrologException err) {
+        throw new RuleEvalException("Exception calling " + filterRule
+            + " on change " + change.getId() + " of "
+            + parentState.getProject().getName(), err);
+      } catch (RuntimeException err) {
+        throw new RuleEvalException("Exception calling " + filterRule
+            + " on change " + change.getId() + " of "
+            + parentState.getProject().getName(), err);
+      }
+      childEnv = parentEnv;
+    }
+    return results;
+  }
+
+  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 list;
+  }
+
+  public Term getSubmitRule() {
+    return submitRule;
+  }
+
+  private String getProjectName() {
+    if (projectName == null) {
+      projectName = projectControl.getProjectState().getProject().getName();
+    }
+    return projectName;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryRewriter.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryRewriter.java
index b3c10ef..2a6bae0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryRewriter.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/QueryRewriter.java
@@ -478,28 +478,6 @@
     return r;
   }
 
-  private static <T> void expand(final List<Predicate<T>> out,
-      final List<Predicate<T>> allOR, final List<Predicate<T>> tmp,
-      final List<Predicate<T>> nonOR) {
-    if (tmp.size() == allOR.size()) {
-      final int sz = nonOR.size() + tmp.size();
-      final List<Predicate<T>> newList = new ArrayList<Predicate<T>>(sz);
-      newList.addAll(nonOR);
-      newList.addAll(tmp);
-      out.add(Predicate.and(newList));
-
-    } else {
-      for (final Predicate<T> c : allOR.get(tmp.size()).getChildren()) {
-        try {
-          tmp.add(c);
-          expand(out, allOR, tmp, nonOR);
-        } finally {
-          tmp.remove(tmp.size() - 1);
-        }
-      }
-    }
-  }
-
   private static <T> boolean isAND(final Predicate<T> p) {
     return p instanceof AndPredicate;
   }
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 3a0bfa3..7f726a7 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
@@ -14,11 +14,19 @@
 
 package com.google.gerrit.server.query.change;
 
+import com.google.common.base.Function;
+import com.google.common.base.Throwables;
+import com.google.common.collect.FluentIterable;
+import com.google.common.collect.Iterables;
+import com.google.common.collect.Lists;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.query.AndPredicate;
 import com.google.gerrit.server.query.Predicate;
 import com.google.gwtorm.server.ListResultSet;
 import com.google.gwtorm.server.OrmException;
+import com.google.gwtorm.server.OrmRuntimeException;
 import com.google.gwtorm.server.ResultSet;
+import com.google.inject.Provider;
 
 import java.util.ArrayList;
 import java.util.Collection;
@@ -35,14 +43,6 @@
           int bi = b instanceof ChangeDataSource ? 0 : 1;
           int cmp = ai - bi;
 
-          if (cmp == 0 //
-              && a instanceof ChangeDataSource //
-              && b instanceof ChangeDataSource) {
-            ai = ((ChangeDataSource) a).hasChange() ? 0 : 1;
-            bi = ((ChangeDataSource) b).hasChange() ? 0 : 1;
-            cmp = ai - bi;
-          }
-
           if (cmp == 0) {
             cmp = a.getCost() - b.getCost();
           }
@@ -53,6 +53,11 @@
             ChangeDataSource as = (ChangeDataSource) a;
             ChangeDataSource bs = (ChangeDataSource) b;
             cmp = as.getCardinality() - bs.getCardinality();
+
+            if (cmp == 0) {
+              cmp = (as.hasChange() ? 0 : 1)
+                  - (bs.hasChange() ? 0 : 1);
+            }
           }
 
           return cmp;
@@ -67,10 +72,12 @@
     return r;
   }
 
+  private final Provider<ReviewDb> db;
   private int cardinality = -1;
 
-  AndSource(final Collection<? extends Predicate<ChangeData>> that) {
+  AndSource(Provider<ReviewDb> db, Collection<? extends Predicate<ChangeData>> that) {
     super(sort(that));
+    this.db = db;
   }
 
   @Override
@@ -81,17 +88,24 @@
 
   @Override
   public ResultSet<ChangeData> read() throws OrmException {
+    try {
+      return readImpl();
+    } catch (OrmRuntimeException err) {
+      Throwables.propagateIfInstanceOf(err.getCause(), OrmException.class);
+      throw new OrmException(err);
+    }
+  }
+
+  private ResultSet<ChangeData> readImpl() throws OrmException {
     ChangeDataSource source = source();
     if (source == null) {
       throw new OrmException("No ChangeDataSource: " + this);
     }
 
-    // TODO(spearce) This probably should be more lazy.
-    //
-    ArrayList<ChangeData> r = new ArrayList<ChangeData>();
+    List<ChangeData> r = Lists.newArrayList();
     ChangeData last = null;
     boolean skipped = false;
-    for (ChangeData data : source.read()) {
+    for (ChangeData data : buffer(source, source.read())) {
       if (match(data)) {
         r.add(data);
       } else {
@@ -101,7 +115,7 @@
     }
 
     if (skipped && last != null && source instanceof Paginated) {
-      // If we our source is a paginated source and we skipped at
+      // If our source is a paginated source and we skipped at
       // least one of its results, we may not have filled the full
       // limit the caller wants.  Restart the source and continue.
       //
@@ -110,7 +124,7 @@
         ChangeData lastBeforeRestart = last;
         skipped = false;
         last = null;
-        for (ChangeData data : p.restart(lastBeforeRestart)) {
+        for (ChangeData data : buffer(source, p.restart(lastBeforeRestart))) {
           if (match(data)) {
             r.add(data);
           } else {
@@ -124,6 +138,27 @@
     return new ListResultSet<ChangeData>(r);
   }
 
+  private Iterable<ChangeData> buffer(
+      ChangeDataSource source,
+      ResultSet<ChangeData> scanner) {
+    final boolean loadChange = !source.hasChange();
+    return FluentIterable
+      .from(Iterables.partition(scanner, 50))
+      .transformAndConcat(new Function<List<ChangeData>, List<ChangeData>>() {
+        @Override
+        public List<ChangeData> apply(List<ChangeData> buffer) {
+          if (loadChange) {
+            try {
+              ChangeData.ensureChangeLoaded(db, buffer);
+            } catch (OrmException e) {
+              throw new OrmRuntimeException(e);
+            }
+          }
+          return buffer;
+        }
+      });
+  }
+
   private ChangeDataSource source() {
     for (Predicate<ChangeData> p : getChildren()) {
       if (p instanceof ChangeDataSource) {
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 d6762db..506fabc 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,14 +14,20 @@
 
 package com.google.gerrit.server.query.change;
 
+import com.google.common.base.Function;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.ListMultimap;
 import com.google.common.collect.Lists;
 import com.google.common.collect.Maps;
+import com.google.common.collect.Ordering;
+import com.google.common.collect.Sets;
+import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.ChangeMessage;
 import com.google.gerrit.reviewdb.client.Patch;
 import com.google.gerrit.reviewdb.client.PatchLineComment;
 import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSet.Id;
 import com.google.gerrit.reviewdb.client.PatchSetApproval;
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.reviewdb.client.TrackingId;
@@ -43,15 +49,29 @@
 import org.eclipse.jgit.revwalk.RevWalk;
 
 import java.io.IOException;
+import java.sql.Timestamp;
 import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collection;
 import java.util.Collections;
-import java.util.HashMap;
 import java.util.List;
 import java.util.Map;
+import java.util.Set;
 
 public class ChangeData {
+  private static Ordering<PatchSetApproval> SORT_APPROVALS = Ordering.natural()
+      .onResultOf(new Function<PatchSetApproval, Timestamp>() {
+            @Override
+            public Timestamp apply(PatchSetApproval a) {
+              return a.getGranted();
+            }
+          });
+
+  public static List<PatchSetApproval> sortApprovals(
+      Iterable<PatchSetApproval> approvals) {
+    return SORT_APPROVALS.sortedCopy(approvals);
+  }
+
   public static void ensureChangeLoaded(
       Provider<ReviewDb> db, List<ChangeData> changes) throws OrmException {
     Map<Change.Id, ChangeData> missing = Maps.newHashMap();
@@ -79,7 +99,9 @@
       for (PatchSet ps : db.get().patchSets().get(missing.keySet())) {
         ChangeData cd = missing.get(ps.getId());
         cd.currentPatchSet = ps;
-        cd.patches = Lists.newArrayList(ps);
+        if (cd.limitedIds == null) {
+          cd.patches = Lists.newArrayList(ps);
+        }
       }
     }
   }
@@ -88,7 +110,7 @@
       Provider<ReviewDb> db, List<ChangeData> changes) throws OrmException {
     List<ResultSet<PatchSetApproval>> pending = Lists.newArrayList();
     for (ChangeData cd : changes) {
-      if (cd.currentApprovals == null && cd.approvals == null) {
+      if (cd.currentApprovals == null && cd.limitedApprovals == null) {
         pending.add(db.get().patchSetApprovals()
             .byPatchSet(cd.change(db).currentPatchSetId()));
       }
@@ -96,8 +118,8 @@
     if (!pending.isEmpty()) {
       int idx = 0;
       for (ChangeData cd : changes) {
-        if (cd.currentApprovals == null && cd.approvals == null) {
-          cd.currentApprovals = pending.get(idx++).toList();
+        if (cd.currentApprovals == null && cd.limitedApprovals == null) {
+          cd.currentApprovals = sortApprovals(pending.get(idx++));
         }
       }
     }
@@ -107,16 +129,18 @@
   private Change change;
   private String commitMessage;
   private PatchSet currentPatchSet;
+  private Set<PatchSet.Id> limitedIds;
   private Collection<PatchSet> patches;
-  private Collection<PatchSetApproval> approvals;
-  private Map<PatchSet.Id,Collection<PatchSetApproval>> approvalsMap;
-  private Collection<PatchSetApproval> currentApprovals;
+  private ListMultimap<PatchSet.Id, PatchSetApproval> limitedApprovals;
+  private ListMultimap<PatchSet.Id, PatchSetApproval> allApprovals;
+  private List<PatchSetApproval> currentApprovals;
   private String[] currentFiles;
   private Collection<PatchLineComment> comments;
   private Collection<TrackingId> trackingIds;
   private CurrentUser visibleTo;
   private ChangeControl changeControl;
   private List<ChangeMessage> messages;
+  private List<SubmitRecord> submitRecords;
 
   public ChangeData(final Change.Id id) {
     legacyId = id;
@@ -127,6 +151,27 @@
     change = c;
   }
 
+  public ChangeData(final ChangeControl c) {
+    legacyId = c.getChange().getId();
+    change = c.getChange();
+    changeControl = c;
+  }
+
+  public void limitToPatchSets(Collection<PatchSet.Id> ids) {
+    limitedIds = Sets.newLinkedHashSetWithExpectedSize(ids.size());
+    for (PatchSet.Id id : ids) {
+      if (!id.getParentKey().equals(legacyId)) {
+        throw new IllegalArgumentException(String.format(
+            "invalid patch set %s for change %s", id, legacyId));
+      }
+      limitedIds.add(id);
+    }
+  }
+
+  public Collection<PatchSet.Id> getLimitedPatchSets() {
+    return limitedIds;
+  }
+
   public void setCurrentFilePaths(String[] filePaths) {
     currentFiles = filePaths;
   }
@@ -163,10 +208,14 @@
           case COPIED:
             r.add(e.getNewName());
             break;
+
           case RENAMED:
             r.add(e.getOldName());
             r.add(e.getNewName());
             break;
+
+          case REWRITE:
+            break;
         }
       }
       currentFiles = r.toArray(new String[r.size()]);
@@ -191,7 +240,7 @@
     return visibleTo == user;
   }
 
-  ChangeControl changeControl() {
+  public ChangeControl changeControl() {
     return changeControl;
   }
 
@@ -223,22 +272,20 @@
     return currentPatchSet;
   }
 
-  public Collection<PatchSetApproval> currentApprovals(Provider<ReviewDb> db)
+  public List<PatchSetApproval> currentApprovals(Provider<ReviewDb> db)
       throws OrmException {
     if (currentApprovals == null) {
       Change c = change(db);
       if (c == null) {
         currentApprovals = Collections.emptyList();
-      } else if (approvals != null) {
-        Map<Id, Collection<PatchSetApproval>> map = approvalsMap(db);
-        currentApprovals = map.get(c.currentPatchSetId());
-        if (currentApprovals == null) {
-          currentApprovals = Collections.emptyList();
-          map.put(c.currentPatchSetId(), currentApprovals);
-        }
+      } else if (allApprovals != null) {
+        return allApprovals.get(c.currentPatchSetId());
+      } else if (limitedApprovals != null &&
+          (limitedIds == null || limitedIds.contains(c.currentPatchSetId()))) {
+        return limitedApprovals.get(c.currentPatchSetId());
       } else {
-        currentApprovals = db.get().patchSetApprovals()
-            .byPatchSet(c.currentPatchSetId()).toList();
+        currentApprovals = sortApprovals(db.get().patchSetApprovals()
+            .byPatchSet(c.currentPatchSetId()));
       }
     }
     return currentApprovals;
@@ -266,37 +313,98 @@
     return commitMessage;
   }
 
+  /**
+   * @param db review database.
+   * @return patches for the change. If {@link #limitToPatchSets(Collection)}
+   *     was previously called, only contains patches with the specified IDs.
+   * @throws OrmException an error occurred reading the database.
+   */
   public Collection<PatchSet> patches(Provider<ReviewDb> db)
       throws OrmException {
     if (patches == null) {
-      patches = db.get().patchSets().byChange(legacyId).toList();
+      if (limitedIds != null) {
+        patches = Lists.newArrayList();
+        for (PatchSet ps : db.get().patchSets().byChange(legacyId)) {
+          if (limitedIds.contains(ps.getId())) {
+            patches.add(ps);
+          }
+        }
+      } else {
+        patches = db.get().patchSets().byChange(legacyId).toList();
+      }
     }
     return patches;
   }
 
-  public Collection<PatchSetApproval> approvals(Provider<ReviewDb> db)
+  /**
+   * @param db review database.
+   * @return patch set approvals for the change in timestamp order. If
+   *     {@link #limitToPatchSets(Collection)} was previously called, only contains
+   *     approvals for the patches with the specified IDs.
+   * @throws OrmException an error occurred reading the database.
+   */
+  public List<PatchSetApproval> approvals(Provider<ReviewDb> db)
       throws OrmException {
-    if (approvals == null) {
-      approvals = db.get().patchSetApprovals().byChange(legacyId).toList();
-    }
-    return approvals;
+    return ImmutableList.copyOf(approvalsMap(db).values());
   }
 
-  public Map<PatchSet.Id,Collection<PatchSetApproval>> approvalsMap(Provider<ReviewDb> db)
-      throws OrmException {
-    if (approvalsMap == null) {
-      Collection<PatchSetApproval> all = approvals(db);
-      approvalsMap = new HashMap<PatchSet.Id,Collection<PatchSetApproval>>(all.size());
-      for (PatchSetApproval psa : all) {
-        Collection<PatchSetApproval> c = approvalsMap.get(psa.getPatchSetId());
-        if (c == null) {
-          c = new ArrayList<PatchSetApproval>();
-          approvalsMap.put(psa.getPatchSetId(), c);
+  /**
+   * @param db review database.
+   * @return patch set approvals for the change, keyed by ID, ordered by
+   *     timestamp within each patch set. If
+   *     {@link #limitToPatchSets(Collection)} was previously called, only
+   *     contains approvals for the patches with the specified IDs.
+   * @throws OrmException an error occurred reading the database.
+   */
+  public ListMultimap<PatchSet.Id, PatchSetApproval> approvalsMap(
+      Provider<ReviewDb> db) throws OrmException {
+    if (limitedApprovals == null) {
+      limitedApprovals = ArrayListMultimap.create();
+      if (allApprovals != null) {
+        for (PatchSet.Id id : limitedIds) {
+          limitedApprovals.putAll(id, allApprovals.get(id));
         }
-        c.add(psa);
+      } else {
+        for (PatchSetApproval psa : sortApprovals(
+            db.get().patchSetApprovals().byChange(legacyId))) {
+          if (limitedIds == null || limitedIds.contains(legacyId)) {
+            limitedApprovals.put(psa.getPatchSetId(), psa);
+          }
+        }
       }
     }
-    return approvalsMap;
+    return limitedApprovals;
+  }
+
+  /**
+   * @param db review database.
+   * @return all patch set approvals for the change in timestamp order
+   *     (regardless of whether {@link #limitToPatchSets(Collection)} was
+   *     previously called).
+   * @throws OrmException an error occurred reading the database.
+   */
+  public List<PatchSetApproval> allApprovals(Provider<ReviewDb> db)
+      throws OrmException {
+    return ImmutableList.copyOf(allApprovalsMap(db).values());
+  }
+
+  /**
+   * @param db review database.
+   * @return all patch set approvals for the change (regardless of whether
+   *     {@link #limitToPatchSets(Collection)} was previously called), keyed by
+   *     ID, ordered by timestamp within each patch set.
+   * @throws OrmException an error occurred reading the database.
+   */
+  public ListMultimap<PatchSet.Id, PatchSetApproval> allApprovalsMap(
+      Provider<ReviewDb> db) throws OrmException {
+    if (allApprovals == null) {
+      allApprovals = ArrayListMultimap.create();
+      for (PatchSetApproval psa : sortApprovals(
+          db.get().patchSetApprovals().byChange(legacyId))) {
+        allApprovals.put(psa.getPatchSetId(), psa);
+      }
+    }
+    return allApprovals;
   }
 
   public Collection<PatchLineComment> comments(Provider<ReviewDb> db)
@@ -322,4 +430,12 @@
     }
     return messages;
   }
+
+  public void setSubmitRecords(List<SubmitRecord> records) {
+    submitRecords = records;
+  }
+
+  public List<SubmitRecord> getSubmitRecords() {
+    return submitRecords;
+  }
 }
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 e80ad67..7bbb073 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,7 +15,6 @@
 package com.google.gerrit.server.query.change;
 
 import com.google.common.collect.Lists;
-import com.google.gerrit.common.data.ApprovalTypes;
 import com.google.gerrit.common.data.GroupReference;
 import com.google.gerrit.reviewdb.client.Account;
 import com.google.gerrit.reviewdb.client.AccountGroup;
@@ -107,11 +106,9 @@
     final Provider<ChangeQueryRewriter> rewriter;
     final IdentifiedUser.GenericFactory userFactory;
     final CapabilityControl.Factory capabilityControlFactory;
-    final ChangeControl.Factory changeControlFactory;
     final ChangeControl.GenericFactory changeControlGenericFactory;
     final AccountResolver accountResolver;
     final GroupBackend groupBackend;
-    final ApprovalTypes approvalTypes;
     final AllProjectsName allProjectsName;
     final PatchListCache patchListCache;
     final GitRepositoryManager repoManager;
@@ -122,11 +119,9 @@
         Provider<ChangeQueryRewriter> rewriter,
         IdentifiedUser.GenericFactory userFactory,
         CapabilityControl.Factory capabilityControlFactory,
-        ChangeControl.Factory changeControlFactory,
         ChangeControl.GenericFactory changeControlGenericFactory,
         AccountResolver accountResolver,
         GroupBackend groupBackend,
-        ApprovalTypes approvalTypes,
         AllProjectsName allProjectsName,
         PatchListCache patchListCache,
         GitRepositoryManager repoManager,
@@ -135,11 +130,9 @@
       this.rewriter = rewriter;
       this.userFactory = userFactory;
       this.capabilityControlFactory = capabilityControlFactory;
-      this.changeControlFactory = changeControlFactory;
       this.changeControlGenericFactory = changeControlGenericFactory;
       this.accountResolver = accountResolver;
       this.groupBackend = groupBackend;
-      this.approvalTypes = approvalTypes;
       this.allProjectsName = allProjectsName;
       this.patchListCache = patchListCache;
       this.repoManager = repoManager;
@@ -304,8 +297,9 @@
 
   @Operator
   public Predicate<ChangeData> label(String name) {
-    return new LabelPredicate(args.changeControlGenericFactory,
-        args.userFactory, args.dbProvider, args.approvalTypes, name);
+    return new LabelPredicate(args.projectCache,
+        args.changeControlGenericFactory, args.userFactory, args.dbProvider,
+        name);
   }
 
   @Operator
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ChangeQueryRewriter.java
index 34ec2f4..e6251bc 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
@@ -39,7 +39,7 @@
               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));
 
   private final Provider<ReviewDb> dbProvider;
@@ -52,7 +52,7 @@
 
   @Override
   public Predicate<ChangeData> and(Collection<? extends Predicate<ChangeData>> l) {
-    return hasSource(l) ? new AndSource(l) : super.and(l);
+    return hasSource(l) ? new AndSource(dbProvider, l) : super.and(l);
   }
 
   @Override
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 bf21261..936a47d 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
@@ -14,19 +14,24 @@
 
 package com.google.gerrit.server.query.change;
 
-import com.google.gerrit.common.data.ApprovalType;
-import com.google.gerrit.common.data.ApprovalTypes;
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.common.data.Permission;
-import com.google.gerrit.reviewdb.client.ApprovalCategory;
+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.IdentifiedUser;
 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.query.OperatorPredicate;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Provider;
 
+import java.util.HashSet;
+import java.util.Set;
 import java.util.regex.Matcher;
 import java.util.regex.Pattern;
 
@@ -54,34 +59,24 @@
     abstract boolean match(int psValue, int expValue);
   }
 
-  private static ApprovalCategory category(ApprovalTypes types, String toFind) {
+  private static LabelType type(LabelTypes types, String toFind) {
     if (types.byLabel(toFind) != null) {
-      return types.byLabel(toFind).getCategory();
+      return types.byLabel(toFind);
     }
 
-    if (types.byId(new ApprovalCategory.Id(toFind)) != null) {
-      return types.byId(new ApprovalCategory.Id(toFind)).getCategory();
-    }
-
-    for (ApprovalType at : types.getApprovalTypes()) {
-      ApprovalCategory category = at.getCategory();
-
-      if (toFind.equalsIgnoreCase(category.getName())) {
-        return category;
-
-      } else if (toFind.equalsIgnoreCase(category.getName().replace(" ", ""))) {
-        return category;
+    for (LabelType lt : types.getLabelTypes()) {
+      if (toFind.equalsIgnoreCase(lt.getName())) {
+        return lt;
       }
     }
 
-    for (ApprovalType at : types.getApprovalTypes()) {
-      ApprovalCategory category = at.getCategory();
-      if (toFind.equalsIgnoreCase(category.getAbbreviatedName())) {
-        return category;
+    for (LabelType lt : types.getLabelTypes()) {
+      if (toFind.equalsIgnoreCase(lt.getAbbreviatedName())) {
+        return lt;
       }
     }
 
-    return new ApprovalCategory(new ApprovalCategory.Id(toFind), toFind);
+    return LabelType.withDefaultValues(toFind);
   }
 
   private static Test op(String op) {
@@ -106,72 +101,108 @@
     return Integer.parseInt(value);
   }
 
+  private final ProjectCache projectCache;
   private final ChangeControl.GenericFactory ccFactory;
   private final IdentifiedUser.GenericFactory userFactory;
   private final Provider<ReviewDb> dbProvider;
   private final Test test;
-  private final ApprovalCategory category;
-  private final String permissionName;
+  private final String type;
   private final int expVal;
 
-  LabelPredicate(ChangeControl.GenericFactory ccFactory,
-      IdentifiedUser.GenericFactory userFactory, Provider<ReviewDb> dbProvider,
-      ApprovalTypes types, String value) {
+  LabelPredicate(ProjectCache projectCache,
+      ChangeControl.GenericFactory ccFactory,
+      IdentifiedUser.GenericFactory userFactory,
+      Provider<ReviewDb> dbProvider,
+      String value) {
     super(ChangeQueryBuilder.FIELD_LABEL, value);
     this.ccFactory = ccFactory;
+    this.projectCache = projectCache;
     this.userFactory = userFactory;
     this.dbProvider = dbProvider;
 
     Matcher m1 = Pattern.compile("(=|>=|<=)([+-]?\\d+)$").matcher(value);
     Matcher m2 = Pattern.compile("([+-]\\d+)$").matcher(value);
     if (m1.find()) {
-      category = category(types, value.substring(0, m1.start()));
+      type = value.substring(0, m1.start());
       test = op(m1.group(1));
       expVal = value(m1.group(2));
 
     } else if (m2.find()) {
-      category = category(types, value.substring(0, m2.start()));
+      type = value.substring(0, m2.start());
       test = Test.EQ;
       expVal = value(m2.group(1));
 
     } else {
-      category = category(types, value);
+      type = value;
       test = Test.EQ;
       expVal = 1;
     }
-
-    this.permissionName = Permission.forLabel(category.getLabelName());
   }
 
   @Override
   public boolean match(final ChangeData object) throws OrmException {
+    final Change c = object.change(dbProvider);
+    if (c == null) {
+      // The change has disappeared.
+      //
+      return false;
+    }
+    final ProjectState project = projectCache.get(c.getDest().getParentKey());
+    if (project == null) {
+      // The project has disappeared.
+      //
+      return false;
+    }
+    final LabelType labelType = type(project.getLabelTypes(), type);
+    final Set<Account.Id> allApprovers = new HashSet<Account.Id>();
+    final Set<Account.Id> approversThatVotedInCategory = new HashSet<Account.Id>();
     for (PatchSetApproval p : object.currentApprovals(dbProvider)) {
-      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(dbProvider.get())) {
-              // The user can't see the change anymore.
-              //
-              continue;
-            }
-            psVal = cc.getRange(permissionName).squash(psVal);
-          } catch (NoSuchChangeException e) {
-            // The project has disappeared.
-            //
-            continue;
-          }
-
-          if (test.match(psVal, expVal)) {
-            return true;
-          }
+      allApprovers.add(p.getAccountId());
+      if (labelType.matches(p)) {
+        approversThatVotedInCategory.add(p.getAccountId());
+        if (match(c, p.getValue(), p.getAccountId(), labelType)) {
+          return true;
         }
       }
     }
+
+    final Set<Account.Id> approversThatDidNotVoteInCategory = new HashSet<Account.Id>(allApprovers);
+    approversThatDidNotVoteInCategory.removeAll(approversThatVotedInCategory);
+    for (Account.Id a : approversThatDidNotVoteInCategory) {
+      if (match(c, 0, a, labelType)) {
+        return true;
+      }
+    }
+
+    return false;
+  }
+
+  private boolean match(final Change change, final int value,
+      final Account.Id approver, final LabelType type)
+      throws OrmException {
+    int psVal = value;
+    if (test.match(psVal, expVal)) {
+      // Double check the value is still permitted for the user.
+      //
+      try {
+        ChangeControl cc = ccFactory.controlFor(change, //
+            userFactory.create(dbProvider, approver));
+        if (!cc.isVisible(dbProvider.get())) {
+          // The user can't see the change anymore.
+          //
+          return false;
+        }
+        psVal = cc.getRange(Permission.forLabel(type.getName())).squash(psVal);
+      } catch (NoSuchChangeException e) {
+        // The project has disappeared.
+        //
+        return false;
+      }
+
+      if (test.match(psVal, expVal)) {
+        return true;
+      }
+    }
     return false;
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ListChanges.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ListChanges.java
deleted file mode 100644
index f0ed66a..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/ListChanges.java
+++ /dev/null
@@ -1,672 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.server.query.change;
-
-import static com.google.gerrit.common.changes.ListChangesOption.ALL_COMMITS;
-import static com.google.gerrit.common.changes.ListChangesOption.ALL_FILES;
-import static com.google.gerrit.common.changes.ListChangesOption.ALL_REVISIONS;
-import static com.google.gerrit.common.changes.ListChangesOption.CURRENT_COMMIT;
-import static com.google.gerrit.common.changes.ListChangesOption.CURRENT_FILES;
-import static com.google.gerrit.common.changes.ListChangesOption.CURRENT_REVISION;
-import static com.google.gerrit.common.changes.ListChangesOption.LABELS;
-
-import com.google.common.base.Strings;
-import com.google.common.collect.Lists;
-import com.google.common.collect.Maps;
-import com.google.gerrit.common.changes.ListChangesOption;
-import com.google.gerrit.common.data.ApprovalType;
-import com.google.gerrit.common.data.ApprovalTypes;
-import com.google.gerrit.common.data.SubmitRecord;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.ChangeMessage;
-import com.google.gerrit.reviewdb.client.Patch;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
-import com.google.gerrit.reviewdb.client.PatchSetInfo;
-import com.google.gerrit.reviewdb.client.PatchSetInfo.ParentInfo;
-import com.google.gerrit.reviewdb.client.UserIdentity;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.server.AnonymousUser;
-import com.google.gerrit.server.CurrentUser;
-import com.google.gerrit.server.IdentifiedUser;
-import com.google.gerrit.server.OutputFormat;
-import com.google.gerrit.server.config.CanonicalWebUrl;
-import com.google.gerrit.server.config.GerritServerConfig;
-import com.google.gerrit.server.events.AccountAttribute;
-import com.google.gerrit.server.patch.PatchList;
-import com.google.gerrit.server.patch.PatchListCache;
-import com.google.gerrit.server.patch.PatchListEntry;
-import com.google.gerrit.server.patch.PatchListNotAvailableException;
-import com.google.gerrit.server.patch.PatchSetInfoFactory;
-import com.google.gerrit.server.patch.PatchSetInfoNotAvailableException;
-import com.google.gerrit.server.project.ChangeControl;
-import com.google.gerrit.server.project.NoSuchChangeException;
-import com.google.gerrit.server.query.QueryParseException;
-import com.google.gerrit.server.ssh.SshInfo;
-import com.google.gson.reflect.TypeToken;
-import com.google.gwtorm.server.OrmException;
-import com.google.inject.Inject;
-import com.google.inject.Provider;
-import com.google.inject.Singleton;
-
-import com.jcraft.jsch.HostKey;
-
-import org.eclipse.jgit.lib.Config;
-import org.kohsuke.args4j.Option;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.IOException;
-import java.io.Writer;
-import java.sql.Timestamp;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.Comparator;
-import java.util.EnumSet;
-import java.util.List;
-import java.util.Map;
-
-public class ListChanges {
-  private static final Logger log = LoggerFactory.getLogger(ListChanges.class);
-
-  @Singleton
-  static class Urls {
-    final String git;
-    final String http;
-
-    @Inject
-    Urls(@GerritServerConfig Config cfg) {
-      this.git = ensureSlash(cfg.getString("gerrit", null, "canonicalGitUrl"));
-      this.http = ensureSlash(cfg.getString("gerrit", null, "gitHttpUrl"));
-    }
-
-    private static String ensureSlash(String in) {
-      if (in != null && !in.endsWith("/")) {
-        return in + "/";
-      }
-      return in;
-    }
-  }
-
-  private final QueryProcessor imp;
-  private final Provider<ReviewDb> db;
-  private final ApprovalTypes approvalTypes;
-  private final CurrentUser user;
-  private final AnonymousUser anonymous;
-  private final ChangeControl.Factory changeControlFactory;
-  private final PatchSetInfoFactory patchSetInfoFactory;
-  private final PatchListCache patchListCache;
-  private final SshInfo sshInfo;
-  private final Provider<String> urlProvider;
-  private final Urls urls;
-  private boolean reverse;
-  private Map<Account.Id, AccountAttribute> accounts;
-  private Map<Change.Id, ChangeControl> controls;
-  private EnumSet<ListChangesOption> options;
-
-  @Option(name = "--format", metaVar = "FMT", usage = "Output display format")
-  private OutputFormat format = OutputFormat.TEXT;
-
-  @Option(name = "--query", aliases = {"-q"}, metaVar = "QUERY", multiValued = true, usage = "Query string")
-  private List<String> queries;
-
-  @Option(name = "--limit", aliases = {"-n"}, metaVar = "CNT", usage = "Maximum number of results to return")
-  public void setLimit(int limit) {
-    imp.setLimit(limit);
-  }
-
-  @Option(name = "-o", multiValued = true, usage = "Output options per change")
-  public void addOption(ListChangesOption o) {
-    options.add(o);
-  }
-
-  @Option(name = "-O", usage = "Output option flags, in hex")
-  void setOptionFlagsHex(String hex) {
-    options.addAll(ListChangesOption.fromBits(Integer.parseInt(hex, 16)));
-  }
-
-  @Option(name = "-P", metaVar = "SORTKEY", usage = "Previous changes before SORTKEY")
-  public void setSortKeyAfter(String key) {
-    // Querying for the prior page of changes requires sortkey_after predicate.
-    // Changes are shown most recent->least recent. The previous page of
-    // results contains changes that were updated after the given key.
-    imp.setSortkeyAfter(key);
-    reverse = true;
-  }
-
-  @Option(name = "-N", metaVar = "SORTKEY", usage = "Next changes after SORTKEY")
-  public void setSortKeyBefore(String key) {
-    // Querying for the next page of changes requires sortkey_before predicate.
-    // Changes are shown most recent->least recent. The next page contains
-    // changes that were updated before the given key.
-    imp.setSortkeyBefore(key);
-  }
-
-  @Inject
-  ListChanges(QueryProcessor qp,
-      Provider<ReviewDb> db,
-      ApprovalTypes at,
-      CurrentUser u,
-      AnonymousUser au,
-      ChangeControl.Factory cf,
-      PatchSetInfoFactory psi,
-      PatchListCache plc,
-      SshInfo sshInfo,
-      @CanonicalWebUrl Provider<String> curl,
-      Urls urls) {
-    this.imp = qp;
-    this.db = db;
-    this.approvalTypes = at;
-    this.user = u;
-    this.anonymous = au;
-    this.changeControlFactory = cf;
-    this.patchSetInfoFactory = psi;
-    this.patchListCache = plc;
-    this.sshInfo = sshInfo;
-    this.urlProvider = curl;
-    this.urls = urls;
-
-    accounts = Maps.newHashMap();
-    controls = Maps.newHashMap();
-    options = EnumSet.noneOf(ListChangesOption.class);
-  }
-
-  public OutputFormat getFormat() {
-    return format;
-  }
-
-  public ListChanges setFormat(OutputFormat fmt) {
-    this.format = fmt;
-    return this;
-  }
-
-  public ListChanges addQuery(String query) {
-    if (queries == null) {
-      queries = Lists.newArrayList();
-    }
-    queries.add(query);
-    return this;
-  }
-
-  public void query(Writer out)
-      throws OrmException, QueryParseException, IOException {
-    if (imp.isDisabled()) {
-      throw new QueryParseException("query disabled");
-    }
-    if (queries == null || queries.isEmpty()) {
-      queries = Collections.singletonList("status:open");
-    } else if (queries.size() > 10) {
-      // Hard-code a default maximum number of queries to prevent
-      // users from submitting too much to the server in a single call.
-      throw new QueryParseException("limit of 10 queries");
-    }
-
-    List<List<ChangeInfo>> res = Lists.newArrayListWithCapacity(queries.size());
-    for (String query : queries) {
-      List<ChangeData> changes = imp.queryChanges(query);
-      boolean moreChanges = imp.getLimit() > 0 && changes.size() > imp.getLimit();
-      if (moreChanges) {
-        if (reverse) {
-          changes = changes.subList(1, changes.size());
-        } else {
-          changes = changes.subList(0, imp.getLimit());
-        }
-      }
-      ChangeData.ensureChangeLoaded(db, changes);
-      ChangeData.ensureCurrentPatchSetLoaded(db, changes);
-      ChangeData.ensureCurrentApprovalsLoaded(db, changes);
-
-      List<ChangeInfo> info = Lists.newArrayListWithCapacity(changes.size());
-      for (ChangeData cd : changes) {
-        info.add(toChangeInfo(cd));
-      }
-      if (moreChanges && !info.isEmpty()) {
-        if (reverse) {
-          info.get(0)._moreChanges = true;
-        } else {
-          info.get(info.size() - 1)._moreChanges = true;
-        }
-      }
-      res.add(info);
-    }
-
-    if (!accounts.isEmpty()) {
-      for (Account account : db.get().accounts().get(accounts.keySet())) {
-        AccountAttribute a = accounts.get(account.getId());
-        a.name = Strings.emptyToNull(account.getFullName());
-      }
-    }
-
-    if (format.isJson()) {
-      format.newGson().toJson(
-          res.size() == 1 ? res.get(0) : res,
-          new TypeToken<List<ChangeInfo>>() {}.getType(),
-          out);
-      out.write('\n');
-    } else {
-      boolean firstQuery = true;
-      for (List<ChangeInfo> info : res) {
-        if (firstQuery) {
-          firstQuery = false;
-        } else {
-          out.write('\n');
-        }
-        for (ChangeInfo c : info) {
-          String id = new Change.Key(c.id).abbreviate();
-          String subject = c.subject;
-          if (subject.length() + id.length() > 80) {
-            subject = subject.substring(0, 80 - id.length());
-          }
-          out.write(id);
-          out.write(' ');
-          out.write(subject.replace('\n', ' '));
-          out.write('\n');
-        }
-      }
-    }
-  }
-
-  private ChangeInfo toChangeInfo(ChangeData cd) throws OrmException {
-    ChangeInfo out = new ChangeInfo();
-    Change in = cd.change(db);
-    out.project = in.getProject().get();
-    out.branch = in.getDest().getShortName();
-    out.topic = in.getTopic();
-    out.id = in.getKey().get();
-    out.subject = in.getSubject();
-    out.status = in.getStatus();
-    out.owner = asAccountAttribute(in.getOwner());
-    out.created = in.getCreatedOn();
-    out.updated = in.getLastUpdatedOn();
-    out._number = in.getId().get();
-    out._sortkey = in.getSortKey();
-    out.starred = user.getStarredChanges().contains(in.getId()) ? true : null;
-    out.reviewed = in.getStatus().isOpen() && isChangeReviewed(cd) ? true : null;
-    out.labels = options.contains(LABELS) ? labelsFor(cd) : null;
-
-    if (options.contains(ALL_REVISIONS) || options.contains(CURRENT_REVISION)) {
-      out.revisions = revisions(cd);
-      for (String commit : out.revisions.keySet()) {
-        if (out.revisions.get(commit).isCurrent) {
-          out.current_revision = commit;
-          break;
-        }
-      }
-    }
-
-    return out;
-  }
-
-  private AccountAttribute asAccountAttribute(Account.Id user) {
-    if (user == null) {
-      return null;
-    }
-    AccountAttribute a = accounts.get(user);
-    if (a == null) {
-      a = new AccountAttribute();
-      accounts.put(user, a);
-    }
-    return a;
-  }
-
-  private ChangeControl control(ChangeData cd) throws OrmException {
-    ChangeControl ctrl = cd.changeControl();
-    if (ctrl != null && ctrl.getCurrentUser() == user) {
-      return ctrl;
-    }
-
-    ctrl = controls.get(cd.getId());
-    if (ctrl != null) {
-      return ctrl;
-    }
-
-    try {
-      ctrl = changeControlFactory.controlFor(cd.change(db));
-    } catch (NoSuchChangeException e) {
-      return null;
-    }
-    controls.put(cd.getId(), ctrl);
-    return ctrl;
-  }
-
-  private Map<String, LabelInfo> labelsFor(ChangeData cd) throws OrmException {
-    ChangeControl ctl = control(cd);
-    if (ctl == null) {
-      return Collections.emptyMap();
-    }
-
-    PatchSet ps = cd.currentPatchSet(db);
-    if (ps == null) {
-      return Collections.emptyMap();
-    }
-
-    Map<String, LabelInfo> labels = Maps.newLinkedHashMap();
-    for (SubmitRecord rec : ctl.canSubmit(db.get(), ps, cd, true, false)) {
-      if (rec.labels == null) {
-        continue;
-      }
-      for (SubmitRecord.Label r : rec.labels) {
-        LabelInfo p = labels.get(r.label);
-        if (p == null || p._status.compareTo(r.status) < 0) {
-          LabelInfo n = new LabelInfo();
-          n._status = r.status;
-          switch (r.status) {
-            case OK:
-              n.approved = asAccountAttribute(r.appliedBy);
-              break;
-            case REJECT:
-              n.rejected = asAccountAttribute(r.appliedBy);
-              break;
-          }
-          n.optional = n._status == SubmitRecord.Label.Status.MAY ? true : null;
-          labels.put(r.label, n);
-        }
-      }
-    }
-
-    Collection<PatchSetApproval> approvals = null;
-    for (Map.Entry<String, LabelInfo> e : labels.entrySet()) {
-      if (e.getValue().approved != null || e.getValue().rejected != null) {
-        continue;
-      }
-
-      ApprovalType type = approvalTypes.byLabel(e.getKey());
-      if (type == null || type.getMin() == null || type.getMax() == null) {
-        // Unknown or misconfigured type can't have intermediate scores.
-        continue;
-      }
-
-      short min = type.getMin().getValue();
-      short max = type.getMax().getValue();
-      if (-1 <= min && max <= 1) {
-        // Types with a range of -1..+1 can't have intermediate scores.
-        continue;
-      }
-
-      if (approvals == null) {
-        approvals = cd.currentApprovals(db);
-      }
-      for (PatchSetApproval psa : approvals) {
-        short val = psa.getValue();
-        if (val != 0 && min < val && val < max
-            && psa.getCategoryId().equals(type.getCategory().getId())) {
-          if (0 < val) {
-            e.getValue().recommended = asAccountAttribute(psa.getAccountId());
-            e.getValue().value = val != 1 ? val : null;
-          } else {
-            e.getValue().disliked = asAccountAttribute(psa.getAccountId());
-            e.getValue().value = val != -1 ? val : null;
-          }
-        }
-      }
-    }
-    return labels;
-  }
-
-  private boolean isChangeReviewed(ChangeData cd) throws OrmException {
-    if (user instanceof IdentifiedUser) {
-      PatchSet currentPatchSet = cd.currentPatchSet(db);
-      if (currentPatchSet == null) {
-        return false;
-      }
-
-      List<ChangeMessage> messages =
-          db.get().changeMessages().byPatchSet(currentPatchSet.getId()).toList();
-
-      if (messages.isEmpty()) {
-        return false;
-      }
-
-      // Sort messages to let the most recent ones at the beginning.
-      Collections.sort(messages, new Comparator<ChangeMessage>() {
-        @Override
-        public int compare(ChangeMessage a, ChangeMessage b) {
-          return b.getWrittenOn().compareTo(a.getWrittenOn());
-        }
-      });
-
-      Account.Id currentUserId = ((IdentifiedUser) user).getAccountId();
-      Account.Id changeOwnerId = cd.change(db).getOwner();
-      for (ChangeMessage cm : messages) {
-        if (currentUserId.equals(cm.getAuthor())) {
-          return true;
-        } else if (changeOwnerId.equals(cm.getAuthor())) {
-          return false;
-        }
-      }
-    }
-    return false;
-  }
-
-  private Map<String, RevisionInfo> revisions(ChangeData cd) throws OrmException {
-    ChangeControl ctl = control(cd);
-    if (ctl == null) {
-      return Collections.emptyMap();
-    }
-
-    Collection<PatchSet> src;
-    if (options.contains(ALL_REVISIONS)) {
-      src = cd.patches(db);
-    } else {
-      src = Collections.singletonList(cd.currentPatchSet(db));
-    }
-    Map<String, RevisionInfo> res = Maps.newLinkedHashMap();
-    for (PatchSet in : src) {
-      if (ctl.isPatchVisible(in, db.get())) {
-        res.put(in.getRevision().get(), toRevisionInfo(cd, in));
-      }
-    }
-    return res;
-  }
-
-  private RevisionInfo toRevisionInfo(ChangeData cd, PatchSet in)
-      throws OrmException {
-    RevisionInfo out = new RevisionInfo();
-    out.isCurrent = in.getId().equals(cd.change(db).currentPatchSetId());
-    out._number = in.getId().get();
-    out.draft = in.isDraft() ? true : null;
-    out.fetch = makeFetchMap(cd, in);
-
-    if (options.contains(ALL_COMMITS)
-        || (out.isCurrent && options.contains(CURRENT_COMMIT))) {
-      try {
-        PatchSetInfo info = patchSetInfoFactory.get(db.get(), in.getId());
-        out.commit = new CommitInfo();
-        out.commit.parents = Lists.newArrayListWithCapacity(info.getParents().size());
-        out.commit.author = toGitPerson(info.getAuthor());
-        out.commit.committer = toGitPerson(info.getCommitter());
-        out.commit.subject = info.getSubject();
-        out.commit.message = info.getMessage();
-
-        for (ParentInfo parent : info.getParents()) {
-          CommitInfo i = new CommitInfo();
-          i.commit = parent.id.get();
-          i.subject = parent.shortMessage;
-          out.commit.parents.add(i);
-        }
-      } catch (PatchSetInfoNotAvailableException e) {
-        log.warn("Cannot load PatchSetInfo " + in.getId(), e);
-      }
-    }
-
-    if (options.contains(ALL_FILES)
-        || (out.isCurrent && options.contains(CURRENT_FILES))) {
-      PatchList list;
-      try {
-        list = patchListCache.get(cd.change(db), in);
-      } catch (PatchListNotAvailableException e) {
-        log.warn("Cannot load PatchList " + in.getId(), e);
-        list = null;
-      }
-      if (list != null) {
-        out.files = Maps.newTreeMap();
-        for (PatchListEntry e : list.getPatches()) {
-          if (Patch.COMMIT_MSG.equals(e.getNewName())) {
-            continue;
-          }
-
-          FileInfo d = new FileInfo();
-          d.status = e.getChangeType() != Patch.ChangeType.MODIFIED
-              ? e.getChangeType().getCode()
-              : null;
-          d.oldPath = e.getOldName();
-          if (e.getPatchType() == Patch.PatchType.BINARY) {
-            d.binary = true;
-          } else {
-            d.linesInserted = e.getInsertions() > 0 ? e.getInsertions() : null;
-            d.linesDeleted = e.getDeletions() > 0 ? e.getDeletions() : null;
-          }
-
-          FileInfo o = out.files.put(e.getNewName(), d);
-          if (o != null) {
-            // This should only happen on a delete-add break created by JGit
-            // when the file was rewritten and too little content survived. Write
-            // a single record with data from both sides.
-            d.status = Patch.ChangeType.REWRITE.getCode();
-            if (o.binary != null && o.binary) {
-              d.binary = true;
-            }
-            if (o.linesInserted != null) {
-              d.linesInserted = o.linesInserted;
-            }
-            if (o.linesDeleted != null) {
-              d.linesDeleted = o.linesDeleted;
-            }
-          }
-        }
-      }
-    }
-    return out;
-  }
-
-  private Map<String, FetchInfo> makeFetchMap(ChangeData cd, PatchSet in)
-      throws OrmException {
-    Map<String, FetchInfo> r = Maps.newLinkedHashMap();
-    String refName = in.getRefName();
-    ChangeControl ctl = control(cd);
-    if (ctl != null && ctl.forUser(anonymous).isPatchVisible(in, db.get())) {
-      if (urls.git != null) {
-        r.put("git", new FetchInfo(urls.git
-            + cd.change(db).getProject().get(), refName));
-      }
-    }
-    if (urls.http != null) {
-      r.put("http", new FetchInfo(urls.http
-          + cd.change(db).getProject().get(), refName));
-    } else {
-      String http = urlProvider.get();
-      if (!Strings.isNullOrEmpty(http)) {
-        r.put("http", new FetchInfo(http
-            + cd.change(db).getProject().get(), refName));
-      }
-    }
-    if (!sshInfo.getHostKeys().isEmpty()) {
-      HostKey host = sshInfo.getHostKeys().get(0);
-      r.put("ssh", new FetchInfo(String.format(
-          "ssh://%s/%s",
-          host.getHost(), cd.change(db).getProject().get()),
-          refName));
-    }
-
-    return r;
-  }
-
-  private static GitPerson toGitPerson(UserIdentity committer) {
-    GitPerson p = new GitPerson();
-    p.name = committer.getName();
-    p.email = committer.getEmail();
-    p.date = committer.getDate();
-    p.tz = committer.getTimeZone();
-    return p;
-  }
-
-  static class ChangeInfo {
-    String project;
-    String branch;
-    String topic;
-    String id;
-    String subject;
-    Change.Status status;
-    Timestamp created;
-    Timestamp updated;
-    Boolean starred;
-    Boolean reviewed;
-
-    String _sortkey;
-    int _number;
-
-    AccountAttribute owner;
-    Map<String, LabelInfo> labels;
-    String current_revision;
-    Map<String, RevisionInfo> revisions;
-
-    Boolean _moreChanges;
-  }
-
-  static class RevisionInfo {
-    private transient boolean isCurrent;
-    Boolean draft;
-    int _number;
-    Map<String, FetchInfo> fetch;
-    CommitInfo commit;
-    Map<String, FileInfo> files;
-  }
-
-  static class FetchInfo {
-    String url;
-    String ref;
-
-    FetchInfo(String url, String ref) {
-      this.url = url;
-      this.ref = ref;
-    }
-  }
-
-  static class GitPerson {
-    String name;
-    String email;
-    Timestamp date;
-    int tz;
-  }
-
-  static class CommitInfo {
-    String commit;
-    List<CommitInfo> parents;
-    GitPerson author;
-    GitPerson committer;
-    String subject;
-    String message;
-  }
-
-  static class FileInfo {
-    Character status;
-    Boolean binary;
-    String oldPath;
-    Integer linesInserted;
-    Integer linesDeleted;
-  }
-
-  static class LabelInfo {
-    transient SubmitRecord.Label.Status _status;
-    AccountAttribute approved;
-    AccountAttribute rejected;
-
-    AccountAttribute recommended;
-    AccountAttribute disliked;
-    Short value;
-    Boolean optional;
-  }
-}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryChanges.java b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryChanges.java
new file mode 100644
index 0000000..a005940
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryChanges.java
@@ -0,0 +1,163 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.query.change;
+
+import com.google.common.collect.Lists;
+import com.google.gerrit.common.changes.ListChangesOption;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.RestReadView;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.server.change.ChangeJson;
+import com.google.gerrit.server.change.ChangeJson.ChangeInfo;
+import com.google.gerrit.server.project.ChangeControl;
+import com.google.gerrit.server.query.QueryParseException;
+import com.google.gerrit.server.ssh.SshInfo;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+
+import org.kohsuke.args4j.Option;
+
+import java.util.BitSet;
+import java.util.Collections;
+import java.util.EnumSet;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+public class QueryChanges implements RestReadView<TopLevelResource> {
+  private final ChangeJson json;
+  private final QueryProcessor imp;
+  private boolean reverse;
+  private EnumSet<ListChangesOption> options;
+
+  @Option(name = "--query", aliases = {"-q"}, metaVar = "QUERY", multiValued = true, usage = "Query string")
+  private List<String> queries;
+
+  @Option(name = "--limit", aliases = {"-n"}, metaVar = "CNT", usage = "Maximum number of results to return")
+  public void setLimit(int limit) {
+    imp.setLimit(limit);
+  }
+
+  @Option(name = "-o", multiValued = true, usage = "Output options per change")
+  public void addOption(ListChangesOption o) {
+    options.add(o);
+  }
+
+  @Option(name = "-O", usage = "Output option flags, in hex")
+  void setOptionFlagsHex(String hex) {
+    options.addAll(ListChangesOption.fromBits(Integer.parseInt(hex, 16)));
+  }
+
+  @Option(name = "-P", metaVar = "SORTKEY", usage = "Previous changes before SORTKEY")
+  public void setSortKeyAfter(String key) {
+    // Querying for the prior page of changes requires sortkey_after predicate.
+    // Changes are shown most recent->least recent. The previous page of
+    // results contains changes that were updated after the given key.
+    imp.setSortkeyAfter(key);
+    reverse = true;
+  }
+
+  @Option(name = "-N", metaVar = "SORTKEY", usage = "Next changes after SORTKEY")
+  public void setSortKeyBefore(String key) {
+    // Querying for the next page of changes requires sortkey_before predicate.
+    // Changes are shown most recent->least recent. The next page contains
+    // changes that were updated before the given key.
+    imp.setSortkeyBefore(key);
+  }
+
+  @Inject
+  QueryChanges(ChangeJson json,
+      QueryProcessor qp,
+      SshInfo sshInfo,
+      ChangeControl.Factory cf) {
+    this.json = json;
+    this.imp = qp;
+
+    options = EnumSet.noneOf(ListChangesOption.class);
+    json.setSshInfo(sshInfo);
+    json.setChangeControlFactory(cf);
+  }
+
+  public void addQuery(String query) {
+    if (queries == null) {
+      queries = Lists.newArrayList();
+    }
+    queries.add(query);
+  }
+
+  @Override
+  public Object apply(TopLevelResource rsrc)
+      throws BadRequestException, AuthException, OrmException {
+    List<List<ChangeInfo>> out;
+    try {
+      out = query();
+    } catch (QueryParseException e) {
+      // This is a hack to detect an operator that requires authentication.
+      Pattern p = Pattern.compile("^Error in operator (.*:self)$");
+      Matcher m = p.matcher(e.getMessage());
+      if (m.matches()) {
+        String op = m.group(1);
+        throw new AuthException("Must be signed-in to use " + op);
+      }
+      throw new BadRequestException(e.getMessage());
+    }
+    return out.size() == 1 ? out.get(0) : out;
+  }
+
+  private List<List<ChangeInfo>> query()
+      throws OrmException, QueryParseException {
+    if (imp.isDisabled()) {
+      throw new QueryParseException("query disabled");
+    }
+    if (queries == null || queries.isEmpty()) {
+      queries = Collections.singletonList("status:open");
+    } else if (queries.size() > 10) {
+      // Hard-code a default maximum number of queries to prevent
+      // users from submitting too much to the server in a single call.
+      throw new QueryParseException("limit of 10 queries");
+    }
+
+    int cnt = queries.size();
+    BitSet more = new BitSet(cnt);
+    List<List<ChangeData>> data = Lists.newArrayListWithCapacity(cnt);
+    for (int n = 0; n < cnt; n++) {
+      String query = queries.get(n);
+      List<ChangeData> changes = imp.queryChanges(query);
+      if (imp.getLimit() > 0 && changes.size() > imp.getLimit()) {
+        if (reverse) {
+          changes = changes.subList(1, changes.size());
+        } else {
+          changes = changes.subList(0, imp.getLimit());
+        }
+        more.set(n, true);
+      }
+      data.add(changes);
+    }
+
+    List<List<ChangeInfo>> res = json.addOptions(options).formatList2(data);
+    for (int n = 0; n < cnt; n++) {
+      List<ChangeInfo> info = res.get(n);
+      if (more.get(n) && !info.isEmpty()) {
+        if (reverse) {
+          info.get(0)._moreChanges = true;
+        } else {
+          info.get(info.size() - 1)._moreChanges = true;
+        }
+      }
+    }
+    return res;
+  }
+}
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 f44282a..fc35df3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/query/change/QueryProcessor.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.server.query.change;
 
 import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.common.data.LabelTypes;
 import com.google.gerrit.common.data.SubmitRecord;
 import com.google.gerrit.reviewdb.client.Change;
 import com.google.gerrit.reviewdb.client.PatchSet;
@@ -25,6 +26,8 @@
 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.project.ChangeControl;
+import com.google.gerrit.server.project.NoSuchChangeException;
 import com.google.gerrit.server.query.Predicate;
 import com.google.gerrit.server.query.QueryParseException;
 import com.google.gson.Gson;
@@ -93,6 +96,7 @@
   private final ChangeQueryRewriter queryRewriter;
   private final Provider<ReviewDb> db;
   private final GitRepositoryManager repoManager;
+  private final ChangeControl.Factory changeControlFactory;
   private final int maxLimit;
 
   private OutputFormat outputFormat = OutputFormat.TEXT;
@@ -110,20 +114,24 @@
 
   private OutputStream outputStream = DisabledOutputStream.INSTANCE;
   private PrintWriter out;
+  private boolean moreResults;
 
   @Inject
   QueryProcessor(EventFactory eventFactory,
       ChangeQueryBuilder.Factory queryBuilder, CurrentUser currentUser,
       ChangeQueryRewriter queryRewriter, Provider<ReviewDb> db,
-      GitRepositoryManager repoManager) {
+      GitRepositoryManager repoManager,
+      ChangeControl.Factory changeControlFactory) {
     this.eventFactory = eventFactory;
     this.queryBuilder = queryBuilder.create(currentUser);
     this.queryRewriter = queryRewriter;
     this.db = db;
     this.repoManager = repoManager;
+    this.changeControlFactory = changeControlFactory;
     this.maxLimit = currentUser.getCapabilities()
       .getRange(GlobalCapability.QUERY_LIMIT)
       .getMax();
+    this.moreResults = false;
   }
 
   int getLimit() {
@@ -234,6 +242,9 @@
 
     Collections.sort(results, sortkeyAfter != null ? cmpAfter : cmpBefore);
     int limit = limit(s);
+    if (results.size() > maxLimit) {
+      moreResults = true;
+    }
     if (limit < results.size()) {
       results = results.subList(0, limit);
     }
@@ -260,17 +271,19 @@
         stats.runTimeMilliseconds = System.currentTimeMillis();
 
         List<ChangeData> results = queryChanges(queryString);
+        ChangeAttribute c = null;
         for (ChangeData d : results) {
-          ChangeAttribute c = eventFactory.asChangeAttribute(d.getChange());
+          LabelTypes labelTypes = changeControlFactory.controlFor(d.getChange())
+              .getLabelTypes();
+          c = eventFactory.asChangeAttribute(d.getChange());
           eventFactory.extend(c, d.getChange());
           eventFactory.addTrackingIds(c, d.trackingIds(db));
 
           if (includeSubmitRecords) {
             PatchSet.Id psId = d.getChange().currentPatchSetId();
             PatchSet patchSet = db.get().patchSets().get(psId);
-            Change.Id changeId = psId.getParentKey();
             List<SubmitRecord> submitResult = d.changeControl().canSubmit( //
-                db.get(), patchSet, null, false, true);
+                db.get(), patchSet, null, false, true, true);
             eventFactory.addSubmitRecords(c, submitResult);
           }
 
@@ -281,11 +294,12 @@
           if (includePatchSets) {
             if (includeFiles) {
               eventFactory.addPatchSets(c, d.patches(db),
-                includeApprovals ? d.approvalsMap(db) : null,
-                includeFiles, d.change(db));
+                includeApprovals ? d.approvalsMap(db).asMap() : null,
+                includeFiles, d.change(db), labelTypes);
             } else {
               eventFactory.addPatchSets(c, d.patches(db),
-                  includeApprovals ? d.approvalsMap(db) : null);
+                  includeApprovals ? d.approvalsMap(db).asMap() : null,
+                  labelTypes);
             }
           }
 
@@ -293,8 +307,8 @@
             PatchSet current = d.currentPatchSet(db);
             if (current != null) {
               c.currentPatchSet = eventFactory.asPatchSetAttribute(current);
-              eventFactory.addApprovals(c.currentPatchSet, //
-                  d.currentApprovals(db));
+              eventFactory.addApprovals(c.currentPatchSet,
+                  d.currentApprovals(db), labelTypes);
 
               if (includeFiles) {
                 eventFactory.addPatchSetFileNames(c.currentPatchSet,
@@ -320,6 +334,9 @@
         }
 
         stats.rowCount = results.size();
+        if (moreResults) {
+          stats.resumeSortKey = c.sortKey;
+        }
         stats.runTimeMilliseconds =
             System.currentTimeMillis() - stats.runTimeMilliseconds;
         show(stats);
@@ -334,6 +351,11 @@
         ErrorMessage m = new ErrorMessage();
         m.message = e.getMessage();
         show(m);
+      } catch (NoSuchChangeException e) {
+        log.error("Missing change: " + e.getMessage(), e);
+        ErrorMessage m = new ErrorMessage();
+        m.message = "missing change " + e.getMessage();
+        show(m);
       }
     } finally {
       try {
@@ -350,7 +372,7 @@
 
   private int limit(Predicate<ChangeData> s) {
     int n = queryBuilder.hasLimit(s) ? queryBuilder.getLimit(s) : maxLimit;
-    return limit > 0 ? Math.min(n, limit) + 1 : n;
+    return limit > 0 ? Math.min(n, limit) + 1 : n + 1;
   }
 
   @SuppressWarnings("unchecked")
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 270b2e7..4b71719 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
@@ -17,7 +17,6 @@
 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.account.CapabilityControl;
 import com.google.gerrit.server.account.GroupMembership;
@@ -37,7 +36,7 @@
 
   public SingleGroupUser(CapabilityControl.Factory capabilityControlFactory,
       Set<AccountGroup.UUID> groups) {
-    super(capabilityControlFactory, AccessPath.UNKNOWN);
+    super(capabilityControlFactory);
     this.groups = new ListGroupMembership(groups);
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/BaseDataSourceType.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/BaseDataSourceType.java
new file mode 100644
index 0000000..e72c3e9
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/BaseDataSourceType.java
@@ -0,0 +1,61 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.schema;
+
+import com.google.gerrit.reviewdb.server.ReviewDb;
+
+import java.io.IOException;
+import java.io.InputStream;
+
+public abstract class BaseDataSourceType implements DataSourceType {
+
+  private final String driver;
+
+  protected BaseDataSourceType(String driver) {
+    this.driver = driver;
+  }
+
+  @Override
+  public final String getDriver() {
+    return driver;
+  }
+
+  @Override
+  public boolean usePool() {
+    return true;
+  }
+
+  @Override
+  public ScriptRunner getIndexScript() throws IOException {
+    return getScriptRunner("index_generic.sql");
+  }
+
+  protected static final ScriptRunner getScriptRunner(String path) throws IOException {
+    if (path == null) {
+      return ScriptRunner.NOOP;
+    }
+    InputStream in =  ReviewDb.class.getResourceAsStream(path);
+    if (in == null) {
+      throw new IllegalStateException("SQL script " + path + " not found");
+    }
+    ScriptRunner runner;
+    try {
+      runner = new ScriptRunner(path, in);
+    } finally {
+      in.close();
+    }
+    return runner;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceModule.java
new file mode 100644
index 0000000..4066ad3
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceModule.java
@@ -0,0 +1,29 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.schema;
+
+import com.google.inject.AbstractModule;
+import com.google.inject.name.Names;
+
+public class DataSourceModule extends AbstractModule {
+
+  @Override
+  protected void configure() {
+    bind(DataSourceType.class).annotatedWith(Names.named("h2")).to(H2.class);
+    bind(DataSourceType.class).annotatedWith(Names.named("jdbc")).to(JDBC.class);
+    bind(DataSourceType.class).annotatedWith(Names.named("mysql")).to(MySql.class);
+    bind(DataSourceType.class).annotatedWith(Names.named("postgresql")).to(PostgreSQL.class);
+  }
+}
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 cc48019..6c9ca59d 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
@@ -14,11 +14,12 @@
 
 package com.google.gerrit.server.schema;
 
-import static com.google.gerrit.server.config.ConfigUtil.getEnum;
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static java.util.concurrent.TimeUnit.SECONDS;
 
+import com.google.common.base.Strings;
 import com.google.gerrit.extensions.events.LifecycleListener;
+import com.google.gerrit.server.config.ConfigSection;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePaths;
@@ -31,8 +32,6 @@
 import org.apache.commons.dbcp.BasicDataSource;
 import org.eclipse.jgit.lib.Config;
 
-import java.io.File;
-import java.io.IOException;
 import java.sql.SQLException;
 import java.util.Properties;
 
@@ -40,18 +39,30 @@
 
 /** Provides access to the DataSource. */
 @Singleton
-public final class DataSourceProvider implements Provider<DataSource>,
+public class DataSourceProvider implements Provider<DataSource>,
     LifecycleListener {
-  private final DataSource ds;
+  private final SitePaths site;
+  private final Config cfg;
+  private final Context ctx;
+  private final DataSourceType dst;
+  private DataSource ds;
 
   @Inject
-  DataSourceProvider(final SitePaths site,
-      @GerritServerConfig final Config cfg, Context ctx) {
-    ds = open(site, cfg, ctx);
+  protected DataSourceProvider(SitePaths site,
+      @GerritServerConfig Config cfg,
+      Context ctx,
+      DataSourceType dst) {
+    this.site = site;
+    this.cfg = cfg;
+    this.ctx = ctx;
+    this.dst = dst;
   }
 
   @Override
   public synchronized DataSource get() {
+    if (ds == null) {
+      ds = open(site, cfg, ctx, dst);
+    }
     return ds;
   }
 
@@ -74,100 +85,27 @@
     SINGLE_USER, MULTI_USER;
   }
 
-  public static enum Type {
-    H2, POSTGRESQL, MYSQL, JDBC;
-  }
-
   private DataSource open(final SitePaths site, final Config cfg,
-      final Context context) {
-    Type type = getEnum(cfg, "database", null, "type", Type.values(), null);
-    String driver = optional(cfg, "driver");
-    String url = optional(cfg, "url");
-    String username = optional(cfg, "username");
-    String password = optional(cfg, "password");
-
-    if (url == null || url.isEmpty()) {
-      if (type == null) {
-        type = Type.H2;
-      }
-
-      switch (type) {
-        case H2: {
-          String database = optional(cfg, "database");
-          if (database == null || database.isEmpty()) {
-            database = "db/ReviewDB";
-          }
-          File db = site.resolve(database);
-          try {
-            db = db.getCanonicalFile();
-          } catch (IOException e) {
-            db = db.getAbsoluteFile();
-          }
-          url = "jdbc:h2:" + db.toURI().toString();
-          break;
-        }
-
-        case POSTGRESQL: {
-          final StringBuilder b = new StringBuilder();
-          b.append("jdbc:postgresql://");
-          b.append(hostname(optional(cfg, "hostname")));
-          b.append(port(optional(cfg, "port")));
-          b.append("/");
-          b.append(required(cfg, "database"));
-          url = b.toString();
-          break;
-        }
-
-        case MYSQL: {
-          final StringBuilder b = new StringBuilder();
-          b.append("jdbc:mysql://");
-          b.append(hostname(optional(cfg, "hostname")));
-          b.append(port(optional(cfg, "port")));
-          b.append("/");
-          b.append(required(cfg, "database"));
-          url = b.toString();
-          break;
-        }
-
-        case JDBC:
-          driver = required(cfg, "driver");
-          url = required(cfg, "url");
-          break;
-
-        default:
-          throw new IllegalArgumentException(type + " not supported");
-      }
+      final Context context, final DataSourceType dst) {
+    ConfigSection dbs = new ConfigSection(cfg, "database");
+    String driver = dbs.optional("driver");
+    if (Strings.isNullOrEmpty(driver)) {
+      driver = dst.getDriver();
     }
 
-    if (driver == null || driver.isEmpty()) {
-      if (url.startsWith("jdbc:h2:")) {
-        driver = "org.h2.Driver";
-
-      } else if (url.startsWith("jdbc:postgresql:")) {
-        driver = "org.postgresql.Driver";
-
-      } else if (url.startsWith("jdbc:mysql:")) {
-        driver = "com.mysql.jdbc.Driver";
-
-      } else {
-        throw new IllegalArgumentException("database.driver must be set");
-      }
+    String url = dbs.optional("url");
+    if (Strings.isNullOrEmpty(url)) {
+      url = dst.getUrl();
     }
 
+    String username = dbs.optional("username");
+    String password = dbs.optional("password");
+
     boolean usePool;
-    if (url.startsWith("jdbc:mysql:")) {
-      // MySQL has given us trouble with the connection pool,
-      // sometimes the backend disconnects and the pool winds
-      // up with a stale connection. Fortunately opening up
-      // a new MySQL connection is usually very fast.
-      //
-      usePool = false;
-    } else {
-      usePool = true;
-    }
-    usePool = cfg.getBoolean("database", "connectionpool", usePool);
     if (context == Context.SINGLE_USER) {
       usePool = false;
+    } else {
+      usePool = cfg.getBoolean("database", "connectionpool", dst.usePool());
     }
 
     if (usePool) {
@@ -207,33 +145,4 @@
       }
     }
   }
-
-  private static String hostname(String hostname) {
-    if (hostname == null || hostname.isEmpty()) {
-      hostname = "localhost";
-
-    } else if (hostname.contains(":") && !hostname.startsWith("[")) {
-      hostname = "[" + hostname + "]";
-    }
-    return hostname;
-  }
-
-  private static String port(String port) {
-    if (port != null && !port.isEmpty()) {
-      return ":" + port;
-    }
-    return "";
-  }
-
-  private static String optional(final Config config, final String name) {
-    return config.getString("database", null, name);
-  }
-
-  private static String required(final Config config, final String name) {
-    final String v = optional(config, name);
-    if (v == null || "".equals(v)) {
-      throw new IllegalArgumentException("No database." + name + " configured");
-    }
-    return v;
-  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceType.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceType.java
new file mode 100644
index 0000000..14cb780
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/DataSourceType.java
@@ -0,0 +1,36 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.schema;
+
+import java.io.IOException;
+
+
+/** Abstraction of a supported database platform */
+public interface DataSourceType {
+
+  public String getDriver();
+
+  public String getUrl();
+
+  public boolean usePool();
+
+  /**
+   * Return a ScriptRunner that runs the index script. Must not return
+   * <code>null</code>, but may return a ScriptRunner that does nothing.
+   *
+   * @throws IOException
+   */
+  public ScriptRunner getIndexScript() throws IOException;
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/H2.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/H2.java
new file mode 100644
index 0000000..f43530f
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/H2.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.schema;
+
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.config.SitePaths;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.lib.Config;
+
+import java.io.File;
+import java.io.IOException;
+
+class H2 extends BaseDataSourceType {
+
+  protected final Config cfg;
+  private final SitePaths site;
+
+  @Inject
+  H2(final SitePaths site, @GerritServerConfig final Config cfg) {
+    super("org.h2.Driver");
+    this.cfg = cfg;
+    this.site = site;
+  }
+
+  @Override
+  public String getUrl() {
+    String database = cfg.getString("database", null, "database");
+    if (database == null || database.isEmpty()) {
+      database = "db/ReviewDB";
+    }
+    File db = site.resolve(database);
+    try {
+      db = db.getCanonicalFile();
+    } catch (IOException e) {
+      db = db.getAbsoluteFile();
+    }
+    return "jdbc:h2:" + db.toURI().toString();
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/JDBC.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/JDBC.java
new file mode 100644
index 0000000..2c2051d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/JDBC.java
@@ -0,0 +1,37 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.server.config.ConfigUtil;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.lib.Config;
+
+class JDBC extends BaseDataSourceType {
+
+  protected final Config cfg;
+
+  @Inject
+  JDBC(@GerritServerConfig final Config cfg) {
+    super(ConfigUtil.getRequired(cfg, "database", "driver"));
+    this.cfg = cfg;
+  }
+
+  @Override
+  public String getUrl() {
+    return ConfigUtil.getRequired(cfg, "database", "url");
+  }
+}
\ No newline at end of file
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/JdbcUtil.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/JdbcUtil.java
new file mode 100644
index 0000000..90ca43d
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/JdbcUtil.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.schema;
+
+public class JdbcUtil {
+
+  public static String hostname(String hostname) {
+    if (hostname == null || hostname.isEmpty()) {
+      hostname = "localhost";
+
+    } else if (hostname.contains(":") && !hostname.startsWith("[")) {
+      hostname = "[" + hostname + "]";
+    }
+    return hostname;
+  }
+
+  static String port(String port) {
+    if (port != null && !port.isEmpty()) {
+      return ":" + port;
+    }
+    return "";
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/MySql.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/MySql.java
new file mode 100644
index 0000000..308cec8
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/MySql.java
@@ -0,0 +1,56 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.server.schema.JdbcUtil.hostname;
+import static com.google.gerrit.server.schema.JdbcUtil.port;
+
+import com.google.gerrit.server.config.ConfigSection;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.lib.Config;
+
+class MySql extends BaseDataSourceType {
+
+  private Config cfg;
+
+  @Inject
+  public MySql(@GerritServerConfig final Config cfg) {
+    super("com.mysql.jdbc.Driver");
+    this.cfg = cfg;
+  }
+
+  @Override
+  public String getUrl() {
+    final StringBuilder b = new StringBuilder();
+    final ConfigSection dbs = new ConfigSection(cfg, "database");
+    b.append("jdbc:mysql://");
+    b.append(hostname(dbs.optional("hostname")));
+    b.append(port(dbs.optional("port")));
+    b.append("/");
+    b.append(dbs.required("database"));
+    return b.toString();
+  }
+
+  @Override
+  public boolean usePool() {
+    // MySQL has given us trouble with the connection pool,
+    // sometimes the backend disconnects and the pool winds
+    // up with a stale connection. Fortunately opening up
+    // a new MySQL connection is usually very fast.
+    return false;
+  }
+}
\ No newline at end of file
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/PostgreSQL.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/PostgreSQL.java
new file mode 100644
index 0000000..c58d0c2
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/PostgreSQL.java
@@ -0,0 +1,54 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.schema;
+
+import static com.google.gerrit.server.schema.JdbcUtil.hostname;
+import static com.google.gerrit.server.schema.JdbcUtil.port;
+
+import com.google.gerrit.server.config.ConfigSection;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.lib.Config;
+
+import java.io.IOException;
+
+class PostgreSQL extends BaseDataSourceType {
+
+  private Config cfg;
+
+  @Inject
+  public PostgreSQL(@GerritServerConfig final Config cfg) {
+    super("org.postgresql.Driver");
+    this.cfg = cfg;
+  }
+
+  @Override
+  public String getUrl() {
+    final StringBuilder b = new StringBuilder();
+    final ConfigSection dbc = new ConfigSection(cfg, "database");
+    b.append("jdbc:postgresql://");
+    b.append(hostname(dbc.optional("hostname")));
+    b.append(port(dbc.optional("port")));
+    b.append("/");
+    b.append(dbc.required("database"));
+    return b.toString();
+  }
+
+  @Override
+  public ScriptRunner getIndexScript() throws IOException {
+    return getScriptRunner("index_postgres.sql");
+  }
+}
\ No newline at end of file
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 fd379b2..50247b9 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,17 +14,19 @@
 
 package com.google.gerrit.server.schema;
 
+import com.google.common.collect.ImmutableList;
 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.LabelType;
+import com.google.gerrit.common.data.LabelValue;
 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.Project.InheritableBoolean;
 import com.google.gerrit.reviewdb.client.SystemConfig;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.GerritPersonIdent;
@@ -38,10 +40,6 @@
 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;
 
@@ -54,7 +52,6 @@
 
 import java.io.File;
 import java.io.IOException;
-import java.util.ArrayList;
 import java.util.Collections;
 
 /** Creates the current database schema and populates initial code rows. */
@@ -65,11 +62,9 @@
   private final GitRepositoryManager mgr;
   private final AllProjectsName allProjectsName;
   private final PersonIdent serverUser;
+  private final DataSourceType dataSourceType;
 
   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;
@@ -81,23 +76,23 @@
       @Current SchemaVersion version,
       GitRepositoryManager mgr,
       AllProjectsName allProjectsName,
-      @GerritPersonIdent PersonIdent au) {
-    this(site.site_path, version, mgr, allProjectsName, au);
+      @GerritPersonIdent PersonIdent au,
+      DataSourceType dst) {
+    this(site.site_path, version, mgr, allProjectsName, au, dst);
   }
 
   public SchemaCreator(@SitePath File site,
       @Current SchemaVersion version,
       GitRepositoryManager gitMgr,
       AllProjectsName ap,
-      @GerritPersonIdent PersonIdent au) {
+      @GerritPersonIdent PersonIdent au,
+      DataSourceType dst) {
     site_path = site;
     mgr = gitMgr;
     allProjectsName = ap;
     serverUser = au;
+    dataSourceType = dst;
     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, IOException,
@@ -114,29 +109,9 @@
     sVer.versionNbr = versionNbr;
     db.schemaVersion().insert(Collections.singleton(sVer));
 
-    final SystemConfig sConfig = initSystemConfig(db);
-    initVerifiedCategory(db);
-    initCodeReviewCategory(db, sConfig);
-
-    if (mgr != null) {
-      // TODO This should never be null when initializing a site.
-      initWildCardProject();
-    }
-
-    final SqlDialect d = jdbc.getDialect();
-    if (d instanceof DialectH2) {
-      index_generic.run(db);
-
-    } else if (d instanceof DialectMySQL) {
-      index_generic.run(db);
-      mysql_nextval.run(db);
-
-    } else if (d instanceof DialectPostgreSQL) {
-      index_postgres.run(db);
-
-    } else {
-      throw new OrmException("Unsupported database " + d.getClass().getName());
-    }
+    initSystemConfig(db);
+    initAllProjects();
+    dataSourceType.getIndexScript().run(db);
   }
 
   private AccountGroup newGroup(ReviewDb c, String name, AccountGroup.UUID uuid)
@@ -202,23 +177,32 @@
     return s;
   }
 
-  private void initWildCardProject() throws IOException, ConfigInvalidException {
-    Repository git;
+  private void initAllProjects() throws IOException, ConfigInvalidException {
+    Repository git = null;
     try {
       git = mgr.openRepository(allProjectsName);
+      initAllProjects(git);
     } 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);
+        initAllProjects(git);
         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);
       }
+    } finally {
+      if (git != null) {
+        git.close();
+      }
     }
-    try {
+  }
+
+  private void initAllProjects(Repository git) throws IOException,
+      ConfigInvalidException {
       MetaDataUpdate md =
           new MetaDataUpdate(GitReferenceUpdated.DISABLED, allProjectsName, git);
       md.getCommitBuilder().setAuthor(serverUser);
@@ -226,40 +210,77 @@
 
       ProjectConfig config = ProjectConfig.read(md);
       Project p = config.getProject();
-      p.setDescription("Rights inherited by all other projects");
-      p.setUseContributorAgreements(false);
+      p.setDescription("Access inherited by all other projects.");
+      p.setRequireChangeID(InheritableBoolean.TRUE);
+      p.setUseContentMerge(InheritableBoolean.TRUE);
+      p.setUseContributorAgreements(InheritableBoolean.FALSE);
+      p.setUseSignedOffBy(InheritableBoolean.FALSE);
 
       AccessSection cap = config.getAccessSection(AccessSection.GLOBAL_CAPABILITIES, true);
       AccessSection all = config.getAccessSection(AccessSection.ALL, true);
       AccessSection heads = config.getAccessSection(AccessSection.HEADS, true);
+      AccessSection tags = config.getAccessSection("refs/tags/*", true);
       AccessSection meta = config.getAccessSection(GitRepositoryManager.REF_CONFIG, true);
+      AccessSection magic = config.getAccessSection("refs/for/" + AccessSection.ALL, true);
 
-      cap.getPermission(GlobalCapability.ADMINISTRATE_SERVER, true)
-        .add(rule(config, admin));
+      grant(config, cap, GlobalCapability.ADMINISTRATE_SERVER, admin);
+      grant(config, all, Permission.READ, admin, anonymous);
 
-      PermissionRule review = rule(config, registered);
-      review.setRange(-1, 1);
-      heads.getPermission(Permission.LABEL + "Code-Review", true).add(review);
+      LabelType cr = initCodeReviewLabel(config);
+      grant(config, heads, cr, -1, 1, registered);
+      grant(config, heads, cr, -2, 2, admin, owners);
+      grant(config, heads, Permission.CREATE, admin, owners);
+      grant(config, heads, Permission.PUSH, admin, owners);
+      grant(config, heads, Permission.SUBMIT, admin, owners);
+      grant(config, heads, Permission.FORGE_AUTHOR, registered);
+      grant(config, heads, Permission.FORGE_COMMITTER, admin, owners);
+      grant(config, heads, Permission.EDIT_TOPIC_NAME, true, admin, owners);
 
-      all.getPermission(Permission.READ, true) //
-          .add(rule(config, admin));
-      all.getPermission(Permission.READ, true) //
-          .add(rule(config, anonymous));
+      grant(config, tags, Permission.PUSH_TAG, admin, owners);
+      grant(config, tags, Permission.PUSH_SIGNED_TAG, admin, owners);
 
-      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));
+      grant(config, magic, Permission.PUSH, registered);
+      grant(config, magic, Permission.PUSH_MERGE, registered);
 
-      Permission metaReadPermission = meta.getPermission(Permission.READ, true);
-      metaReadPermission.setExclusiveGroup(true);
-      metaReadPermission.add(rule(config, owners));
+      meta.getPermission(Permission.READ, true).setExclusiveGroup(true);
+      grant(config, meta, Permission.READ, admin, owners);
+      grant(config, meta, cr, -2, 2, admin, owners);
+      grant(config, meta, Permission.PUSH, admin, owners);
+      grant(config, meta, Permission.SUBMIT, admin, owners);
 
       md.setMessage("Initialized Gerrit Code Review " + Version.getVersion());
       config.commit(md);
-    } finally {
-      git.close();
+  }
+
+  private PermissionRule grant(ProjectConfig config, AccessSection section,
+      String permission, AccountGroup group1, AccountGroup... groupList) {
+    return grant(config, section, permission, false, group1, groupList);
+  }
+
+  private PermissionRule grant(ProjectConfig config, AccessSection section,
+      String permission, boolean force, AccountGroup group1,
+      AccountGroup... groupList) {
+    Permission p = section.getPermission(permission, true);
+    PermissionRule rule = rule(config, group1);
+    rule.setForce(force);
+    p.add(rule);
+    for (AccountGroup group : groupList) {
+      rule = rule(config, group);
+      rule.setForce(force);
+      p.add(rule);
+    }
+    return rule;
+  }
+
+  private void grant(ProjectConfig config,
+      AccessSection section, LabelType type,
+      int min, int max, AccountGroup... groupList) {
+    String name = Permission.LABEL + type.getName();
+    Permission p = section.getPermission(name, true);
+    for (AccountGroup group : groupList) {
+      PermissionRule r = rule(config, group);
+      r.setRange(min, max);
+      p.add(r);
     }
   }
 
@@ -267,43 +288,16 @@
     return new PermissionRule(config.resolve(group));
   }
 
-  private void initVerifiedCategory(final ReviewDb c) throws OrmException {
-    final ApprovalCategory cat;
-    final ArrayList<ApprovalCategoryValue> vals;
-
-    cat = new ApprovalCategory(new ApprovalCategory.Id("VRIF"), "Verified");
-    cat.setPosition((short) 0);
-    cat.setAbbreviatedName("V");
-    vals = new ArrayList<ApprovalCategoryValue>();
-    vals.add(value(cat, 1, "Verified"));
-    vals.add(value(cat, 0, "No score"));
-    vals.add(value(cat, -1, "Fails"));
-    c.approvalCategories().insert(Collections.singleton(cat));
-    c.approvalCategoryValues().insert(vals);
-  }
-
-  private void initCodeReviewCategory(final ReviewDb c,
-      final SystemConfig sConfig) throws OrmException {
-    final ApprovalCategory cat;
-    final ArrayList<ApprovalCategoryValue> vals;
-
-    cat = new ApprovalCategory(new ApprovalCategory.Id("CRVW"), "Code Review");
-    cat.setPosition((short) 1);
-    cat.setAbbreviatedName("R");
-    cat.setCopyMinScore(true);
-    vals = new ArrayList<ApprovalCategoryValue>();
-    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"));
-    c.approvalCategories().insert(Collections.singleton(cat));
-    c.approvalCategoryValues().insert(vals);
-  }
-
-  private static ApprovalCategoryValue value(final ApprovalCategory cat,
-      final int value, final String name) {
-    return new ApprovalCategoryValue(new ApprovalCategoryValue.Id(cat.getId(),
-        (short) value), name);
+  public static LabelType initCodeReviewLabel(ProjectConfig c) {
+    LabelType type = new LabelType("Code-Review", ImmutableList.of(
+        new LabelValue((short) 2, "Looks good to me, approved"),
+        new LabelValue((short) 1, "Looks good to me, but someone else must approve"),
+        new LabelValue((short) 0, "No score"),
+        new LabelValue((short) -1, "I would prefer that you didn't submit this"),
+        new LabelValue((short) -2, "Do not submit")));
+    type.setAbbreviatedName("CR");
+    type.setCopyMinScore(true);
+    c.getLabelSections().put(type.getName(), type);
+    return type;
   }
 }
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 f127a21..4de3888 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersion.java
@@ -32,7 +32,7 @@
 /** A version of the database schema. */
 public abstract class SchemaVersion {
   /** The current schema version. */
-  public static final Class<Schema_73> C = Schema_73.class;
+  public static final Class<Schema_77> C = Schema_77.class;
 
   public static class Module extends AbstractModule {
     @Override
@@ -49,7 +49,7 @@
     this.versionNbr = guessVersion(getClass());
   }
 
-  private static int guessVersion(Class<?> c) {
+  public static int guessVersion(Class<?> c) {
     String n = c.getName();
     n = n.substring(n.lastIndexOf('_') + 1);
     while (n.startsWith("0"))
@@ -83,6 +83,11 @@
       throws OrmException, SQLException {
     final JdbcSchema s = (JdbcSchema) db;
 
+    if (curr.versionNbr > versionNbr) {
+      throw new OrmException("Cannot downgrade database schema from version " + curr.versionNbr
+          + " to " + versionNbr + ".");
+    }
+
     prior.get().check(ui, curr, db, false);
 
     ui.message("Upgrading database schema from version " + curr.versionNbr
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersionCheck.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersionCheck.java
index 133b856..2e12f5c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersionCheck.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/SchemaVersionCheck.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.lifecycle.LifecycleModule;
 import com.google.gerrit.reviewdb.client.CurrentSchemaVersion;
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.config.SitePaths;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
@@ -37,14 +38,17 @@
   }
 
   private final SchemaFactory<ReviewDb> schema;
+  private final SitePaths site;
 
   @Current
   private final Provider<SchemaVersion> version;
 
   @Inject
   public SchemaVersionCheck(SchemaFactory<ReviewDb> schemaFactory,
+      final SitePaths site,
       @Current Provider<SchemaVersion> version) {
     this.schema = schemaFactory;
+    this.site = site;
     this.version = version;
   }
 
@@ -52,17 +56,25 @@
     try {
       final ReviewDb db = schema.open();
       try {
-        final CurrentSchemaVersion sVer = getSchemaVersion(db);
-        final int eVer = version.get().getVersionNbr();
+        final CurrentSchemaVersion currentVer = getSchemaVersion(db);
+        final int expectedVer = version.get().getVersionNbr();
 
-        if (sVer == null) {
+        if (currentVer == null) {
           throw new ProvisionException("Schema not yet initialized."
-              + "  Run init to initialize the schema.");
+              + "  Run init to initialize the schema:\n"
+              + "$ java -jar gerrit.war init -d "
+              + site.site_path.getAbsolutePath());
         }
-        if (sVer.versionNbr != eVer) {
+        if (currentVer.versionNbr < expectedVer) {
           throw new ProvisionException("Unsupported schema version "
-              + sVer.versionNbr + "; expected schema version " + eVer
-              + ".  Run init to upgrade.");
+              + currentVer.versionNbr + "; expected schema version " + expectedVer
+              + ".  Run init to upgrade:\n"
+              + "$ java -jar " + site.gerrit_war.getAbsolutePath() + " init -d "
+              + site.site_path.getAbsolutePath());
+        } else if (currentVer.versionNbr > expectedVer) {
+          throw new ProvisionException("Unsupported schema version "
+              + currentVer.versionNbr + "; expected schema version " + expectedVer
+              + ". Downgrade is not supported.");
         }
       } finally {
         db.close();
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_53.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_53.java
index 8207c31..e4aae2c 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_53.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_53.java
@@ -28,11 +28,13 @@
 
 import com.google.gerrit.common.data.AccessSection;
 import com.google.gerrit.common.data.GroupReference;
+import com.google.gerrit.common.data.LabelType;
 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.PatchSetApproval.LabelId;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.Project.InheritableBoolean;
 import com.google.gerrit.reviewdb.client.SystemConfig;
 import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.GerritPersonIdent;
@@ -41,6 +43,7 @@
 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.schema.Schema_77.LegacyLabelTypes;
 import com.google.gwtorm.jdbc.JdbcSchema;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -67,7 +70,7 @@
 
   private SystemConfig systemConfig;
   private Map<AccountGroup.Id, GroupReference> groupMap;
-  private Map<ApprovalCategory.Id, ApprovalCategory> categoryMap;
+  private LegacyLabelTypes labelTypes;
   private GroupReference projectOwners;
 
   private Map<Project.NameKey, Project.NameKey> parentsByProject;
@@ -92,7 +95,7 @@
   protected void migrateData(ReviewDb db, UpdateUI ui) throws OrmException,
       SQLException {
     systemConfig = db.systemConfig().get(new SystemConfig.Key());
-    categoryMap = db.approvalCategories().toMap(db.approvalCategories().all());
+    labelTypes = Schema_77.getLegacyTypes(db);
 
     assignGroupUUIDs(db);
     readOldRefRights(db);
@@ -103,13 +106,17 @@
   }
 
   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);
+    try {
+      Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
+      try {
+        stmt.executeUpdate(
+            "DELETE FROM approval_categories WHERE position < 0");
+      } finally {
+        stmt.close();
       }
+    } catch (SQLException e) {
+      throw new OrmException(e);
     }
-    db.approvalCategories().delete(delete);
   }
 
   private void assignGroupUUIDs(ReviewDb db) throws OrmException {
@@ -200,8 +207,7 @@
   private void loadProject(ResultSet rs, Project project) throws SQLException,
       OrmException {
     project.setDescription(rs.getString("description"));
-    project.setUseContributorAgreements("Y".equals(rs
-        .getString("use_contributor_agreements")));
+    project.setUseContributorAgreements(asInheritableBoolean(rs, "use_contributor_agreements"));
 
     switch (rs.getString("submit_type").charAt(0)) {
       case 'F':
@@ -221,12 +227,19 @@
             + 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.setUseSignedOffBy(asInheritableBoolean(rs, "use_signed_off_by"));
+    project.setRequireChangeID(asInheritableBoolean(rs, "require_change_id"));
+    project.setUseContentMerge(asInheritableBoolean(rs, "use_content_merge"));
     project.setParentName(rs.getString("parent_name"));
   }
 
+  private static InheritableBoolean asInheritableBoolean(ResultSet rs, String col)
+      throws SQLException {
+    return "Y".equals(rs.getString(col))
+        ? Project.InheritableBoolean.TRUE
+        : Project.InheritableBoolean.INHERIT;
+  }
+
   private void readOldRefRights(ReviewDb db) throws SQLException {
     rightsByProject = new HashMap<Project.NameKey, List<OldRefRight>>();
 
@@ -358,8 +371,7 @@
 
         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 if (3 <= inheritedMax(config, old)) { add(section, FORGE_SERVER, old.exclusive, deny(group));
         }
 
       } else {
@@ -368,7 +380,9 @@
         if (old.min_value == 0 && old.max_value == 0) {
           rule.setDeny();
         }
-        add(section, LABEL + varNameOf(old.category), old.exclusive, rule);
+        LabelType type = labelTypes.byLabel(new LabelId(old.category));
+        String name = type != null ? type.getName() : old.category;
+        add(section, LABEL + name, old.exclusive, rule);
       }
     }
   }
@@ -436,14 +450,6 @@
     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);
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_57.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_57.java
index 3a288e2..bf488e3 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_57.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_57.java
@@ -30,6 +30,7 @@
 import com.google.gerrit.server.git.LocalDiskRepositoryManager;
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gwtorm.jdbc.JdbcSchema;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -41,6 +42,8 @@
 import org.eclipse.jgit.util.FS;
 
 import java.io.IOException;
+import java.sql.PreparedStatement;
+import java.sql.SQLException;
 import java.util.Collections;
 
 public class Schema_57 extends SchemaVersion {
@@ -95,6 +98,11 @@
 
         // Move the repository.*.createGroup to Create Project.
         String[] createGroupList = cfg.getStringList("repository", "*", "createGroup");
+
+        // Prepare the account_group_includes query
+        PreparedStatement stmt = ((JdbcSchema) db).getConnection().
+            prepareStatement("SELECT * FROM account_group_includes WHERE group_id = ?");
+
         for (String name : createGroupList) {
           AccountGroup.NameKey key = new AccountGroup.NameKey(name);
           AccountGroupName groupName = db.accountGroupNames().get(key);
@@ -117,9 +125,10 @@
         }
 
         AccountGroup batch = db.accountGroups().get(sc.batchUsersGroupId);
+        stmt.setInt(1, sc.batchUsersGroupId.get());
         if (batch != null
             && db.accountGroupMembers().byGroup(sc.batchUsersGroupId).toList().isEmpty()
-            &&  db.accountGroupIncludes().byGroup(sc.batchUsersGroupId).toList().isEmpty()) {
+            &&  stmt.executeQuery().first() != false) {
           // If the batch user group is not used, delete it.
           //
           db.accountGroups().delete(Collections.singleton(batch));
@@ -136,6 +145,8 @@
 
         md.setMessage("Upgrade to Gerrit Code Review schema 57\n");
         config.commit(md);
+      } catch (SQLException err) {
+        throw new OrmException( "Cannot read account_group_includes", err);
       } finally {
         git.close();
       }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_74.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_74.java
new file mode 100644
index 0000000..ca012d1
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_74.java
@@ -0,0 +1,114 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuid;
+import com.google.gerrit.reviewdb.client.AccountGroupIncludeByUuidAudit;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gwtorm.jdbc.JdbcSchema;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+import java.sql.Connection;
+import java.sql.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.ArrayList;
+import java.util.HashMap;
+
+/* Handles copying all entries from AccountGroupIncludes(Audit) to the new tables */
+public class Schema_74 extends SchemaVersion {
+  @Inject
+  Schema_74(Provider<Schema_73> prior) {
+    super(prior);
+  }
+
+  @Override
+  protected void migrateData(final ReviewDb db, final UpdateUI ui)
+      throws SQLException, OrmException {
+    // Grab all the groups since we don't have the cache available
+    HashMap<AccountGroup.Id, AccountGroup.UUID> allGroups =
+        new HashMap<AccountGroup.Id, AccountGroup.UUID>();
+    for( AccountGroup ag : db.accountGroups().all() ) {
+      allGroups.put(ag.getId(), ag.getGroupUUID());
+    }
+
+    // Initialize some variables
+    Connection conn = ((JdbcSchema) db).getConnection();
+    ArrayList<AccountGroupIncludeByUuid> newIncludes =
+        new ArrayList<AccountGroupIncludeByUuid>();
+    ArrayList<AccountGroupIncludeByUuidAudit> newIncludeAudits =
+        new ArrayList<AccountGroupIncludeByUuidAudit>();
+
+    // Iterate over all entries in account_group_includes
+    Statement oldGroupIncludesStmt = conn.createStatement();
+    ResultSet oldGroupIncludes = oldGroupIncludesStmt.
+        executeQuery("SELECT * FROM account_group_includes");
+    while (oldGroupIncludes.next()) {
+      AccountGroup.Id oldGroupId =
+          new AccountGroup.Id(oldGroupIncludes.getInt("group_id"));
+      AccountGroup.Id oldIncludeId =
+          new AccountGroup.Id(oldGroupIncludes.getInt("include_id"));
+      AccountGroup.UUID uuidFromIncludeId = allGroups.get(oldIncludeId);
+
+      // If we've got an include, but the group no longer exists, don't bother converting
+      if (uuidFromIncludeId == null) {
+        ui.message("Skipping group_id = \"" + oldIncludeId.get() +
+            "\", not a current group");
+        continue;
+      }
+
+      // Create the new include entry
+      AccountGroupIncludeByUuid destIncludeEntry = new AccountGroupIncludeByUuid(
+          new AccountGroupIncludeByUuid.Key(oldGroupId, uuidFromIncludeId));
+
+      // Iterate over all the audits (for this group)
+      PreparedStatement oldAuditsQuery = conn.prepareStatement(
+          "SELECT * FROM account_group_includes_audit WHERE group_id=? AND include_id=?");
+      oldAuditsQuery.setInt(1, oldGroupId.get());
+      oldAuditsQuery.setInt(2, oldIncludeId.get());
+      ResultSet oldGroupIncludeAudits = oldAuditsQuery.executeQuery();
+      while (oldGroupIncludeAudits.next()) {
+        Account.Id addedBy = new Account.Id(oldGroupIncludeAudits.getInt("added_by"));
+        int removedBy = oldGroupIncludeAudits.getInt("removed_by");
+
+        // Create the new audit entry
+        AccountGroupIncludeByUuidAudit destAuditEntry =
+            new AccountGroupIncludeByUuidAudit(destIncludeEntry, addedBy,
+                oldGroupIncludeAudits.getTimestamp("added_on"));
+
+        // If this was a "removed on" entry, note that
+        if (removedBy > 0) {
+          destAuditEntry.removed(new Account.Id(removedBy),
+              oldGroupIncludeAudits.getTimestamp("removed_on"));
+        }
+        newIncludeAudits.add(destAuditEntry);
+      }
+      newIncludes.add(destIncludeEntry);
+      oldAuditsQuery.close();
+      oldGroupIncludeAudits.close();
+    }
+    oldGroupIncludes.close();
+    oldGroupIncludesStmt.close();
+
+    // Now insert all of the new entries to the database
+    db.accountGroupIncludesByUuid().insert(newIncludes);
+    db.accountGroupIncludesByUuidAudit().insert(newIncludeAudits);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_75.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_75.java
new file mode 100644
index 0000000..a847f25
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_75.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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_75 extends SchemaVersion {
+  @Inject
+  Schema_75(Provider<Schema_74> prior) {
+    super(prior);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_76.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_76.java
new file mode 100644
index 0000000..8b0b557
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_76.java
@@ -0,0 +1,25 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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_76 extends SchemaVersion {
+  @Inject
+  Schema_76(Provider<Schema_75> prior) {
+    super(prior);
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_77.java b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_77.java
new file mode 100644
index 0000000..b86e5a7
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/schema/Schema_77.java
@@ -0,0 +1,266 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.server.schema;
+
+import static com.google.common.base.Preconditions.checkArgument;
+
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.common.data.LabelTypes;
+import com.google.gerrit.common.data.LabelValue;
+import com.google.gerrit.reviewdb.client.PatchSetApproval.LabelId;
+import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.GerritPersonIdent;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.extensions.events.GitReferenceUpdated;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.MetaDataUpdate;
+import com.google.gerrit.server.git.ProjectConfig;
+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 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.PreparedStatement;
+import java.sql.ResultSet;
+import java.sql.SQLException;
+import java.sql.Statement;
+import java.util.List;
+import java.util.Map;
+
+public class Schema_77 extends SchemaVersion {
+  private final GitRepositoryManager mgr;
+  private final AllProjectsName allProjects;
+  private final PersonIdent serverUser;
+
+  @Inject
+  Schema_77(Provider<Schema_76> 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 {
+    try {
+      LegacyLabelTypes labelTypes = getLegacyTypes(db);
+      SqlDialect dialect = ((JdbcSchema) db).getDialect();
+      if (dialect instanceof DialectH2) {
+        alterTable(db, "ALTER TABLE %s ALTER COLUMN %s varchar(255)");
+      } else if (dialect instanceof DialectPostgreSQL) {
+        alterTable(db, "ALTER TABLE %s ALTER %s TYPE varchar(255)");
+      } else if (dialect instanceof DialectMySQL) {
+        alterTable(db, "ALTER TABLE %s MODIFY %s varchar(255) BINARY");
+      } else {
+        alterTable(db, "ALTER TABLE %s MODIFY %s varchar(255)");
+      }
+      migratePatchSetApprovals(db, labelTypes);
+      migrateLabelsToAllProjects(db, labelTypes);
+    } catch (RepositoryNotFoundException e) {
+      throw new OrmException(e);
+    } catch (SQLException e) {
+      throw new OrmException(e);
+    } catch (IOException e) {
+      throw new OrmException(e);
+    } catch (ConfigInvalidException e) {
+      throw new OrmException(e);
+    }
+    ui.message(
+        "Migrated label types from database to All-Projects project.config");
+  }
+
+  private void alterTable(ReviewDb db, String sqlFormat) throws SQLException {
+    Statement stmt = ((JdbcSchema) db).getConnection().createStatement();
+    try {
+      stmt.executeUpdate(
+          String.format(sqlFormat, "patch_set_approvals", "category_id"));
+    } finally {
+      stmt.close();
+    }
+  }
+
+  private void migrateLabelsToAllProjects(ReviewDb db,
+      LegacyLabelTypes labelTypes) throws SQLException,
+      RepositoryNotFoundException, IOException, ConfigInvalidException {
+    Repository git = mgr.openRepository(allProjects);
+
+    try {
+      MetaDataUpdate md =
+          new MetaDataUpdate(GitReferenceUpdated.DISABLED, allProjects, git);
+      md.getCommitBuilder().setAuthor(serverUser);
+      md.getCommitBuilder().setCommitter(serverUser);
+
+      ProjectConfig config = ProjectConfig.read(md);
+      Map<String, LabelType> configTypes = config.getLabelSections();
+      List<LabelType> newTypes = Lists.newArrayList();
+      for (LegacyLabelType type : labelTypes.getLegacyLabelTypes()) {
+        if (!configTypes.containsKey(type.getName())) {
+          newTypes.add(type);
+        }
+      }
+      newTypes.addAll(configTypes.values());
+      configTypes.clear();
+      for (LabelType type : newTypes) {
+        configTypes.put(type.getName(), type);
+      }
+      md.setMessage("Upgrade to Gerrit Code Review schema 77\n");
+      config.commit(md);
+    } finally {
+      git.close();
+    }
+  }
+
+  private void migratePatchSetApprovals(ReviewDb db,
+      LegacyLabelTypes labelTypes) throws SQLException {
+    PreparedStatement stmt = ((JdbcSchema) db).getConnection().prepareStatement(
+        "UPDATE patch_set_approvals SET category_id = ? WHERE category_id = ?");
+    try {
+      for (LegacyLabelType type : labelTypes.getLegacyLabelTypes()) {
+        stmt.setString(1, type.getName());
+        stmt.setString(2, type.getId());
+        stmt.addBatch();
+      }
+      stmt.executeBatch();
+    } finally {
+      stmt.close();
+    }
+  }
+
+  static class LegacyLabelType extends LabelType {
+    private String id;
+
+    private LegacyLabelType(String name, List<LabelValue> values) {
+      super(name, values);
+    }
+
+    String getId() {
+      return id;
+    }
+
+    private void setId(String id) {
+      checkArgument(id.length() <= 4, "Invalid legacy label ID: \"%s\"", id);
+      this.id = id;
+    }
+  }
+
+  static class LegacyLabelTypes extends LabelTypes {
+    private final List<LegacyLabelType> legacyTypes;
+
+    private final Map<String, LegacyLabelType> byId;
+
+    private LegacyLabelTypes(List<LegacyLabelType> types) {
+      super(types);
+      legacyTypes = types;
+      byId = Maps.newHashMap();
+      for (LegacyLabelType type : types) {
+        byId.put(type.getId(), type);
+      }
+    }
+
+    List<LegacyLabelType> getLegacyLabelTypes() {
+      return legacyTypes;
+    }
+
+    @Override
+    public LegacyLabelType byLabel(LabelId labelId) {
+      LegacyLabelType t = byId.get(labelId.get());
+      return t != null ? t : (LegacyLabelType) super.byLabel(labelId);
+    }
+
+    LegacyLabelType byId(LabelId id) {
+      return byId.get(id.get());
+    }
+  }
+
+  static LegacyLabelTypes getLegacyTypes(ReviewDb db) throws SQLException {
+    List<LegacyLabelType> types = Lists.newArrayListWithCapacity(2);
+    Statement catStmt = null;
+    PreparedStatement valStmt = null;
+    ResultSet catRs = null;
+    try {
+      catStmt = ((JdbcSchema) db).getConnection().createStatement();
+      catRs = catStmt.executeQuery(
+          "SELECT category_id, name, abbreviated_name, function_name, "
+          + " copy_min_score"
+          + " FROM approval_categories"
+          + " ORDER BY position, name");
+      valStmt = ((JdbcSchema) db).getConnection().prepareStatement(
+          "SELECT value, name"
+          + " FROM approval_category_values"
+          + " WHERE category_id = ?");
+      while (catRs.next()) {
+        String id = catRs.getString("category_id");
+        valStmt.setString(1, id);
+        List<LabelValue> values = Lists.newArrayListWithCapacity(5);
+        ResultSet valRs = valStmt.executeQuery();
+        try {
+          while (valRs.next()) {
+            values.add(new LabelValue(
+                valRs.getShort("value"), valRs.getString("name")));
+          }
+        } finally {
+          valRs.close();
+        }
+        LegacyLabelType type =
+            new LegacyLabelType(getLabelName(catRs.getString("name")), values);
+        type.setId(id);
+        type.setAbbreviatedName(catRs.getString("abbreviated_name"));
+        type.setFunctionName(catRs.getString("function_name"));
+        type.setCopyMinScore("Y".equals(catRs.getString("copy_min_score")));
+        types.add(type);
+      }
+    } finally {
+      if (valStmt != null) {
+        valStmt.close();
+      }
+      if (catRs != null) {
+        catRs.close();
+      }
+      if (catStmt != null) {
+        catStmt.close();
+      }
+    }
+    return new LegacyLabelTypes(types);
+  }
+
+  private static String getLabelName(String name) {
+    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') //
+          || (c == '-')) {
+        r.append(c);
+      } else if (c == ' ') {
+        r.append('-');
+      }
+    }
+    return r.toString();
+  }
+}
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 8cf2f26..b5aead8 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
@@ -19,7 +19,6 @@
 import com.google.gwtorm.server.OrmException;
 
 import java.io.BufferedReader;
-import java.io.FileNotFoundException;
 import java.io.IOException;
 import java.io.InputStream;
 import java.io.InputStreamReader;
@@ -34,10 +33,15 @@
   private final String name;
   private final List<String> commands;
 
-  ScriptRunner(final String name) {
+  static final ScriptRunner NOOP = new ScriptRunner(null, null) {
+    void run(final ReviewDb db) {
+    };
+  };
+
+  ScriptRunner(final String scriptName, final InputStream script) {
+    this.name = scriptName;
     try {
-      this.name = name;
-      this.commands = parse(name);
+      this.commands = script != null ? parse(script) : null;
     } catch (IOException e) {
       throw new IllegalStateException("Cannot parse " + name, e);
     }
@@ -63,12 +67,7 @@
     }
   }
 
-  private List<String> parse(final String name) throws IOException {
-    InputStream in = ReviewDb.class.getResourceAsStream(name);
-    if (in == null) {
-      throw new FileNotFoundException("SQL script " + name + " not found");
-    }
-
+  private List<String> parse(final InputStream in) throws IOException {
     BufferedReader br = new BufferedReader(new InputStreamReader(in, "UTF-8"));
     try {
       String delimiter = ";";
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ssh/NoSshInfo.java b/gerrit-server/src/main/java/com/google/gerrit/server/ssh/NoSshInfo.java
index 519b922..6ceec2d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ssh/NoSshInfo.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ssh/NoSshInfo.java
@@ -19,7 +19,7 @@
 import java.util.Collections;
 import java.util.List;
 
-class NoSshInfo implements SshInfo {
+public 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
index 6a07ca7..0957594 100644
--- 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
@@ -16,8 +16,21 @@
 
 import com.google.gerrit.common.errors.InvalidSshKeyException;
 import com.google.gerrit.reviewdb.client.AccountSshKey;
+import com.google.inject.AbstractModule;
+import com.google.inject.Module;
 
-class NoSshKeyCache implements SshKeyCache {
+
+public class NoSshKeyCache implements SshKeyCache {
+
+  public static Module module() {
+    return new AbstractModule() {
+      @Override
+      protected void configure() {
+        bind(SshKeyCache.class).to(NoSshKeyCache.class);
+      }
+    };
+  }
+
   @Override
   public void evict(String username) {
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ssh/NoSshModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/ssh/NoSshModule.java
index 21b1a54..8781d46 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/ssh/NoSshModule.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ssh/NoSshModule.java
@@ -23,6 +23,5 @@
   @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/SshAddressesModule.java b/gerrit-server/src/main/java/com/google/gerrit/server/ssh/SshAddressesModule.java
new file mode 100644
index 0000000..4c1283a
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ssh/SshAddressesModule.java
@@ -0,0 +1,106 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.common.collect.Lists;
+import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.util.SocketUtil;
+import com.google.inject.AbstractModule;
+import com.google.inject.Provides;
+import com.google.inject.Singleton;
+
+import org.eclipse.jgit.lib.Config;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.net.InetSocketAddress;
+import java.net.SocketAddress;
+import java.util.Arrays;
+import java.util.List;
+
+public class SshAddressesModule extends AbstractModule {
+  private static final Logger log =
+      LoggerFactory.getLogger(SshAddressesModule.class);
+
+  public static final int DEFAULT_PORT = 29418;
+  public static final int IANA_SSH_PORT = 22;
+
+  @Override
+  protected void configure() {
+  }
+
+  @Provides
+  @Singleton
+  @SshListenAddresses
+  List<SocketAddress> getListenAddresses(@GerritServerConfig Config cfg) {
+    List<SocketAddress> listen = Lists.newArrayListWithExpectedSize(2);
+    String[] want = cfg.getStringList("sshd", null, "listenaddress");
+    if (want == null || want.length == 0) {
+      listen.add(new InetSocketAddress(DEFAULT_PORT));
+      return listen;
+    }
+
+    if (want.length == 1 && isOff(want[0])) {
+      return listen;
+    }
+
+    for (final String desc : want) {
+      try {
+        listen.add(SocketUtil.resolve(desc, DEFAULT_PORT));
+      } catch (IllegalArgumentException e) {
+        log.error("Bad sshd.listenaddress: " + desc + ": " + e.getMessage());
+      }
+    }
+    return listen;
+  }
+
+  private static boolean isOff(String listenHostname) {
+    return "off".equalsIgnoreCase(listenHostname)
+        || "none".equalsIgnoreCase(listenHostname)
+        || "no".equalsIgnoreCase(listenHostname);
+  }
+
+  @Provides
+  @Singleton
+  @SshAdvertisedAddresses
+  List<String> getAdvertisedAddresses(@GerritServerConfig Config cfg,
+      @SshListenAddresses List<SocketAddress> listen) {
+    String[] want = cfg.getStringList("sshd", null, "advertisedaddress");
+    if (want.length > 0) {
+      return Arrays.asList(want);
+    }
+    List<InetSocketAddress> pub = Lists.newArrayList();
+    List<InetSocketAddress> local = Lists.newArrayList();
+
+    for (SocketAddress addr : listen) {
+      if (addr instanceof InetSocketAddress) {
+        InetSocketAddress inetAddr = (InetSocketAddress) addr;
+        if (inetAddr.getAddress().isLoopbackAddress()) {
+          local.add(inetAddr);
+        } else {
+          pub.add(inetAddr);
+        }
+      }
+    }
+    if (pub.isEmpty()) {
+      pub = local;
+    }
+    List<String> adv = Lists.newArrayListWithCapacity(pub.size());
+    for (InetSocketAddress addr : pub) {
+      adv.add(SocketUtil.format(addr, IANA_SSH_PORT));
+    }
+    return adv;
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ssh/SshAdvertisedAddresses.java b/gerrit-server/src/main/java/com/google/gerrit/server/ssh/SshAdvertisedAddresses.java
new file mode 100644
index 0000000..2219298
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ssh/SshAdvertisedAddresses.java
@@ -0,0 +1,30 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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 static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.inject.BindingAnnotation;
+
+import java.lang.annotation.Retention;
+
+/**
+ * Marker on the list of {@link SocketAddress}es configured to be advertised by
+ * the server.
+ */
+@Retention(RUNTIME)
+@BindingAnnotation
+public @interface SshAdvertisedAddresses {
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/ssh/SshListenAddresses.java b/gerrit-server/src/main/java/com/google/gerrit/server/ssh/SshListenAddresses.java
new file mode 100644
index 0000000..be40567
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/ssh/SshListenAddresses.java
@@ -0,0 +1,30 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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 static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import com.google.inject.BindingAnnotation;
+
+import java.lang.annotation.Retention;
+
+/**
+ * Marker on the list of {@link SocketAddress}es on which the SSH daemon is
+ * configured to listen.
+ */
+@Retention(RUNTIME)
+@BindingAnnotation
+public @interface SshListenAddresses {
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/FallbackRequestContext.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/FallbackRequestContext.java
index 686a108..5514ef5 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/FallbackRequestContext.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/FallbackRequestContext.java
@@ -14,9 +14,12 @@
 
 package com.google.gerrit.server.util;
 
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.AnonymousUser;
 import com.google.gerrit.server.CurrentUser;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
+import com.google.inject.ProvisionException;
 import com.google.inject.Singleton;
 
 /**
@@ -37,4 +40,15 @@
   public CurrentUser getCurrentUser() {
     return user;
   }
+
+  @Override
+  public Provider<ReviewDb> getReviewDbProvider() {
+    return new Provider<ReviewDb>() {
+      @Override
+      public ReviewDb get() {
+        throw new ProvisionException(
+            "Automatic ReviewDb only available in request scope");
+      }
+    };
+  }
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/GuiceRequestScopePropagator.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/GuiceRequestScopePropagator.java
index fa07176..4b5d736 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/GuiceRequestScopePropagator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/GuiceRequestScopePropagator.java
@@ -17,6 +17,7 @@
 import com.google.common.collect.Maps;
 import com.google.gerrit.server.RemotePeer;
 import com.google.gerrit.server.config.CanonicalWebUrl;
+import com.google.gerrit.server.config.RequestScopedReviewDbProvider;
 import com.google.inject.Inject;
 import com.google.inject.Key;
 import com.google.inject.Provider;
@@ -42,8 +43,9 @@
   GuiceRequestScopePropagator(
       @CanonicalWebUrl @Nullable Provider<String> urlProvider,
       @RemotePeer Provider<SocketAddress> remotePeerProvider,
-      ThreadLocalRequestContext local) {
-    super(ServletScopes.REQUEST, local);
+      ThreadLocalRequestContext local,
+      Provider<RequestScopedReviewDbProvider> dbProviderProvider) {
+    super(ServletScopes.REQUEST, local, dbProviderProvider);
     this.url = urlProvider != null ? urlProvider.get() : null;
     this.peer = remotePeerProvider.get();
   }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/IdGenerator.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/IdGenerator.java
index b20bbf6..78eb657 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/IdGenerator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/IdGenerator.java
@@ -48,8 +48,12 @@
 
   private static final int salt = 0x9e3779b9;
 
+  static int mix(int in) {
+    return mix(salt, in);
+  }
+
   /** A very simple bit permutation to mask a simple incrementer. */
-  static int mix(final int in) {
+  public static int mix(final int salt, final int in) {
     short v0 = hi16(in);
     short v1 = lo16(in);
     v0 += ((v1 << 2) + 0 ^ v1) + (salt ^ (v1 >>> 3)) + 1;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/MagicBranch.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/MagicBranch.java
index 510deaa02..60844ee 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/MagicBranch.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/MagicBranch.java
@@ -46,12 +46,23 @@
 
   /** 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 refName.startsWith(NEW_DRAFT_CHANGE)
+        || refName.startsWith(NEW_PUBLISH_CHANGE)
+        || refName.startsWith(NEW_CHANGE);
+  }
+
+  /** Returns the ref name prefix for a magic branch, <code>null</code> if the branch is not magic */
+  public static String getMagicRefNamePrefix(String refName) {
+    if (refName.startsWith(NEW_DRAFT_CHANGE)) {
+      return NEW_DRAFT_CHANGE;
     }
-    return false;
+    if (refName.startsWith(NEW_PUBLISH_CHANGE)) {
+      return NEW_PUBLISH_CHANGE;
+    }
+    if (refName.startsWith(NEW_CHANGE)) {
+      return NEW_CHANGE;
+    }
+    return null;
   }
 
   /**
@@ -80,11 +91,6 @@
     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;
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/PluginRequestContext.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/PluginRequestContext.java
new file mode 100644
index 0000000..a836fd7
--- /dev/null
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/PluginRequestContext.java
@@ -0,0 +1,46 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY 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.server.ReviewDb;
+import com.google.gerrit.server.CurrentUser;
+import com.google.gerrit.server.PluginUser;
+import com.google.inject.Provider;
+import com.google.inject.ProvisionException;
+
+/** RequestContext active while plugins load or unload. */
+public class PluginRequestContext implements RequestContext {
+  private final PluginUser user;
+
+  public PluginRequestContext(PluginUser user) {
+    this.user = user;
+  }
+
+  @Override
+  public CurrentUser getCurrentUser() {
+    return user;
+  }
+
+  @Override
+  public Provider<ReviewDb> getReviewDbProvider() {
+    return new Provider<ReviewDb>() {
+      @Override
+      public ReviewDb get() {
+        throw new ProvisionException(
+            "Automatic ReviewDb only available in request scope");
+      }
+    };
+  }
+}
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestContext.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestContext.java
index ca8573f..86c74e0 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestContext.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestContext.java
@@ -14,13 +14,15 @@
 
 package com.google.gerrit.server.util;
 
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
+import com.google.inject.Provider;
 
 /**
  * The RequestContext is an interface exposing the fields that are needed
  * by the GerritGlobalModule scope.
  */
 public interface RequestContext {
-
   CurrentUser getCurrentUser();
+  Provider<ReviewDb> getReviewDbProvider();
 }
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestScopePropagator.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestScopePropagator.java
index 84c61e9..f63da5439 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestScopePropagator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/RequestScopePropagator.java
@@ -14,8 +14,14 @@
 
 package com.google.gerrit.server.util;
 
-import com.google.gerrit.reviewdb.client.Project.NameKey;
+import static com.google.common.base.Preconditions.checkNotNull;
+
+import com.google.common.base.Throwables;
+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.RequestCleanup;
+import com.google.gerrit.server.config.RequestScopedReviewDbProvider;
 import com.google.gerrit.server.git.ProjectRunnable;
 import com.google.inject.Key;
 import com.google.inject.Provider;
@@ -43,19 +49,26 @@
 
   private final Scope scope;
   private final ThreadLocalRequestContext local;
+  private final Provider<RequestScopedReviewDbProvider> dbProviderProvider;
 
   protected RequestScopePropagator(Scope scope,
-      ThreadLocalRequestContext local) {
+      ThreadLocalRequestContext local,
+      Provider<RequestScopedReviewDbProvider> dbProviderProvider) {
     this.scope = scope;
     this.local = local;
+    this.dbProviderProvider = dbProviderProvider;
   }
 
   /**
-   * 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
+   * Ensures that the current request state is available when the passed in
+   * Callable is invoked.
+   *
+   * If needed wraps the passed in Callable in a new {@link Callable} that
+   * propagates the current request state when the returned 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 or is in
+   * the same 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
@@ -73,12 +86,17 @@
    * @return a new Callable which will execute in the current request scope.
    */
   public final <T> Callable<T> wrap(final Callable<T> callable) {
+    final RequestContext callerContext = checkNotNull(local.getContext());
     final Callable<T> wrapped =
-        wrapImpl(context(local.getContext(), cleanup(callable)));
+        wrapImpl(context(callerContext, cleanup(callable)));
     return new Callable<T>() {
       @Override
       public T call() throws Exception {
-        return wrapped.call();
+        if (callerContext == local.getContext()) {
+          return callable.call();
+        } else {
+          return wrapped.call();
+        }
       }
 
       @Override
@@ -111,15 +129,14 @@
         public void run() {
           try {
             wrapped.call();
-          } catch (RuntimeException e) {
-            throw e;
           } catch (Exception e) {
+            Throwables.propagateIfPossible(e);
             throw new RuntimeException(e); // Not possible.
           }
         }
 
         @Override
-        public NameKey getProjectNameKey() {
+        public Project.NameKey getProjectNameKey() {
           return ((ProjectRunnable) runnable).getProjectNameKey();
         }
 
@@ -169,7 +186,17 @@
     return new Callable<T>() {
       @Override
       public T call() throws Exception {
-        RequestContext old = local.setContext(context);
+        RequestContext old = local.setContext(new RequestContext() {
+          @Override
+          public CurrentUser getCurrentUser() {
+            return context.getCurrentUser();
+          }
+
+          @Override
+          public Provider<ReviewDb> getReviewDbProvider() {
+            return dbProviderProvider.get();
+          }
+        });
         try {
           return callable.call();
         } finally {
@@ -191,7 +218,6 @@
                 return new RequestCleanup();
               }
             }).get();
-
         try {
           return callable.call();
         } finally {
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
index 7310703..fbd8236 100644
--- 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
@@ -81,7 +81,7 @@
           && branch != null && branch.length() > 0) {
         // All required fields filled.
 
-        boolean urlIsRelative = url.startsWith("/");
+        boolean urlIsRelative = url.startsWith("../");
         String server = null;
         if (!urlIsRelative) {
           // It is actually an URI. It could be ssh://localhost/project-a.
@@ -103,8 +103,9 @@
             fromIndex = urlExtractedPath.lastIndexOf('/', fromIndex - 1);
             projectName = urlExtractedPath.substring(fromIndex + 1);
 
-            if (projectName.endsWith(".git")) {
-              projectName = projectName.substring(0, projectName.length() - 4);
+            if (projectName.endsWith(Constants.DOT_GIT_EXT)) {
+              projectName = projectName.substring(0, //
+                  projectName.length() - Constants.DOT_GIT_EXT.length());
             }
 
             if (repoManager.list().contains(new Project.NameKey(projectName))) {
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestContext.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestContext.java
index b411512..bd43e9d 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestContext.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestContext.java
@@ -16,6 +16,7 @@
 
 import com.google.common.base.Objects;
 import com.google.gerrit.common.errors.NotSignedInException;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.inject.AbstractModule;
@@ -64,6 +65,11 @@
         throw new ProvisionException(NotSignedInException.MESSAGE,
             new NotSignedInException());
       }
+
+      @Provides
+      ReviewDb provideReviewDb(RequestContext ctx) {
+        return ctx.getReviewDbProvider().get();
+      }
     };
   }
 
diff --git a/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestScopePropagator.java b/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestScopePropagator.java
index 7728d6f..a31c7c7 100644
--- a/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestScopePropagator.java
+++ b/gerrit-server/src/main/java/com/google/gerrit/server/util/ThreadLocalRequestScopePropagator.java
@@ -14,7 +14,9 @@
 
 package com.google.gerrit.server.util;
 
+import com.google.gerrit.server.config.RequestScopedReviewDbProvider;
 import com.google.inject.OutOfScopeException;
+import com.google.inject.Provider;
 import com.google.inject.Scope;
 
 import java.util.concurrent.Callable;
@@ -31,8 +33,10 @@
   private final ThreadLocal<C> threadLocal;
 
   protected ThreadLocalRequestScopePropagator(Scope scope,
-      ThreadLocal<C> threadLocal, ThreadLocalRequestContext local) {
-    super(scope, local);
+      ThreadLocal<C> threadLocal,
+      ThreadLocalRequestContext local,
+      Provider<RequestScopedReviewDbProvider> dbProviderProvider) {
+    super(scope, local, dbProviderProvider);
     this.threadLocal = threadLocal;
   }
 
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
deleted file mode 100644
index f6f1bfb..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/workflow/CategoryFunction.java
+++ /dev/null
@@ -1,77 +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.client.ApprovalCategory;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
-
-import java.util.HashMap;
-import java.util.Map;
-
-/** Function to control {@link PatchSetApproval}s in an {@link ApprovalCategory}. */
-public abstract class CategoryFunction {
-  private static Map<String, CategoryFunction> all =
-      new HashMap<String, CategoryFunction>();
-  static {
-    all.put(MaxWithBlock.NAME, new MaxWithBlock());
-    all.put(MaxNoBlock.NAME, new MaxNoBlock());
-    all.put(NoOpFunction.NAME, new NoOpFunction());
-    all.put(NoBlock.NAME, new NoBlock());
-  }
-
-  /**
-   * Locate a function by category.
-   *
-   * @param category the category the function is for.
-   * @return the function implementation; {@link NoOpFunction} if the function
-   *         is not known to Gerrit and thus cannot be executed.
-   */
-  public static CategoryFunction forCategory(final ApprovalCategory category) {
-    final CategoryFunction r = all.get(category.getFunctionName());
-    return r != null ? r : new NoOpFunction();
-  }
-
-  /**
-   * Normalize ChangeApprovals and set the valid flag for this category.
-   * <p>
-   * Implementors should invoke:
-   *
-   * <pre>
-   * state.valid(at, true);
-   * </pre>
-   * <p>
-   * If the set of approvals from <code>state.getApprovals(at)</code> covers the
-   * requirements for the function, indicating the category has been completed.
-   * <p>
-   * An example implementation which requires at least one positive and no
-   * negatives might be:
-   *
-   * <pre>
-   * boolean neg = false, pos = false;
-   * for (final ChangeApproval ca : state.getApprovals(at)) {
-   *   state.normalize(ca);
-   *   neg |= ca.getValue() &lt; 0;
-   *   pos |= ca.getValue() &gt; 0;
-   * }
-   * state.valid(at, !neg &amp;&amp; pos);
-   * </pre>
-   *
-   * @param at the cached category description to process.
-   * @param state state to read approvals and project rights from, and to update
-   *        the valid status into.
-   */
-  public abstract void run(ApprovalType at, FunctionState state);
-}
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
deleted file mode 100644
index 74c97f3..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/workflow/FunctionState.java
+++ /dev/null
@@ -1,158 +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.common.data.ApprovalTypes;
-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.project.ChangeControl;
-import com.google.inject.Inject;
-import com.google.inject.assistedinject.Assisted;
-
-import java.util.ArrayList;
-import java.util.Collection;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-/** State passed through to a {@link CategoryFunction}. */
-public class FunctionState {
-  public interface Factory {
-    FunctionState create(ChangeControl c, PatchSet.Id psId,
-        Collection<PatchSetApproval> all);
-  }
-
-  private final ApprovalTypes approvalTypes;
-  private final IdentifiedUser.GenericFactory userFactory;
-
-  private final Map<ApprovalCategory.Id, Collection<PatchSetApproval>> approvals =
-      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;
-
-  @Inject
-  FunctionState(final ApprovalTypes approvalTypes,
-      final IdentifiedUser.GenericFactory userFactory,
-      @Assisted final ChangeControl c, @Assisted final PatchSet.Id psId,
-      @Assisted final Collection<PatchSetApproval> all) {
-    this.approvalTypes = approvalTypes;
-    this.userFactory = userFactory;
-
-    callerChangeControl = c;
-    change = c.getChange();
-
-    for (final PatchSetApproval ca : all) {
-      if (psId.equals(ca.getPatchSetId())) {
-        Collection<PatchSetApproval> l = approvals.get(ca.getCategoryId());
-        if (l == null) {
-          l = new ArrayList<PatchSetApproval>();
-          approvals.put(ca.getCategoryId(), l);
-        }
-        l.add(ca);
-      }
-    }
-  }
-
-  List<ApprovalType> getApprovalTypes() {
-    return approvalTypes.getApprovalTypes();
-  }
-
-  Change getChange() {
-    return change;
-  }
-
-  public void valid(final ApprovalType at, final boolean v) {
-    valid.put(id(at), v);
-  }
-
-  public boolean isValid(final ApprovalType at) {
-    return isValid(id(at));
-  }
-
-  public boolean isValid(final ApprovalCategory.Id id) {
-    final Boolean b = valid.get(id);
-    return b != null && b;
-  }
-
-  public Collection<PatchSetApproval> getApprovals(final ApprovalType at) {
-    return getApprovals(id(at));
-  }
-
-  public Collection<PatchSetApproval> getApprovals(final ApprovalCategory.Id id) {
-    final Collection<PatchSetApproval> l = approvals.get(id);
-    return l != null ? l : Collections.<PatchSetApproval> emptySet();
-  }
-
-  /**
-   * Normalize the approval record down to the range permitted by the type, in
-   * case the type was modified since the approval was originally granted.
-   * <p>
-   */
-  private void applyTypeFloor(final ApprovalType at, final PatchSetApproval a) {
-    final ApprovalCategoryValue atMin = at.getMin();
-
-    if (atMin != null && a.getValue() < atMin.getValue()) {
-      a.setValue(atMin.getValue());
-    }
-
-    final ApprovalCategoryValue atMax = at.getMax();
-    if (atMax != null && a.getValue() > atMax.getValue()) {
-      a.setValue(atMax.getValue());
-    }
-  }
-
-  /**
-   * Normalize the approval record to be inside the maximum range permitted by
-   * the RefRights granted to groups the account is a member of.
-   * <p>
-   * If multiple RefRights are matched (assigned to different groups the account
-   * is a member of) the lowest minValue and the highest maxValue of the union
-   * of them is used.
-   * <p>
-   */
-  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());
-    final PermissionRange range = controlFor(user).getRange(permission);
-    a.setValue((short) range.squash(a.getValue()));
-  }
-
-  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(at, ca);
-  }
-
-  private static Id id(final ApprovalType at) {
-    return at.getCategory().getId();
-  }
-}
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
deleted file mode 100644
index 3272f8a..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/workflow/MaxNoBlock.java
+++ /dev/null
@@ -1,48 +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.workflow;
-
-import com.google.gerrit.common.data.ApprovalType;
-import com.google.gerrit.reviewdb.client.ApprovalCategory;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
-
-/**
- * Computes an {@link ApprovalCategory} by looking at maximum values.
- * <p>
- * In order to be considered "approved" this function requires that:
- * <ul>
- * <li>The maximum positive value is used at least once;</li>
- * <li>The user approving the maximum positive has been granted that.</li>
- * </ul>
- * <p>
- * This function is primarily useful for advisory review fields.
- */
-public class MaxNoBlock extends CategoryFunction {
-  public static String NAME = "MaxNoBlock";
-
-  @Override
-  public void run(final ApprovalType at, final FunctionState state) {
-    boolean passed = false;
-    for (final PatchSetApproval a : state.getApprovals(at)) {
-      state.normalize(at, a);
-
-      passed |= at.isMaxPositive(a);
-    }
-
-    // The type must have at least one max positive (a full accept).
-    //
-    state.valid(at, passed);
-  }
-}
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
deleted file mode 100644
index ce84499..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/workflow/MaxWithBlock.java
+++ /dev/null
@@ -1,63 +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.client.ApprovalCategory;
-import com.google.gerrit.reviewdb.client.PatchSetApproval;
-
-/**
- * Computes an {@link ApprovalCategory} by looking at maximum values.
- * <p>
- * In order to be considered "approved" this function requires that:
- * <ul>
- * <li>The maximum negative value is never used;</li>
- * <li>The maximum positive value is used at least once;</li>
- * <li>The user approving the maximum positive has been granted that.</li>
- * </ul>
- * <p>
- * This function is primarily useful for review fields, with values such as:
- * <ul>
- * <li>+2: Approved change.</li>
- * <li>+1: Looks ok, but get another approval from someone with more depth.</li>
- * <li>-1: Soft reject, it isn't a great change but its OK if approved.</li>
- * <li>-2: Rejected, must not be submitted.
- * </ul>
- * <p>
- * Note that projects using this function would typically want to assign out the
- * middle range (-1 .. +1) to almost everyone, so people can indicate how they
- * feel about a change, but the extremes of -2 and +2 should be reserved for the
- * project's long-term maintainers, those who are most familiar with its code.
- */
-public class MaxWithBlock extends CategoryFunction {
-  public static String NAME = "MaxWithBlock";
-
-  @Override
-  public void run(final ApprovalType at, final FunctionState state) {
-    boolean rejected = false;
-    boolean passed = false;
-    for (final PatchSetApproval a : state.getApprovals(at)) {
-      state.normalize(at, a);
-
-      rejected |= at.isMaxNegative(a);
-      passed |= at.isMaxPositive(a);
-    }
-
-    // The type must not have had its max negative (a forceful reject)
-    // and must have at least one max positive (a full accept).
-    //
-    state.valid(at, !rejected && passed);
-  }
-}
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
deleted file mode 100644
index 08d0705..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/workflow/NoBlock.java
+++ /dev/null
@@ -1,27 +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.workflow;
-
-import com.google.gerrit.common.data.ApprovalType;
-
-/** A function that does nothing. */
-public class NoBlock extends CategoryFunction {
-  public static String NAME = "NoBlock";
-
-  @Override
-  public void run(final ApprovalType at, final FunctionState state) {
-    state.valid(at, 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
deleted file mode 100644
index 8c76cc5..0000000
--- a/gerrit-server/src/main/java/com/google/gerrit/server/workflow/NoOpFunction.java
+++ /dev/null
@@ -1,26 +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;
-
-/** A function that does nothing. */
-public class NoOpFunction extends CategoryFunction {
-  public static String NAME = "NoOp";
-
-  @Override
-  public void run(final ApprovalType at, final FunctionState state) {
-  }
-}
diff --git a/gerrit-server/src/main/java/gerrit/PRED__load_commit_labels_1.java b/gerrit-server/src/main/java/gerrit/PRED__load_commit_labels_1.java
index a26a492..bee046c 100644
--- a/gerrit-server/src/main/java/gerrit/PRED__load_commit_labels_1.java
+++ b/gerrit-server/src/main/java/gerrit/PRED__load_commit_labels_1.java
@@ -2,12 +2,11 @@
 
 package gerrit;
 
-import com.google.gerrit.common.data.ApprovalType;
-import com.google.gerrit.common.data.ApprovalTypes;
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.common.data.LabelTypes;
 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.gerrit.server.query.change.ChangeData;
 import com.google.gwtorm.server.OrmException;
@@ -42,11 +41,11 @@
 
     Term listHead = Prolog.Nil;
     try {
-      PrologEnvironment env = (PrologEnvironment) engine.control;
       ReviewDb db = StoredValues.REVIEW_DB.get(engine);
       PatchSet patchSet = StoredValues.PATCH_SET.get(engine);
       ChangeData cd = StoredValues.CHANGE_DATA.getOrNull(engine);
-      ApprovalTypes types = env.getInjector().getInstance(ApprovalTypes.class);
+      LabelTypes types =
+          StoredValues.CHANGE_CONTROL.get(engine).getLabelTypes();
 
       Iterable<PatchSetApproval> approvals;
       if (cd != null) {
@@ -60,14 +59,14 @@
           continue;
         }
 
-        ApprovalType t = types.byId(a.getCategoryId());
+        LabelType t = types.byLabel(a.getLabelId());
         if (t == null) {
           continue;
         }
 
         StructureTerm labelTerm = new StructureTerm(
             sym_label,
-            SymbolTerm.intern(t.getCategory().getLabelName()),
+            SymbolTerm.intern(t.getName()),
             new IntegerTerm(a.getValue()));
 
         StructureTerm userTerm = new StructureTerm(
diff --git a/gerrit-server/src/main/java/gerrit/PRED_commit_delta_4.java b/gerrit-server/src/main/java/gerrit/PRED_commit_delta_4.java
index 9ce7098..51a871c 100644
--- a/gerrit-server/src/main/java/gerrit/PRED_commit_delta_4.java
+++ b/gerrit-server/src/main/java/gerrit/PRED_commit_delta_4.java
@@ -169,6 +169,8 @@
         return rename;
       case COPIED:
         return copy;
+      case REWRITE:
+        break;
     }
     throw new IllegalArgumentException("ChangeType not recognized");
   }
diff --git a/gerrit-server/src/main/java/gerrit/PRED_commit_stats_3.java b/gerrit-server/src/main/java/gerrit/PRED_commit_stats_3.java
new file mode 100644
index 0000000..daf9948
--- /dev/null
+++ b/gerrit-server/src/main/java/gerrit/PRED_commit_stats_3.java
@@ -0,0 +1,61 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package gerrit;
+
+import com.google.gerrit.rules.StoredValues;
+import com.google.gerrit.server.patch.PatchList;
+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.Term;
+
+/**
+ * Exports basic commit statistics.
+ *
+ * <pre>
+ *   'commit_stats'(-Files, -Insertions, -Deletions)
+ * </pre>
+ */
+public class PRED_commit_stats_3 extends Predicate.P3 {
+  public PRED_commit_stats_3(Term a1, Term a2, Term a3, Operation n) {
+    arg1 = a1;
+    arg2 = a2;
+    arg3 = a3;
+    cont = n;
+  }
+
+  @Override
+  public Operation exec(Prolog engine) throws PrologException {
+    engine.setB0();
+
+    Term a1 = arg1.dereference();
+    Term a2 = arg2.dereference();
+    Term a3 = arg3.dereference();
+
+    PatchList pl = StoredValues.PATCH_LIST.get(engine);
+    if(!a1.unify(new IntegerTerm(pl.getPatches().size() -1),engine.trail)) { //Account for /COMMIT_MSG.
+      return engine.fail();
+    }
+    if(!a2.unify(new IntegerTerm(pl.getInsertions()),engine.trail)) {
+      return engine.fail();
+    }
+    if(!a3.unify(new IntegerTerm(pl.getDeletions()),engine.trail)) {
+      return engine.fail();
+    }
+    return cont;
+  }
+}
diff --git a/gerrit-server/src/main/java/gerrit/PRED_current_user_1.java b/gerrit-server/src/main/java/gerrit/PRED_current_user_1.java
index 09a46f7..11911bd 100644
--- a/gerrit-server/src/main/java/gerrit/PRED_current_user_1.java
+++ b/gerrit-server/src/main/java/gerrit/PRED_current_user_1.java
@@ -36,7 +36,6 @@
   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;
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
deleted file mode 100644
index cbe0fd8..0000000
--- a/gerrit-server/src/main/java/gerrit/PRED_get_legacy_approval_types_1.java
+++ /dev/null
@@ -1,82 +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 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 {
-  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/java/gerrit/PRED_get_legacy_label_types_1.java b/gerrit-server/src/main/java/gerrit/PRED_get_legacy_label_types_1.java
new file mode 100644
index 0000000..161da79
--- /dev/null
+++ b/gerrit-server/src/main/java/gerrit/PRED_get_legacy_label_types_1.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 gerrit;
+
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.rules.PrologEnvironment;
+import com.google.gerrit.rules.StoredValues;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectState;
+
+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 label types from the server configuration.
+ * <p>
+ * Unifies to a Prolog list of: {@code label_type(Label, Fun, Min, Max)}
+ * where:
+ * <ul>
+ * <li>{@code Label} - the newer style label name</li>
+ * <li>{@code Fun} - legacy function name</li>
+ * <li>{@code Min, Max} - the smallest and largest configured values.</li>
+ * </ul>
+ */
+class PRED_get_legacy_label_types_1 extends Predicate.P1 {
+  PRED_get_legacy_label_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;
+    ProjectState state = env.getInjector().getInstance(ProjectCache.class)
+        .get(StoredValues.CHANGE.get(engine).getDest().getParentKey());
+    if (state == null) {
+      return engine.fail();
+    }
+    List<LabelType> list = state.getLabelTypes().getLabelTypes();
+    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 symLabelType = SymbolTerm.intern(
+      "label_type", 4);
+
+  static Term export(LabelType type) {
+    return new StructureTerm(symLabelType,
+        SymbolTerm.intern(type.getName()),
+        SymbolTerm.intern(type.getFunctionName()),
+        new IntegerTerm(type.getMin().getValue()),
+        new IntegerTerm(type.getMax().getValue()));
+  }
+}
diff --git a/gerrit-server/src/main/java/gerrit/PRED_project_default_submit_type_1.java b/gerrit-server/src/main/java/gerrit/PRED_project_default_submit_type_1.java
new file mode 100644
index 0000000..0f173c7
--- /dev/null
+++ b/gerrit-server/src/main/java/gerrit/PRED_project_default_submit_type_1.java
@@ -0,0 +1,58 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package gerrit;
+
+import com.google.gerrit.reviewdb.client.Project.SubmitType;
+import com.google.gerrit.rules.StoredValues;
+import com.google.gerrit.server.project.ChangeControl;
+
+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_project_default_submit_type_1 extends Predicate.P1 {
+
+  private static final SymbolTerm[] term;
+
+  static {
+    SubmitType[] val = SubmitType.values();
+    term = new SymbolTerm[val.length];
+    for (int i = 0; i < val.length; i++) {
+      term[i] = SymbolTerm.create(val[i].name());
+    }
+  }
+
+  public PRED_project_default_submit_type_1(Term a1, Operation n) {
+    arg1 = a1;
+    cont = n;
+  }
+
+  @Override
+  public Operation exec(Prolog engine) throws PrologException {
+    engine.setB0();
+    Term a1 = arg1.dereference();
+
+    ChangeControl control = StoredValues.CHANGE_CONTROL.get(engine);
+    SubmitType submitType = control.getProject().getSubmitType();
+
+    if (!a1.unify(term[submitType.ordinal()], engine.trail)) {
+      return engine.fail();
+    }
+    return cont;
+  }
+}
diff --git a/gerrit-server/src/main/prolog/gerrit_common.pl b/gerrit-server/src/main/prolog/gerrit_common.pl
index a75acc0..71e7383 100644
--- a/gerrit-server/src/main/prolog/gerrit_common.pl
+++ b/gerrit-server/src/main/prolog/gerrit_common.pl
@@ -140,12 +140,12 @@
 :- public can_submit/2.
 %%
 can_submit(SubmitRule, S) :-
-  call_submit_rule(SubmitRule, Tmp),
+  call_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.
+call_rule(P:X, Arg) :- !, F =.. [X, Arg], P:F.
+call_rule(X, Arg) :- !, F =.. [X, Arg], F.
 
 is_all_ok([]).
 is_all_ok([label(_, ok(__)) | Ls]) :- is_all_ok(Ls).
@@ -155,6 +155,21 @@
 
 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
 %%
+%% locate_helper
+%%
+%%   Returns user:Func if it exists otherwise returns gerrit:Default
+
+locate_helper(Func, Default, Arity, user:Func) :-
+    '$compiled_predicate'(user, Func, Arity), !.
+locate_helper(Func, Default, Arity, user:Func) :-
+    listN(Arity, P), C =.. [Func | P], clause(user:C, _), !.
+locate_helper(Func, Default, _, gerrit:Default).
+
+listN(0, []).
+listN(N, [_|T]) :- N > 0, N1 is N - 1, listN(N1, T).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%
 %% locate_submit_rule/1:
 %%
 %%   Finds a submit_rule depending on what rules are available.
@@ -164,17 +179,32 @@
 %%
 
 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.
+  locate_helper(submit_rule, default_submit, 1, RuleName).
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%
+%% get_submit_type/2:
+%%
+%%   Executes the SubmitTypeRule and return the first solution
+%%
+:- public get_submit_type/2.
+%%
+get_submit_type(SubmitTypeRule, A) :-
+  call_rule(SubmitTypeRule, A), !.
+
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%
+%% locate_submit_type/1:
+%%
+%%   Finds a submit_type_rule depending on what rules are available.
+%%   If none are available, use project_default_submit_type/1.
+%%
+:- public locate_submit_type/1.
+%%
+locate_submit_type(RuleName) :-
+  locate_helper(submit_type, project_default_submit_type, 1, RuleName).
 
 
 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
@@ -184,22 +214,22 @@
 :- public default_submit/1.
 %%
 default_submit(P) :-
-  get_legacy_approval_types(ApprovalTypes),
-  default_submit(ApprovalTypes, P).
+  get_legacy_label_types(LabelTypes),
+  default_submit(LabelTypes, 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.
+% loop by scanning over all of the label types to build up the
+% submit record.
 %
-default_submit(ApprovalTypes, P) :-
-  default_submit(ApprovalTypes, [], Tmp),
+default_submit(LabelTypes, P) :-
+  default_submit(LabelTypes, [], 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),
+  label_type(Label, Fun, Min, Max) = Type,
+  legacy_submit_rule(Fun, Label, Min, Max, Status),
   R = label(Label, Status),
   default_submit(Types, [R | Tmp], Out).
 
@@ -208,12 +238,11 @@
 %%
 %% 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 = may(_).
-legacy_submit_rule('NoOp', Label, Id, Min, Max, T) :- !, T = may(_).
-legacy_submit_rule(Fun, Label, Id, Min, Max, T) :- T = impossible(unsupported(Fun)).
-
+legacy_submit_rule('MaxWithBlock', Label, Min, Max, T) :- !, max_with_block(Label, Min, Max, T).
+legacy_submit_rule('MaxNoBlock', Label, Min, Max, T) :- !, max_no_block(Label, Max, T).
+legacy_submit_rule('NoBlock', Label, Min, Max, T) :- !, T = may(_).
+legacy_submit_rule('NoOp', Label, Min, Max, T) :- !, T = may(_).
+legacy_submit_rule(Fun, Label, Min, Max, T) :- T = impossible(unsupported(Fun)).
 
 %% max_with_block:
 %%
@@ -222,6 +251,10 @@
 %%
 :- public max_with_block/4.
 %%
+max_with_block(Min, Max, Label, label(Label, S)) :-
+  number(Min), number(Max), atom(Label),
+  !,
+  max_with_block(Label, Min, Max, S).
 max_with_block(Label, Min, Max, reject(Who)) :-
   check_label_range_permission(Label, Min, ok(Who)),
   !
@@ -306,6 +339,17 @@
 call_submit_filter(P:X, R, S) :- !, F =.. [X, R, S], P:F.
 call_submit_filter(X, R, S) :- F =.. [X, R, S], F.
 
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%
+%% filter_submit_type_results/3:
+%%
+%%   Executes the submit_type_filter against the result,
+%%   returns the filtered result.
+%%
+:- public filter_submit_type_results/3.
+%%
+filter_submit_type_results(Filter, In, Out) :- call_submit_filter(Filter, In, Out).
+
 
 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
 %%
@@ -316,15 +360,26 @@
 :- 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
-  .
+  locate_helper(submit_filter, noop_filter, 2, FilterName).
 
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%
+%% noop_filter/2:
+%%
+:- public noop_filter/2.
+%%
+noop_filter(In, In).
+
+%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+%%
+%% locate_submit_type_filter/1:
+%%
+%%   Finds a submit_type_filter if available.
+%%
+:- public locate_submit_type_filter/1.
+%%
+locate_submit_type_filter(FilterName) :-
+  locate_helper(submit_type_filter, noop_filter, 2, FilterName).
 
 %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
 %%
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/change/ChangeMessages.properties b/gerrit-server/src/main/resources/com/google/gerrit/server/change/ChangeMessages.properties
new file mode 100644
index 0000000..11384a1
--- /dev/null
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/change/ChangeMessages.properties
@@ -0,0 +1,8 @@
+# Changes to this file should also be made in
+# gerrit-gwtui/src/main/java/com/google/gerrit/client/changes/ChangeMessages.properties
+revertChangeDefaultMessage = Revert \"{0}\"\n\nThis reverts commit {1}
+reviewerNotFound = {0} does not identify a registered user or group
+
+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?
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 a67c38c..acec1d1 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
@@ -31,7 +31,7 @@
 ## The ChangeSubject.vm template will determine the contents of the email
 ## subject line for ALL emails related to changes.
 ##
-#macro(elipses $length $str)
-#if($str.length() > $length)${str.substring(0,$length)}...#else$str#end
+#macro(ellipsis $length $str)
+#if($str.length() > $length)#set($length = $length - 3)${str.substring(0,$length)}...#else$str#end
 #end
-Change in $projectName.replaceAll('/.*/', '...')[$branch.shortName]: #elipses(60, $change.subject)
+Change in $projectName.replaceAll('/.*/', '...')[$branch.shortName]: #ellipsis(63, $change.subject)
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 9af98a6..e64677d 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
@@ -30,9 +30,9 @@
 ## --------------
 ## The Comment.vm template will determine the contents of the email related to
 ## a user submitting comments on changes.  It is a ChangeEmail: see
-## ChangeSubject.vm and ChangeFooter.vm.
+## ChangeSubject.vm, ChangeFooter.vm and CommentFooter.vm.
 ##
-#if ($email.coverLetter || $email.inlineComments)
+#if ($email.coverLetter || $email.hasInlineComments())
 $fromName has posted comments on this change.
 
 Change subject: $change.subject
@@ -45,9 +45,9 @@
 #end
 ##
 ## It is possible to increase the span of the quoted lines by using the line
-## count parameter when calling $email.inlineComments as a function.
+## count parameter when calling $email.getInlineComments as a function.
 ##
-## Example: #if($email.inlineComments)$email.getInlineComments(5)#end
+## Example: #if($email.hasInlineComments())$email.getInlineComments(5)#end
 ##
-#if($email.inlineComments)$email.inlineComments#end
+#if($email.hasInlineComments())$email.inlineComments#end
 #end
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/CommentFooter.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/CommentFooter.vm
new file mode 100644
index 0000000..e0832e6
--- /dev/null
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/CommentFooter.vm
@@ -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.
+##
+##
+## 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 CommentFooter.vm template will determine the contents of the footer
+## text that will be appended to emails related to a user submitting comments
+## on changes.
+##
+## See ChangeSubject.vm and ChangeFooter.vm.
+#if($email.hasInlineComments())
+Gerrit-HasComments: Yes
+#else
+Gerrit-HasComments: No
+#end
diff --git a/gerrit-server/src/main/resources/com/google/gerrit/server/mail/CommitMessageEdited.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/CommitMessageEdited.vm
new file mode 100644
index 0000000..f583101
--- /dev/null
+++ b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/CommitMessageEdited.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 CommitMessageUpdated.vm template will determine the contents of the email
+## related to a user editing the commit message 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 change.#if($email.changeUrl)  Please visit
+
+    $email.changeUrl
+
+to look at the new patch set with edited commit message (#$patchSet.patchSetId).
+#end
+#else
+$fromName has created a new patch set by editing the commit message 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/Merged.vm b/gerrit-server/src/main/resources/com/google/gerrit/server/mail/Merged.vm
index bcbc7bd..22e29e8 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
@@ -32,9 +32,6 @@
 ## a change successfully merged to the head.  It is a ChangeEmail: see
 ## ChangeSubject.vm and ChangeFooter.vm.
 ##
-#macro(elipses $length $str)
-#if($str.length() > $length)${str.substring(0,$length)}...#else$str#end
-#end
 $fromName has submitted this change and it was merged.
 
 Change subject: $change.subject
@@ -42,3 +39,7 @@
 
 
 $email.changeDetail$email.approvals
+
+#if($email.includeDiff)
+$email.UnifiedDiff
+#end
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 06926df..b29e25c 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
@@ -153,7 +153,7 @@
 		if (unprinted) {
 			print "Change-Id: I'"$id"'"
 		}
-	}' "$MSG" > $T && mv $T "$MSG" || rm -f $T
+	}' "$MSG" > "$T" && mv "$T" "$MSG" || rm -f "$T"
 }
 _gen_ChangeIdInput() {
 	echo "tree `git write-tree`"
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
index 9f2ccbf..98b0b4a 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/rules/GerritCommonTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/rules/GerritCommonTest.java
@@ -14,68 +14,115 @@
 
 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.gerrit.common.data.LabelType;
+import com.google.gerrit.common.data.LabelTypes;
+import com.google.gerrit.common.data.LabelValue;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.AccountGroup;
+import com.google.gerrit.reviewdb.client.Branch;
+import com.google.gerrit.reviewdb.client.Change;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.git.ProjectConfig;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectState;
 import com.google.inject.AbstractModule;
 
-import java.util.ArrayList;
 import java.util.Arrays;
-import java.util.List;
+import java.util.Set;
 
 public class GerritCommonTest extends PrologTestCase {
+  private Projects projects;
+
   @Override
   public void setUp() throws Exception {
     super.setUp();
-
-    final ApprovalTypes types = new ApprovalTypes(Arrays.asList(
-        codeReviewCategory(),
-        verifiedCategory()
-    ));
-
+    projects = new Projects(new LabelTypes(Arrays.asList(
+        category("Code-Review",
+            value(2, "Looks good to me, approved"),
+            value(1, "Looks good to me, but someone else must approve"),
+            value(0, "No score"),
+            value(-1, "I would prefer that you didn't submit this"),
+            value(-2, "Do not submit")),
+        category("Verified", value(1, "Verified"),
+            value(0, "No score"), value(-1, "Fails")))));
     load("gerrit", "gerrit_common_test.pl", new AbstractModule() {
       @Override
       protected void configure() {
-        bind(ApprovalTypes.class).toInstance(types);
+        bind(ProjectCache.class).toInstance(projects);
       }
     });
   }
 
-  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);
+  @Override
+  protected void setUpEnvironment(PrologEnvironment env) {
+    env.set(StoredValues.CHANGE, new Change(
+        new Change.Key("Ibeef"), new Change.Id(1), new Account.Id(2),
+        new Branch.NameKey(projects.allProjectsName, "master")));
   }
 
-  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 LabelValue value(int value, String text) {
+    return new LabelValue((short) value, text);
   }
 
-  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 LabelType category(String name, LabelValue... values) {
+    return new LabelType(name, Arrays.asList(values));
   }
 
-  private static ArrayList<ApprovalCategoryValue> newList() {
-    return new ArrayList<ApprovalCategoryValue>();
-  }
+  private static class Projects implements ProjectCache {
+    private final AllProjectsName allProjectsName;
+    private final ProjectState allProjects;
 
-  private static ApprovalCategoryValue value(ApprovalCategory c, int v, String n) {
-    return new ApprovalCategoryValue(
-        new ApprovalCategoryValue.Id(c.getId(), (short) v),
-        n);
+    private Projects(LabelTypes labelTypes) {
+      allProjectsName = new AllProjectsName("All-Projects");
+      ProjectConfig config = new ProjectConfig(allProjectsName);
+      config.createInMemory();
+      for (LabelType label : labelTypes.getLabelTypes()) {
+        config.getLabelSections().put(label.getName(), label);
+      }
+      allProjects = new ProjectState(this, allProjectsName, null,
+          null, null, null, config);
+    }
+
+    @Override
+    public ProjectState getAllProjects() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public ProjectState get(Project.NameKey projectName) {
+      assertEquals(allProjectsName, projectName);
+      return allProjects;
+    }
+
+    @Override
+    public void evict(Project p) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void remove(Project p) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Iterable<Project.NameKey> all() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Set<AccountGroup.UUID> guessRelevantGroupUUIDs() {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public Iterable<Project.NameKey> byName(String prefix) {
+      throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public void onCreateProject(Project.NameKey newProjectName) {
+      throw new UnsupportedOperationException();
+    }
   }
 }
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
index cc9d40f..83809a4 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/rules/PrologTestCase.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/rules/PrologTestCase.java
@@ -79,6 +79,9 @@
     machine = PrologMachineCopy.save(env);
   }
 
+  protected void setUpEnvironment(PrologEnvironment env) {
+  }
+
   private PrologMachineCopy newMachine() {
     BufferingPrologControl ctl = new BufferingPrologControl();
     ctl.setMaxDatabaseSize(16 * 1024);
@@ -117,6 +120,7 @@
 
     for (Term test : tests) {
       PrologEnvironment env = envFactory.create(machine);
+      setUpEnvironment(env);
       env.setEnabled(Prolog.Feature.IO, true);
 
       System.out.format("Prolog %-60s ...", removePackage(test));
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/git/SubmoduleOpTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/git/SubmoduleOpTest.java
index 8d061eb..322ffe2 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/git/SubmoduleOpTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/git/SubmoduleOpTest.java
@@ -14,10 +14,13 @@
 
 package com.google.gerrit.server.git;
 
+import static org.easymock.EasyMock.capture;
 import static org.easymock.EasyMock.createStrictMock;
+import static org.easymock.EasyMock.eq;
 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.Account;
 import com.google.gerrit.reviewdb.client.Branch;
@@ -34,6 +37,7 @@
 import com.google.gwtorm.server.StandardKeyEncoder;
 import com.google.inject.Provider;
 
+import org.easymock.Capture;
 import org.eclipse.jgit.api.Git;
 import org.eclipse.jgit.dircache.DirCacheBuilder;
 import org.eclipse.jgit.dircache.DirCacheEntry;
@@ -43,6 +47,7 @@
 import org.eclipse.jgit.lib.FileMode;
 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.revwalk.RevCommit;
 import org.eclipse.jgit.revwalk.RevWalk;
@@ -72,7 +77,7 @@
   private ReviewDb schema;
   private Provider<String> urlProvider;
   private GitRepositoryManager repoManager;
-  private GitReferenceUpdated replication;
+  private GitReferenceUpdated gitRefUpdated;
 
   @SuppressWarnings("unchecked")
   @Override
@@ -85,22 +90,22 @@
     subscriptions = createStrictMock(SubmoduleSubscriptionAccess.class);
     urlProvider = createStrictMock(Provider.class);
     repoManager = createStrictMock(GitRepositoryManager.class);
-    replication = createStrictMock(GitReferenceUpdated.class);
+    gitRefUpdated = createStrictMock(GitReferenceUpdated.class);
   }
 
   private void doReplay() {
     replay(schemaFactory, schema, subscriptions, urlProvider, repoManager,
-        replication);
+        gitRefUpdated);
   }
 
   private void doVerify() {
     verify(schemaFactory, schema, subscriptions, urlProvider, repoManager,
-        replication);
+        gitRefUpdated);
   }
 
   /**
    * 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
+   * (it does not have a .gitmodules file) and the project the commit was merged
    * is not a submodule of other project.
    *
    * @throws Exception If an exception occurs.
@@ -603,17 +608,17 @@
 
     final CodeReviewCommit codeReviewCommit =
         new CodeReviewCommit(sourceMergeTip.toObjectId());
-    final Change submitedChange =
+    final Change submittedChange =
         new Change(new Change.Key(sourceMergeTip.toObjectId().getName()),
             new Change.Id(1), new Account.Id(1), sourceBranchNameKey);
-    codeReviewCommit.change = submitedChange;
+    codeReviewCommit.change = submittedChange;
 
     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 List<Change> submitted = new ArrayList<Change>();
+    submitted.add(submittedChange);
 
     final Repository targetRepository = createWorkRepository();
     final Git targetGit = new Git(targetRepository);
@@ -639,8 +644,9 @@
     expect(repoManager.openRepository(targetBranchNameKey.getParentKey()))
         .andReturn(targetRepository);
 
-    replication.fire(targetBranchNameKey.getParentKey(),
-        targetBranchNameKey.get());
+    Capture<RefUpdate> ruCapture = new Capture<RefUpdate>();
+    gitRefUpdated.fire(eq(targetBranchNameKey.getParentKey()),
+        capture(ruCapture));
 
     expect(schema.submoduleSubscriptions()).andReturn(subscriptions);
     final ResultSet<SubmoduleSubscription> emptySubscriptions =
@@ -658,12 +664,14 @@
     final SubmoduleOp submoduleOp =
         new SubmoduleOp(sourceBranchNameKey, sourceMergeTip, new RevWalk(
             sourceRepository), urlProvider, schemaFactory, sourceRepository,
-            new Project(sourceBranchNameKey.getParentKey()), submited,
-            mergedCommits, myIdent, repoManager, replication);
+            new Project(sourceBranchNameKey.getParentKey()), submitted,
+            mergedCommits, myIdent, repoManager, gitRefUpdated);
 
     submoduleOp.update();
 
     doVerify();
+    RefUpdate ru = ruCapture.getValue();
+    assertEquals(ru.getName(), targetBranchNameKey.get());
   }
 
   /**
@@ -704,17 +712,17 @@
 
     final CodeReviewCommit codeReviewCommit =
         new CodeReviewCommit(sourceMergeTip.toObjectId());
-    final Change submitedChange =
+    final Change submittedChange =
         new Change(new Change.Key(sourceMergeTip.toObjectId().getName()),
             new Change.Id(1), new Account.Id(1), sourceBranchNameKey);
-    codeReviewCommit.change = submitedChange;
+    codeReviewCommit.change = submittedChange;
 
     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 List<Change> submitted = new ArrayList<Change>();
+    submitted.add(submittedChange);
 
     final Repository targetRepository = createWorkRepository();
     final Git targetGit = new Git(targetRepository);
@@ -740,8 +748,9 @@
     expect(repoManager.openRepository(targetBranchNameKey.getParentKey()))
         .andReturn(targetRepository);
 
-    replication.fire(targetBranchNameKey.getParentKey(),
-        targetBranchNameKey.get());
+    Capture<RefUpdate> ruCapture = new Capture<RefUpdate>();
+    gitRefUpdated.fire(eq(targetBranchNameKey.getParentKey()),
+        capture(ruCapture));
 
     expect(schema.submoduleSubscriptions()).andReturn(subscriptions);
     final ResultSet<SubmoduleSubscription> incorrectSubscriptions =
@@ -761,12 +770,14 @@
     final SubmoduleOp submoduleOp =
         new SubmoduleOp(sourceBranchNameKey, sourceMergeTip, new RevWalk(
             sourceRepository), urlProvider, schemaFactory, sourceRepository,
-            new Project(sourceBranchNameKey.getParentKey()), submited,
-            mergedCommits, myIdent, repoManager, replication);
+            new Project(sourceBranchNameKey.getParentKey()), submitted,
+            mergedCommits, myIdent, repoManager, gitRefUpdated);
 
     submoduleOp.update();
 
     doVerify();
+    RefUpdate ru = ruCapture.getValue();
+    assertEquals(ru.getName(), targetBranchNameKey.get());
   }
 
   /**
@@ -784,7 +795,7 @@
    * @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.
+   *        .gitmodules file.
    * @throws Exception If an exception occurs.
    */
   private void doOneSubscriptionInsert(final String gitModulesFileContent,
@@ -815,7 +826,7 @@
    * source of another project (no subscribers found to this project).
    * </p>
    *
-   * @param gitModulesFileContent The .gitmodule file content.
+   * @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
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 d4b07ae..51ea5a2 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,6 +14,7 @@
 
 package com.google.gerrit.server.project;
 
+import static com.google.gerrit.common.data.Permission.EDIT_TOPIC_NAME;
 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;
@@ -33,7 +34,6 @@
 import com.google.gerrit.reviewdb.client.Project;
 import com.google.gerrit.rules.PrologEnvironment;
 import com.google.gerrit.rules.RulesCache;
-import com.google.gerrit.server.AccessPath;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.account.CapabilityControl;
 import com.google.gerrit.server.account.GroupMembership;
@@ -254,6 +254,139 @@
     assertFalse("u can't vote -2", range.contains(-2));
     assertFalse("u can't vote 2", range.contains(2));
   }
+
+  public void testUnblockNoForce() {
+    grant(local, PUSH, anonymous, "refs/heads/*").setBlock();
+    grant(local, PUSH, devs, "refs/heads/*");
+
+    ProjectControl u = user(devs);
+    assertTrue("u can push", u.controlForRef("refs/heads/master").canUpdate());
+  }
+
+  public void testUnblockForce() {
+    PermissionRule r = grant(local, PUSH, anonymous, "refs/heads/*");
+    r.setBlock();
+    r.setForce(true);
+    grant(local, PUSH, devs, "refs/heads/*").setForce(true);
+
+    ProjectControl u = user(devs);
+    assertTrue("u can force push", u.controlForRef("refs/heads/master").canForceUpdate());
+  }
+
+  public void testUnblockForceWithAllowNoForce_NotPossible() {
+    PermissionRule r = grant(local, PUSH, anonymous, "refs/heads/*");
+    r.setBlock();
+    r.setForce(true);
+    grant(local, PUSH, devs, "refs/heads/*");
+
+    ProjectControl u = user(devs);
+    assertFalse("u can't force push", u.controlForRef("refs/heads/master").canForceUpdate());
+  }
+
+  public void testUnblockMoreSpecificRef_Fails() {
+    grant(local, PUSH, anonymous, "refs/heads/*").setBlock();
+    grant(local, PUSH, devs, "refs/heads/master");
+
+    ProjectControl u = user(devs);
+    assertFalse("u can't push", u.controlForRef("refs/heads/master").canUpdate());
+  }
+
+  public void testUnblockLargerScope_Fails() {
+    grant(local, PUSH, anonymous, "refs/heads/master").setBlock();
+    grant(local, PUSH, devs, "refs/heads/*");
+
+    ProjectControl u = user(devs);
+    assertFalse("u can't push", u.controlForRef("refs/heads/master").canUpdate());
+  }
+
+  public void testUnblockInLocal_Fails() {
+    grant(parent, PUSH, anonymous, "refs/heads/*").setBlock();
+    grant(local, PUSH, fixers, "refs/heads/*");
+
+    ProjectControl f = user(fixers);
+    assertFalse("u can't push", f.controlForRef("refs/heads/master").canUpdate());
+  }
+
+  public void testUnblockInParentBlockInLocal() {
+    grant(parent, PUSH, anonymous, "refs/heads/*").setBlock();
+    grant(parent, PUSH, devs, "refs/heads/*");
+    grant(local, PUSH, devs, "refs/heads/*").setBlock();
+
+    ProjectControl d = user(devs);
+    assertFalse("u can't push", d.controlForRef("refs/heads/master").canUpdate());
+  }
+
+  public void testUnblockVisibilityByRegisteredUsers() {
+    grant(local, READ, anonymous, "refs/heads/*").setBlock();
+    grant(local, READ, registered, "refs/heads/*");
+
+    ProjectControl u = user(registered);
+    assertTrue("u can read", u.controlForRef("refs/heads/master").isVisibleByRegisteredUsers());
+  }
+
+  public void testUnblockInLocalVisibilityByRegisteredUsers_Fails() {
+    grant(parent, READ, anonymous, "refs/heads/*").setBlock();
+    grant(local, READ, registered, "refs/heads/*");
+
+    ProjectControl u = user(registered);
+    assertFalse("u can't read", u.controlForRef("refs/heads/master").isVisibleByRegisteredUsers());
+  }
+
+  public void testUnblockForceEditTopicName() {
+    grant(local, EDIT_TOPIC_NAME, anonymous, "refs/heads/*").setBlock();
+    grant(local, EDIT_TOPIC_NAME, devs, "refs/heads/*").setForce(true);
+
+    ProjectControl u = user(devs);
+    assertTrue("u can edit topic name", u.controlForRef("refs/heads/master").canForceEditTopicName());
+  }
+
+  public void testUnblockInLocalForceEditTopicName_Fails() {
+    grant(parent, EDIT_TOPIC_NAME, anonymous, "refs/heads/*").setBlock();
+    grant(local, EDIT_TOPIC_NAME, devs, "refs/heads/*").setForce(true);
+
+    ProjectControl u = user(registered);
+    assertFalse("u can't edit topic name", u.controlForRef("refs/heads/master").canForceEditTopicName());
+  }
+
+  public void testUnblockRange() {
+    grant(local, LABEL + "Code-Review", -1, +1, anonymous, "refs/heads/*").setBlock();
+    grant(local, LABEL + "Code-Review", -2, +2, devs, "refs/heads/*");
+
+    ProjectControl u = user(devs);
+    PermissionRange range = u.controlForRef("refs/heads/master").getRange(LABEL + "Code-Review");
+    assertTrue("u can vote -2", range.contains(-2));
+    assertTrue("u can vote +2", range.contains(2));
+  }
+
+  public void testUnblockRangeOnMoreSpecificRef_Fails() {
+    grant(local, LABEL + "Code-Review", -1, +1, anonymous, "refs/heads/*").setBlock();
+    grant(local, LABEL + "Code-Review", -2, +2, devs, "refs/heads/master");
+
+    ProjectControl u = user(devs);
+    PermissionRange range = u.controlForRef("refs/heads/master").getRange(LABEL + "Code-Review");
+    assertFalse("u can't vote -2", range.contains(-2));
+    assertFalse("u can't vote +2", range.contains(-2));
+  }
+
+  public void testUnblockRangeOnLargerScope_Fails() {
+    grant(local, LABEL + "Code-Review", -1, +1, anonymous, "refs/heads/master").setBlock();
+    grant(local, LABEL + "Code-Review", -2, +2, devs, "refs/heads/*");
+
+    ProjectControl u = user(devs);
+    PermissionRange range = u.controlForRef("refs/heads/master").getRange(LABEL + "Code-Review");
+    assertFalse("u can't vote -2", range.contains(-2));
+    assertFalse("u can't vote +2", range.contains(-2));
+  }
+
+  public void testUnblockInLocalRange_Fails() {
+    grant(parent, LABEL + "Code-Review", -1, 1, anonymous, "refs/heads/*").setBlock();
+    grant(local, LABEL + "Code-Review", -2, +2, devs, "refs/heads/*");
+
+    ProjectControl u = user(devs);
+    PermissionRange range = u.controlForRef("refs/heads/master").getRange(LABEL + "Code-Review");
+    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;
@@ -307,6 +440,11 @@
       @Override
       public void onCreateProject(Project.NameKey newProjectName) {
       }
+
+      @Override
+      public Set<AccountGroup.UUID> guessRelevantGroupUUIDs() {
+        return Collections.emptySet();
+      }
     };
 
     Injector injector = Guice.createInjector(new FactoryModule() {
@@ -415,7 +553,7 @@
     private final GroupMembership groups;
 
     MockUser(String name, AccountGroup.UUID[] groupId) {
-      super(RefControlTest.this.capabilityControlFactory, AccessPath.UNKNOWN);
+      super(RefControlTest.this.capabilityControlFactory);
       username = name;
       ArrayList<AccountGroup.UUID> groupIds = Lists.newArrayList(groupId);
       groupIds.add(registered);
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 8750ee4..18eddc9 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,23 +14,31 @@
 
 package com.google.gerrit.server.schema;
 
-import com.google.gerrit.reviewdb.client.ApprovalCategory;
-import com.google.gerrit.reviewdb.client.ApprovalCategoryValue;
-import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Lists;
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.common.data.LabelTypes;
+import com.google.gerrit.common.data.LabelValue;
+import com.google.gerrit.server.config.AllProjectsName;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.testutil.InMemoryDatabase;
 import com.google.gwtorm.jdbc.JdbcSchema;
 import com.google.gwtorm.server.OrmException;
 
 import junit.framework.TestCase;
 
+import org.eclipse.jgit.lib.Repository;
+
 import java.io.File;
 import java.io.IOException;
 import java.sql.ResultSet;
 import java.sql.SQLException;
-import java.util.HashSet;
+import java.util.Arrays;
+import java.util.List;
 
 public class SchemaCreatorTest extends TestCase {
-  private ApprovalCategory.Id codeReview = new ApprovalCategory.Id("CRVW");
   private InMemoryDatabase db;
 
   @Override
@@ -79,53 +87,46 @@
     assertEquals(sitePath.getCanonicalPath(), db.getSystemConfig().sitePath);
   }
 
-  public void testCreateSchema_ApprovalCategory_CodeReview()
-      throws OrmException {
-    final ReviewDb c = db.create().open();
+  private LabelTypes getLabelTypes() throws Exception {
+    db.create();
+    AllProjectsName allProjects = db.getInstance(AllProjectsName.class);
+    ProjectConfig c = new ProjectConfig(allProjects);
+    Repository repo = db.getInstance(GitRepositoryManager.class)
+        .openRepository(allProjects);
     try {
-      final ApprovalCategory cat;
-
-      cat = c.approvalCategories().get(codeReview);
-      assertNotNull(cat);
-      assertEquals(codeReview, cat.getId());
-      assertEquals("Code Review", cat.getName());
-      assertEquals("R", cat.getAbbreviatedName());
-      assertEquals("MaxWithBlock", cat.getFunctionName());
-      assertTrue(cat.isCopyMinScore());
-      assertTrue(0 <= cat.getPosition());
+      c.load(repo);
+      return new LabelTypes(
+          ImmutableList.copyOf(c.getLabelSections().values()));
     } finally {
-      c.close();
+      repo.close();
     }
-    assertValueRange(codeReview, -2, -1, 0, 1, 2);
   }
 
-  private void assertValueRange(ApprovalCategory.Id cat, int... range)
-      throws OrmException {
-    final HashSet<ApprovalCategoryValue.Id> act =
-        new HashSet<ApprovalCategoryValue.Id>();
-    final ReviewDb c = db.open();
-    try {
-      for (ApprovalCategoryValue v : c.approvalCategoryValues().byCategory(cat)) {
-        assertNotNull(v.getId());
-        assertNotNull(v.getName());
-        assertEquals(cat, v.getCategoryId());
-        assertFalse(v.getName().isEmpty());
-
-        act.add(v.getId());
-      }
-    } finally {
-      c.close();
+  public void testCreateSchema_LabelTypes() throws Exception {
+    List<String> labels = Lists.newArrayList();
+    for (LabelType label : getLabelTypes().getLabelTypes()) {
+      labels.add(label.getName());
     }
+    assertEquals(ImmutableList.of("Code-Review"), labels);
+  }
 
-    for (int value : range) {
-      final ApprovalCategoryValue.Id exp =
-          new ApprovalCategoryValue.Id(cat, (short) value);
-      if (!act.remove(exp)) {
-        fail("Category " + cat + " lacks value " + value);
-      }
-    }
-    if (!act.isEmpty()) {
-      fail("Category " + cat + " has additional values: " + act);
+  public void testCreateSchema_Label_CodeReview() throws Exception {
+    LabelType codeReview = getLabelTypes().byLabel("Code-Review");
+    assertNotNull(codeReview);
+    assertEquals("Code-Review", codeReview.getName());
+    assertEquals("CR", codeReview.getAbbreviatedName());
+    assertEquals("MaxWithBlock", codeReview.getFunctionName());
+    assertTrue(codeReview.isCopyMinScore());
+    assertValueRange(codeReview, 2, 1, 0, -1, -2);
+  }
+
+  private void assertValueRange(LabelType label, Integer... range) {
+    assertEquals(Arrays.asList(range), label.getValuesAsList());
+    assertEquals(range[0], Integer.valueOf(label.getMax().getValue()));
+    assertEquals(range[range.length - 1],
+      Integer.valueOf(label.getMin().getValue()));
+    for (LabelValue v : label.getValues()) {
+      assertFalse(Strings.isNullOrEmpty(v.getText()));
     }
   }
 }
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 cc8d47d..bdd2258 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
@@ -27,6 +27,7 @@
 import com.google.gerrit.server.git.GitRepositoryManager;
 import com.google.gerrit.server.git.LocalDiskRepositoryManager;
 import com.google.gerrit.testutil.InMemoryDatabase;
+import com.google.gerrit.testutil.InMemoryH2Type;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.gwtorm.server.StatementExecutor;
@@ -94,6 +95,8 @@
         bind(String.class) //
           .annotatedWith(AnonymousCowardName.class) //
           .toProvider(AnonymousCowardNameProvider.class);
+
+        bind(DataSourceType.class).to(InMemoryH2Type.class);
       }
     }).getInstance(SchemaUpdater.class);
 
diff --git a/gerrit-server/src/test/java/com/google/gerrit/server/util/SubmoduleSectionParserTest.java b/gerrit-server/src/test/java/com/google/gerrit/server/util/SubmoduleSectionParserTest.java
index 93d86e5..7294d4c 100644
--- a/gerrit-server/src/test/java/com/google/gerrit/server/util/SubmoduleSectionParserTest.java
+++ b/gerrit-server/src/test/java/com/google/gerrit/server/util/SubmoduleSectionParserTest.java
@@ -27,6 +27,7 @@
 
 import org.eclipse.jgit.junit.LocalDiskRepositoryTestCase;
 import org.eclipse.jgit.lib.BlobBasedConfig;
+import org.eclipse.jgit.lib.Constants;
 import org.junit.Before;
 import org.junit.Test;
 
@@ -214,8 +215,9 @@
         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.endsWith(Constants.DOT_GIT_EXT)) {
+            projectNameCandidate = projectNameCandidate.substring(0, //
+                projectNameCandidate.length() - Constants.DOT_GIT_EXT.length());
           }
           if (projectNameCandidate.equals(reposToBeFound.get(id))) {
             expect(repoManager.list()).andReturn(
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 b1f956f..303c497 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,19 +14,22 @@
 
 package com.google.gerrit.testutil;
 
+import static com.google.inject.Scopes.SINGLETON;
+
 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.AllProjectsNameProvider;
 import com.google.gerrit.server.config.AnonymousCowardName;
 import com.google.gerrit.server.config.AnonymousCowardNameProvider;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.config.SitePath;
 import com.google.gerrit.server.git.GitRepositoryManager;
-import com.google.gerrit.server.git.LocalDiskRepositoryManager;
 import com.google.gerrit.server.schema.Current;
+import com.google.gerrit.server.schema.DataSourceType;
 import com.google.gerrit.server.schema.SchemaCreator;
 import com.google.gerrit.server.schema.SchemaVersion;
 import com.google.gwtorm.jdbc.Database;
@@ -35,6 +38,7 @@
 import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.AbstractModule;
 import com.google.inject.Guice;
+import com.google.inject.Injector;
 import com.google.inject.Key;
 
 import junit.framework.TestCase;
@@ -81,6 +85,7 @@
   private Database<ReviewDb> database;
   private boolean created;
   private SchemaVersion schemaVersion;
+  private Injector injector;
 
   public InMemoryDatabase() throws OrmException {
     try {
@@ -96,46 +101,54 @@
       //
       database = new Database<ReviewDb>(dataSource, ReviewDb.class);
 
-      schemaVersion =
-          Guice.createInjector(new AbstractModule() {
-            @Override
-            protected void configure() {
-              install(new SchemaVersion.Module());
+      injector = Guice.createInjector(new AbstractModule() {
+        @Override
+        protected void configure() {
+          install(new SchemaVersion.Module());
 
-              bind(File.class) //
-                  .annotatedWith(SitePath.class) //
-                  .toInstance(new File("."));
+          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");
+          Config cfg = new Config();
+          cfg.setString("gerrit", null, "basePath", "git");
+          cfg.setString("gerrit", null, "allProjects", "Test-Projects");
+          cfg.setString("user", null, "name", "Gerrit Code Review");
+          cfg.setString("user", null, "email", "gerrit@localhost");
 
-              bind(Config.class) //
-                  .annotatedWith(GerritServerConfig.class) //
-                  .toInstance(cfg);
+          bind(Config.class) //
+              .annotatedWith(GerritServerConfig.class) //
+              .toInstance(cfg);
 
-              bind(PersonIdent.class) //
-                  .annotatedWith(GerritPersonIdent.class) //
-                  .toProvider(GerritPersonIdentProvider.class);
+          bind(PersonIdent.class) //
+              .annotatedWith(GerritPersonIdent.class) //
+              .toProvider(GerritPersonIdentProvider.class);
 
-              bind(AllProjectsName.class)
-                  .toInstance(new AllProjectsName("All-Projects"));
+          bind(AllProjectsName.class) //
+              .toProvider(AllProjectsNameProvider.class);
 
-              bind(GitRepositoryManager.class) //
-                  .to(LocalDiskRepositoryManager.class);
+          bind(GitRepositoryManager.class) //
+              .to(InMemoryRepositoryManager.class).in(SINGLETON);
 
-              bind(String.class) //
-                .annotatedWith(AnonymousCowardName.class) //
-                .toProvider(AnonymousCowardNameProvider.class);
-            }
-          }).getBinding(Key.get(SchemaVersion.class, Current.class))
-              .getProvider().get();
+          bind(String.class) //
+            .annotatedWith(AnonymousCowardName.class) //
+            .toProvider(AnonymousCowardNameProvider.class);
+
+          bind(DataSourceType.class) //
+            .to(InMemoryH2Type.class);
+        }
+      });
+      schemaVersion = injector.getInstance(
+          Key.get(SchemaVersion.class, Current.class));
     } catch (SQLException e) {
       throw new OrmException(e);
     }
   }
 
+  public <T> T getInstance(Class<T> clazz) {
+    return injector.getInstance(clazz);
+  }
+
   public Database<ReviewDb> getDatabase() {
     return database;
   }
@@ -152,12 +165,7 @@
       final ReviewDb c = open();
       try {
         try {
-          new SchemaCreator(
-              new File("."),
-              schemaVersion,
-              null,
-              new AllProjectsName("Test-Projects"),
-              new PersonIdent("name", "email@site")).create(c);
+          getInstance(SchemaCreator.class).create(c);
         } catch (IOException e) {
           throw new OrmException("Cannot create in-memory database", e);
         } catch (ConfigInvalidException e) {
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryH2Type.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryH2Type.java
new file mode 100644
index 0000000..25a6534
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryH2Type.java
@@ -0,0 +1,30 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.testutil;
+
+import com.google.gerrit.server.schema.BaseDataSourceType;
+
+public class InMemoryH2Type extends BaseDataSourceType {
+
+  protected InMemoryH2Type() {
+    super(null);
+  }
+
+  @Override
+  public String getUrl() {
+    // not used
+    throw new UnsupportedOperationException();
+  }
+}
\ No newline at end of file
diff --git a/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryRepositoryManager.java b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryRepositoryManager.java
new file mode 100644
index 0000000..8e2bce6
--- /dev/null
+++ b/gerrit-server/src/test/java/com/google/gerrit/testutil/InMemoryRepositoryManager.java
@@ -0,0 +1,111 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.testutil;
+
+import com.google.common.collect.ImmutableSortedSet;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Sets;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.RepositoryCaseMismatchException;
+
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.internal.storage.dfs.DfsRepository;
+import org.eclipse.jgit.internal.storage.dfs.DfsRepositoryDescription;
+import org.eclipse.jgit.internal.storage.dfs.InMemoryRepository;
+import org.eclipse.jgit.lib.Repository;
+
+import java.util.Map;
+import java.util.SortedSet;
+
+/** Repository manager that uses in-memory repositories. */
+public class InMemoryRepositoryManager implements GitRepositoryManager {
+  private static class Description extends DfsRepositoryDescription {
+    private String desc;
+
+    private Description(Project.NameKey name) {
+      super(name.get());
+      desc = "In-memory repository " + name.get();
+    }
+  }
+
+  private static class Repo extends InMemoryRepository {
+    private Repo(Project.NameKey name) {
+      super(new Description(name));
+    }
+
+    @Override
+    public Description getDescription() {
+      return (Description) super.getDescription();
+    }
+  }
+
+  private Map<String, Repo> repos = Maps.newHashMap();
+
+  @Override
+  public Repository openRepository(Project.NameKey name)
+      throws RepositoryNotFoundException {
+    return get(name);
+  }
+
+  @Override
+  public Repository createRepository(Project.NameKey name)
+      throws RepositoryCaseMismatchException, RepositoryNotFoundException {
+    Repo repo;
+    try {
+      repo = get(name);
+      if (!repo.getDescription().getRepositoryName().equals(name.get())) {
+        throw new RepositoryCaseMismatchException(name);
+      }
+    } catch (RepositoryNotFoundException e) {
+      repo = new Repo(name);
+      repos.put(name.get().toLowerCase(), repo);
+    }
+    return repo;
+  }
+
+  @Override
+  public SortedSet<Project.NameKey> list() {
+    SortedSet<Project.NameKey> names = Sets.newTreeSet();
+    for (DfsRepository repo : repos.values()) {
+      names.add(new Project.NameKey(repo.getDescription().getRepositoryName()));
+    }
+    return ImmutableSortedSet.copyOf(names);
+  }
+
+  @Override
+  public String getProjectDescription(Project.NameKey name)
+      throws RepositoryNotFoundException {
+    return get(name).getDescription().desc;
+  }
+
+  @Override
+  public void setProjectDescription(Project.NameKey name, String description) {
+    try {
+      get(name).getDescription().desc = description;
+    } catch (RepositoryNotFoundException e) {
+      // Ignore.
+    }
+  }
+
+  private Repo get(Project.NameKey name) throws RepositoryNotFoundException {
+    Repo repo = repos.get(name.get().toLowerCase());
+    if (repo != null) {
+      return repo;
+    } else {
+      throw new RepositoryNotFoundException(name.get());
+    }
+  }
+}
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
index db899a7..c993394 100644
--- 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
@@ -22,13 +22,13 @@
   not_same(label(e, ok(a)), label(e, ok(b))).
 
 
-%% get_legacy_approval_types
+%% get_legacy_label_types
 %%
-test(get_legacy_approval_types) :-
-  get_legacy_approval_types(T),
+test(get_legacy_label_types) :-
+  get_legacy_label_types(T),
   T = [C, V],
-  C = approval_type('Code-Review', 'CRVW', 'MaxWithBlock', -2, 2),
-  V = approval_type('Verified', 'VRIF', 'MaxWithBlock', -1, 1).
+  C = label_type('Code-Review', 'MaxWithBlock', -2, 2),
+  V = label_type('Verified', 'MaxWithBlock', -1, 1).
 
 
 %% commit_label
diff --git a/gerrit-sshd/.settings/org.eclipse.jdt.core.prefs b/gerrit-sshd/.settings/org.eclipse.jdt.core.prefs
index 470942d..941fb31 100644
--- a/gerrit-sshd/.settings/org.eclipse.jdt.core.prefs
+++ b/gerrit-sshd/.settings/org.eclipse.jdt.core.prefs
@@ -7,6 +7,7 @@
 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.annotationSuperInterface=ignore
 org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
 org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
 org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
diff --git a/gerrit-sshd/pom.xml b/gerrit-sshd/pom.xml
index a26b1b2..e8fcc11 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.5-SNAPSHOT</version>
+    <version>2.6-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-sshd</artifactId>
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 af5df25..90ffebd 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
@@ -21,7 +21,6 @@
 import com.google.gerrit.server.project.ProjectControl;
 import com.google.gerrit.sshd.SshScope.Context;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 
 import org.apache.sshd.server.Environment;
 import org.eclipse.jgit.errors.RepositoryNotFoundException;
@@ -29,7 +28,6 @@
 import org.kohsuke.args4j.Argument;
 
 import java.io.IOException;
-import java.net.SocketAddress;
 
 public abstract class AbstractGitCommand extends BaseCommand {
   @Argument(index = 0, metaVar = "PROJECT.git", required = true, usage = "project name")
@@ -84,13 +82,10 @@
   }
 
   private SshSession newSession() {
-    return new SshSession(session, session.getRemoteAddress(), userFactory
-        .create(AccessPath.GIT, new Provider<SocketAddress>() {
-          @Override
-          public SocketAddress get() {
-            return session.getRemoteAddress();
-          }
-        }, user.getAccountId()));
+    SshSession n = new SshSession(session, session.getRemoteAddress(),
+        userFactory.create(session.getRemoteAddress(), user.getAccountId()));
+    n.setAccessPath(AccessPath.GIT);
+    return n;
   }
 
   private void service() throws IOException, Failure {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommand.java
index 9582c93..b46fced 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/AliasCommand.java
@@ -60,26 +60,26 @@
   }
 
   private void begin(Environment env) throws UnloggedFailure, IOException {
-    Map<String, Provider<Command>> map = root.getMap();
+    Map<String, CommandProvider> map = root.getMap();
     for (String name : chain(command)) {
-      Provider<? extends Command> p = map.get(name);
+      CommandProvider p = map.get(name);
       if (p == null) {
         throw new UnloggedFailure(1, getName() + ": not found");
       }
 
-      Command cmd = p.get();
+      Command cmd = p.getProvider().get();
       if (!(cmd instanceof DispatchCommand)) {
         throw new UnloggedFailure(1, getName() + ": not found");
       }
       map = ((DispatchCommand) cmd).getMap();
     }
 
-    Provider<? extends Command> p = map.get(command.value());
+    CommandProvider p = map.get(command.value());
     if (p == null) {
       throw new UnloggedFailure(1, getName() + ": not found");
     }
 
-    Command cmd = p.get();
+    Command cmd = p.getProvider().get();
     checkRequiresCapability(cmd);
     if (cmd instanceof BaseCommand) {
       BaseCommand bc = (BaseCommand)cmd;
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 9e04f05..98b740c 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
@@ -62,7 +62,6 @@
   static final int STATUS_NOT_FOUND = PRIVATE_STATUS | 2;
   public static final int STATUS_NOT_ADMIN = PRIVATE_STATUS | 3;
 
-  @SuppressWarnings("unused")
   @Option(name = "--", usage = "end of options", handler = EndOfOptionsHandler.class)
   private boolean endOfOptions;
 
@@ -429,7 +428,7 @@
           try {
             thunk.run();
           } catch (NoSuchProjectException e) {
-            throw new UnloggedFailure(1, e.getMessage() + " no such project");
+            throw new UnloggedFailure(1, e.getMessage());
           } catch (NoSuchChangeException e) {
             throw new UnloggedFailure(1, e.getMessage() + " no such change");
           }
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 7f08d49..910f5f3 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
@@ -16,9 +16,11 @@
 
 import com.google.common.util.concurrent.Atomics;
 import com.google.common.util.concurrent.ThreadFactoryBuilder;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.config.GerritServerConfig;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.sshd.SshScope.Context;
+import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
@@ -56,15 +58,17 @@
   private final SshScope sshScope;
   private final ScheduledExecutorService startExecutor;
   private final Executor destroyExecutor;
+  private final SchemaFactory<ReviewDb> schemaFactory;
 
   @Inject
   CommandFactoryProvider(
       @CommandName(Commands.ROOT) final DispatchCommandProvider d,
       @GerritServerConfig final Config cfg, final WorkQueue workQueue,
-      final SshLog l, final SshScope s) {
+      final SshLog l, final SshScope s, SchemaFactory<ReviewDb> sf) {
     dispatcher = d;
     log = l;
     sshScope = s;
+    schemaFactory = sf;
 
     int threads = cfg.getInt("sshd","commandStartThreads", 2);
     startExecutor = workQueue.createQueue(threads, "SshCommandStart");
@@ -122,7 +126,7 @@
 
     public void setSession(final ServerSession session) {
       final SshSession s = session.getAttribute(SshSession.KEY);
-      this.ctx = sshScope.newContext(s, commandLine);
+      this.ctx = sshScope.newContext(schemaFactory, s, commandLine);
     }
 
     public void start(final Environment env) throws IOException {
@@ -192,7 +196,7 @@
 
     private void log(final int rc) {
       if (logged.compareAndSet(false, true)) {
-        log.onExecute(rc);
+        log.onExecute(cmd, rc);
       }
     }
 
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandMetaData.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandMetaData.java
new file mode 100644
index 0000000..cfcee6a
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandMetaData.java
@@ -0,0 +1,31 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.sshd;
+
+import static java.lang.annotation.RetentionPolicy.RUNTIME;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.Target;
+
+/**
+ * Annotation tagged on a concrete Command to describe what it is doing
+ */
+@Target( {ElementType.TYPE})
+@Retention(RUNTIME)
+public @interface CommandMetaData {
+  String name();
+  String descr() default "";
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandModule.java
index 7699bdd..e7e8a44 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandModule.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandModule.java
@@ -48,7 +48,7 @@
   /**
    * Configure a command to be invoked by name.
    *
-   *@param parent context of the parent command, that this command is a
+   * @param parent context of the parent command, that this command is a
    *        subcommand of.
    * @param name the name of the command the client will provide in order to
    *        call the command.
@@ -61,6 +61,42 @@
   }
 
   /**
+   * Configure a command to be invoked by name. The command is bound to the passed class.
+   *
+   * @param parent context of the parent command, that this command is a
+   *        subcommand of.
+   * @param clazz class of the command with {@link CommandMetaData} annotation
+   *        to retrieve the name and the description from
+   */
+  protected void command(final CommandName parent,
+      final Class<? extends BaseCommand> clazz) {
+    CommandMetaData meta = (CommandMetaData)clazz.getAnnotation(CommandMetaData.class);
+    if (meta == null) {
+      throw new IllegalStateException("no CommandMetaData annotation found");
+    }
+    bind(Commands.key(parent, meta.name(), meta.descr())).to(clazz);
+  }
+
+  /**
+   * Alias one command to another. The alias is bound to the passed class.
+   *
+   * @param parent context of the parent command, that this command is a
+   *        subcommand of.
+   * @param name the name of the command the client will provide in order to
+   *        call the command.
+   * @param clazz class of the command with {@link CommandMetaData} annotation
+   *        to retrieve the description from
+   */
+  protected void alias(final CommandName parent, final String name,
+      final Class<? extends BaseCommand> clazz) {
+    CommandMetaData meta = (CommandMetaData)clazz.getAnnotation(CommandMetaData.class);
+    if (meta == null) {
+      throw new IllegalStateException("no CommandMetaData annotation found");
+    }
+    bind(Commands.key(parent, name, meta.descr())).to(clazz);
+  }
+
+  /**
    * Alias one command to another.
    *
    * @param from the new command name that when called will actually delegate to
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandProvider.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandProvider.java
new file mode 100644
index 0000000..9a7c97b
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/CommandProvider.java
@@ -0,0 +1,38 @@
+// Copyright (C) 2013 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.sshd;
+
+import com.google.inject.Provider;
+
+import org.apache.sshd.server.Command;
+
+final class CommandProvider {
+
+  private final Provider<Command> provider;
+  private final String description;
+
+  CommandProvider(final Provider<Command> p, final String d) {
+    this.provider = p;
+    this.description = d;
+  }
+
+  public Provider<Command> getProvider() {
+    return provider;
+  }
+
+  public String getDescription() {
+    return description;
+  }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/Commands.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/Commands.java
index 5340d6f..929a895 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/Commands.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/Commands.java
@@ -16,6 +16,7 @@
 
 import com.google.inject.Key;
 
+import org.apache.commons.lang.StringUtils;
 import org.apache.sshd.server.Command;
 
 import java.lang.annotation.Annotation;
@@ -41,6 +42,11 @@
     return Key.get(Command.class, named(parent, name));
   }
 
+  public static Key<Command> key(final CommandName parent,
+      final String name, final String descr) {
+    return Key.get(Command.class, named(parent, name, descr));
+  }
+
   /** Create a CommandName annotation for the supplied name. */
   public static CommandName named(final String name) {
     return new CommandName() {
@@ -78,6 +84,12 @@
     return new NestedCommandNameImpl(parent, name);
   }
 
+  /** Create a CommandName annotation for the supplied name and description. */
+  public static CommandName named(final CommandName parent, final String name,
+      final String descr) {
+    return new NestedCommandNameImpl(parent, name, descr);
+  }
+
   /** Return the name of this command, possibly including any parents. */
   public static String nameOf(final CommandName name) {
     if (name instanceof NestedCommandNameImpl) {
@@ -104,13 +116,22 @@
     return null;
   }
 
-  private static final class NestedCommandNameImpl implements CommandName {
+  static final class NestedCommandNameImpl implements CommandName {
     private final CommandName parent;
     private final String name;
+    private final String descr;
 
     NestedCommandNameImpl(final CommandName parent, final String name) {
       this.parent = parent;
       this.name = name;
+      this.descr = StringUtils.EMPTY;
+    }
+
+    NestedCommandNameImpl(final CommandName parent, final String name,
+        final String descr) {
+      this.parent = parent;
+      this.name = name;
+      this.descr = descr;
     }
 
     @Override
@@ -118,6 +139,10 @@
       return name;
     }
 
+    public String descr() {
+      return descr;
+    }
+
     @Override
     public Class<? extends Annotation> annotationType() {
       return CommandName.class;
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 a69a2f1..83bc8a5 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
@@ -15,7 +15,6 @@
 package com.google.gerrit.sshd;
 
 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;
@@ -23,7 +22,6 @@
 import com.google.gerrit.server.config.SitePaths;
 import com.google.gerrit.sshd.SshScope.Context;
 import com.google.inject.Inject;
-import com.google.inject.Provider;
 import com.google.inject.Singleton;
 
 import org.apache.commons.codec.binary.Base64;
@@ -43,7 +41,6 @@
 import java.io.FileNotFoundException;
 import java.io.FileReader;
 import java.io.IOException;
-import java.net.SocketAddress;
 import java.security.KeyPair;
 import java.security.PublicKey;
 import java.util.Collection;
@@ -173,7 +170,7 @@
       // session, record a login event in the log and add
       // a close listener to record a logout event.
       //
-      Context ctx = sshScope.newContext(sd, null);
+      Context ctx = sshScope.newContext(null, sd, null);
       Context old = sshScope.set(ctx);
       try {
         sshLog.onLogin();
@@ -185,7 +182,7 @@
           new IoFutureListener<IoFuture>() {
             @Override
             public void operationComplete(IoFuture future) {
-              final Context ctx = sshScope.newContext(sd, null);
+              final Context ctx = sshScope.newContext(null, sd, null);
               final Context old = sshScope.set(ctx);
               try {
                 sshLog.onLogout();
@@ -201,13 +198,7 @@
 
   private IdentifiedUser createUser(final SshSession sd,
       final SshKeyCacheEntry key) {
-    return userFactory.create(AccessPath.SSH_COMMAND,
-        new Provider<SocketAddress>() {
-          @Override
-          public SocketAddress get() {
-            return sd.getRemoteAddress();
-          }
-        }, key.getAccount());
+    return userFactory.create(sd.getRemoteAddress(), key.getAccount());
   }
 
   private SshKeyCacheEntry find(final Iterable<SshKeyCacheEntry> keyList,
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 691f3a0..455b732 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommand.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.sshd;
 
+import com.google.common.base.Strings;
 import com.google.common.collect.Sets;
 import com.google.common.util.concurrent.Atomics;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
@@ -29,6 +30,7 @@
 import org.kohsuke.args4j.Argument;
 
 import java.io.IOException;
+import java.io.StringWriter;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.Map;
@@ -39,14 +41,14 @@
  */
 final class DispatchCommand extends BaseCommand {
   interface Factory {
-    DispatchCommand create(Map<String, Provider<Command>> map);
+    DispatchCommand create(Map<String, CommandProvider> map);
   }
 
   private final Provider<CurrentUser> currentUser;
-  private final Map<String, Provider<Command>> commands;
+  private final Map<String, CommandProvider> commands;
   private final AtomicReference<Command> atomicCmd;
 
-  @Argument(index = 0, required = true, metaVar = "COMMAND", handler = SubcommandHandler.class)
+  @Argument(index = 0, required = false, metaVar = "COMMAND", handler = SubcommandHandler.class)
   private String commandName;
 
   @Argument(index = 1, multiValued = true, metaVar = "ARG")
@@ -54,13 +56,13 @@
 
   @Inject
   DispatchCommand(final Provider<CurrentUser> cu,
-      @Assisted final Map<String, Provider<Command>> all) {
+      @Assisted final Map<String, CommandProvider> all) {
     currentUser = cu;
     commands = all;
     atomicCmd = Atomics.newReference();
   }
 
-  Map<String, Provider<Command>> getMap() {
+  Map<String, CommandProvider> getMap() {
     return commands;
   }
 
@@ -68,8 +70,13 @@
   public void start(final Environment env) throws IOException {
     try {
       parseCommandLine();
+      if (Strings.isNullOrEmpty(commandName)) {
+        StringWriter msg = new StringWriter();
+        msg.write(usage());
+        throw new UnloggedFailure(1, msg.toString());
+      }
 
-      final Provider<Command> p = commands.get(commandName);
+      final CommandProvider p = commands.get(commandName);
       if (p == null) {
         String msg =
             (getName().isEmpty() ? "Gerrit Code Review" : getName()) + ": "
@@ -77,7 +84,7 @@
         throw new UnloggedFailure(1, msg);
       }
 
-      final Command cmd = p.get();
+      final Command cmd = p.getProvider().get();
       checkRequiresCapability(cmd);
       if (cmd instanceof BaseCommand) {
         final BaseCommand bc = (BaseCommand) cmd;
@@ -138,9 +145,17 @@
     }
     usage.append(" are:\n");
     usage.append("\n");
+
+    int maxLength = -1;
+    for (String name : commands.keySet()) {
+      maxLength = Math.max(maxLength, name.length());
+    }
+    String format = "%-" + maxLength + "s   %s";
     for (String name : Sets.newTreeSet(commands.keySet())) {
+      final CommandProvider p = commands.get(name);
       usage.append("   ");
-      usage.append(name);
+      usage.append(String.format(format, name,
+          Strings.nullToEmpty(p.getDescription())));
       usage.append("\n");
     }
     usage.append("\n");
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommandProvider.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommandProvider.java
index b76ff71..e9a31c9 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommandProvider.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/DispatchCommandProvider.java
@@ -39,7 +39,7 @@
   private DispatchCommand.Factory factory;
 
   private final CommandName parent;
-  private volatile ConcurrentMap<String, Provider<Command>> map;
+  private volatile ConcurrentMap<String, CommandProvider> map;
 
   public DispatchCommandProvider(final CommandName cn) {
     this.parent = cn;
@@ -52,8 +52,8 @@
 
   public RegistrationHandle register(final CommandName name,
       final Provider<Command> cmd) {
-    final ConcurrentMap<String, Provider<Command>> m = getMap();
-    if (m.putIfAbsent(name.value(), cmd) != null) {
+    final ConcurrentMap<String, CommandProvider> m = getMap();
+    if (m.putIfAbsent(name.value(), new CommandProvider(cmd, null)) != null) {
       throw new IllegalArgumentException(name.value() + " exists");
     }
     return new RegistrationHandle() {
@@ -66,8 +66,8 @@
 
   public RegistrationHandle replace(final CommandName name,
       final Provider<Command> cmd) {
-    final ConcurrentMap<String, Provider<Command>> m = getMap();
-    m.put(name.value(), cmd);
+    final ConcurrentMap<String, CommandProvider> m = getMap();
+    m.put(name.value(), new CommandProvider(cmd, null));
     return new RegistrationHandle() {
       @Override
       public void remove() {
@@ -76,7 +76,7 @@
     };
   }
 
-  ConcurrentMap<String, Provider<Command>> getMap() {
+  ConcurrentMap<String, CommandProvider> getMap() {
     if (map == null) {
       synchronized (this) {
         if (map == null) {
@@ -88,14 +88,21 @@
   }
 
   @SuppressWarnings("unchecked")
-  private ConcurrentMap<String, Provider<Command>> createMap() {
-    ConcurrentMap<String, Provider<Command>> m = Maps.newConcurrentMap();
+  private ConcurrentMap<String, CommandProvider> createMap() {
+    ConcurrentMap<String, CommandProvider> m = Maps.newConcurrentMap();
     for (final Binding<?> b : allCommands()) {
       final Annotation annotation = b.getKey().getAnnotation();
       if (annotation instanceof CommandName) {
         final CommandName n = (CommandName) annotation;
         if (!Commands.CMD_ROOT.equals(n) && Commands.isChild(parent, n)) {
-          m.put(n.value(), (Provider<Command>) b.getProvider());
+          String descr = null;
+          if (annotation instanceof Commands.NestedCommandNameImpl) {
+            Commands.NestedCommandNameImpl impl =
+                ((Commands.NestedCommandNameImpl) annotation);
+            descr = impl.descr();
+          }
+          m.put(n.value(),
+              new CommandProvider((Provider<Command>) b.getProvider(), descr));
         }
       }
     }
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 d2fdf78..cde7ae8 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
@@ -15,10 +15,12 @@
 package com.google.gerrit.sshd;
 
 import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.config.CanonicalWebUrl;
 import com.google.gerrit.server.ssh.SshInfo;
 import com.google.gerrit.sshd.SshScope.Context;
+import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
@@ -58,6 +60,7 @@
 
   static class SendMessage implements Command, SessionAware {
     private final Provider<MessageFactory> messageFactory;
+    private final SchemaFactory<ReviewDb> schemaFactory;
     private final SshScope sshScope;
 
     private InputStream in;
@@ -67,8 +70,10 @@
     private Context context;
 
     @Inject
-    SendMessage(Provider<MessageFactory> messageFactory, SshScope sshScope) {
+    SendMessage(Provider<MessageFactory> messageFactory,
+        SchemaFactory<ReviewDb> sf, SshScope sshScope) {
       this.messageFactory = messageFactory;
+      this.schemaFactory = sf;
       this.sshScope = sshScope;
     }
 
@@ -89,7 +94,8 @@
     }
 
     public void setSession(final ServerSession session) {
-      this.context = sshScope.newContext(session.getAttribute(SshSession.KEY), "");
+      SshSession s = session.getAttribute(SshSession.KEY);
+      this.context = sshScope.newContext(schemaFactory, s, "");
     }
 
     public void start(final Environment env) throws IOException {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/PluginCommandModule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/PluginCommandModule.java
index 4dbb8d7..013f070 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/PluginCommandModule.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/PluginCommandModule.java
@@ -16,21 +16,17 @@
 
 import com.google.common.base.Preconditions;
 import com.google.gerrit.extensions.annotations.PluginName;
-import com.google.gerrit.sshd.CommandName;
-import com.google.gerrit.sshd.Commands;
-import com.google.gerrit.sshd.DispatchCommandProvider;
-import com.google.inject.AbstractModule;
 import com.google.inject.binder.LinkedBindingBuilder;
 
 import org.apache.sshd.server.Command;
 
 import javax.inject.Inject;
 
-public abstract class PluginCommandModule extends AbstractModule {
+public abstract class PluginCommandModule extends CommandModule {
   private CommandName command;
 
   @Inject
-  void setPluginName(@PluginName String name) {
+  void setPluginName(@PluginName String name, final String descr) {
     this.command = Commands.named(name);
   }
 
@@ -46,4 +42,13 @@
   protected LinkedBindingBuilder<Command> command(String subCmd) {
     return bind(Commands.key(command, subCmd));
   }
+
+  protected void command(Class<? extends BaseCommand> clazz) {
+    command(command, clazz);
+  }
+
+  protected void alias(final String name, Class<? extends BaseCommand> clazz) {
+    alias(command, name, clazz);
+  }
+
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshAutoRegisterModuleGenerator.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshAutoRegisterModuleGenerator.java
index 03485f7..afb2537 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshAutoRegisterModuleGenerator.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshAutoRegisterModuleGenerator.java
@@ -14,22 +14,29 @@
 
 package com.google.gerrit.sshd;
 
+import static com.google.gerrit.server.plugins.AutoRegisterUtil.calculateBindAnnotation;
+
 import com.google.common.base.Preconditions;
+import com.google.common.collect.LinkedListMultimap;
 import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
 import com.google.gerrit.extensions.annotations.Export;
 import com.google.gerrit.server.plugins.InvalidPluginException;
 import com.google.gerrit.server.plugins.ModuleGenerator;
 import com.google.inject.AbstractModule;
 import com.google.inject.Module;
+import com.google.inject.TypeLiteral;
 
 import org.apache.sshd.server.Command;
 
+import java.lang.annotation.Annotation;
 import java.util.Map;
 
 class SshAutoRegisterModuleGenerator
     extends AbstractModule
     implements ModuleGenerator {
   private final Map<String, Class<Command>> commands = Maps.newHashMap();
+  private final Multimap<TypeLiteral<?>, Class<?>> listeners = LinkedListMultimap.create();
   private CommandName command;
 
   @Override
@@ -39,6 +46,16 @@
     for (Map.Entry<String, Class<Command>> e : commands.entrySet()) {
       bind(Commands.key(command, e.getKey())).to(e.getValue());
     }
+    for (Map.Entry<TypeLiteral<?>, Class<?>> e : listeners.entries()) {
+      @SuppressWarnings("unchecked")
+      TypeLiteral<Object> type = (TypeLiteral<Object>) e.getKey();
+
+      @SuppressWarnings("unchecked")
+      Class<Object> impl = (Class<Object>) e.getValue();
+
+      Annotation n = calculateBindAnnotation(impl);
+      bind(type).annotatedWith(n).to(impl);
+    }
   }
 
   public void setPluginName(String name) {
@@ -66,6 +83,12 @@
     }
   }
 
+
+  @Override
+  public void listen(TypeLiteral<?> tl, Class<?> clazz) {
+    listeners.put(tl, clazz);
+  }
+
   @Override
   public Module create() throws InvalidPluginException {
     Preconditions.checkState(command != null, "pluginName must be provided");
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 664ce45..c43de60 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
@@ -14,6 +14,8 @@
 
 package com.google.gerrit.sshd;
 
+import static com.google.gerrit.server.ssh.SshAddressesModule.IANA_SSH_PORT;
+
 import static java.util.concurrent.TimeUnit.MILLISECONDS;
 import static java.util.concurrent.TimeUnit.SECONDS;
 
@@ -21,7 +23,9 @@
 import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.server.config.ConfigUtil;
 import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.ssh.SshAdvertisedAddresses;
 import com.google.gerrit.server.ssh.SshInfo;
+import com.google.gerrit.server.ssh.SshListenAddresses;
 import com.google.gerrit.server.util.IdGenerator;
 import com.google.gerrit.server.util.SocketUtil;
 import com.google.inject.Inject;
@@ -114,13 +118,10 @@
  */
 @Singleton
 public class SshDaemon extends SshServer implements SshInfo, LifecycleListener {
-  private static final int IANA_SSH_PORT = 22;
-  public static final int DEFAULT_PORT = 29418;
-
   private static final Logger log = LoggerFactory.getLogger(SshDaemon.class);
 
   private final List<SocketAddress> listen;
-  private final List<String> advertisedAddress;
+  private final List<String> advertised;
   private final boolean keepAlive;
   private final List<HostKey> hostKeys;
   private volatile IoAcceptor acceptor;
@@ -129,11 +130,13 @@
   SshDaemon(final CommandFactory commandFactory, final NoShell noShell,
       final PublickeyAuthenticator userAuth,
       final KeyPairProvider hostKeyProvider, final IdGenerator idGenerator,
-      @GerritServerConfig final Config cfg, final SshLog sshLog) {
+      @GerritServerConfig final Config cfg, final SshLog sshLog,
+      @SshListenAddresses final List<SocketAddress> listen,
+      @SshAdvertisedAddresses final List<String> advertised) {
     setPort(IANA_SSH_PORT /* never used */);
 
-    listen = parseListen(cfg);
-    advertisedAddress = parseAdvertisedAddress(cfg);
+    this.listen = listen;
+    this.advertised = advertised;
     reuseAddress = cfg.getBoolean("sshd", "reuseaddress", true);
     keepAlive = cfg.getBoolean("sshd", "tcpkeepalive", true);
 
@@ -149,6 +152,18 @@
         String.valueOf(MILLISECONDS.convert(ConfigUtil.getTimeUnit(cfg, "sshd",
             null, "loginGraceTime", 120, SECONDS), SECONDS)));
 
+    long idleTimeoutSeconds = ConfigUtil.getTimeUnit(cfg, "sshd", null,
+        "idleTimeout", 0, SECONDS);
+    if (idleTimeoutSeconds == 0) {
+      // Since Apache SSHD does not allow to turn off closing idle connections,
+      // we fake it by using the highest timeout allowed by Apache SSHD, which
+      // amounts to ~24 days.
+      idleTimeoutSeconds = MILLISECONDS.toSeconds(Integer.MAX_VALUE);
+    }
+    getProperties().put(
+        IDLE_TIMEOUT,
+        String.valueOf(SECONDS.toMillis(idleTimeoutSeconds)));
+
     final int maxConnectionsPerUser =
         cfg.getInt("sshd", "maxConnectionsPerUser", 64);
     if (0 < maxConnectionsPerUser) {
@@ -268,7 +283,7 @@
       buf.putRawPublicKey(pub);
       final byte[] keyBin = buf.getCompactData();
 
-      for (final String addr : myAdvertisedAddresses()) {
+      for (final String addr : advertised) {
         try {
           r.add(new HostKey(addr, keyBin));
         } catch (JSchException e) {
@@ -279,36 +294,6 @@
     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>();
-
-    for (final SocketAddress addr : listen) {
-      if (addr instanceof InetSocketAddress) {
-        final InetSocketAddress inetAddr = (InetSocketAddress) addr;
-        if (inetAddr.getAddress().isLoopbackAddress()) {
-          local.add(inetAddr);
-        } else {
-          pub.add(inetAddr);
-        }
-      }
-    }
-    return pub.isEmpty() ? local : pub;
-  }
-
   private List<PublicKey> myHostKeys() {
     final KeyPairProvider p = getKeyPairProvider();
     final List<PublicKey> keys = new ArrayList<PublicKey>(2);
@@ -336,42 +321,6 @@
     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");
-    if (want == null || want.length == 0) {
-      bind.add(new InetSocketAddress(DEFAULT_PORT));
-      return bind;
-    }
-
-    if (want.length == 1 && isOff(want[0])) {
-      return bind;
-    }
-
-    for (final String desc : want) {
-      try {
-        bind.add(SocketUtil.resolve(desc, DEFAULT_PORT));
-      } catch (IllegalArgumentException e) {
-        log.error("Bad sshd.listenaddress: " + desc + ": " + e.getMessage());
-      }
-    }
-    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/SshLog.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLog.java
index c9ac3e7..336490c 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLog.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshLog.java
@@ -14,9 +14,11 @@
 
 package com.google.gerrit.sshd;
 
-import com.google.gerrit.extensions.events.LifecycleListener;
-import com.google.gerrit.audit.AuditEvent;
+import com.google.common.collect.ArrayListMultimap;
+import com.google.common.collect.Multimap;
 import com.google.gerrit.audit.AuditService;
+import com.google.gerrit.audit.SshAuditEvent;
+import com.google.gerrit.extensions.events.LifecycleListener;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PeerDaemonUser;
@@ -42,7 +44,6 @@
 import java.io.File;
 import java.io.IOException;
 import java.text.SimpleDateFormat;
-import java.util.Arrays;
 import java.util.Calendar;
 import java.util.Date;
 import java.util.TimeZone;
@@ -101,7 +102,7 @@
 
   void onLogin() {
     async.append(log("LOGIN FROM " + session.get().getRemoteAddressAsString()));
-    audit(context.get(), "0", "LOGIN", new String[] {});
+    audit(context.get(), "0", "LOGIN");
   }
 
   void onAuthFail(final SshSession sd) {
@@ -127,18 +128,14 @@
     }
 
     async.append(event);
-    audit(null, "FAIL", "AUTH", new String[] {sd.getRemoteAddressAsString()});
+    audit(null, "FAIL", "AUTH");
   }
 
-  void onExecute(int exitValue) {
+  void onExecute(DispatchCommand dcmd, int exitValue) {
     final Context ctx = context.get();
     ctx.finished = System.currentTimeMillis();
 
-    final String commandLine = ctx.getCommandLine();
-    String cmd = QuotedString.BOURNE.quote(commandLine);
-    if (cmd == commandLine) {
-      cmd = "'" + commandLine + "'";
-    }
+    String cmd = extractWhat(dcmd);
 
     final LoggingEvent event = log(cmd);
     event.setProperty(P_WAIT, (ctx.started - ctx.created) + "ms");
@@ -165,19 +162,54 @@
     event.setProperty(P_STATUS, status);
 
     async.append(event);
-    audit(context.get(), status, getCommand(commandLine),
-        CommandFactoryProvider.split(commandLine));
+    audit(context.get(), status, dcmd);
   }
 
-  private String getCommand(String commandLine) {
-    commandLine = commandLine.trim();
-    int spacePos = commandLine.indexOf(' ');
-    return (spacePos > 0 ? commandLine.substring(0, spacePos):commandLine);
+  private Multimap<String, ?> extractParameters(DispatchCommand dcmd) {
+    String[] cmdArgs = dcmd.getArguments();
+    String paramName = null;
+    int argPos = 0;
+    Multimap<String, String> parms = ArrayListMultimap.create();
+    for (int i = 2; i < cmdArgs.length; i++) {
+      String arg = cmdArgs[i];
+      // -- stop parameters parsing
+      if (arg.equals("--")) {
+        for (i++; i < cmdArgs.length; i++) {
+          parms.put("$" + argPos++, cmdArgs[i]);
+        }
+        break;
+      }
+      // --param=value
+      int eqPos = arg.indexOf('=');
+      if (arg.startsWith("--") && eqPos > 0) {
+        parms.put(arg.substring(0, eqPos), arg.substring(eqPos + 1));
+        continue;
+      }
+      // -p value or --param value
+      if (arg.startsWith("-")) {
+        if (paramName != null) {
+          parms.put(paramName, null);
+        }
+        paramName = arg;
+        continue;
+      }
+      // value
+      if (paramName == null) {
+        parms.put("$" + argPos++, arg);
+      } else {
+        parms.put(paramName, arg);
+        paramName = null;
+      }
+    }
+    if (paramName != null) {
+      parms.put(paramName, null);
+    }
+    return parms;
   }
 
   void onLogout() {
     async.append(log("LOGOUT"));
-    audit(context.get(), "0", "LOGOUT", new String[] {});
+    audit(context.get(), "0", "LOGOUT");
   }
 
   private LoggingEvent log(final String msg) {
@@ -416,21 +448,28 @@
     }
   }
 
-  void audit(Context ctx, Object result, String commandName, String[] args) {
+  void audit(Context ctx, Object result, String cmd) {
     final String sid = extractSessionId(ctx);
     final long created = extractCreated(ctx);
-    final String what = extractWhat(commandName, args);
-    auditService.dispatch(new AuditEvent(sid, extractCurrentUser(ctx), "ssh:"
-        + what, created, Arrays.asList(args), result));
+    auditService.dispatch(new SshAuditEvent(sid, extractCurrentUser(ctx), cmd,
+        created, null, result));
   }
 
-  private String extractWhat(String commandName, String[] args) {
-    String result = commandName;
-    if ("gerrit".equals(commandName)) {
-      if (args.length > 1)
-        result = "gerrit"+"."+args[1];
+  void audit(Context ctx, Object result, DispatchCommand cmd) {
+    final String sid = extractSessionId(ctx);
+    final long created = extractCreated(ctx);
+    auditService.dispatch(new SshAuditEvent(sid, extractCurrentUser(ctx),
+        extractWhat(cmd), created, extractParameters(cmd), result));
+  }
+
+  private String extractWhat(DispatchCommand dcmd) {
+    String commandName = dcmd.getCommandName();
+    String[] args = dcmd.getArguments();
+    if (args.length > 1) {
+      return commandName + "." + args[1];
+    } else {
+      return commandName;
     }
-    return result;
   }
 
   private long extractCreated(final Context ctx) {
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 7f4a1f7..6fad42b 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
@@ -22,11 +22,10 @@
 import com.google.gerrit.server.CmdLineParserModule;
 import com.google.gerrit.server.PeerDaemonUser;
 import com.google.gerrit.server.RemotePeer;
-import com.google.gerrit.server.account.AccountManager;
-import com.google.gerrit.server.account.ChangeUserName;
 import com.google.gerrit.server.config.FactoryModule;
 import com.google.gerrit.server.config.GerritRequestModule;
 import com.google.gerrit.server.config.GerritServerConfig;
+import com.google.gerrit.server.git.AsyncReceiveCommits;
 import com.google.gerrit.server.git.QueueProvider;
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.plugins.ModuleGenerator;
@@ -44,6 +43,7 @@
 import org.apache.sshd.server.CommandFactory;
 import org.apache.sshd.server.PublickeyAuthenticator;
 import org.eclipse.jgit.lib.Config;
+
 import java.net.SocketAddress;
 import java.util.Map;
 
@@ -66,10 +66,10 @@
     bind(SshScope.class).in(SINGLETON);
 
     configureRequestScope();
+    install(new AsyncReceiveCommits.Module());
     install(new CmdLineParserModule());
     configureAliases();
 
-    install(SshKeyCacheImpl.module());
     bind(SshLog.class);
     bind(SshInfo.class).to(SshDaemon.class).in(SINGLETON);
     factory(DispatchCommand.Factory.class);
@@ -83,8 +83,6 @@
     bind(WorkQueue.Executor.class).annotatedWith(StreamCommandExecutor.class)
         .toProvider(StreamCommandExecutorProvider.class).in(SINGLETON);
     bind(QueueProvider.class).to(CommandExecutorQueueProvider.class).in(SINGLETON);
-    bind(AccountManager.class);
-    factory(ChangeUserName.Factory.class);
 
     bind(PublickeyAuthenticator.class).to(DatabasePubKeyAuth.class);
     bind(KeyPairProvider.class).toProvider(HostKeyProvider.class).in(SINGLETON);
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshScope.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshScope.java
index d6f66ca..64a1a42 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshScope.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshScope.java
@@ -14,19 +14,23 @@
 
 package com.google.gerrit.sshd;
 
+import com.google.common.collect.Maps;
+import com.google.gerrit.reviewdb.server.ReviewDb;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.RequestCleanup;
+import com.google.gerrit.server.config.RequestScopedReviewDbProvider;
 import com.google.gerrit.server.util.RequestContext;
 import com.google.gerrit.server.util.ThreadLocalRequestContext;
 import com.google.gerrit.server.util.ThreadLocalRequestScopePropagator;
+import com.google.gwtorm.server.SchemaFactory;
 import com.google.inject.Inject;
 import com.google.inject.Key;
 import com.google.inject.OutOfScopeException;
 import com.google.inject.Provider;
 import com.google.inject.Scope;
+import com.google.inject.util.Providers;
 
-import java.util.HashMap;
 import java.util.Map;
 
 /** Guice scopes for state during an SSH connection. */
@@ -34,29 +38,34 @@
   private static final Key<RequestCleanup> RC_KEY =
       Key.get(RequestCleanup.class);
 
+  private static final Key<RequestScopedReviewDbProvider> DB_KEY =
+      Key.get(RequestScopedReviewDbProvider.class);
+
   class Context implements RequestContext {
-    private final RequestCleanup cleanup;
+    private final RequestCleanup cleanup = new RequestCleanup();
+    private final Map<Key<?>, Object> map = Maps.newHashMap();
+    private final SchemaFactory<ReviewDb> schemaFactory;
     private final SshSession session;
     private final String commandLine;
-    private final Map<Key<?>, Object> map;
 
     final long created;
     volatile long started;
     volatile long finished;
 
-    private Context(final SshSession s, final String c, final long at) {
-      cleanup = new RequestCleanup();
+    private Context(SchemaFactory<ReviewDb> sf, final SshSession s,
+        final String c, final long at) {
+      schemaFactory = sf;
       session = s;
       commandLine = c;
-
-      map = new HashMap<Key<?>, Object>();
-      map.put(RC_KEY, cleanup);
-
       created = started = finished = at;
+      map.put(RC_KEY, cleanup);
+      map.put(DB_KEY, new RequestScopedReviewDbProvider(
+          schemaFactory,
+          Providers.of(cleanup)));
     }
 
     private Context(Context p, SshSession s, String c) {
-      this(s, c, p.created);
+      this(p.schemaFactory, s, c, p.created);
       started = p.started;
       finished = p.finished;
     }
@@ -73,12 +82,16 @@
     public CurrentUser getCurrentUser() {
       final CurrentUser user = session.getCurrentUser();
       if (user instanceof IdentifiedUser) {
-        return userFactory.create(user.getAccessPath(), //
-            ((IdentifiedUser) user).getAccountId());
+        return userFactory.create(((IdentifiedUser) user).getAccountId());
       }
       return user;
     }
 
+    @Override
+    public Provider<ReviewDb> getReviewDbProvider() {
+      return (RequestScopedReviewDbProvider) map.get(DB_KEY);
+    }
+
     synchronized <T> T get(Key<T> key, Provider<T> creator) {
       @SuppressWarnings("unchecked")
       T t = (T) map.get(key);
@@ -114,8 +127,9 @@
     private final SshScope sshScope;
 
     @Inject
-    Propagator(SshScope sshScope, ThreadLocalRequestContext local) {
-      super(REQUEST, current, local);
+    Propagator(SshScope sshScope, ThreadLocalRequestContext local,
+        Provider<RequestScopedReviewDbProvider> dbProviderProvider) {
+      super(REQUEST, current, local, dbProviderProvider);
       this.sshScope = sshScope;
     }
 
@@ -148,8 +162,8 @@
     this.userFactory = userFactory;
   }
 
-  Context newContext(SshSession session, String commandLine) {
-    return new Context(session, commandLine, System.currentTimeMillis());
+  Context newContext(SchemaFactory<ReviewDb> sf, SshSession s, String cmd) {
+    return new Context(sf, s, cmd, System.currentTimeMillis());
   }
 
   private Context newContinuingContext(Context ctx) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshSession.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshSession.java
index 3c4d4f8..2cc16b5 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshSession.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/SshSession.java
@@ -14,6 +14,7 @@
 
 package com.google.gerrit.sshd;
 
+import com.google.gerrit.server.AccessPath;
 import com.google.gerrit.server.CurrentUser;
 
 import org.apache.sshd.common.Session.AttributeKey;
@@ -43,6 +44,7 @@
   }
 
   SshSession(SshSession parent, SocketAddress peer, CurrentUser user) {
+    user.setAccessPath(AccessPath.SSH_COMMAND);
     this.sessionId = parent.sessionId;
     this.remoteAddress = peer;
     if (parent.remoteAddress == peer) {
@@ -83,6 +85,10 @@
     authError = error;
   }
 
+  void setAccessPath(AccessPath path) {
+    identity.setAccessPath(path);
+  }
+
   /** @return {@code true} if the authentication did not succeed. */
   boolean isAuthenticationError() {
     return authError != null;
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 33459c2..ccf23e1 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
@@ -16,7 +16,6 @@
 
 import com.google.common.util.concurrent.Atomics;
 import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.server.AccessPath;
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.PeerDaemonUser;
@@ -116,14 +115,8 @@
     } else {
       peer = peerAddress;
     }
-
-    return new SshSession(session.get(), peer, userFactory.create(
-        AccessPath.SSH_COMMAND, new Provider<SocketAddress>() {
-          @Override
-          public SocketAddress get() {
-            return peer;
-          }
-        }, accountId));
+    return new SshSession(session.get(), peer,
+        userFactory.create(peer, accountId));
   }
 
   private static String join(List<String> args) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminQueryShell.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminQueryShell.java
index 08c650c..15229dc 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminQueryShell.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminQueryShell.java
@@ -15,8 +15,11 @@
 package com.google.gerrit.sshd.commands;
 
 import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.common.errors.PermissionDeniedException;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.sshd.AdminHighPriorityCommand;
+import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
 
@@ -24,11 +27,15 @@
 
 /** Opens a query processor. */
 @AdminHighPriorityCommand
-@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+@RequiresCapability(GlobalCapability.ACCESS_DATABASE)
+@CommandMetaData(name = "gsql", descr = "Administrative interface to active database")
 final class AdminQueryShell extends SshCommand {
   @Inject
   private QueryShell.Factory factory;
 
+  @Inject
+  private IdentifiedUser currentUser;
+
   @Option(name = "--format", usage = "Set output format")
   private QueryShell.OutputFormat format = QueryShell.OutputFormat.PRETTY;
 
@@ -36,13 +43,37 @@
   private String query;
 
   @Override
-  protected void run() {
-    final QueryShell shell = factory.create(in, out);
-    shell.setOutputFormat(format);
-    if (query != null) {
-      shell.execute(query);
-    } else {
-      shell.run();
+  protected void run() throws Failure {
+    try {
+      checkPermission();
+
+      final QueryShell shell = factory.create(in, out);
+      shell.setOutputFormat(format);
+      if (query != null) {
+        shell.execute(query);
+      } else {
+        shell.run();
+      }
+    } catch (PermissionDeniedException err) {
+      throw new UnloggedFailure("fatal: " + err.getMessage());
+    }
+  }
+
+  /**
+   * Assert that the current user is permitted to perform raw queries.
+   * <p>
+   * As the @RequireCapability guards at various entry points of internal
+   * commands implicitly add administrators (which we want to avoid), we also
+   * check permissions within QueryShell and grant access only to those who
+   * canPerformRawQuery, regardless of whether they are administrators or not.
+   *
+   * @throws PermissionDeniedException
+   */
+  private void checkPermission() throws PermissionDeniedException {
+    if (!currentUser.getCapabilities().canAccessDatabase()) {
+      throw new PermissionDeniedException(String.format(
+          "%s does not have \"Access Database\" capability.",
+          currentUser.getUserName()));
     }
   }
 }
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 6483e24..faf2224 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/AdminSetParent.java
@@ -14,6 +14,9 @@
 
 package com.google.gerrit.sshd.commands;
 
+import com.google.common.base.Function;
+import com.google.common.collect.ImmutableSet;
+import com.google.common.collect.Iterables;
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.reviewdb.client.Project;
@@ -23,6 +26,7 @@
 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.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
 
@@ -35,11 +39,13 @@
 
 import java.io.IOException;
 import java.util.ArrayList;
+import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
 import java.util.Set;
 
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+@CommandMetaData(name = "set-project-parent", descr = "Change the project permissions are inherited from")
 final class AdminSetParent extends SshCommand {
   private static final Logger log = LoggerFactory.getLogger(AdminSetParent.class);
 
@@ -197,17 +203,15 @@
   }
 
   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;
+    ProjectState ps = projectCache.get(projectName);
+    return ImmutableSet.copyOf(Iterables.transform(
+      ps != null ? ps.parents() : Collections.<ProjectState> emptySet(),
+      new Function<ProjectState, Project.NameKey> () {
+        @Override
+        public Project.NameKey apply(ProjectState in) {
+          return in.getProject().getNameKey();
+        }
+      }));
   }
 
   private List<Project> getChildren(final Project.NameKey parentName) {
@@ -219,7 +223,7 @@
         continue;
       }
 
-      if (parentName.equals(e.getProject().getParent(projectName))) {
+      if (parentName.equals(e.getProject().getParent(allProjectsName))) {
         childProjects.add(e.getProject());
       }
     }
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 a1b8988..29250d3 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
@@ -14,9 +14,8 @@
 
 package com.google.gerrit.sshd.commands;
 
-import com.google.gerrit.common.data.ApprovalType;
-import com.google.gerrit.reviewdb.client.ApprovalCategory;
-import com.google.gerrit.reviewdb.client.ApprovalCategoryValue;
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.common.data.LabelValue;
 
 import org.kohsuke.args4j.CmdLineException;
 import org.kohsuke.args4j.CmdLineParser;
@@ -31,11 +30,11 @@
 final class ApproveOption implements Option, Setter<Short> {
   private final String name;
   private final String usage;
-  private final ApprovalType type;
+  private final LabelType type;
 
   private Short value;
 
-  ApproveOption(final String name, final String usage, final ApprovalType type) {
+  ApproveOption(final String name, final String usage, final LabelType type) {
     this.name = name;
     this.usage = usage;
     this.type = type;
@@ -100,8 +99,8 @@
     return false;
   }
 
-  ApprovalCategory.Id getCategoryId() {
-    return type.getCategory().getId();
+  String getLabelName() {
+    return type.getName();
   }
 
   public static class Handler extends OneArgumentOptionHandler<Short> {
@@ -122,8 +121,8 @@
       }
 
       final short value = Short.parseShort(argument);
-      final ApprovalCategoryValue min = cmdOption.type.getMin();
-      final ApprovalCategoryValue max = cmdOption.type.getMax();
+      final LabelValue min = cmdOption.type.getMin();
+      final LabelValue max = cmdOption.type.getMax();
 
       if (value < min.getValue() || value > max.getValue()) {
         final String name = cmdOption.name();
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java
index 939d68a..0268bc0 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BanCommitCommand.java
@@ -19,6 +19,7 @@
 import com.google.gerrit.server.git.BanCommitResult;
 import com.google.gerrit.server.git.MergeException;
 import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
 
@@ -32,6 +33,7 @@
 import java.util.ArrayList;
 import java.util.List;
 
+@CommandMetaData(name = "ban-commit", descr = "Ban a commit from a project's repository")
 public class BanCommitCommand extends SshCommand {
   @Option(name = "--reason", aliases = {"-r"}, metaVar = "REASON", usage = "reason for banning the commit")
   private String reason;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BaseTestPrologCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BaseTestPrologCommand.java
new file mode 100644
index 0000000..0e13fd6
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/BaseTestPrologCommand.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.sshd.commands;
+
+import com.google.gerrit.extensions.restapi.IdString;
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.extensions.restapi.TopLevelResource;
+import com.google.gerrit.server.OutputFormat;
+import com.google.gerrit.server.change.ChangesCollection;
+import com.google.gerrit.server.change.RevisionResource;
+import com.google.gerrit.server.change.Revisions;
+import com.google.gerrit.server.change.TestSubmitRule.Filters;
+import com.google.gerrit.server.change.TestSubmitRule.Input;
+import com.google.gerrit.sshd.SshCommand;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.util.IO;
+import org.eclipse.jgit.util.RawParseUtils;
+import org.kohsuke.args4j.Argument;
+import org.kohsuke.args4j.Option;
+
+import java.nio.ByteBuffer;
+
+abstract class BaseTestPrologCommand extends SshCommand {
+  private Input input = new Input();
+
+  @Inject
+  private ChangesCollection changes;
+
+  @Inject
+  private Revisions revisions;
+
+  @Argument(index = 0, required = true, usage = "ChangeId to load in prolog environment")
+  protected String changeId;
+
+  @Option(name = "-s",
+      usage = "Read prolog script from stdin instead of reading rules.pl from the refs/meta/config branch")
+  protected boolean useStdin;
+
+  @Option(name = "--no-filters", aliases = {"-n"},
+      usage = "Don't run the submit_filter/2 from the parent projects")
+  void setNoFilters(boolean no) {
+    input.filters = no ? Filters.SKIP : Filters.RUN;
+  }
+
+  protected abstract RestModifyView<RevisionResource, Input> createView();
+
+  protected final void run() throws UnloggedFailure {
+    try {
+      RevisionResource revision = revisions.parse(
+          changes.parse(
+              TopLevelResource.INSTANCE,
+              IdString.fromUrl(changeId)),
+          IdString.fromUrl("current"));
+      if (useStdin) {
+        ByteBuffer buf = IO.readWholeStream(in, 4096);
+        input.rule = RawParseUtils.decode(
+            buf.array(),
+            buf.arrayOffset(),
+            buf.limit());
+      }
+      Object result = createView().apply(revision, input);
+      OutputFormat.JSON.newGson().toJson(result, stdout);
+      stdout.print('\n');
+    } catch (Exception e) {
+      throw new UnloggedFailure("Processing of prolog script failed: " + e);
+    }
+  }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
index d6fecab..c209249 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateAccountCommand.java
@@ -28,6 +28,7 @@
 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.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.gwtorm.server.OrmDuplicateKeyException;
 import com.google.gwtorm.server.OrmException;
@@ -47,6 +48,7 @@
 
 /** Create a new user account. **/
 @RequiresCapability(GlobalCapability.CREATE_ACCOUNT)
+@CommandMetaData(name = "create-account", descr = "Create a new batch/role account")
 final class CreateAccountCommand extends SshCommand {
   @Option(name = "--group", aliases = {"-g"}, metaVar = "GROUP", usage = "groups to add account to")
   private List<AccountGroup.Id> groups = new ArrayList<AccountGroup.Id>();
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
index 728c20c..660460a 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateGroupCommand.java
@@ -21,6 +21,7 @@
 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.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
@@ -37,6 +38,7 @@
  * Optionally, puts an initial set of user in the newly created group.
  */
 @RequiresCapability(GlobalCapability.CREATE_GROUP)
+@CommandMetaData(name = "create-group", descr = "Create a new account group")
 final class CreateGroupCommand extends SshCommand {
   @Option(name = "--owner", aliases = {"-o"}, metaVar = "GROUP", usage = "owning group, if not specified the group will be self-owning")
   private AccountGroup.Id ownerGroupId;
@@ -57,10 +59,10 @@
   @Option(name = "--visible-to-all", usage = "to make the group visible to all registered users")
   private boolean visibleToAll;
 
-  private final Set<AccountGroup.Id> initialGroups = new HashSet<AccountGroup.Id>();
+  private final Set<AccountGroup.UUID> initialGroups = new HashSet<AccountGroup.UUID>();
 
   @Option(name = "--group", aliases = "-g", metaVar = "GROUP", usage = "initial set of groups to be included in the group")
-  void addGroup(final AccountGroup.Id id) {
+  void addGroup(final AccountGroup.UUID id) {
     initialGroups.add(id);
   }
 
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
index 5f5b1e3..bd624ae 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/CreateProjectCommand.java
@@ -19,11 +19,13 @@
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.Project.InheritableBoolean;
 import com.google.gerrit.reviewdb.client.Project.SubmitType;
-import com.google.gerrit.server.project.CreateProject;
+import com.google.gerrit.server.project.PerformCreateProject;
 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.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
 
@@ -34,6 +36,7 @@
 
 /** Create a new project. **/
 @RequiresCapability(GlobalCapability.CREATE_PROJECT)
+@CommandMetaData(name = "create-project", descr = "Create a new project and associated Git repository")
 final class CreateProjectCommand extends SshCommand {
   @Option(name = "--name", aliases = {"-n"}, metaVar = "NAME", usage = "name of project to be created (deprecated option)")
   void setProjectNameFromOption(String name) {
@@ -64,17 +67,37 @@
       + "(default: MERGE_IF_NECESSARY)")
   private SubmitType submitType = SubmitType.MERGE_IF_NECESSARY;
 
+  @Option(name = "--contributor-agreements", usage = "if contributor agreement is required")
+  private InheritableBoolean contributorAgreements = InheritableBoolean.INHERIT;
+
+  @Option(name = "--signed-off-by", usage = "if signed-off-by is required")
+  private InheritableBoolean signedOffBy = InheritableBoolean.INHERIT;
+
+  @Option(name = "--content-merge", usage = "allow automatic conflict resolving within files")
+  private InheritableBoolean contentMerge = InheritableBoolean.INHERIT;
+
+  @Option(name = "--change-id", usage = "if change-id is required")
+  private InheritableBoolean requireChangeID = InheritableBoolean.INHERIT;
+
   @Option(name = "--use-contributor-agreements", aliases = {"--ca"}, usage = "if contributor agreement is required")
-  private boolean contributorAgreements;
+  void setUseContributorArgreements(boolean on) {
+    contributorAgreements = InheritableBoolean.TRUE;
+  }
 
   @Option(name = "--use-signed-off-by", aliases = {"--so"}, usage = "if signed-off-by is required")
-  private boolean signedOffBy;
+  void setUseSignedOffBy(boolean on) {
+    signedOffBy = InheritableBoolean.TRUE;
+  }
 
   @Option(name = "--use-content-merge", usage = "allow automatic conflict resolving within files")
-  private boolean contentMerge;
+  void setUseContentMerge(boolean on) {
+    contentMerge = InheritableBoolean.TRUE;
+  }
 
   @Option(name = "--require-change-id", aliases = {"--id"}, usage = "if change-id is required")
-  private boolean requireChangeID;
+  void setRequireChangeId(boolean on) {
+    requireChangeID = InheritableBoolean.TRUE;
+  }
 
   @Option(name = "--branch", aliases = {"-b"}, metaVar = "BRANCH", usage = "initial branch name\n"
       + "(default: master)")
@@ -95,7 +118,7 @@
   }
 
   @Inject
-  private CreateProject.Factory CreateProjectFactory;
+  private PerformCreateProject.Factory factory;
 
   @Inject
   private SuggestParentCandidates.Factory suggestParentCandidatesFactory;
@@ -121,8 +144,7 @@
         args.branch = branch;
         args.createEmptyCommit = createEmptyCommit;
 
-        final CreateProject createProject =
-            CreateProjectFactory.create(args);
+        final PerformCreateProject createProject = factory.create(args);
         createProject.createProject();
       } else {
         List<Project.NameKey> parentCandidates =
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 2a4dedc..35a4e14 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
@@ -36,25 +36,28 @@
     // SlaveCommandModule.
 
     command(gerrit).toProvider(new DispatchCommandProvider(gerrit));
-    command(gerrit, "ban-commit").to(BanCommitCommand.class);
-    command(gerrit, "flush-caches").to(FlushCaches.class);
-    command(gerrit, "ls-projects").to(ListProjectsCommand.class);
-    command(gerrit, "ls-groups").to(ListGroupsCommand.class);
-    command(gerrit, "query").to(Query.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);
+    command(gerrit, BanCommitCommand.class);
+    command(gerrit, FlushCaches.class);
+    command(gerrit, ListProjectsCommand.class);
+    command(gerrit, ListGroupsCommand.class);
+    command(gerrit, LsUserRefs.class);
+    command(gerrit, Query.class);
+    command(gerrit, ShowCaches.class);
+    command(gerrit, ShowConnections.class);
+    command(gerrit, ShowQueue.class);
+    command(gerrit, StreamEvents.class);
+    command(gerrit, VersionCommand.class);
+    command(gerrit, GarbageCollectionCommand.class);
 
     command(gerrit, "plugin").toProvider(new DispatchCommandProvider(plugin));
-    command(plugin, "ls").to(PluginLsCommand.class);
-    command(plugin, "enable").to(PluginEnableCommand.class);
-    command(plugin, "install").to(PluginInstallCommand.class);
-    command(plugin, "reload").to(PluginReloadCommand.class);
-    command(plugin, "remove").to(PluginRemoveCommand.class);
-    command(plugin, "add").to(Commands.key(plugin, "install"));
-    command(plugin, "rm").to(Commands.key(plugin, "remove"));
+
+    command(plugin, PluginLsCommand.class);
+    command(plugin, PluginEnableCommand.class);
+    command(plugin, PluginInstallCommand.class);
+    command(plugin, PluginReloadCommand.class);
+    command(plugin, PluginRemoveCommand.class);
+    alias(plugin, "add", PluginInstallCommand.class);
+    alias(plugin, "rm", PluginRemoveCommand.class);
 
     command(git).toProvider(new DispatchCommandProvider(git));
     command(git, "receive-pack").to(Commands.key(gerrit, "receive-pack"));
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/FlushCaches.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/FlushCaches.java
index fa63041..13abdf4 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/FlushCaches.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/FlushCaches.java
@@ -19,6 +19,7 @@
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.sshd.BaseCommand;
+import com.google.gerrit.sshd.CommandMetaData;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
@@ -31,6 +32,7 @@
 
 /** Causes the caches to purge all entries and reload. */
 @RequiresCapability(GlobalCapability.FLUSH_CACHES)
+@CommandMetaData(name = "flush-caches", descr = "Flush some/all server caches from memory")
 final class FlushCaches extends CacheCommand {
   private static final String WEB_SESSIONS = "web_sessions";
 
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/GarbageCollectionCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/GarbageCollectionCommand.java
new file mode 100644
index 0000000..c561153
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/GarbageCollectionCommand.java
@@ -0,0 +1,122 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.sshd.commands;
+
+import com.google.common.collect.Lists;
+import com.google.gerrit.common.data.GarbageCollectionResult;
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.server.git.GarbageCollection;
+import com.google.gerrit.server.project.ProjectCache;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.sshd.BaseCommand;
+import com.google.gerrit.sshd.CommandMetaData;
+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.io.PrintWriter;
+import java.util.ArrayList;
+import java.util.List;
+
+/** Runs the Git garbage collection. */
+@RequiresCapability(GlobalCapability.RUN_GC)
+@CommandMetaData(name = "gc", descr = "Run Git garbage collection")
+public class GarbageCollectionCommand extends BaseCommand {
+
+  @Option(name = "--all", usage = "runs the Git garbage collection for all projects")
+  private boolean all;
+
+  @Argument(index = 0, required = false, multiValued = true, metaVar = "NAME",
+      usage = "projects for which the Git garbage collection should be run")
+  private List<ProjectControl> projects = new ArrayList<ProjectControl>();
+
+  @Inject
+  private ProjectCache projectCache;
+
+  @Inject
+  private GarbageCollection.Factory garbageCollectionFactory;
+
+  private PrintWriter stdout;
+
+  @Override
+  public void start(Environment env) throws IOException {
+    startThread(new CommandRunnable() {
+      @Override
+      public void run() throws Exception {
+        stdout = toPrintWriter(out);
+        try {
+          parseCommandLine();
+          verifyCommandLine();
+          runGC();
+        } finally {
+          stdout.flush();
+        }
+      }
+    });
+  }
+
+  private void verifyCommandLine() throws UnloggedFailure {
+    if (!all && projects.isEmpty()) {
+      throw new UnloggedFailure(1,
+          "needs projects as command arguments or --all option");
+    }
+    if (all && !projects.isEmpty()) {
+      throw new UnloggedFailure(1,
+          "either specify projects as command arguments or use --all option");
+    }
+  }
+
+  private void runGC() {
+    List<Project.NameKey> projectNames;
+    if (all) {
+      projectNames = Lists.newArrayList(projectCache.all());
+    } else {
+      projectNames = Lists.newArrayListWithCapacity(projects.size());
+      for (ProjectControl pc : projects) {
+        projectNames.add(pc.getProject().getNameKey());
+      }
+    }
+
+    GarbageCollectionResult result =
+        garbageCollectionFactory.create().run(projectNames, stdout);
+    if (result.hasErrors()) {
+      for (GarbageCollectionResult.Error e : result.getErrors()) {
+        String msg;
+        switch (e.getType()) {
+          case REPOSITORY_NOT_FOUND:
+            msg = "error: project \"" + e.getProjectName() + "\" not found";
+            break;
+          case GC_ALREADY_SCHEDULED:
+            msg = "error: garbage collection for project \""
+                + e.getProjectName() + "\" was already scheduled";
+            break;
+          case GC_FAILED:
+            msg = "error: garbage collection for project \"" + e.getProjectName()
+                + "\" failed";
+            break;
+          default:
+            msg = "error: garbage collection for project \"" + e.getProjectName()
+                + "\" failed: " + e.getType();
+        }
+        stdout.print(msg + "\n");
+      }
+    }
+  }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java
index f8856f2..e5b4203 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListGroupsCommand.java
@@ -14,93 +14,91 @@
 
 package com.google.gerrit.sshd.commands;
 
-import com.google.gerrit.common.data.GroupList;
-import com.google.gerrit.common.errors.NoSuchGroupException;
-import com.google.gerrit.reviewdb.client.Account;
+import com.google.common.base.Objects;
+import com.google.common.base.Strings;
+import com.google.gerrit.extensions.restapi.Url;
 import com.google.gerrit.reviewdb.client.AccountGroup;
 import com.google.gerrit.server.IdentifiedUser;
+import com.google.gerrit.server.account.GetGroups;
 import com.google.gerrit.server.account.GroupCache;
-import com.google.gerrit.server.account.VisibleGroups;
+import com.google.gerrit.server.account.GroupControl;
+import com.google.gerrit.server.group.GroupJson;
+import com.google.gerrit.server.group.GroupJson.GroupInfo;
+import com.google.gerrit.server.group.ListGroups;
 import com.google.gerrit.server.ioutil.ColumnFormatter;
-import com.google.gerrit.server.project.ProjectControl;
-import com.google.gerrit.sshd.SshCommand;
-import com.google.gwtorm.client.KeyUtil;
+import com.google.gerrit.sshd.BaseCommand;
+import com.google.gerrit.sshd.CommandMetaData;
+import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 
+import org.apache.sshd.server.Environment;
 import org.kohsuke.args4j.Option;
 
-import java.util.ArrayList;
-import java.util.List;
+import java.io.PrintWriter;
 
-public class ListGroupsCommand extends SshCommand {
+@CommandMetaData(name = "ls-groups", descr = "List groups visible to the caller")
+public class ListGroupsCommand extends BaseCommand {
   @Inject
-  private GroupCache groupCache;
-
-  @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;
-
-  @Option(name = "--verbose", aliases = {"-v"},
-      usage = "verbose output format with tab-separated columns for the " +
-          "group name, UUID, description, type, owner group name, " +
-          "owner group UUID, and whether the group is visible to all")
-  private boolean verboseOutput;
+  private MyListGroups impl;
 
   @Override
-  protected void run() throws Failure {
-    try {
-      if (user != null && !projects.isEmpty()) {
-        throw new UnloggedFailure(1, "fatal: --user and --project options are not compatible.");
+  public void start(final Environment env) {
+    startThread(new CommandRunnable() {
+      @Override
+      public void run() throws Exception {
+        parseCommandLine(impl);
+        if (impl.getUser() != null && !impl.getProjects().isEmpty()) {
+          throw new UnloggedFailure(1, "fatal: --user and --project options are not compatible.");
+        }
+        final PrintWriter stdout = toPrintWriter(out);
+        try {
+          impl.display(stdout);
+        } finally {
+          stdout.flush();
+        }
       }
+    });
+  }
 
-      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();
-      }
+  private static class MyListGroups extends ListGroups {
+    @Option(name = "--verbose", aliases = {"-v"},
+        usage = "verbose output format with tab-separated columns for the " +
+            "group name, UUID, description, owner group name, " +
+            "owner group UUID, and whether the group is visible to all")
+    private boolean verboseOutput;
 
-      final ColumnFormatter formatter = new ColumnFormatter(stdout, '\t');
-      for (final AccountGroup g : groupList.getGroups()) {
-        formatter.addColumn(g.getName());
+    @Inject
+    MyListGroups(final GroupCache groupCache,
+        final GroupControl.Factory groupControlFactory,
+        final GroupControl.GenericFactory genericGroupControlFactory,
+        final Provider<IdentifiedUser> identifiedUser,
+        final IdentifiedUser.GenericFactory userFactory,
+        final Provider<GetGroups> accountGetGroups,
+        final GroupJson json) {
+      super(groupCache, groupControlFactory, genericGroupControlFactory,
+          identifiedUser, userFactory, accountGetGroups, json);
+    }
+
+    void display(final PrintWriter out) throws OrmException {
+      final ColumnFormatter formatter = new ColumnFormatter(out, '\t');
+      for (final GroupInfo info : get()) {
+        formatter.addColumn(Objects.firstNonNull(info.name, "n/a"));
         if (verboseOutput) {
-          formatter.addColumn(KeyUtil.decode(g.getGroupUUID().toString()));
-          formatter.addColumn(
-              g.getDescription() != null ? g.getDescription() : "");
-          formatter.addColumn(g.getType().toString());
-          final AccountGroup owningGroup =
-              groupCache.get(g.getOwnerGroupUUID());
-          formatter.addColumn(
-              owningGroup != null ? owningGroup.getName() : "n/a");
-          formatter.addColumn(KeyUtil.decode(g.getOwnerGroupUUID().toString()));
-          formatter.addColumn(Boolean.toString(g.isVisibleToAll()));
+          AccountGroup o = info.ownerId != null
+              ? groupCache.get(new AccountGroup.UUID(Url.decode(info.ownerId)))
+              : null;
+
+          formatter.addColumn(Url.decode(info.id));
+          formatter.addColumn(Strings.nullToEmpty(info.description));
+          formatter.addColumn(o != null ? o.getName() : "n/a");
+          formatter.addColumn(o != null ? o.getGroupUUID().get() : "");
+          formatter.addColumn(Boolean.toString(Objects.firstNonNull(
+              info.options.visibleToAll, Boolean.FALSE)));
         }
         formatter.nextLine();
       }
       formatter.finish();
-    } catch (NoSuchGroupException e) {
-      throw die(e);
     }
   }
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java
index 13e3f17..ab70395 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ListProjectsCommand.java
@@ -16,10 +16,14 @@
 
 import com.google.gerrit.server.project.ListProjects;
 import com.google.gerrit.sshd.BaseCommand;
+import com.google.gerrit.sshd.CommandMetaData;
 import com.google.inject.Inject;
 
 import org.apache.sshd.server.Environment;
 
+import java.util.List;
+
+@CommandMetaData(name = "ls-projects", descr = "List projects visible to the caller")
 final class ListProjectsCommand extends BaseCommand {
   @Inject
   private ListProjects impl;
@@ -31,7 +35,8 @@
       public void run() throws Exception {
         parseCommandLine(impl);
         if (!impl.getFormat().isJson()) {
-          if (impl.isShowTree() && (impl.getShowBranch() != null)) {
+          List<String> showBranch = impl.getShowBranch();
+          if (impl.isShowTree() && (showBranch != null) && !showBranch.isEmpty()) {
             throw new UnloggedFailure(1, "fatal: --tree and --show-branch options are not compatible.");
           }
           if (impl.isShowTree() && impl.isShowDescription()) {
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/LsUserRefs.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/LsUserRefs.java
new file mode 100644
index 0000000..58abf95
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/LsUserRefs.java
@@ -0,0 +1,115 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.sshd.commands;
+
+import com.google.gerrit.common.data.GlobalCapability;
+import com.google.gerrit.extensions.annotations.RequiresCapability;
+import com.google.gerrit.reviewdb.client.Account;
+import com.google.gerrit.reviewdb.client.Branch;
+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.git.ChangeCache;
+import com.google.gerrit.server.git.GitRepositoryManager;
+import com.google.gerrit.server.git.TagCache;
+import com.google.gerrit.server.git.VisibleRefFilter;
+import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.sshd.CommandMetaData;
+import com.google.gerrit.sshd.SshCommand;
+import com.google.gwtorm.server.OrmException;
+import com.google.inject.Inject;
+
+import org.eclipse.jgit.errors.RepositoryNotFoundException;
+import org.eclipse.jgit.lib.Ref;
+import org.eclipse.jgit.lib.Repository;
+import org.kohsuke.args4j.Option;
+
+import java.io.IOException;
+import java.util.Map;
+
+@RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+@CommandMetaData(name = "ls-user-refs", descr = "List refs visible to a specific user")
+public class LsUserRefs extends SshCommand {
+  @Inject
+  private AccountResolver accountResolver;
+
+  @Inject
+  private IdentifiedUser.GenericFactory userFactory;
+
+  @Inject
+  private ReviewDb db;
+
+  @Inject
+  private TagCache tagCache;
+
+  @Inject
+  private ChangeCache changeCache;
+
+  @Option(name = "--project", aliases = {"-p"}, metaVar = "PROJECT",
+      required = true, usage = "project for which the refs should be listed")
+  private ProjectControl projectControl;
+
+  @Option(name = "--user", aliases = {"-u"},  metaVar = "USER",
+      required = true, usage = "user for which the groups should be listed")
+  private String userName;
+
+  @Option(name = "--only-refs-heads", usage = "list only refs under refs/heads")
+  private boolean onlyRefsHeads;
+
+  @Inject
+  private GitRepositoryManager repoManager;
+
+  @Override
+  protected void run() throws Failure {
+    Account userAccount = null;
+    try {
+      userAccount = accountResolver.find(userName);
+    } catch (OrmException e) {
+      throw die(e);
+    }
+
+    if (userAccount == null) {
+      stdout.print("No single user could be found when searching for: " + userName + '\n');
+      stdout.flush();
+      return;
+    }
+
+    IdentifiedUser user = userFactory.create(userAccount.getId());
+    ProjectControl userProjectControl = projectControl.forUser(user);
+    Repository repo = null;
+    try {
+      repo = repoManager.openRepository(userProjectControl.getProject()
+              .getNameKey());
+
+      Map<String, Ref> refsMap =
+          new VisibleRefFilter(tagCache, changeCache, repo, userProjectControl,
+              db, true).filter(repo.getAllRefs(), false);
+
+      for (final String ref : refsMap.keySet()) {
+        if (!onlyRefsHeads || ref.startsWith(Branch.R_HEADS)) {
+          stdout.println(ref);
+        }
+      }
+    } catch (RepositoryNotFoundException e) {
+      throw new UnloggedFailure("fatal: '"
+          + projectControl.getProject().getNameKey() + "': not a git archive");
+    } catch (IOException e) {
+      throw new UnloggedFailure("fatal: Error opening: '"
+          + projectControl.getProject().getNameKey());
+    } finally {
+      repo.close();
+    }
+  }
+}
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 90bc07e..30f3c95 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
@@ -17,6 +17,7 @@
 import com.google.gerrit.sshd.CommandModule;
 import com.google.gerrit.sshd.CommandName;
 import com.google.gerrit.sshd.Commands;
+import com.google.gerrit.sshd.DispatchCommandProvider;
 
 
 /** Register the commands a Gerrit server in master mode supports. */
@@ -24,19 +25,24 @@
   @Override
   protected void configure() {
     final CommandName gerrit = Commands.named("gerrit");
+    final CommandName testSubmit = Commands.named(gerrit, "test-submit");
 
-    command(gerrit, "approve").to(ReviewCommand.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, "test-submit-rule").to(TestSubmitRule.class);
-    command(gerrit, "set-reviewers").to(SetReviewersCommand.class);
-    command(gerrit, "receive-pack").to(Receive.class);
-    command(gerrit, "set-project-parent").to(AdminSetParent.class);
-    command(gerrit, "review").to(ReviewCommand.class);
-    command(gerrit, "set-account").to(SetAccountCommand.class);
-    command(gerrit, "set-project").to(SetProjectCommand.class);
+    command(gerrit, CreateAccountCommand.class);
+    command(gerrit, CreateGroupCommand.class);
+    command(gerrit, RenameGroupCommand.class);
+    command(gerrit, CreateProjectCommand.class);
+    command(gerrit, AdminQueryShell.class);
+    command(gerrit, SetReviewersCommand.class);
+    command(gerrit, Receive.class);
+    command(gerrit, AdminSetParent.class);
+    command(gerrit, ReviewCommand.class);
+    // deprecated alias to review command
+    alias(gerrit, "approve", ReviewCommand.class);
+    command(gerrit, SetAccountCommand.class);
+    command(gerrit, SetProjectCommand.class);
+
+    command(gerrit, "test-submit").toProvider(new DispatchCommandProvider(testSubmit));
+    command(testSubmit, TestSubmitRuleCommand.class);
+    command(testSubmit, TestSubmitTypeCommand.class);
   }
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginEnableCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginEnableCommand.java
index 4df3aee..709e337 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginEnableCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginEnableCommand.java
@@ -19,6 +19,7 @@
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.server.plugins.PluginInstallException;
 import com.google.gerrit.server.plugins.PluginLoader;
+import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
 
@@ -27,6 +28,7 @@
 import java.util.List;
 
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+@CommandMetaData(name = "enable", descr = "Enable plugins")
 final class PluginEnableCommand extends SshCommand {
   @Argument(index = 0, metaVar = "NAME", required = true, usage = "plugin(s) to enable")
   List<String> names;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginInstallCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginInstallCommand.java
index 12722ec..fc036fe 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginInstallCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginInstallCommand.java
@@ -19,6 +19,7 @@
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.server.plugins.PluginInstallException;
 import com.google.gerrit.server.plugins.PluginLoader;
+import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
 
@@ -34,6 +35,7 @@
 import java.net.URL;
 
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+@CommandMetaData(name = "install", descr = "Install/Add a plugin")
 final class PluginInstallCommand extends SshCommand {
   @Option(name = "--name", aliases = {"-n"}, usage = "install under name")
   private String name;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginLsCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginLsCommand.java
index 6d7490f..ab6c978 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginLsCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginLsCommand.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.server.plugins.ListPlugins;
 import com.google.gerrit.sshd.BaseCommand;
+import com.google.gerrit.sshd.CommandMetaData;
 import com.google.inject.Inject;
 
 import org.apache.sshd.server.Environment;
@@ -25,6 +26,7 @@
 import java.io.IOException;
 
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+@CommandMetaData(name = "ls", descr = "List the installed plugins")
 final class PluginLsCommand extends BaseCommand {
   @Inject
   private ListPlugins impl;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginReloadCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginReloadCommand.java
index d2429a9..85ade03 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginReloadCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginReloadCommand.java
@@ -19,6 +19,7 @@
 import com.google.gerrit.server.plugins.PluginInstallException;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.server.plugins.PluginLoader;
+import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
 
@@ -27,6 +28,7 @@
 import java.util.List;
 
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+@CommandMetaData(name = "reload", descr = "Reload/Restart plugins")
 final class PluginReloadCommand extends SshCommand {
   @Argument(index = 0, metaVar = "NAME", usage = "plugins to reload/restart")
   private List<String> names;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginRemoveCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginRemoveCommand.java
index 8baab77..96adb8fe 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginRemoveCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/PluginRemoveCommand.java
@@ -18,6 +18,7 @@
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.server.plugins.PluginLoader;
+import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
 
@@ -26,6 +27,7 @@
 import java.util.List;
 
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+@CommandMetaData(name = "remove", descr = "Disable plugins")
 final class PluginRemoveCommand extends SshCommand {
   @Argument(index = 0, metaVar = "NAME", required = true, usage = "plugin to remove")
   List<String> names;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Query.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Query.java
index 63680f8..2d17876 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Query.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/Query.java
@@ -15,6 +15,7 @@
 package com.google.gerrit.sshd.commands;
 
 import com.google.gerrit.server.query.change.QueryProcessor;
+import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
 
@@ -23,6 +24,7 @@
 
 import java.util.List;
 
+@CommandMetaData(name = "query", descr = "Query the change database")
 class Query extends SshCommand {
   @Inject
   private QueryProcessor processor;
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 18ce77f..1630d11 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
@@ -63,9 +63,8 @@
 
   @Inject
   QueryShell(final SchemaFactory<ReviewDb> dbFactory,
-
-  @Assisted final InputStream in, @Assisted final OutputStream out)
-      throws UnsupportedEncodingException {
+      @Assisted final InputStream in, @Assisted final OutputStream out)
+          throws UnsupportedEncodingException {
     this.dbFactory = dbFactory;
     this.in = new BufferedReader(new InputStreamReader(in, "UTF-8"));
     this.out = new PrintWriter(new OutputStreamWriter(out, "UTF-8"));
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 b4de75b..e0dd38f 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
@@ -22,6 +22,7 @@
 import com.google.gerrit.server.git.TransferConfig;
 import com.google.gerrit.server.git.VisibleRefFilter;
 import com.google.gerrit.sshd.AbstractGitCommand;
+import com.google.gerrit.sshd.CommandMetaData;
 import com.google.inject.Inject;
 
 import org.eclipse.jgit.errors.TooLargeObjectInPackException;
@@ -41,6 +42,7 @@
 import java.util.Set;
 
 /** Receives change upload over SSH using the Git receive-pack protocol. */
+@CommandMetaData(name = "receive-pack", descr = "Standard Git server side command for client side git push")
 final class Receive extends AbstractGitCommand {
   private static final Logger log = LoggerFactory.getLogger(Receive.class);
 
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/RenameGroupCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/RenameGroupCommand.java
index b9abc92..f3c1bb3 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/RenameGroupCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/RenameGroupCommand.java
@@ -18,12 +18,14 @@
 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.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.gwtorm.server.OrmException;
 import com.google.inject.Inject;
 
 import org.kohsuke.args4j.Argument;
 
+@CommandMetaData(name = "rename-group", descr = "Rename an account group")
 public class RenameGroupCommand extends SshCommand {
   @Argument(index = 0, required = true, metaVar = "GROUP", usage = "name of the group to be renamed")
   private String groupName;
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 5ebb6c7..5769a22 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ReviewCommand.java
@@ -14,25 +14,36 @@
 
 package com.google.gerrit.sshd.commands;
 
-import com.google.gerrit.common.data.ApprovalType;
-import com.google.gerrit.common.data.ApprovalTypes;
+import com.google.common.base.Splitter;
+import com.google.common.base.Strings;
+import com.google.common.collect.ImmutableList;
+import com.google.common.collect.Maps;
+import com.google.gerrit.common.data.LabelType;
+import com.google.gerrit.common.data.LabelValue;
 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.common.data.ReviewResult.Error.Type;
+import com.google.gerrit.extensions.restapi.AuthException;
+import com.google.gerrit.extensions.restapi.BadRequestException;
+import com.google.gerrit.extensions.restapi.ResourceConflictException;
 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.change.Abandon;
+import com.google.gerrit.server.change.ChangeResource;
+import com.google.gerrit.server.change.PostReview;
+import com.google.gerrit.server.change.Restore;
+import com.google.gerrit.server.change.RevisionResource;
+import com.google.gerrit.server.change.Submit;
 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.config.AllProjectsName;
+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.NoSuchProjectException;
 import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.gerrit.util.cli.CmdLineParser;
 import com.google.gwtorm.server.OrmException;
@@ -40,7 +51,6 @@
 import com.google.inject.Inject;
 import com.google.inject.Provider;
 
-import org.eclipse.jgit.errors.RepositoryNotFoundException;
 import org.kohsuke.args4j.Argument;
 import org.kohsuke.args4j.Option;
 import org.slf4j.Logger;
@@ -48,11 +58,12 @@
 
 import java.io.IOException;
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.HashSet;
 import java.util.List;
+import java.util.Map;
 import java.util.Set;
 
+@CommandMetaData(name = "review", descr = "Verify, approve and/or submit one or more patch sets")
 public class ReviewCommand extends SshCommand {
   private static final Logger log =
       LoggerFactory.getLogger(ReviewCommand.class);
@@ -66,13 +77,12 @@
     return parser;
   }
 
-  private final Set<PatchSet.Id> patchSetIds = new HashSet<PatchSet.Id>();
+  private final Set<PatchSet> patchSets = new HashSet<PatchSet>();
 
-  @Argument(index = 0, required = true, multiValued = true, metaVar = "{COMMIT | CHANGE,PATCHSET}",
-      usage = "list of commits or patch sets to review")
+  @Argument(index = 0, required = true, multiValued = true, metaVar = "{COMMIT | CHANGE,PATCHSET}", usage = "list of commits or patch sets to review")
   void addPatchSetId(final String token) {
     try {
-      patchSetIds.addAll(parsePatchSetId(token));
+      patchSets.add(parsePatchSet(token));
     } catch (UnloggedFailure e) {
       throw new IllegalArgumentException(e.getMessage(), e);
     } catch (OrmException e) {
@@ -105,31 +115,54 @@
   @Option(name = "--delete", usage = "delete the specified draft patch set(s)")
   private boolean deleteDraftPatchSet;
 
-  @Inject
-  private ReviewDb db;
+  @Option(name = "--label", aliases = "-l", usage = "custom label(s) to assign", metaVar = "LABEL=VALUE")
+  void addLabel(final String token) {
+    List<String> parts = ImmutableList.copyOf(Splitter.on('=').split(token));
+    if (parts.size() != 2) {
+      throw new IllegalArgumentException("invalid custom label " + token);
+    }
+    short value;
+    try {
+      value = Short.parseShort(parts.get(1));
+    } catch (IllegalArgumentException e) {
+      throw new IllegalArgumentException("invalid custom label value "
+          + parts.get(1));
+    }
+    customLabels.put(parts.get(0), value);
+  }
 
   @Inject
-  private ApprovalTypes approvalTypes;
+  private ReviewDb db;
 
   @Inject
   private DeleteDraftPatchSet.Factory deleteDraftPatchSetFactory;
 
   @Inject
-  private Provider<AbandonChange> abandonChangeProvider;
+  private ProjectControl.Factory projectControlFactory;
 
   @Inject
-  private PublishComments.Factory publishCommentsFactory;
+  private AllProjectsName allProjects;
+
+  @Inject
+  private ChangeControl.Factory changeControlFactory;
+
+  @Inject
+  private Provider<Abandon> abandonProvider;
+
+  @Inject
+  private Provider<PostReview> reviewProvider;
 
   @Inject
   private PublishDraft.Factory publishDraftFactory;
 
   @Inject
-  private Provider<RestoreChange> restoreChangeProvider;
+  private Provider<Restore> restoreProvider;
 
   @Inject
-  private Submit.Factory submitFactory;
+  private Provider<Submit> submitProvider;
 
   private List<ApproveOption> optionList;
+  private Map<String, Short> customLabels;
 
   @Override
   protected void run() throws UnloggedFailure {
@@ -160,20 +193,20 @@
     }
 
     boolean ok = true;
-    for (final PatchSet.Id patchSetId : patchSetIds) {
+    for (final PatchSet patchSet : patchSets) {
       try {
-        approveOne(patchSetId);
+        approveOne(patchSet);
       } catch (UnloggedFailure e) {
         ok = false;
         writeError("error: " + e.getMessage() + "\n");
       } catch (NoSuchChangeException e) {
         ok = false;
-        writeError("no such change " + patchSetId.getParentKey().get());
+        writeError("no such change " + patchSet.getId().getParentKey().get());
       } catch (Exception e) {
         ok = false;
         writeError("fatal: internal server error while approving "
-            + patchSetId + "\n");
-        log.error("internal error while approving " + patchSetId, e);
+            + patchSet.getId() + "\n");
+        log.error("internal error while approving " + patchSet.getId(), e);
       }
     }
 
@@ -183,54 +216,97 @@
     }
   }
 
-  private void approveOne(final PatchSet.Id patchSetId)
-      throws NoSuchChangeException, OrmException, EmailException, Failure,
-      RepositoryNotFoundException, IOException {
+  private void applyReview(final ChangeControl ctl, final PatchSet patchSet,
+      final PostReview.Input review) throws Exception {
+    reviewProvider.get().apply(new RevisionResource(
+        new ChangeResource(ctl), patchSet), review);
+  }
+
+  private void approveOne(final PatchSet patchSet) throws Exception {
 
     if (changeComment == null) {
       changeComment = "";
     }
 
-    Set<ApprovalCategoryValue.Id> aps = new HashSet<ApprovalCategoryValue.Id>();
+    PostReview.Input review = new PostReview.Input();
+    review.message = Strings.emptyToNull(changeComment);
+    review.labels = Maps.newTreeMap();
+    review.drafts = PostReview.DraftHandling.PUBLISH;
+    review.strictLabels = false;
     for (ApproveOption ao : optionList) {
       Short v = ao.value();
       if (v != null) {
-        aps.add(new ApprovalCategoryValue.Id(ao.getCategoryId(), v));
+        review.labels.put(ao.getLabelName(), v);
       }
     }
+    review.labels.putAll(customLabels);
+
+    // If review labels are being applied, the comment will be included
+    // on the review note. We don't need to add it again on the abandon
+    // or restore comment.
+    if (!review.labels.isEmpty() && (abandonChange || restoreChange)) {
+      changeComment = null;
+    }
 
     try {
-      publishCommentsFactory.create(patchSetId, changeComment, aps, forceMessage).call();
+      ChangeControl ctl =
+          changeControlFactory.controlFor(patchSet.getId().getParentKey());
 
       if (abandonChange) {
-        final AbandonChange abandonChange = abandonChangeProvider.get();
-        abandonChange.setChangeId(patchSetId.getParentKey());
-        abandonChange.setMessage(changeComment);
-        final ReviewResult result = abandonChange.call();
-        handleReviewResultErrors(result);
+        final Abandon abandon = abandonProvider.get();
+        final Abandon.Input input = new Abandon.Input();
+        input.message = changeComment;
+        applyReview(ctl, patchSet, review);
+        try {
+          abandon.apply(new ChangeResource(ctl), input);
+        } catch (AuthException e) {
+          writeError("error: " + parseError(Type.ABANDON_NOT_PERMITTED) + "\n");
+        } catch (ResourceConflictException e) {
+          writeError("error: " + parseError(Type.CHANGE_IS_CLOSED) + "\n");
+        }
       } else if (restoreChange) {
-        final RestoreChange restoreChange = restoreChangeProvider.get();
-        restoreChange.setChangeId(patchSetId.getParentKey());
-        restoreChange.setMessage(changeComment);
-        final ReviewResult result = restoreChange.call();
-        handleReviewResultErrors(result);
+        final Restore restore = restoreProvider.get();
+        final Restore.Input input = new Restore.Input();
+        input.message = changeComment;
+        try {
+          restore.apply(new ChangeResource(ctl), input);
+          applyReview(ctl, patchSet, review);
+        } catch (AuthException e) {
+          writeError("error: " + parseError(Type.RESTORE_NOT_PERMITTED) + "\n");
+        } catch (ResourceConflictException e) {
+          writeError("error: " + parseError(Type.CHANGE_NOT_ABANDONED) + "\n");
+        }
+      } else {
+        applyReview(ctl, patchSet, review);
       }
+
       if (submitChange) {
-        final ReviewResult result = submitFactory.create(patchSetId).call();
-        handleReviewResultErrors(result);
+        Submit submit = submitProvider.get();
+        Submit.Input input = new Submit.Input();
+        input.waitForMerge = true;
+        submit.apply(new RevisionResource(
+            new ChangeResource(ctl), patchSet),
+          input);
       }
     } catch (InvalidChangeOperationException e) {
       throw error(e.getMessage());
     } catch (IllegalStateException e) {
       throw error(e.getMessage());
+    } catch (AuthException e) {
+      throw error(e.getMessage());
+    } catch (BadRequestException e) {
+      throw error(e.getMessage());
+    } catch (ResourceConflictException e) {
+      throw error(e.getMessage());
     }
 
     if (publishPatchSet) {
-      final ReviewResult result = publishDraftFactory.create(patchSetId).call();
+      final ReviewResult result =
+          publishDraftFactory.create(patchSet.getId()).call();
       handleReviewResultErrors(result);
     } else if (deleteDraftPatchSet) {
       final ReviewResult result =
-          deleteDraftPatchSetFactory.create(patchSetId).call();
+          deleteDraftPatchSetFactory.create(patchSet.getId()).call();
       handleReviewResultErrors(result);
     }
   }
@@ -238,46 +314,7 @@
   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 CHANGE_NOT_ABANDONED:
-          errMsg += "change is not abandoned";
-          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/patch set is not a draft";
-          break;
-        case GIT_ERROR:
-          errMsg += "error writing change to git repository";
-          break;
-        case DEST_BRANCH_NOT_FOUND:
-          errMsg += "destination branch not found";
-          break;
-        default:
-          errMsg += "failure in review";
-      }
+      errMsg += parseError(resultError.getType());
       if (resultError.getMessage() != null) {
         errMsg += ": " + resultError.getMessage();
       }
@@ -285,7 +322,38 @@
     }
   }
 
-  private Set<PatchSet.Id> parsePatchSetId(final String patchIdentity)
+  private String parseError(Type type) {
+    switch (type) {
+      case ABANDON_NOT_PERMITTED:
+        return "not permitted to abandon change";
+      case RESTORE_NOT_PERMITTED:
+        return "not permitted to restore change";
+      case SUBMIT_NOT_PERMITTED:
+        return "not permitted to submit change";
+      case SUBMIT_NOT_READY:
+        return "approvals or dependencies lacking";
+      case CHANGE_IS_CLOSED:
+        return "change is closed";
+      case CHANGE_NOT_ABANDONED:
+        return "change is not abandoned";
+      case PUBLISH_NOT_PERMITTED:
+        return "not permitted to publish change";
+      case DELETE_NOT_PERMITTED:
+        return "not permitted to delete change/patch set";
+      case RULE_ERROR:
+        return "rule error";
+      case NOT_A_DRAFT:
+        return "change/patch set is not a draft";
+      case GIT_ERROR:
+        return "error writing change to git repository";
+      case DEST_BRANCH_NOT_FOUND:
+        return "destination branch not found";
+      default:
+        return "failure in review";
+    }
+  }
+
+  private PatchSet parsePatchSet(final String patchIdentity)
       throws UnloggedFailure, OrmException {
     // By commit?
     //
@@ -298,17 +366,17 @@
         patches = db.patchSets().byRevisionRange(id, id.max());
       }
 
-      final Set<PatchSet.Id> matches = new HashSet<PatchSet.Id>();
+      final Set<PatchSet> matches = new HashSet<PatchSet>();
       for (final PatchSet ps : patches) {
         final Change change = db.changes().get(ps.getId().getParentKey());
         if (inProject(change)) {
-          matches.add(ps.getId());
+          matches.add(ps);
         }
       }
 
       switch (matches.size()) {
         case 1:
-          return matches;
+          return matches.iterator().next();
         case 0:
           throw error("\"" + patchIdentity + "\" no such patch set");
         default:
@@ -325,7 +393,8 @@
       } catch (IllegalArgumentException e) {
         throw error("\"" + patchIdentity + "\" is not a valid patch set");
       }
-      if (db.patchSets().get(patchSetId) == null) {
+      final PatchSet patchSet = db.patchSets().get(patchSetId);
+      if (patchSet == null) {
         throw error("\"" + patchIdentity + "\" no such patch set");
       }
       if (projectControl != null) {
@@ -335,7 +404,7 @@
               + projectControl.getProject().getName());
         }
       }
-      return Collections.singleton(patchSetId);
+      return patchSet;
     }
 
     throw error("\"" + patchIdentity + "\" is not a valid patch set");
@@ -352,18 +421,24 @@
   @Override
   protected void parseCommandLine() throws UnloggedFailure {
     optionList = new ArrayList<ApproveOption>();
+    customLabels = Maps.newHashMap();
 
-    for (ApprovalType type : approvalTypes.getApprovalTypes()) {
+    ProjectControl allProjectsControl;
+    try {
+      allProjectsControl = projectControlFactory.controlFor(allProjects);
+    } catch (NoSuchProjectException e) {
+      throw new UnloggedFailure("missing " + allProjects.get());
+    }
+
+    for (LabelType type : allProjectsControl.getLabelTypes().getLabelTypes()) {
       String usage = "";
-      final ApprovalCategory category = type.getCategory();
-      usage = "score for " + category.getName() + "\n";
+      usage = "score for " + type.getName() + "\n";
 
-      for (ApprovalCategoryValue v : type.getValues()) {
+      for (LabelValue v : type.getValues()) {
         usage += v.format() + "\n";
       }
 
-      final String name =
-          "--" + category.getName().toLowerCase().replace(' ', '-');
+      final String name = "--" + type.getName().toLowerCase();
       optionList.add(new ApproveOption(name, usage, type));
     }
 
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ScpCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ScpCommand.java
index 09c25ff..987380f 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ScpCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ScpCommand.java
@@ -92,6 +92,7 @@
 
   private void runImp() {
     try {
+      readAck();
       if (error != null) {
         throw error;
       }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
index 9940fc8..a3e9d6e 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetAccountCommand.java
@@ -29,6 +29,7 @@
 import com.google.gerrit.server.account.Realm;
 import com.google.gerrit.server.ssh.SshKeyCache;
 import com.google.gerrit.sshd.BaseCommand;
+import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Inject;
@@ -46,6 +47,7 @@
 import java.util.List;
 
 /** Set a user's account settings. **/
+@CommandMetaData(name = "set-account", descr = "Change an account's settings")
 final class SetAccountCommand extends BaseCommand {
 
   @Argument(index = 0, required = true, metaVar = "USER", usage = "full name, email-address, ssh username or account id")
@@ -145,6 +147,7 @@
     if (fullName != null) {
       if (realm.allowsEdit(FieldName.FULL_NAME)) {
         account.setFullName(fullName);
+        accountUpdated = true;
       } else {
         throw new UnloggedFailure(1, "The realm doesn't allow editing names");
       }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetProjectCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetProjectCommand.java
index 9143f5b..8c06c97d 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetProjectCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetProjectCommand.java
@@ -17,12 +17,14 @@
 import com.google.gerrit.common.data.GlobalCapability;
 import com.google.gerrit.extensions.annotations.RequiresCapability;
 import com.google.gerrit.reviewdb.client.Project;
+import com.google.gerrit.reviewdb.client.Project.InheritableBoolean;
 import com.google.gerrit.reviewdb.client.Project.State;
 import com.google.gerrit.reviewdb.client.Project.SubmitType;
 import com.google.gerrit.server.git.MetaDataUpdate;
 import com.google.gerrit.server.git.ProjectConfig;
 import com.google.gerrit.server.project.ProjectCache;
 import com.google.gerrit.server.project.ProjectControl;
+import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
 
@@ -36,6 +38,7 @@
 import java.io.IOException;
 
 @RequiresCapability(GlobalCapability.ADMINISTRATE_SERVER)
+@CommandMetaData(name = "set-project", descr = "Change a project's settings")
 final class SetProjectCommand extends SshCommand {
   private static final Logger log = LoggerFactory
       .getLogger(SetProjectCommand.class);
@@ -50,29 +53,57 @@
       + "(default: MERGE_IF_NECESSARY)")
   private SubmitType submitType;
 
+  @Option(name = "--contributor-agreements", usage = "if contributor agreement is required")
+  private InheritableBoolean contributorAgreements;
+
+  @Option(name = "--signed-off-by", usage = "if signed-off-by is required")
+  private InheritableBoolean signedOffBy;
+
+  @Option(name = "--content-merge", usage = "allow automatic conflict resolving within files")
+  private InheritableBoolean contentMerge;
+
+  @Option(name = "--change-id", usage = "if change-id is required")
+  private InheritableBoolean requireChangeID;
+
   @Option(name = "--use-contributor-agreements", aliases = {"--ca"}, usage = "if contributor agreement is required")
-  private Boolean contributorAgreements;
+  void setUseContributorArgreements(boolean on) {
+    contributorAgreements = InheritableBoolean.TRUE;
+  }
 
   @Option(name = "--no-contributor-agreements", aliases = {"--nca"}, usage = "if contributor agreement is not required")
-  private Boolean noContributorAgreements;
+  void setNoContributorArgreements(boolean on) {
+    contributorAgreements = InheritableBoolean.FALSE;
+  }
 
   @Option(name = "--use-signed-off-by", aliases = {"--so"}, usage = "if signed-off-by is required")
-  private Boolean signedOffBy;
+  void setUseSignedOffBy(boolean on) {
+    signedOffBy = InheritableBoolean.TRUE;
+  }
 
   @Option(name = "--no-signed-off-by", aliases = {"--nso"}, usage = "if signed-off-by is not required")
-  private Boolean noSignedOffBy;
+  void setNoSignedOffBy(boolean on) {
+    signedOffBy = InheritableBoolean.FALSE;
+  }
 
   @Option(name = "--use-content-merge", usage = "allow automatic conflict resolving within files")
-  private Boolean contentMerge;
+  void setUseContentMerge(boolean on) {
+    contentMerge = InheritableBoolean.TRUE;
+  }
 
   @Option(name = "--no-content-merge", usage = "don't allow automatic conflict resolving within files")
-  private Boolean noContentMerge;
+  void setNoContentMerge(boolean on) {
+    contentMerge = InheritableBoolean.FALSE;
+  }
 
   @Option(name = "--require-change-id", aliases = {"--id"}, usage = "if change-id is required")
-  private Boolean requireChangeID;
+  void setRequireChangeId(boolean on) {
+    requireChangeID = InheritableBoolean.TRUE;
+  }
 
   @Option(name = "--no-change-id", aliases = {"--nid"}, usage = "if change-id is not required")
-  private Boolean noRequireChangeID;
+  void setNoChangeId(boolean on) {
+    requireChangeID = InheritableBoolean.FALSE;
+  }
 
   @Option(name = "--project-state", aliases = {"--ps"}, usage = "project's visibility state")
   private State state;
@@ -85,7 +116,6 @@
 
   @Override
   protected void run() throws Failure {
-    validate();
     Project ctlProject = projectControl.getProject();
     Project.NameKey nameKey = ctlProject.getNameKey();
     String name = ctlProject.getName();
@@ -97,38 +127,27 @@
         ProjectConfig config = ProjectConfig.read(md);
         Project project = config.getProject();
 
-        project.setRequireChangeID(requireChangeID != null ? requireChangeID
-            : project.isRequireChangeID());
-
-        project.setRequireChangeID(noRequireChangeID != null
-            ? !noRequireChangeID : project.isRequireChangeID());
-
-        project.setSubmitType(submitType != null ? submitType : project
-            .getSubmitType());
-
-        project.setUseContentMerge(contentMerge != null ? contentMerge
-            : project.isUseContentMerge());
-
-        project.setUseContentMerge(noContentMerge != null ? !noContentMerge
-            : project.isUseContentMerge());
-
-        project.setUseContributorAgreements(contributorAgreements != null
-            ? contributorAgreements : project.isUseContributorAgreements());
-
-        project.setUseContributorAgreements(noContributorAgreements != null
-            ? !noContributorAgreements : project.isUseContributorAgreements());
-
-        project.setUseSignedOffBy(signedOffBy != null ? signedOffBy : project
-            .isUseSignedOffBy());
-
-        project.setUseContentMerge(noSignedOffBy != null ? !noSignedOffBy
-            : project.isUseContentMerge());
-
-        project.setDescription(projectDescription != null ? projectDescription
-            : project.getDescription());
-
-        project.setState(state != null ? state : project.getState());
-
+        if (requireChangeID != null) {
+          project.setRequireChangeID(requireChangeID);
+        }
+        if (submitType != null) {
+          project.setSubmitType(submitType);
+        }
+        if (contentMerge != null) {
+          project.setUseContentMerge(contentMerge);
+        }
+        if (contributorAgreements != null) {
+          project.setUseContributorAgreements(contributorAgreements);
+        }
+        if (signedOffBy != null) {
+          project.setUseSignedOffBy(signedOffBy);
+        }
+        if (projectDescription != null) {
+          project.setDescription(projectDescription);
+        }
+        if (state != null) {
+          project.setState(state);
+        }
         md.setMessage("Project settings updated");
         config.commit(md);
       } finally {
@@ -154,18 +173,4 @@
       throw new UnloggedFailure(1, err.toString());
     }
   }
-
-  private void validate() throws UnloggedFailure {
-    checkExclusivity(contentMerge, "--use-content-merge",
-        noContentMerge, "--no-content-merge");
-
-    checkExclusivity(contributorAgreements, "--use-contributor-agreements",
-        noContributorAgreements, "--no-contributor-agreements");
-
-    checkExclusivity(signedOffBy, "--use-signed-off-by",
-        noSignedOffBy, "--no-signed-off-by");
-
-    checkExclusivity(requireChangeID, "--require-change-id",
-        noRequireChangeID, "--no-change-id");
-  }
 }
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
index f873824..8e9bcac 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/SetReviewersCommand.java
@@ -14,21 +14,25 @@
 
 package com.google.gerrit.sshd.commands;
 
-import com.google.gerrit.common.data.ReviewerResult;
+import com.google.gerrit.extensions.restapi.ResourceNotFoundException;
 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.change.ChangeResource;
+import com.google.gerrit.server.change.DeleteReviewer;
+import com.google.gerrit.server.change.PostReviewers;
+import com.google.gerrit.server.change.ReviewerResource;
 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.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.gwtorm.server.OrmException;
 import com.google.gwtorm.server.ResultSet;
 import com.google.inject.Inject;
+import com.google.inject.Provider;
 
 import org.kohsuke.args4j.Argument;
 import org.kohsuke.args4j.Option;
@@ -36,12 +40,12 @@
 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;
 
+@CommandMetaData(name = "set-reviewers", descr = "Add or remove reviewers on a change")
 public class SetReviewersCommand extends SshCommand {
   private static final Logger log =
       LoggerFactory.getLogger(SetReviewersCommand.class);
@@ -72,10 +76,13 @@
   private ReviewDb db;
 
   @Inject
-  private AddReviewer.Factory addReviewerFactory;
+  private ReviewerResource.Factory reviewerFactory;
 
   @Inject
-  private RemoveReviewer.Factory removeReviewerFactory;
+  private Provider<PostReviewers> postReviewersProvider;
+
+  @Inject
+  private Provider<DeleteReviewer> deleteReviewerProvider;
 
   @Inject
   private ChangeControl.Factory changeControlFactory;
@@ -102,62 +109,48 @@
   }
 
   private boolean modifyOne(Change.Id changeId) throws Exception {
-    changeControlFactory.validateFor(changeId);
-
-    ReviewerResult result;
+    ChangeResource changeRsrc =
+        new ChangeResource(changeControlFactory.validateFor(changeId));
     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}";
+    DeleteReviewer delete = deleteReviewerProvider.get();
+    for (Account.Id reviewer : toRemove) {
+      ReviewerResource rsrc = reviewerFactory.create(changeRsrc, reviewer);
+      String error = null;;
+      try {
+        delete.apply(rsrc, new DeleteReviewer.Input());
+      } catch (ResourceNotFoundException e) {
+        error = String.format("could not remove %s: not found", reviewer);
+      } catch (Exception e) {
+        error = String.format("could not remove %s: %s",
+            reviewer, e.getMessage());
       }
-      writeError("error", MessageFormat.format(message,
-          resultError.getName(), changeId, resultError.getType()));
+      if (error != null) {
+        ok = false;
+        writeError("error", error);
+      }
     }
 
     // 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}";
+    PostReviewers post = postReviewersProvider.get();
+    for (String reviewer : toAdd) {
+      PostReviewers.Input input = new PostReviewers.Input();
+      input.reviewer = reviewer;
+      String error;
+      try {
+        error = post.apply(changeRsrc, input).error;
+      } catch (ResourceNotFoundException e) {
+        error = String.format("could not add %s: not found", reviewer);
+      } catch (Exception e) {
+        error = String.format("could not add %s: %s", reviewer, e.getMessage());
       }
-      writeError("error", MessageFormat.format(message,
-          resultError.getName(), changeId, resultError.getType()));
+      if (error != null) {
+        ok = false;
+        writeError("error", error);
+      }
     }
 
     return ok;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowCaches.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowCaches.java
index bdcb4fb..3366841 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowCaches.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowCaches.java
@@ -25,6 +25,7 @@
 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.CommandMetaData;
 import com.google.gerrit.sshd.SshDaemon;
 import com.google.inject.Inject;
 import com.google.inject.Provider;
@@ -32,7 +33,7 @@
 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.eclipse.jgit.internal.storage.file.WindowCacheStatAccessor;
 import org.kohsuke.args4j.Option;
 
 import java.io.File;
@@ -50,6 +51,7 @@
 
 /** Show the current cache states. */
 @RequiresCapability(GlobalCapability.VIEW_CACHES)
+@CommandMetaData(name = "show-caches", descr = "Display current cache statistics")
 final class ShowCaches extends CacheCommand {
   private static volatile long serverStarted;
 
@@ -250,6 +252,10 @@
         case RUNNING: tasksRunning++; break;
         case READY: tasksReady++; break;
         case SLEEPING: tasksSleeping++; break;
+        case CANCELLED:
+        case DONE:
+        case OTHER:
+          break;
       }
     }
     stdout.format(
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowConnections.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowConnections.java
index a1a5b8f..1c3d828 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowConnections.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowConnections.java
@@ -19,6 +19,7 @@
 import com.google.gerrit.server.CurrentUser;
 import com.google.gerrit.server.IdentifiedUser;
 import com.google.gerrit.server.util.IdGenerator;
+import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.gerrit.sshd.SshDaemon;
 import com.google.gerrit.sshd.SshSession;
@@ -41,6 +42,7 @@
 
 /** Show the current SSH connections. */
 @RequiresCapability(GlobalCapability.VIEW_CONNECTIONS)
+@CommandMetaData(name = "show-connections", descr = "Display active client SSH connections")
 final class ShowConnections extends SshCommand {
   @Option(name = "--numeric", aliases = {"-n"}, usage = "don't resolve names")
   private boolean numeric;
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 f862484..fee5275 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowQueue.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/ShowQueue.java
@@ -23,6 +23,7 @@
 import com.google.gerrit.server.project.ProjectState;
 import com.google.gerrit.server.util.IdGenerator;
 import com.google.gerrit.sshd.AdminHighPriorityCommand;
+import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 import com.google.inject.Inject;
 
@@ -39,6 +40,7 @@
 
 /** Display the current work queue. */
 @AdminHighPriorityCommand
+@CommandMetaData(name = "show-queue", descr = "Display the background work queues, including replication")
 final class ShowQueue extends SshCommand {
   @Option(name = "-w", usage = "display without line width truncation")
   private boolean wide;
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 ff6dea9..1b81a47 100644
--- 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
@@ -21,6 +21,7 @@
 import com.google.gerrit.server.git.WorkQueue;
 import com.google.gerrit.server.git.WorkQueue.CancelableRunnable;
 import com.google.gerrit.sshd.BaseCommand;
+import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.StreamCommandExecutor;
 import com.google.gson.Gson;
 import com.google.inject.Inject;
@@ -32,6 +33,7 @@
 import java.util.concurrent.Future;
 import java.util.concurrent.LinkedBlockingQueue;
 
+@CommandMetaData(name = "stream-events", descr = "Monitor events occurring in real time")
 final class StreamEvents extends BaseCommand {
   /** Maximum number of events that may be queued up for each connection. */
   private static final int MAX_EVENTS = 128;
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/TestSubmitRule.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/TestSubmitRule.java
deleted file mode 100644
index c8544e4..0000000
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/TestSubmitRule.java
+++ /dev/null
@@ -1,241 +0,0 @@
-// Copyright (C) 2012 The Android Open Source Project
-//
-// Licensed under the Apache License, Version 2.0 (the "License");
-// you may not use this file except in compliance with the License.
-// You may obtain a copy of the License at
-//
-// http://www.apache.org/licenses/LICENSE-2.0
-//
-// Unless required by applicable law or agreed to in writing, software
-// distributed under the License is distributed on an "AS IS" BASIS,
-// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
-// See the License for the specific language governing permissions and
-// limitations under the License.
-
-package com.google.gerrit.sshd.commands;
-
-import com.google.gerrit.common.data.AccountInfo;
-import com.google.gerrit.common.data.SubmitRecord;
-import com.google.gerrit.reviewdb.client.Account;
-import com.google.gerrit.reviewdb.client.Change;
-import com.google.gerrit.reviewdb.client.PatchSet;
-import com.google.gerrit.reviewdb.client.Project;
-import com.google.gerrit.reviewdb.server.ReviewDb;
-import com.google.gerrit.rules.PrologEnvironment;
-import com.google.gerrit.rules.StoredValues;
-import com.google.gerrit.server.OutputFormat;
-import com.google.gerrit.server.account.AccountCache;
-import com.google.gerrit.server.config.AnonymousCowardName;
-import com.google.gerrit.server.events.AccountAttribute;
-import com.google.gerrit.server.events.SubmitLabelAttribute;
-import com.google.gerrit.server.events.SubmitRecordAttribute;
-import com.google.gerrit.server.project.ChangeControl;
-import com.google.gerrit.server.project.ProjectState;
-import com.google.gerrit.sshd.SshCommand;
-import com.google.gson.reflect.TypeToken;
-import com.google.inject.Inject;
-
-import com.googlecode.prolog_cafe.compiler.CompileException;
-import com.googlecode.prolog_cafe.lang.BufferingPrologControl;
-import com.googlecode.prolog_cafe.lang.JavaObjectTerm;
-import com.googlecode.prolog_cafe.lang.ListTerm;
-import com.googlecode.prolog_cafe.lang.Prolog;
-import com.googlecode.prolog_cafe.lang.PrologClassLoader;
-import com.googlecode.prolog_cafe.lang.PrologException;
-import com.googlecode.prolog_cafe.lang.PrologMachineCopy;
-import com.googlecode.prolog_cafe.lang.SymbolTerm;
-import com.googlecode.prolog_cafe.lang.Term;
-import com.googlecode.prolog_cafe.lang.VariableTerm;
-
-import org.kohsuke.args4j.Argument;
-import org.kohsuke.args4j.Option;
-
-import java.io.InputStreamReader;
-import java.io.PushbackReader;
-import java.util.ArrayList;
-import java.util.HashSet;
-import java.util.LinkedList;
-import java.util.List;
-import java.util.Set;
-
-/** Command that allows testing of prolog submit-rules in a live instance. */
-final class TestSubmitRule extends SshCommand {
-  @Inject
-  private ReviewDb db;
-
-  @Inject
-  private PrologEnvironment.Factory envFactory;
-
-  @Inject
-  private ChangeControl.Factory ccFactory;
-
-  @Inject
-  private AccountCache accountCache;
-
-  final @AnonymousCowardName String anonymousCowardName;
-
-  @Argument(index = 0, required = true, usage = "ChangeId to load in prolog environment")
-  private String changeId;
-
-  @Option(name = "-s",
-      usage = "Read prolog script from stdin instead of reading rules.pl from the refs/meta/config branch")
-  private boolean useStdin;
-
-  @Option(name = "--format", metaVar = "FMT", usage = "Output display format")
-  private OutputFormat format = OutputFormat.TEXT;
-
-  @Option(name = "--no-filters", aliases = {"-n"},
-      usage = "Don't run the submit_filter/2 from the parent projects")
-  private boolean skipSubmitFilters;
-
-  private static final String[] PACKAGE_LIST = {Prolog.BUILTIN, "gerrit"};
-
-  @Inject
-  public TestSubmitRule(@AnonymousCowardName String anonymous) {
-    anonymousCowardName = anonymous;
-  }
-  private PrologMachineCopy newMachine() {
-    BufferingPrologControl ctl = new BufferingPrologControl();
-    ctl.setMaxDatabaseSize(16 * 1024);
-    ctl.setPrologClassLoader(new PrologClassLoader(getClass().getClassLoader()));
-    return PrologMachineCopy.save(ctl);
-  }
-
-  @Override
-  protected void run() throws UnloggedFailure {
-    PushbackReader inReader = new PushbackReader(new InputStreamReader(in));
-
-    try {
-      PrologEnvironment pcl;
-
-      List<Change> changeList =
-          db.changes().byKey(new Change.Key(changeId)).toList();
-      if (changeList.size() != 1)
-        throw new UnloggedFailure(1, "Invalid ChangeId");
-
-      Change c = changeList.get(0);
-      PatchSet ps = db.patchSets().get(c.currentPatchSetId());
-      // Will throw exception if current user can not access this change, and
-      // thus will leak information that a change-id is valid even though the
-      // user are not allowed to see the change.
-      // See http://code.google.com/p/gerrit/issues/detail?id=1586
-      ChangeControl cc = ccFactory.controlFor(c);
-      ProjectState projectState = cc.getProjectControl().getProjectState();
-
-      if (useStdin) {
-        pcl = envFactory.create(newMachine());
-      } else {
-        pcl = projectState.newPrologEnvironment();
-      }
-
-      pcl.set(StoredValues.REVIEW_DB, db);
-      pcl.set(StoredValues.CHANGE, c);
-      pcl.set(StoredValues.PATCH_SET, ps);
-      pcl.set(StoredValues.CHANGE_CONTROL, cc);
-      if (useStdin) {
-        pcl.initialize(PACKAGE_LIST);
-        pcl.execute(Prolog.BUILTIN, "consult_stream",
-            SymbolTerm.intern("stdin"), new JavaObjectTerm(inReader));
-      }
-
-      List<Term> results = new ArrayList<Term>();
-      Term submitRule =
-          pcl.once("gerrit", "locate_submit_rule", new VariableTerm());
-
-      for (Term[] template : pcl.all("gerrit", "can_submit", submitRule,
-          new VariableTerm())) {
-        results.add(template[1]);
-      }
-
-      if (!skipSubmitFilters) {
-        runSubmitFilters(projectState, results, pcl);
-      }
-
-      List<SubmitRecord> res = cc.resultsToSubmitRecord(submitRule, results);
-      for (SubmitRecord r : res) {
-        if (format.isJson()) {
-          SubmitRecordAttribute submitRecord = new SubmitRecordAttribute();
-          submitRecord.status = r.status.name();
-
-          List<SubmitLabelAttribute> submitLabels = new LinkedList<SubmitLabelAttribute>();
-          for(SubmitRecord.Label l : r.labels) {
-            SubmitLabelAttribute label = new SubmitLabelAttribute();
-            label.label = l.label;
-            label.status= l.status.name();
-            if(l.appliedBy != null) {
-              Account a = accountCache.get(l.appliedBy).getAccount();
-              label.by = new AccountAttribute();
-              label.by.email = a.getPreferredEmail();
-              label.by.name = a.getFullName();
-              label.by.username = a.getUserName();
-            }
-            submitLabels.add(label);
-          }
-          submitRecord.labels = submitLabels;
-          format.newGson().toJson(submitRecord, new TypeToken<SubmitRecordAttribute>() {}.getType(), stdout);
-          stdout.print('\n');
-        } else {
-          for(SubmitRecord.Label l : r.labels) {
-            stdout.print(l.label + ": " + l.status);
-            if(l.appliedBy != null) {
-              AccountInfo a = new AccountInfo(accountCache.get(l.appliedBy).getAccount());
-              stdout.print(" by " + a.getNameEmail(anonymousCowardName));
-            }
-            stdout.print('\n');
-          }
-          stdout.print("\n" + r.status.name() + "\n");
-        }
-      }
-    } catch (Exception e) {
-      throw new UnloggedFailure("Processing of prolog script failed: " + e);
-    }
-  }
-
-  private void runSubmitFilters(ProjectState projectState, List<Term> results,
-      PrologEnvironment pcl) throws UnloggedFailure {
-    ProjectState parentState = projectState.getParentState();
-    PrologEnvironment childEnv = pcl;
-    Set<Project.NameKey> projectsSeen = new HashSet<Project.NameKey>();
-    projectsSeen.add(projectState.getProject().getNameKey());
-
-    while (parentState != null) {
-      if (!projectsSeen.add(parentState.getProject().getNameKey())) {
-        // parent has been seen before, stop walk up inheritance tree
-        break;
-      }
-      PrologEnvironment parentEnv;
-      try {
-        parentEnv = parentState.newPrologEnvironment();
-      } catch (CompileException err) {
-        throw new UnloggedFailure("Cannot consult rules.pl for "
-            + parentState.getProject().getName() + err);
-      }
-
-      parentEnv.copyStoredValues(childEnv);
-      Term filterRule =
-          parentEnv.once("gerrit", "locate_submit_filter", new VariableTerm());
-      if (filterRule != null) {
-        try {
-          Term resultsTerm = ChangeControl.toListTerm(results);
-          results.clear();
-          Term[] template =
-              parentEnv.once("gerrit", "filter_submit_results", filterRule,
-                  resultsTerm, new VariableTerm());
-          @SuppressWarnings("unchecked")
-          final List<? extends Term> termList =
-              ((ListTerm) template[2]).toJava();
-          results.addAll(termList);
-        } catch (PrologException err) {
-          throw new UnloggedFailure("Exception calling " + filterRule + " of "
-              + parentState.getProject().getName() + err);
-        } catch (RuntimeException err) {
-          throw new UnloggedFailure("Exception calling " + filterRule + " of "
-              + parentState.getProject().getName() + err);
-        }
-      }
-
-      parentState = parentState.getParentState();
-      childEnv = parentEnv;
-    }
-  }
-}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/TestSubmitRuleCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/TestSubmitRuleCommand.java
new file mode 100644
index 0000000..6335160
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/TestSubmitRuleCommand.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package com.google.gerrit.sshd.commands;
+
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.server.change.RevisionResource;
+import com.google.gerrit.server.change.TestSubmitRule;
+import com.google.gerrit.server.change.TestSubmitRule.Input;
+import com.google.gerrit.sshd.CommandMetaData;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+/** Command that allows testing of prolog submit-rules in a live instance. */
+@CommandMetaData(name = "rule", descr = "Test prolog submit rules")
+final class TestSubmitRuleCommand extends BaseTestPrologCommand {
+  @Inject
+  private Provider<TestSubmitRule> view;
+
+  @Override
+  protected RestModifyView<RevisionResource, Input> createView() {
+    return view.get();
+  }
+}
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/TestSubmitTypeCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/TestSubmitTypeCommand.java
new file mode 100644
index 0000000..326ff46
--- /dev/null
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/TestSubmitTypeCommand.java
@@ -0,0 +1,35 @@
+// Copyright (C) 2012 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+//
+
+package com.google.gerrit.sshd.commands;
+
+import com.google.gerrit.extensions.restapi.RestModifyView;
+import com.google.gerrit.server.change.RevisionResource;
+import com.google.gerrit.server.change.TestSubmitRule.Input;
+import com.google.gerrit.server.change.TestSubmitType;
+import com.google.gerrit.sshd.CommandMetaData;
+import com.google.inject.Inject;
+import com.google.inject.Provider;
+
+@CommandMetaData(name = "type", descr = "Test prolog submit type")
+final class TestSubmitTypeCommand extends BaseTestPrologCommand {
+  @Inject
+  private Provider<TestSubmitType> view;
+
+  @Override
+  protected RestModifyView<RevisionResource, Input> createView() {
+    return view.get();
+  }
+}
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 46eb788..fc127ec 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
@@ -15,6 +15,7 @@
 package com.google.gerrit.sshd.commands;
 
 import com.google.gerrit.reviewdb.server.ReviewDb;
+import com.google.gerrit.server.git.ChangeCache;
 import com.google.gerrit.server.git.TagCache;
 import com.google.gerrit.server.git.TransferConfig;
 import com.google.gerrit.server.git.VisibleRefFilter;
@@ -37,6 +38,9 @@
   @Inject
   private TagCache tagCache;
 
+  @Inject
+  private ChangeCache changeCache;
+
   @Override
   protected void runImpl() throws IOException, Failure {
     if (!projectControl.canRunUploadPack()) {
@@ -45,7 +49,7 @@
 
     final UploadPack up = new UploadPack(repo);
     if (!projectControl.allRefsAreVisible()) {
-      up.setAdvertiseRefsHook(new VisibleRefFilter(tagCache, repo,
+      up.setAdvertiseRefsHook(new VisibleRefFilter(tagCache, changeCache, repo,
           projectControl, db.get(), true));
     }
     up.setPackConfig(config.getPackConfig());
diff --git a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/VersionCommand.java b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/VersionCommand.java
index addbb84..2066cc2 100644
--- a/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/VersionCommand.java
+++ b/gerrit-sshd/src/main/java/com/google/gerrit/sshd/commands/VersionCommand.java
@@ -15,9 +15,12 @@
 package com.google.gerrit.sshd.commands;
 
 import com.google.gerrit.common.Version;
+import com.google.gerrit.sshd.CommandMetaData;
 import com.google.gerrit.sshd.SshCommand;
 
+@CommandMetaData(name = "version", descr = "Display gerrit version")
 final class VersionCommand extends SshCommand {
+
   @Override
   protected void run() throws Failure {
     String v = Version.getVersion();
diff --git a/gerrit-util-cli/.settings/org.eclipse.jdt.core.prefs b/gerrit-util-cli/.settings/org.eclipse.jdt.core.prefs
index 470942d..941fb31 100644
--- a/gerrit-util-cli/.settings/org.eclipse.jdt.core.prefs
+++ b/gerrit-util-cli/.settings/org.eclipse.jdt.core.prefs
@@ -7,6 +7,7 @@
 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.annotationSuperInterface=ignore
 org.eclipse.jdt.core.compiler.problem.assertIdentifier=error
 org.eclipse.jdt.core.compiler.problem.enumIdentifier=error
 org.eclipse.jdt.core.compiler.problem.forbiddenReference=warning
diff --git a/gerrit-util-cli/pom.xml b/gerrit-util-cli/pom.xml
index 4886d09..04e3b09 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.5-SNAPSHOT</version>
+    <version>2.6-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-util-cli</artifactId>
@@ -39,6 +39,11 @@
     </dependency>
 
     <dependency>
+      <groupId>com.google.guava</groupId>
+      <artifactId>guava</artifactId>
+    </dependency>
+
+    <dependency>
       <groupId>com.google.inject</groupId>
       <artifactId>guice</artifactId>
     </dependency>
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 a9f5229..69598ce 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
@@ -34,6 +34,10 @@
 
 package com.google.gerrit.util.cli;
 
+import com.google.common.collect.LinkedHashMultimap;
+import com.google.common.collect.Lists;
+import com.google.common.collect.Maps;
+import com.google.common.collect.Multimap;
 import com.google.inject.Inject;
 import com.google.inject.Injector;
 import com.google.inject.Key;
@@ -54,11 +58,9 @@
 import java.io.Writer;
 import java.lang.annotation.Annotation;
 import java.util.ArrayList;
-import java.util.Collections;
 import java.util.List;
 import java.util.Map;
 import java.util.ResourceBundle;
-import java.util.Set;
 
 /**
  * Extended command line parser which handles --foo=value arguments.
@@ -76,6 +78,9 @@
   private final Injector injector;
   private final MyParser parser;
 
+  @SuppressWarnings("rawtypes")
+  private Map<String, OptionHandler> options;
+
   /**
    * Creates a new command line owner that parses arguments/options and set them
    * into the given object.
@@ -186,7 +191,7 @@
   }
 
   public void parseArgument(final String... args) throws CmdLineException {
-    final ArrayList<String> tmp = new ArrayList<String>(args.length);
+    List<String> tmp = Lists.newArrayListWithCapacity(args.length);
     for (int argi = 0; argi < args.length; argi++) {
       final String str = args[argi];
       if (str.equals("--")) {
@@ -211,36 +216,32 @@
 
   public void parseOptionMap(Map<String, String[]> parameters)
       throws CmdLineException {
-    parseOptionMap(parameters, Collections.<String>emptySet());
+    Multimap<String, String> map = LinkedHashMultimap.create();
+    for (Map.Entry<String, String[]> ent : parameters.entrySet()) {
+      for (String val : ent.getValue()) {
+        map.put(ent.getKey(), val);
+      }
+    }
+    parseOptionMap(map);
   }
 
-  public void parseOptionMap(Map<String, String[]> parameters,
-      Set<String> argNames)
+  public void parseOptionMap(Multimap<String, String> params)
       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;
-        }
-      }
+    List<String> tmp = Lists.newArrayListWithCapacity(2 * params.size());
+    for (final String key : params.keySet()) {
+      String name = makeOption(key);
 
-      if (findHandler(name) instanceof BooleanOptionHandler) {
+      if (isBoolean(name)) {
         boolean on = false;
-        for (String value : ent.getValue()) {
-          on = toBoolean(ent.getKey(), value);
+        for (String value : params.get(key)) {
+          on = toBoolean(key, value);
         }
         if (on) {
           tmp.add(name);
         }
       } else {
-        for (String value : ent.getValue()) {
-          if (!argNames.contains(ent.getKey())) {
-            tmp.add(name);
-          }
+        for (String value : params.get(key)) {
+          tmp.add(name);
           tmp.add(value);
         }
       }
@@ -248,22 +249,44 @@
     parser.parseArgument(tmp.toArray(new String[tmp.size()]));
   }
 
+  public boolean isBoolean(String name) {
+    return findHandler(makeOption(name)) instanceof BooleanOptionHandler;
+  }
+
+  private String makeOption(String name) {
+    if (!name.startsWith("-")) {
+      if (name.length() == 1) {
+        name = "-" + name;
+      } else {
+        name = "--" + name;
+      }
+    }
+    return name;
+  }
+
   @SuppressWarnings("rawtypes")
   private OptionHandler findHandler(String name) {
-    for (OptionHandler handler : parser.options) {
+    if (options == null) {
+      options = index(parser.options);
+    }
+    return options.get(name);
+  }
+
+  @SuppressWarnings("rawtypes")
+  private static Map<String, OptionHandler> index(List<OptionHandler> in) {
+    Map<String, OptionHandler> m = Maps.newHashMap();
+    for (OptionHandler handler : in) {
       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;
+        if (!def.isArgument()) {
+          m.put(def.name(), handler);
+          for (String alias : def.aliases()) {
+            m.put(alias, handler);
           }
         }
       }
     }
-    return null;
+    return m;
   }
 
   private boolean toBoolean(String name, String value) throws CmdLineException {
@@ -324,11 +347,10 @@
       return handler;
     }
 
-    @SuppressWarnings("rawtypes")
     private void ensureOptionsInitialized() {
       if (options == null) {
         help = new HelpOption();
-        options = new ArrayList<OptionHandler>();
+        options = Lists.newArrayList();
         addOption(help, help);
       }
     }
diff --git a/gerrit-util-ssl/pom.xml b/gerrit-util-ssl/pom.xml
index beedb8f..69ae3b5 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.5-SNAPSHOT</version>
+    <version>2.6-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-util-ssl</artifactId>
diff --git a/gerrit-war/pom.xml b/gerrit-war/pom.xml
index 1f3750e..0ebbc8a 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.5-SNAPSHOT</version>
+    <version>2.6-SNAPSHOT</version>
   </parent>
 
   <artifactId>gerrit-war</artifactId>
@@ -36,9 +36,8 @@
   <dependencies>
     <dependency>
       <groupId>org.apache.tomcat</groupId>
-      <artifactId>servlet-api</artifactId>
-      <!-- compile, not provided. our embedded Jetty needs this -->
-      <scope>compile</scope>
+      <artifactId>tomcat-servlet-api</artifactId>
+      <scope>provided</scope>
     </dependency>
 
     <dependency>
@@ -59,14 +58,12 @@
     <dependency>
       <groupId>bouncycastle</groupId>
       <artifactId>bcprov-jdk15</artifactId>
-      <version>140</version>
       <scope>provided</scope>
     </dependency>
 
     <dependency>
       <groupId>bouncycastle</groupId>
       <artifactId>bcpg-jdk15</artifactId>
-      <version>140</version>
       <scope>provided</scope>
     </dependency>
 
@@ -102,10 +99,89 @@
       <groupId>com.google.gerrit</groupId>
       <artifactId>gerrit-pgm</artifactId>
       <version>${project.version}</version>
+      <exclusions>
+        <exclusion>
+          <groupId>org.eclipse.jetty</groupId>
+          <artifactId>jetty-servlet</artifactId>
+        </exclusion>
+      </exclusions>
+    </dependency>
+
+    <dependency>
+      <groupId>org.eclipse.jetty</groupId>
+      <artifactId>jetty-servlet</artifactId>
+      <scope>provided</scope>
+    </dependency>
+
+    <dependency>
+      <groupId>org.apache.tomcat</groupId>
+      <artifactId>servlet-api</artifactId>
+      <scope>provided</scope>
     </dependency>
   </dependencies>
 
+  <profiles>
+    <profile>
+      <id>plugins</id>
+      <activation>
+        <property>
+          <name>!gerrit.plugins.skip</name>
+        </property>
+      </activation>
+      <dependencies>
+        <!-- CORE PLUGIN LIST -->
+        <dependency>
+          <groupId>com.googlesource.gerrit.plugins.replication</groupId>
+          <artifactId>replication</artifactId>
+          <version>${project.version}</version>
+          <scope>provided</scope>
+        </dependency>
+        <dependency>
+          <groupId>com.googlesource.gerrit.plugins.reviewnotes</groupId>
+          <artifactId>reviewnotes</artifactId>
+          <version>${project.version}</version>
+          <scope>provided</scope>
+        </dependency>
+        <dependency>
+          <groupId>com.googlesource.gerrit.plugins.validators</groupId>
+          <artifactId>commit-message-length-validator</artifactId>
+          <version>${project.version}</version>
+          <scope>provided</scope>
+        </dependency>
+      </dependencies>
+    </profile>
+  </profiles>
+
   <build>
+	<pluginManagement>
+	  <plugins>
+	    <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-dependency-plugin</artifactId>
+	                <versionRange>[2.0,)</versionRange>
+	                <goals>
+	                  <goal>copy-dependencies</goal>
+	                  <goal>unpack</goal>goal>
+	                </goals>
+	              </pluginExecutionFilter>
+	              <action>
+	                <execute />
+	              </action>
+	            </pluginExecution>
+	          </pluginExecutions>
+	        </lifecycleMappingMetadata>
+	      </configuration>
+	    </plugin>
+	  </plugins>
+	</pluginManagement>
     <plugins>
       <plugin>
         <groupId>org.apache.maven.plugins</groupId>
@@ -138,9 +214,59 @@
 
       <plugin>
         <groupId>org.apache.maven.plugins</groupId>
+        <artifactId>maven-dependency-plugin</artifactId>
+        <executions>
+          <execution>
+            <id>copy-servlet-api</id>
+            <configuration>
+              <includeGroupIds>org.apache.tomcat,org.eclipse.jetty</includeGroupIds>
+              <excludeArtifactIds>servlet-api</excludeArtifactIds>
+            </configuration>
+            <goals>
+              <goal>copy-dependencies</goal>
+            </goals>
+          </execution>
+          <execution>
+            <id>copy-plugins</id>
+            <configuration>
+              <!-- CORE PLUGIN LIST -->
+              <includeArtifactIds>commit-message-length-validator,replication,reviewnotes</includeArtifactIds>
+              <includeTypes>jar</includeTypes>
+              <stripVersion>true</stripVersion>
+              <outputDirectory>${project.build.directory}/${project.build.finalName}/WEB-INF/plugins</outputDirectory>
+            </configuration>
+            <goals>
+              <goal>copy-dependencies</goal>
+            </goals>
+          </execution>
+        </executions>
+      </plugin>
+
+      <plugin>
+        <groupId>org.apache.maven.plugins</groupId>
         <artifactId>maven-antrun-plugin</artifactId>
         <executions>
           <execution>
+            <id>copy-servlet-api</id>
+            <phase>process-classes</phase>
+            <configuration>
+              <target>
+                <property name="src" location="${project.build.directory}/dependency" />
+                <property name="dst" location="${project.build.directory}/${project.build.finalName}/WEB-INF/pgm-lib" />
+
+                <mkdir dir="${dst}" />
+                <copy overwrite="true" todir="${dst}">
+                  <fileset dir="${src}">
+                    <include name="*.jar" />
+                  </fileset>
+                </copy>
+              </target>
+            </configuration>
+            <goals>
+              <goal>run</goal>
+            </goals>
+          </execution>
+          <execution>
             <id>copy-license</id>
             <phase>process-classes</phase>
             <configuration>
@@ -161,7 +287,7 @@
             <id>include-documentation</id>
             <phase>process-classes</phase>
             <configuration>
-              <target if="gerrit.include-documentation">
+              <target unless="gerrit.documentation.skip">
                 <property name="src" location="${basedir}/../Documentation" />
                 <property name="out" location="${project.build.directory}/${project.build.finalName}" />
                 <property name="dst" location="${out}/Documentation" />
@@ -184,6 +310,33 @@
               <goal>run</goal>
             </goals>
           </execution>
+          <execution>
+            <id>include-release-notes</id>
+            <phase>process-classes</phase>
+            <configuration>
+              <target unless="gerrit.documentation.skip">
+                <property name="src" location="${basedir}/../ReleaseNotes" />
+                <property name="out" location="${project.build.directory}/${project.build.finalName}" />
+                <property name="dst" location="${out}/ReleaseNotes" />
+
+                <exec dir="${src}" executable="make">
+                  <arg value="VERSION=${project.version}" />
+                  <arg value="clean" />
+                  <arg value="all" />
+                </exec>
+
+                <mkdir dir="${dst}" />
+                <copy overwrite="true" todir="${dst}">
+                  <fileset dir="${src}">
+                    <include name="*.html" />
+                  </fileset>
+                </copy>
+              </target>
+            </configuration>
+            <goals>
+              <goal>run</goal>
+            </goals>
+          </execution>
         </executions>
       </plugin>
     </plugins>
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 1a556c2..d0b8c38 100644
--- a/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
+++ b/gerrit-war/src/main/java/com/google/gerrit/httpd/WebAppInitializer.java
@@ -18,6 +18,7 @@
 import static com.google.inject.Stage.PRODUCTION;
 
 import com.google.gerrit.common.ChangeHookRunner;
+import com.google.gerrit.httpd.GerritUiOptions;
 import com.google.gerrit.httpd.auth.openid.OpenIdModule;
 import com.google.gerrit.httpd.plugins.HttpPluginModule;
 import com.google.gerrit.lifecycle.LifecycleManager;
@@ -28,6 +29,7 @@
 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.GerritServerConfig;
 import com.google.gerrit.server.config.GerritServerConfigModule;
 import com.google.gerrit.server.config.MasterNodeStartup;
 import com.google.gerrit.server.config.SitePath;
@@ -37,12 +39,16 @@
 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.patch.IntraLineWorkerPool;
 import com.google.gerrit.server.plugins.PluginGuiceEnvironment;
 import com.google.gerrit.server.plugins.PluginModule;
+import com.google.gerrit.server.schema.DataSourceModule;
 import com.google.gerrit.server.schema.DataSourceProvider;
+import com.google.gerrit.server.schema.DataSourceType;
 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.SshKeyCacheImpl;
 import com.google.gerrit.sshd.SshModule;
 import com.google.gerrit.sshd.commands.MasterCommandModule;
 import com.google.inject.AbstractModule;
@@ -56,6 +62,7 @@
 import com.google.inject.servlet.GuiceServletContextListener;
 import com.google.inject.spi.Message;
 
+import org.eclipse.jgit.lib.Config;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
@@ -144,10 +151,35 @@
   private Injector createDbInjector() {
     final List<Module> modules = new ArrayList<Module>();
     if (sitePath != null) {
-      modules.add(new LifecycleModule() {
+      Module sitePathModule = new AbstractModule() {
         @Override
         protected void configure() {
           bind(File.class).annotatedWith(SitePath.class).toInstance(sitePath);
+        }
+      };
+      modules.add(sitePathModule);
+
+      Module configModule = new GerritServerConfigModule();
+      modules.add(configModule);
+
+      Injector cfgInjector = Guice.createInjector(sitePathModule, configModule);
+      Config cfg = cfgInjector.getInstance(Key.get(Config.class,
+          GerritServerConfig.class));
+      String dbType = cfg.getString("database", null, "type");
+
+      final DataSourceType dst = Guice.createInjector(new DataSourceModule(),
+          configModule, sitePathModule).getInstance(
+            Key.get(DataSourceType.class, Names.named(dbType.toLowerCase())));
+      modules.add(new AbstractModule() {
+        @Override
+        protected void configure() {
+          bind(DataSourceType.class).toInstance(dst);
+        }
+      });
+
+      modules.add(new LifecycleModule() {
+        @Override
+        protected void configure() {
           bind(DataSourceProvider.Context.class).toInstance(
               DataSourceProvider.Context.MULTI_USER);
           bind(Key.get(DataSource.class, Names.named("ReviewDb"))).toProvider(
@@ -155,7 +187,6 @@
           listener().to(DataSourceProvider.class);
         }
       });
-      modules.add(new GerritServerConfigModule());
 
     } else {
       modules.add(new LifecycleModule() {
@@ -199,11 +230,11 @@
     modules.add(new WorkQueue.Module());
     modules.add(new ChangeHookRunner.Module());
     modules.add(new ReceiveCommitsExecutorModule());
+    modules.add(new IntraLineWorkerPool.Module());
     modules.add(cfgInjector.getInstance(GerritGlobalModule.class));
     modules.add(new DefaultCacheFactory.Module());
     modules.add(new SmtpEmailSender.Module());
     modules.add(new SignedTokenEmailTokenVerifier.Module());
-    modules.add(new SignedTokenRestTokenVerifier.Module());
     modules.add(new PluginModule());
     modules.add(new CanonicalWebUrlModule() {
       @Override
@@ -211,7 +242,14 @@
         return HttpCanonicalWebUrlProvider.class;
       }
     });
+    modules.add(SshKeyCacheImpl.module());
     modules.add(new MasterNodeStartup());
+    modules.add(new AbstractModule() {
+      @Override
+      protected void configure() {
+        bind(GerritUiOptions.class).toInstance(new GerritUiOptions(false));
+      }
+    });
     return cfgInjector.createChildInjector(modules);
   }
 
diff --git a/gerrit-war/src/main/resources/log4j.properties b/gerrit-war/src/main/resources/log4j.properties
index 45f630e..1fcca6d 100644
--- a/gerrit-war/src/main/resources/log4j.properties
+++ b/gerrit-war/src/main/resources/log4j.properties
@@ -38,10 +38,8 @@
 
 # Silence non-critical messages from openid4java
 #
+log4j.logger.org.apache.http=WARN
 log4j.logger.org.apache.xml=WARN
-log4j.logger.httpclient.wire=WARN
-log4j.logger.org.apache.commons.httpclient=WARN
-log4j.logger.org.apache.commons.httpclient.HttpMethodBase=ERROR
 log4j.logger.org.openid4java=WARN
 log4j.logger.org.openid4java.consumer.ConsumerManager=FATAL
 log4j.logger.org.openid4java.discovery.Discovery=ERROR
@@ -57,3 +55,6 @@
 # Silence non-critical messages from Velocity
 #
 log4j.logger.velocity=WARN
+
+# Silence non-critical messages from apache.http
+log4j.logger.org.apache.http=WARN
diff --git a/plugins/README b/plugins/README
new file mode 100644
index 0000000..00df3c5
--- /dev/null
+++ b/plugins/README
@@ -0,0 +1,11 @@
+If you are adding a directory here:
+
+- Search all pom.xml files for "CORE PLUGIN LIST".
+- Add the new plugin to that location.
+- (optional) Thank the Maven developers for making this easy.
+
+- Ensure the plugin's pom.xml <version> is the same as Gerrit's
+  own pom.xml(s). Gerrit will only embed a plugin that has the
+  same version as itself.
+
+- Register the plugin as a submodule with git submodule.
diff --git a/plugins/commit-message-length-validator b/plugins/commit-message-length-validator
new file mode 160000
index 0000000..74ec54f
--- /dev/null
+++ b/plugins/commit-message-length-validator
@@ -0,0 +1 @@
+Subproject commit 74ec54fecdd3936541d85157a5dd2b022b416941
diff --git a/plugins/replication b/plugins/replication
new file mode 160000
index 0000000..ee687d4
--- /dev/null
+++ b/plugins/replication
@@ -0,0 +1 @@
+Subproject commit ee687d42bff7d1a8260d782654392b50c694216e
diff --git a/plugins/reviewnotes b/plugins/reviewnotes
new file mode 160000
index 0000000..37db5cf
--- /dev/null
+++ b/plugins/reviewnotes
@@ -0,0 +1 @@
+Subproject commit 37db5cfbb27375bfbfaab21eed520509ee1d0710
diff --git a/pom.xml b/pom.xml
index eca4902..be5accf 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.5-SNAPSHOT</version>
+  <version>2.6-SNAPSHOT</version>
 
   <name>Gerrit Code Review - Parent</name>
   <url>http://code.google.com/p/gerrit/</url>
@@ -46,14 +46,15 @@
   </issueManagement>
 
   <properties>
-    <jgitVersion>2.0.0.201206130900-r.23-gb3dbf19</jgitVersion>
-    <gwtormVersion>1.4</gwtormVersion>
+    <jgitVersion>2.3.1.201302201838-r.208-g75e1bdb</jgitVersion>
+    <gwtormVersion>1.6</gwtormVersion>
     <gwtjsonrpcVersion>1.3</gwtjsonrpcVersion>
-    <gwtexpuiVersion>1.2.6</gwtexpuiVersion>
-    <gwtVersion>2.4.0</gwtVersion>
+    <gwtexpuiVersion>1.3.2</gwtexpuiVersion>
+    <gwtVersion>2.5.0</gwtVersion>
+    <bouncyCastleVersion>140</bouncyCastleVersion>
     <slf4jVersion>1.6.1</slf4jVersion>
     <guiceVersion>3.0</guiceVersion>
-    <jettyVersion>7.2.1.v20101111</jettyVersion>
+    <jettyVersion>8.1.7.v20120910</jettyVersion>
 
     <gwt.compileReport>false</gwt.compileReport>
 
@@ -76,6 +77,7 @@
     <module>gerrit-common</module>
     <module>gerrit-cache-h2</module>
     <module>gerrit-httpd</module>
+    <module>gerrit-gwtui</module>
     <module>gerrit-launcher</module>
     <module>gerrit-main</module>
     <module>gerrit-openid</module>
@@ -87,24 +89,29 @@
     <module>gerrit-gwtdebug</module>
     <module>gerrit-war</module>
 
+    <module>gerrit-acceptance-tests</module>
     <module>gerrit-extension-api</module>
-
-    <module>gerrit-gwtui</module>
+    <module>gerrit-plugin-api</module>
+    <module>gerrit-plugin-archetype</module>
+    <module>gerrit-plugin-gwtui</module>
+    <module>gerrit-plugin-js-archetype</module>
+    <module>gerrit-plugin-gwt-archetype</module>
   </modules>
 
   <profiles>
     <profile>
-      <id>all</id>
-      <modules>
-        <module>gerrit-plugin-api</module>
-        <module>gerrit-plugin-archetype</module>
-      </modules>
-    </profile>
-    <profile>
+      <id>plugins</id>
       <activation>
-        <activeByDefault>true</activeByDefault>
+        <property>
+          <name>!gerrit.plugins.skip</name>
+        </property>
       </activation>
-      <id>no-plugins</id>
+      <modules>
+        <!-- CORE PLUGIN LIST -->
+        <module>plugins/commit-message-length-validator</module>
+        <module>plugins/replication</module>
+        <module>plugins/reviewnotes</module>
+      </modules>
     </profile>
   </profiles>
 
@@ -369,7 +376,13 @@
         <plugin>
           <groupId>org.apache.maven.plugins</groupId>
           <artifactId>maven-dependency-plugin</artifactId>
-          <version>2.1</version>
+          <version>2.5.1</version>
+        </plugin>
+
+        <plugin>
+          <groupId>org.apache.maven.plugins</groupId>
+          <artifactId>maven-surefire-plugin</artifactId>
+          <version>2.13</version>
         </plugin>
 
         <plugin>
@@ -381,7 +394,7 @@
         <plugin>
           <groupId>org.codehaus.mojo</groupId>
           <artifactId>gwt-maven-plugin</artifactId>
-          <version>2.4.0</version>
+          <version>2.5.0</version>
         </plugin>
 
         <plugin>
@@ -409,7 +422,10 @@
                     </goals>
                   </pluginExecutionFilter>
                   <action>
-                    <ignore/>
+                     <execute>
+                      <runOnIncremental>false</runOnIncremental>
+                      <runOnConfiguration>true</runOnConfiguration>
+                    </execute>
                   </action>
                 </pluginExecution>
                 <pluginExecution>
@@ -422,7 +438,10 @@
                     </goals>
                   </pluginExecutionFilter>
                   <action>
-                    <ignore/>
+                    <execute>
+                      <runOnIncremental>false</runOnIncremental>
+                      <runOnConfiguration>true</runOnConfiguration>
+                   </execute>
                   </action>
                 </pluginExecution>
               </pluginExecutions>
@@ -478,7 +497,7 @@
       <dependency>
         <groupId>com.google.guava</groupId>
         <artifactId>guava</artifactId>
-        <version>12.0.1</version>
+        <version>14.0</version>
       </dependency>
 
       <dependency>
@@ -563,7 +582,7 @@
       <dependency>
         <groupId>org.apache.sshd</groupId>
         <artifactId>sshd-core</artifactId>
-        <version>0.5.1-r1095809</version>
+        <version>0.6.0</version>
       </dependency>
 
       <dependency>
@@ -688,7 +707,13 @@
       <dependency>
         <groupId>bouncycastle</groupId>
         <artifactId>bcpg-jdk15</artifactId>
-        <version>140</version>
+        <version>${bouncyCastleVersion}</version>
+      </dependency>
+
+      <dependency>
+        <groupId>bouncycastle</groupId>
+        <artifactId>bcprov-jdk15</artifactId>
+        <version>${bouncyCastleVersion}</version>
       </dependency>
 
       <dependency>
@@ -761,19 +786,19 @@
       <dependency>
         <groupId>junit</groupId>
         <artifactId>junit</artifactId>
-        <version>4.8.1</version>
+        <version>4.11</version>
       </dependency>
 
       <dependency>
         <groupId>com.h2database</groupId>
         <artifactId>h2</artifactId>
-        <version>1.2.147</version>
+        <version>1.3.168</version>
       </dependency>
 
       <dependency>
         <groupId>postgresql</groupId>
         <artifactId>postgresql</artifactId>
-        <version>9.0-801.jdbc4</version>
+        <version>9.1-901-1.jdbc4</version>
       </dependency>
 
       <dependency>
@@ -783,8 +808,8 @@
         <exclusions>
           <exclusion>
             <!-- use Apache javax.servlet not CDDL -->
-            <groupId>javax.servlet</groupId>
-            <artifactId>servlet-api</artifactId>
+            <groupId>org.eclipse.jetty.orbit</groupId>
+            <artifactId>javax.servlet</artifactId>
           </exclusion>
         </exclusions>
       </dependency>
@@ -802,6 +827,12 @@
       </dependency>
 
       <dependency>
+        <groupId>org.apache.tomcat</groupId>
+        <artifactId>tomcat-servlet-api</artifactId>
+        <version>7.0.32</version>
+      </dependency>
+
+      <dependency>
         <groupId>com.google.gwt</groupId>
         <artifactId>gwt-servlet</artifactId>
         <version>${gwtVersion}</version>
@@ -826,7 +857,7 @@
       </dependency>
 
       <dependency>
-        <groupId>com.google.gerrit</groupId>
+        <groupId>com.googlecode.juniversalchardet</groupId>
         <artifactId>juniversalchardet</artifactId>
         <version>1.0.3</version>
       </dependency>
@@ -848,6 +879,17 @@
         <artifactId>pegdown</artifactId>
         <version>1.1.0</version>
       </dependency>
+
+      <dependency>
+        <groupId>org.parboiled</groupId>
+        <artifactId>parboiled-core</artifactId>
+        <version>1.1.3</version>
+      </dependency>
+      <dependency>
+        <groupId>org.parboiled</groupId>
+        <artifactId>parboiled-java</artifactId>
+        <version>1.1.3</version>
+      </dependency>
     </dependencies>
   </dependencyManagement>
 
@@ -868,11 +910,6 @@
     </repository>
 
     <repository>
-      <id>objectweb-repository</id>
-      <url>http://maven.objectweb.org/maven2/</url>
-    </repository>
-
-    <repository>
       <id>clojars-repo</id>
       <url>http://clojars.org/repo</url>
     </repository>
diff --git a/tools/release.sh b/tools/release.sh
index de18357..88e4a00 100755
--- a/tools/release.sh
+++ b/tools/release.sh
@@ -1,16 +1,25 @@
 #!/bin/sh
 
-include_docs=-Dgerrit.include-documentation=1
+flags=
 
 while [ $# -gt 0 ]
 do
 	case "$1" in
 	--no-documentation|--without-documentation)
-		include_docs=
+		flags="$flags -Dgerrit.documentation.skip=true"
+		shift
+		;;
+	--no-plugins|--without-plugins)
+		flags="$flags -Dgerrit.plugins.skip=true"
+		shift
+		;;
+	--no-tests|--without-tests)
+		flags="$flags -Dgerrit.acceptance-tests.skip=true"
+		flags="$flags -Dmaven.tests.skip=true"
 		shift
 		;;
 	*)
-		echo >&2 "usage: $0 [--without-documentation]"
+		echo >&2 "usage: $0 [--no-documentation] [--no-plugins] [--no-tests]"
 		exit 1
 	esac
 done
@@ -25,7 +34,7 @@
 fi
 
 ./tools/version.sh --release &&
-mvn clean install $include_docs -P all
+mvn clean package verify $flags
 rc=$?
 ./tools/version.sh --reset
 
diff --git a/tools/version.sh b/tools/version.sh
index d3e4cd5..def099d 100755
--- a/tools/version.sh
+++ b/tools/version.sh
@@ -6,7 +6,15 @@
 # Java based Maven plugin so its fully portable.
 #
 
-POM_FILES=$(git ls-files | grep pom.xml | grep -v gerrit-plugin-archetype/src/main/resources/archetype-resources/pom.xml)
+SERVER_POMS=$(git ls-files | grep pom.xml | grep -v /src/main/resources/archetype-resources/pom.xml)
+POM_FILES=$SERVER_POMS
+
+# CORE PLUGIN LIST
+PLUGINS="commit-message-length-validator replication reviewnotes"
+for p in $PLUGINS
+do
+	POM_FILES="$POM_FILES $(cd plugins/$p && git ls-files | grep pom.xml | sed s,^,plugins/$p/,)"
+done
 
 case "$1" in
 --snapshot=*)
@@ -27,7 +35,11 @@
 	;;
 
 --reset)
-	git checkout HEAD -- $POM_FILES
+	git checkout HEAD -- $SERVER_POMS
+	for p in $PLUGINS
+	do
+		(cd plugins/$p; git checkout $(git ls-files | grep pom.xml))
+	done
 	exit $?
 	;;
 
@@ -40,7 +52,7 @@
 v*) V=$(echo "$V" | perl -pe s/^v//) ;;
 esac
 
-perl -pi -e '
+perl -pi.bak -e '
 	if ($ARGV ne $old_argv) {
 		$seen_version = 0;
 		$old_argv = $ARGV;
@@ -50,3 +62,8 @@
 		s{(<version>).*(</version>)}{${1}'"$V"'${2}};
 	}
 	' $POM_FILES
+
+for pom in $POM_FILES
+do
+	rm -f ${pom}.bak
+done